# CocoIndex Plus security & data flow

> **CocoIndex v1.** This page documents CocoIndex **v1** — a ground-up redesign from v0. When writing code, ignore any v0 flow-builder DSL or deprecated decorators.
>
> Source: https://plus.cocoindex.io/docs/security/ · Docs index: https://plus.cocoindex.io/docs/llms.txt · Agent skill: https://plus.cocoindex.io/docs/skill.md
>
> v0→v1 quick map — if you reach for these v0 symbols, stop and use the v1 form: `@cocoindex.flow_def`/`FlowBuilder` → `coco.App` + a `@coco.fn` main function; `add_collector()`/`collect()`/`export()` → declare target states (`declare_row`, `declare_file`); `cocoindex.sources/functions/targets.*` → connector APIs (`localfs.walk_dir`, `coco.ops.*`, `postgres.declare_table_target`). Full mapping + API reference: https://plus.cocoindex.io/docs/skill.md.

CocoIndex Plus — the CocoIndex Enterprise Edition, distributed as the
`cocoindex-plus` package — is software you run yourself. This page states what
it does on the network and on disk, in terms you can check against your own
deployment. It was checked against **v1.0.53** (released 2026-10-15).

## Architecture & trust model

CocoIndex Plus is a **Python library with a compiled Rust engine**. It runs
inside your own process — your application, a job you schedule, or the
`cocoindex` CLI — with that process's identity and permissions.

- **It isn't a service.** It opens no network listener (no HTTP server, admin
  console, or port) and has no user accounts, SSO, or authentication of its
  own. Your OS, container platform, or scheduler decides who can run a
  pipeline; the credentials you give a pipeline decide what it can reach.
- **CocoIndex operates nothing in the data path and receives none of your
  data.** There's no telemetry, usage analytics, crash reporting, or update
  check: the anonymous usage telemetry of the open-source edition is compiled
  out of the Plus build. [Metrics export](/docs/advanced_topics/observability/)
  (v1.0.59 and later) is not telemetry to us: it is off unless you configure
  it, and it sends only to the OpenTelemetry endpoint you name. The one
  connection to a service we designate is
  [license validation](#license-validation), and an offline-entitled license
  doesn't make it. You can check the compiled engine yourself — the only
  service URL in it is Keygen's (`docs.rs` appears in a TLS library's error
  message; from v1.0.59, `http://localhost` is the OpenTelemetry exporter's
  default collector address, `localhost:4318`, used only when you enable
  [metrics export](#metrics-export) without naming an endpoint):

  ```sh
  unzip -p cocoindex_plus-*.whl 'cocoindex/_internal/core*.so' \
    | strings | grep -oE 'https?://[A-Za-z0-9.-]+' | sort -u
  # http://localhost        (v1.0.59 and later)
  # https://api.keygen.sh
  # https://docs.rs
  ```

  One item in the package isn't code: the v1.0.53 package description (the
  wheel's `METADATA`, copied from the open-source README) embeds a 1×1
  tracking image from `static.scarf.sh`. Nothing in CocoIndex Plus loads it,
  but a package browser or index mirror that renders the description as HTML
  would. Releases after v1.0.53 replace that description with a Plus-specific
  one without the image.
- **Your data stays in systems you choose.** Sources, targets, the engine's
  [state store](#what-cocoindex-plus-stores), and model endpoints are all
  yours to configure. [Network egress](#network-egress--the-complete-list)
  lists every destination.

The `cocoindex` CLI has five commands:

- `update` and `drop` load your app's code and run it (`drop` reverts the
  target states the app created, then clears its state).
- `ls` and `show` inspect the state store. Given an app target, they import
  your app's module, and `ls` also starts its environment, which runs your
  lifespan code; `ls --db PATH` only reads the database.
- `init` scaffolds a project.

The CLI reads environment variables from `--env-file`, or else from a `.env`
file in the current directory or the nearest parent directory that has one —
run it from a directory you trust. litellm, once imported, also loads a
`.env` file, the first one found walking up from its install directory,
unless `LITELLM_MODE` is set to something other than `DEV`. Neither overrides
variables that are already set (litellm does with
`LITELLM_DEV_ENV_HOT_RELOAD=True`).

## Network egress — the complete list

| Destination | When | What it receives |
|---|---|---|
| Your sources and targets — databases, object stores, queues, code hosts, SaaS APIs | When your pipeline reads or writes them | Your data, under credentials you supply |
| Your Postgres state store, if you use one | Throughout a run | Engine state ([what it holds](#what-cocoindex-plus-stores)) |
| Your OpenTelemetry collector or metrics backend, if you configure one | Periodically and at shutdown, only when `OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` or `OTEL_METRICS_EXPORTER=otlp` is set (v1.0.59 and later) | Pipeline metrics, no pipeline data ([what the labels contain](#metrics-export)) |
| Your model providers | Only when your code calls `LiteLLMEmbedder`, `LiteLLMTranscriber`, or `LlmPairResolver`, or gives a Chonkie semantic chunker a hosted embedding model | The text or audio you pass in ([per operation](#aiml-data-handling)) |
| Hugging Face (`huggingface.co` and file hosts under `hf.co`) | Each time a Hugging Face model or tokenizer loads | No pipeline data: model files and metadata ([details below](#third-party-library-requests)) |
| `github.com` (tree-sitter grammars) | When `ChonkieCodeChunker` is built without its grammars cached | No pipeline data: grammar downloads |
| `raw.githubusercontent.com` (litellm metadata) | When litellm is imported, and for some Claude requests | No pipeline data |
| `api.keygen.sh` | At most once per process start, standard licenses only: at the first start after the cached validation is 24 hours old ([details](#license-validation)) | The license key — nothing else |

That's the complete list. How you control each destination:

- **Sources, targets, and the state store:** you choose every endpoint and
  credential. The GitHub and GitLab connectors default to `api.github.com`
  and `gitlab.com`; point them at your own instance to keep traffic internal.
  For the state store, choose its [TLS mode](#postgres-state-store).
- **Model providers:** you name the model, and the endpoint and API key are
  yours. There's no CocoIndex default endpoint, and a self-hosted endpoint
  works. Some providers make their own metadata calls — litellm's
  `huggingface/` embedding provider, for one, looks the model up on
  `huggingface.co`.
- **Metrics export:** off by default. You choose the endpoint and its
  credentials with the standard OpenTelemetry variables, and
  `OTEL_SDK_DISABLED=true` turns it off whatever else is set.
- **License validation:** an [offline-entitled license](#license-validation)
  sends nothing.

### Metrics export

Exported metrics are counts and durations; the values never include your
data. The labels on them name parts of your deployment, so treat a metrics
backend as you would your logs:

- the environment name and app name;
- `coco.stats_group` titles and processor (function) names;
- target provider names and container keys, such as a table name or an
  output directory;
- target action sink names, which are your connector callbacks' qualified
  names;
- exception type names, such as `ValueError` — never exception messages;
- the names and values of counters your code adds with `coco.metrics`.

Component paths, row and file keys, and exception messages are not exported.
See the [metrics reference](/docs/advanced_topics/metrics_reference/) for every
metric and label.

### Third-party library requests

The other rows come from libraries your pipeline loads, and happen only when
it uses the operations that load them. We checked them in huggingface_hub
0.36 and 1.27, transformers 4.57 and 5.15, chonkie 1.6 and 1.7,
tree-sitter-language-pack 1.8 and 1.14, and litellm 1.96–1.105:

- **Hugging Face:** `SentenceTransformerEmbedder`, the Chonkie semantic and
  neural chunkers, and a Hugging Face tokenizer name each load a model or
  tokenizer, which downloads it — files come from hosts such as
  `us.aws.cdn.hf.co` and `cas.xethub.hf.co` — or checks a cached copy for
  updates. Two other requests:
  - For a model published only as `.bin` weights, transformers asks
    Hugging Face's conversion service (`safetensors-convert.hf.space`) to
    convert it, sending the model's name and your Hugging Face token, if one
    is set. `DISABLE_SAFETENSORS_CONVERSION=1` turns this off.
  - huggingface_hub 1.27 fetches a list of AI-agent tools from
    `huggingface.co` at most once a day, to label its requests.
    `HF_HUB_DISABLE_TELEMETRY=1` turns this off.

  `HF_HUB_OFFLINE=1` stops all of these. The exception is chonkie 1.7's
  tokenizer loader, used for a Hugging Face tokenizer name, which ignores
  `HF_HOME` and `HF_HUB_OFFLINE` and reads only `~/.cache/huggingface/hub`.
  [Pre-stage the models](#air-gapped-deployment) in that directory.
- **tree-sitter grammars:** `ChonkieCodeChunker` downloads grammars, as
  native libraries, from the tree-sitter-language-pack project's GitHub
  releases (served from `release-assets.githubusercontent.com`). Under chonkie
  1.7, building the first chunker downloads all of them, about 500 MB.
  [Pre-download them](#air-gapped-deployment).
- **litellm metadata:** litellm fetches its model cost map when it's imported,
  and a beta-headers config for Claude requests that carry beta headers. Set
  `LITELLM_LOCAL_MODEL_COST_MAP=True` and
  `LITELLM_LOCAL_ANTHROPIC_BETA_HEADERS=True` to use the copies bundled with
  litellm.

## License validation

The engine validates the license key in `COCOINDEX_PLUS_LICENSE_KEY` during
`import cocoindex`, before anything else runs; if validation fails, the import
fails. A license change never stops a running process: validation happens only
at start. Which of two lanes a key takes is fixed when we issue the license.

**Offline-entitled license — no network request.** The key is a signed
license: the engine checks its Ed25519 signature against a public key compiled
into CocoIndex Plus, confirms it was issued under our offline policy and for
our account, and enforces the expiry embedded in it. That expiry can't be
extended, and nothing can revoke the key remotely — renewal means we issue a
new key.

**Standard license — at most one HTTPS request per process start.** A start
within 24 hours of the last successful validation (the default
`COCOINDEX_PLUS_LICENSE_REVALIDATE_AFTER`, in seconds; `0` validates at every
start, values above one week count as one week) uses the cached answer and
sends nothing; the first start after that validates online:

- **The request:**
  `POST https://api.keygen.sh/v1/accounts/<CocoIndex account ID>/licenses/actions/validate-key`
  with the body `{"meta":{"key":"<your license key>"}}` and JSON:API
  `Content-Type`/`Accept` headers. No hostname, machine fingerprint, usage
  data, or pipeline data is sent. Keygen, as the receiving service, sees the
  source IP address and time, and records the license's last validation.
- **The answer must be signed.** The response has to carry a valid Ed25519
  signature from the same compiled-in key, so a forged or altered answer
  can't validate the license.
- **Five-second limit.** If Keygen can't be reached, doesn't answer within 5
  seconds, or answers without a signature, the engine falls back to its
  cached answer.
- **Revocation latency.** A license we suspend, revoke, or let expire keeps
  working on a machine that validated it for at most the revalidation
  interval. Set `COCOINDEX_PLUS_LICENSE_REVALIDATE_AFTER=0` where every start
  must check.
- **TLS:** the engine connects with rustls against a compiled-in set of public
  CA roots, not your operating system's trust store. It honors the
  `HTTPS_PROXY`, `ALL_PROXY`, and `NO_PROXY` environment variables (not OS
  proxy settings), so a TLS-inspecting proxy must exempt `api.keygen.sh`.

The cached answer lets a standard license ride out a Keygen outage — it is not
an offline mode:

- Only a fresh answer saying the license is valid is cached, in
  `license_info.json` under `$XDG_STATE_HOME/cocoindexplus/` (default
  `~/.local/state/cocoindexplus/`) on Linux, or
  `~/Library/Application Support/io.cocoindex.CocoIndexPlus/` on macOS.
- It's used only for the key it was issued for (the record carries a SHA-256
  digest of that key), and its signature is checked again every time it's
  read. Within the revalidation interval it stands in for the request; after
  that, only when no signed answer arrives, and only while Keygen's answer is
  at most one week old. Past that, `import cocoindex` fails until Keygen is
  reachable again.

**Caution — The license cache contains the license key**
The cache stores Keygen's signed response verbatim — the signature covers the
whole body, so it can't be trimmed — and that response includes the license
record, key included. In v1.0.53, the file gets your process's default
permissions (0644 under the common umask 022): keep the service account's
home directory private, or `chmod 600` the file. Releases after v1.0.53
create the file readable by its owner only (mode 0600) and rewrite it at
every process start that validates online, so after an upgrade, the first
such start replaces the copy an earlier release wrote.

**We recommend offline-entitled licenses for production and for locked-down
evaluations:** nothing leaves your network for licensing, and starting a
process never waits on Keygen. Ask us for one. The
[installation guide](/docs/getting_started/installation/#set-your-cocoindex-plus-license-key)
covers setting either kind.

## What CocoIndex Plus stores

Everything below lives on infrastructure you control. Retention is yours.

**The state store** is where the engine keeps what it needs for incremental
updates. It's an LMDB directory by default, or a Postgres database when the
path given by `COCOINDEX_DB` (or `Settings.db_path`) is a `postgres://` or
`postgresql://` URL — see [State storage](/docs/programming_guide/state_storage/).
It holds:

- **Component paths**, built from the keys your code passes — typically source
  identifiers such as file paths or record keys.
- **For each target state, its key** (a row's primary key, a file name) and
  the connector's tracking record. The built-in connectors track rows, files,
  and messages by a fingerprint of the value, not the value itself, and track
  tables, collections, and indexes by their declared schema. The Postgres
  connector's SQL-command attachments record their setup and teardown SQL
  verbatim, so keep secrets out of that SQL.
- **Memoized results** of `@coco.fn(memo=True)` functions and memoized
  components — whatever they return (chunk text, embeddings, model output),
  along with fingerprints of their inputs and code, and the state that
  decides whether a memo is still valid (for a file, its modification time
  and a content fingerprint).
- **Values your code persists** with `coco.use_state`.

So the state store can hold copies of, and values derived from, your source
data: protect it like the source.

**Encryption at rest** comes from your storage: CocoIndex Plus doesn't encrypt
the state store itself, so use disk or volume encryption for LMDB and your
database's encryption for Postgres. LMDB's data files are created readable by
their owner only (mode 0600); the directory around them follows your umask.

**Other local files:**

- The [license cache](#license-validation), for standard licenses.
- The Hugging Face model cache (`~/.cache/huggingface`, or `HF_HOME`), when a
  local model is used.
- tree-sitter-language-pack's grammar cache, when `ChonkieCodeChunker` is
  used.

**Removal:** `cocoindex drop` (or `App.drop()`) reverts the target states an
app created — dropping tables, deleting rows — then clears the app's state.
On Postgres, that drops the app's tables. On LMDB, it empties the app's
sub-database without shrinking or scrubbing the file, so freed pages can keep
old bytes until they're reused; to destroy LMDB state, delete its directory.

## Securing the state store

### Postgres state store

The `sslmode` values are libpq's, with these meanings:

| `sslmode` | Encrypted | Certificate chain verified | Hostname verified |
|---|---|---|---|
| `disable` | no | — | — |
| `allow`, `prefer` (the default) | if the server offers TLS | no | no |
| `require` | yes | no | no |
| `verify-ca` | yes | yes | no |
| `verify-full` | yes | yes | yes |

The default, `prefer`, neither requires nor verifies TLS. **Use
`sslmode=verify-full`.** The verifying modes trust your operating system's CA
store, and `sslrootcert=/path/to/ca.pem` adds your private CA to it rather
than replacing it (unlike libpq). So don't rely on `verify-ca`: since it
skips the name check, it accepts any certificate a public CA has issued, for
any name. `sslrootcert` is rejected unless the mode verifies. Two limits of
the TLS stack:

- The server certificate must be signed by a separate CA certificate; a
  self-signed server certificate can't be verified, even through
  `sslrootcert`.
- `verify-full` matches names against the certificate's subject alternative
  names only, not its common name.

Only the DSN configures the connection: libpq's environment variables
(`PGSSLMODE`, `PGPASSWORD`, …), `~/.pgpass`, and `~/.postgresql/root.crt`
aren't read. Client-certificate authentication (`sslcert`/`sslkey`) isn't
supported, so a password, if the server needs one, goes in the DSN — treat
`COCOINDEX_DB` as a secret, and note that the v1.0.53 CLI prints it
([Logging](#logging--audit)). Give CocoIndex its own role and, ideally, its
own schema (`PgSettings(schema=...)` or `COCOINDEX_PG_SCHEMA`);
[State storage](/docs/programming_guide/state_storage/#required-postgres-privileges)
lists the privileges the role needs.

### Who can read and write it

- **Read access** exposes what the state store holds: component paths,
  target-state keys, recorded SQL, and memoized results. `cocoindex ls --db`
  is just a reader.
- **Write access amounts to control over your pipeline's outputs.** A
  memoized value is decoded with msgpack, or, for types that need it, with a
  restricted unpickler that resolves only an allow-list of types: Python's
  basic built-ins (numbers, strings, bytes, lists, tuples, dicts, sets,
  `None`, and `type`), `pathlib` paths, `uuid.UUID`, `datetime` values, NumPy
  arrays, and the types you register with `@coco.serialize_by_pickle` or
  `@coco.unpickle_safe` (see [Serialization](/docs/programming_guide/serialization/)).
  That blocks the usual pickle attack — a payload that names an arbitrary
  function such as `os.system` — but it doesn't make a forged entry harmless.
  Whoever can write the state store chooses the memoized values your pipeline
  reads back, which flow into your targets as if your code had produced them,
  and allow-listed types are constructed from the stored data. A forged
  tracking record can also make the next run delete data in your targets: the
  engine removes target states it believes it created but your code no longer
  declares. And because a Postgres SQL-command attachment runs its recorded
  teardown SQL when it changes or goes away, a forged record there runs
  arbitrary SQL with the pipeline's database role.

Treat write access to the state store like write access to your targets —
and, where you use SQL-command attachments, like the ability to run SQL as
the pipeline. Grant it only to the pipeline's own service identity.

## Logging & audit

- **Engine logs** (Rust `tracing`) go to standard output. `RUST_LOG` sets the
  filter; the default is `info`.
- **Python logs** go through Python's `logging` under `cocoindex.*` logger
  names, so your application's logging configuration applies. The CLI
  configures it from `COCOINDEX_LOG_LEVEL` (default `WARNING`) and writes to
  standard error.
- **What appears in logs:** engine messages can include component paths and
  target-state paths, and a failed component is logged at `ERROR` with its
  exception and traceback — so messages raised by your code or a connector,
  which may contain data values, can reach your logs. Apply your usual log
  retention and access controls.
- **The CLI prints the state store's location:** `cocoindex ls` always, and
  `update` and `drop` unless you pass `--quiet`. In v1.0.53 that's the full
  Postgres DSN, password included, so don't let CLI output from a
  password-bearing `COCOINDEX_DB` reach shared logs. Releases after v1.0.53
  mask the password.
- **Metrics** (v1.0.59 and later) go only to an OpenTelemetry endpoint you
  configure, and only when you configure one; see
  [Metrics export](#metrics-export) for what their labels contain.
- **No audit log.** CocoIndex Plus has no users of its own to audit. Use your
  platform's controls for who runs pipelines, and your databases' audit
  features for the state store and targets. Logs go only to your process's
  standard output and standard error, or wherever your Python logging
  configuration sends them.

## AI/ML data handling

CocoIndex trains no models and never trains on customer data — it receives
none. Model calls happen only where your pipeline code invokes an AI
operation, and go only to the model and endpoint you configure, under your
keys. The operations, and what each one sends:

| Operation | Where it runs | What leaves the process |
|---|---|---|
| `LiteLLMEmbedder` ([LiteLLM](/docs/ops/litellm/)) | Your chosen provider | The texts you embed, in batches — plus, the first time an embedder needs its vector dimension, the probe text `"hello"` |
| `LiteLLMTranscriber` | Your chosen provider | The audio you transcribe, with its file name |
| `LlmPairResolver` ([entity resolution](/docs/ops/entity_resolution/)) | Your chosen provider | Per call: the new entity's name, the candidate names it may match, a fixed instruction prompt, your `entity_type` / `extra_guidance`, and the JSON schema of the expected answer; an answer that names no candidate is retried, up to `retries` (default 2) more calls |
| `SentenceTransformerEmbedder` ([sentence transformers](/docs/ops/sentence_transformers/)) | Locally | Nothing; the model is downloaded from Hugging Face unless you pass a local path, and `trust_remote_code` stays off unless you turn it on |
| Chonkie semantic and neural chunkers ([Chonkie](/docs/ops/chonkie/)) | Locally, with a local model | Nothing; their default models, `minishlab/potion-base-32M` and `mirth/chonky_modernbert_base_1`, download when a chunker is constructed. A hosted embedding model for the semantic chunker (OpenAI's `text-embedding-*`, Cohere's `embed-*`, Jina's, …) sends it the text being chunked |
| Chonkie recursive chunker | Locally | Nothing; the default `character` tokenizer is built in, while a Hugging Face tokenizer name downloads that tokenizer |
| Chonkie code chunker | Locally | Nothing; the first chunker downloads tree-sitter grammars unless they're cached |
| Text splitting, code chunking, and symbol extraction ([text ops](/docs/ops/text/), [code elements](/docs/ops/code_ast/)) | Locally, in the Rust engine | Nothing |

For the LiteLLM-based operations, the model string you pass selects the
provider. `LiteLLMEmbedder` and `LiteLLMTranscriber` take `api_base` and
`api_key` arguments; all three also read LiteLLM's standard provider
environment variables (such as `OPENAI_BASE_URL` or `OLLAMA_API_BASE`), so
each can use an endpoint you host. Results you memoize — embeddings, model
output, entity-resolution decisions — are stored in your
[state store](#what-cocoindex-plus-stores).

## Air-gapped deployment

1. **License:** ask for an offline-entitled license.
2. **Packages:** mirror the `cocoindex-plus` wheels (from our Keygen index)
   and their dependencies (from PyPI) into an index inside your network
   ([supply chain](#supply-chain-verification)).
3. **Local models and tokenizers:** download each one on a connected machine,
   then copy the result to the service account's `~/.cache/huggingface/hub`
   on the target hosts. Use that default location rather than setting
   `HF_HOME` (or `XDG_CACHE_HOME`), because chonkie 1.7's tokenizer loader
   reads only it. Then run in offline mode:

   ```sh
   # On a connected machine: stage into /tmp/hf-stage/hub
   HF_HOME=/tmp/hf-stage hf download minishlab/potion-base-32M
   # On each air-gapped host, after copying /tmp/hf-stage/hub
   # to ~/.cache/huggingface/hub for the service account
   export HF_HUB_OFFLINE=1
   ```

   Set `HF_HUB_OFFLINE=1` even when everything is cached: without it, the
   Hugging Face client keeps trying to reach `huggingface.co` before it falls
   back to the cache. In our tests with the network blocked, loading three
   cached models took 2–7 minutes without the flag, depending on library
   versions, and about 5 seconds with it.
4. **Code grammars** (for `ChonkieCodeChunker`): grammars are native
   libraries, so download them on a machine with the same OS, architecture,
   and `tree-sitter-language-pack` version as your hosts — or while building
   your image — then copy the directory that `cache_dir()` prints to the same
   path for the service account. Download all of them: chonkie 1.7 fetches
   the full set whenever fewer than 20 are cached.

   ```sh
   python - <<'EOF'
   import tree_sitter_language_pack as t
   t.download_all()  # about 500 MB
   print(t.cache_dir())
   EOF
   ```
5. **Model endpoints:** point the LiteLLM-based operations at endpoints inside
   your network, and set `LITELLM_LOCAL_MODEL_COST_MAP=True` and
   `LITELLM_LOCAL_ANTHROPIC_BETA_HEADERS=True`.
6. **Everything else** — sources, targets, the state store — is already
   yours to place inside the network.

## Deployment hardening checklist

- **Installation:** make our index the only source of `cocoindex-plus` — never
  `--extra-index-url` — and install from hashes
  ([supply chain](#supply-chain-verification)).
- **Identity:** run pipelines as a dedicated, non-root service account. Keep
  its home directory (which holds the license cache) and the LMDB directory
  readable by that account only.
- **Secrets:** supply the license key, DSNs, and provider keys as environment
  variables from your secret manager. The license key must be in the
  environment before `import cocoindex`; the CLI can't load it from `.env`.
  A Postgres `COCOINDEX_DB` with a password is a secret too; on v1.0.53, pass
  `--quiet` to `update` and `drop` in automation.
- **State store:** a dedicated database or schema and role, `verify-full`
  TLS, write access for the pipeline identity only, and backups.
- **One process per state database.** Neither backend supports several
  processes sharing one state database yet, and nothing enforces this — make
  it a deployment rule (for example, one replica).
- **Capacity:** up to 1,024 processing components run concurrently per app by
  default (`max_inflight_components` or `COCOINDEX_MAX_INFLIGHT_COMPONENTS`;
  see [Concurrency control](/docs/advanced_topics/concurrency_control/)). The
  Postgres state store opens up to 65 connections per environment
  (`pool_max_size` of 64, plus one); keep that within your server's
  `max_connections`. An LMDB directory holds at most `max_dbs` apps (default
  1,024; `COCOINDEX_LMDB_MAX_DBS`) — a hard cap.
- **Network:** no inbound access is needed. Outbound: your sources, targets,
  state database, and model endpoints, and your OpenTelemetry endpoint if you
  export metrics — plus `api.keygen.sh` for a standard license, and the
  [third-party hosts](#third-party-library-requests) for any models, grammars,
  and litellm metadata you haven't staged or switched to local copies.
- **Model code:** leave `trust_remote_code` off unless you trust the model
  repository's code. Where Hugging Face access stays on, consider
  `DISABLE_SAFETENSORS_CONVERSION=1` and `HF_HUB_DISABLE_TELEMETRY=1`.

## Supply-chain verification

**Distribution.** CocoIndex Plus ships as binary wheels only — there's no
source distribution — from a private package index at Keygen. Fetching a
package requires your install token; an anonymous request is refused. Wheels
exist for Linux x86_64 and aarch64 (`manylinux_2_28`, glibc 2.28+) and macOS
arm64 (11.0+), for CPython 3.11+ (abi3) and free-threaded CPython 3.14.

**Package name.** We publish `cocoindex-plus` to our index only. The
`cocoindex-plus` project on PyPI is a placeholder we hold so no one else can
claim the name there; installing it fails with a pointer to the installation
guide. A `cocoindex-plus` from any other index isn't CocoIndex Plus, and
installing it would run someone else's code in your build and at
`import cocoindex`. The placeholder covers only PyPI, so make our index the
only one your installer asks for `cocoindex-plus`, as the
[installation guide](/docs/getting_started/installation/#install-cocoindex-plus)
shows for each installer. Don't add our index next to another as an equal or a
fallback — pip's `--extra-index-url` or `PIP_EXTRA_INDEX_URL`, uv's
`unsafe-best-match` strategy, or the equivalent in other tools — because a
same-named package on the other index can then win, and if our index rejects
your install token, those setups install from the other index alone, with a
warning at most. Pinning the version doesn't close the gap: for an equal
version, pip can prefer the other index's file. A hash check does —
`pip install --require-hashes`, or uv's lockfile.

As pip's only index, ours answers every lookup and redirects names it doesn't
host to PyPI, so it receives the name of each package you install. uv asks an
`explicit` index only for the packages pinned to it.

**SBOM and notices.** Each wheel embeds two records of the Rust crates
compiled into the engine: a CycloneDX SBOM — 364 components in v1.0.53 — at
`cocoindex_plus-<version>.dist-info/sboms/cocoindex_py.cyclonedx.json`, the
location PEP 770 defines, and their license attribution,
`THIRD_PARTY_NOTICES.html`. Neither covers the Python dependencies, which your
installer resolves; generate their SBOM from your environment.

**Build.** Wheels are built on GitHub-hosted runners. The release job runs
only for tags, in a deployment environment that admits only `v*` tags, and
uploads to the index the same files it attests.

**Build provenance.** Every wheel has a SLSA v1 build-provenance attestation,
signed through GitHub's Sigstore instance with the identity of our release
workflow. Our source repository is private, so GitHub serves these
attestations only to accounts that can read it; ask us for the attestation
bundle (a `.jsonl` file) for the release you install, then verify each wheel
against it — no GitHub credentials needed:

```sh
gh attestation verify \
  cocoindex_plus-1.0.53-cp311-abi3-manylinux_2_28_x86_64.whl \
  --bundle <bundle>.jsonl \
  --repo cocoindex-io/cocoindex-plus \
  --signer-workflow cocoindex-io/cocoindex-plus/.github/workflows/release.yml \
  --source-ref refs/tags/v1.0.53
```

The command exits non-zero if the wheel was modified, or was built from
another tag, repository, or workflow.

**Recommended:** mirror the wheels into your own package index, verify them
there, and install with pinned versions and hashes. If that index also proxies
PyPI, have it serve `cocoindex-plus` only from the mirrored wheels — for
example, by excluding the name from the PyPI proxy. The Rust crates compiled
into the engine are pinned by our lockfile; the Python dependencies are
version ranges that your installer resolves, so lock them on your side too.
On ours, Dependabot alerts and security updates run on the source repository;
developers' commits pass a gitleaks pre-commit hook, and full-history gitleaks
scans run at least quarterly.

## Shared responsibilities

**You:**

- Run and monitor your deployment; keep hosts, Python, and dependencies
  patched; upgrade CocoIndex Plus to receive fixes.
- Back up and recover your state store and targets.
- Manage credentials, network policy, and access to your databases.
- With an offline-entitled license, deploy the renewed key before the current
  one expires: its expiry is fixed, and `import cocoindex` fails after it.

**CocoIndex:**

- Fixes vulnerabilities in shipped versions within these windows, from triage
  to a released fix or documented mitigation:

  | Severity | Window |
  |---|---|
  | Critical, or actively exploited | assessed within 24 hours; fixed or mitigated within **14 days** |
  | High | **30 days** |
  | Medium | **90 days** |
  | Low | **180 days** |

  Fixes ship in new releases. If a vulnerability is exploitable in a shipped
  version, affected customers are notified with the fix release, within the
  same windows.
- Notifies affected customers **within 24 hours** of determining that an
  incident has, or is reasonably suspected to have, affected their data or the
  integrity of software they run.
- Renews licenses: a standard license by extending its expiry at Keygen, with
  nothing to change on your side; an offline-entitled license by issuing a
  new key.

## Reporting a vulnerability

Email **security@cocoindex.io**; please don't open public issues. We
acknowledge reports within 3 business days and prefer coordinated disclosure.
The policy is the
[CocoIndex security policy](https://github.com/cocoindex-io/.github/blob/main/SECURITY.md),
also discoverable through
[`security.txt`](https://cocoindex.io/.well-known/security.txt).
