Data residency and network egress

Sectum AI is built on the premise that a security claim you cannot check yourself is not a security claim. That standard has to apply to Sectum AI. This page is a standing statement of what the open-source sectum-ai CLI sends over a network, what it never sends, and where that boundary is enforced as opposed to merely defaulted — with the file, flag, or command that would disprove each claim. You should not have to take any of it on trust.

The shape of the tool answers most of the question

Sectum AI is a black-box prober. It provisions its own synthetic tenants and users, plants canary markers it generated itself, probes the endpoints you point it at, and emits an evidence pack. Two things follow structurally, not as policy:

What remains is a small, enumerable set of network paths. All of them are below.

Every destination the tool can reach

This is the whole list. The first two rows are negative space — what is never contacted, and the scaffolded default of contacting nothing at all. Every actual path below them is off in the configuration sectum-ai init writes, and each is initiated by an operator decision.

Destination Reached when What is transmitted Endpoint controlled by
Any host Sectum AI controls Never, under any configuration Nothing. There is no code path in this repository.
Nothing at all — no outbound connection The scaffold sectum-ai init writes Nothing This is the default
api.openai.com/v1/embeddings
probes/providers.py:193
detection.embedder.kind: openai Content — marker plaintext and observed window, as separate requests (why) OpenAI, or a base_url you set
api.openai.com/v1/chat/completions
probes/providers.py:223
detection.judge.kind: openai Content — observation text plus foreign-marker plaintext (why) OpenAI, or a base_url you set
api.anthropic.com/v1/messages
probes/providers.py:264
detection.judge.kind: anthropic Content — observation text plus foreign-marker plaintext (why) Anthropic, or a base_url you set
OpenAI embeddings, via the sweep or calibrate
core/embeddings.py:160
A scenario with two or more embedding_models, at least one prefixed openai: — or sectum-ai calibrate --embedder openai:<model>, which reaches the same provider with no scenario and one model (app.py:2084-2109) The synthetic corpus OpenAI. This path takes no base_url.
RFC 3161 timestamp authority, default freetsa.org/tsr
evidence/tsa.py:94
evidence.timestamper: rfc3161, or report --tsa <url> (app.py:1099-1102) A hash of a hash. No content. Any TSA set via --tsa or evidence.tsa_url
Transparency log, default rekor.sigstore.dev
evidence/rekor.py:170
evidence.rekor: true, or report --rekor (app.py:1103-1106) A digest, an ephemeral ECDSA signature, a public key. No content. Any Rekor instance you set
The system under test, your vector database, your backends
agent/http.py, rag/http.py, vector-DB adapters
You configure an adapter Probe traffic and planted markers, within the scope you set You — the endpoint is whatever your adapter config names
api.datadoghq.com (datadog.py:98), api.helicone.ai (helicone.py), api.smith.langchain.com (langsmith.py:46) Class-11 erasure scans against your own observability accounts Queries against your own account; marker matching is client-side for datadog, helicone, langsmith, langfuse and phoenix. The generic otel adapter is the exception — it POSTs {tenant, marker} to the endpoint you supply (otel.py:169). Your SaaS accounts. These hosts are defaults you can override; phoenix, langfuse and otel require you to supply one.
Vendor agent SDKs — api.openai.com, api.anthropic.com
agent/_openai_assistants_live.py:53, agent/_anthropic_tooluse_live.py:54
agent.kind: openai-assistants or anthropic-tooluse — the agent under test is the vendor's Content — probe traffic and planted marker plaintext The vendor you selected as the system under test
api.pinecone.io and *.svc.*.pinecone.io
vector/pinecone.py:64-83
vector_store.kind: pinecone. Without the optional host the index name is resolved through the control plane first; setting host skips that lookup but still reaches a *.svc.*.pinecone.io data-plane endpoint The synthetic corpus and planted markers Pinecone. The only vector kind with no local backend.
huggingface.co — model and tokenizer weights
model/_huggingface_live.py:79,82, core/embeddings.py:127
model.kind: huggingface, or an st: embedding spec, when the weights are not already cached The model identifier. No markers, no observations. Hugging Face. This is what breaks a truly air-gapped run.
OpenSearch, the endpoint you configure
build_search_index, config.py:821
search_index.kind: opensearch Query traffic and planted markers, to your own cluster You — the OpenSearch endpoint in your config
LangSmith eval-set, api.smith.langchain.com
build_eval_set, config.py:850
eval_set.kind: langsmith without api_url Eval-set records to your own LangSmith account LangSmith default host, overridable via api_url
Object storage — AWS S3 or Google Cloud Storage
build_backup, config.py:869
backup.kind: s3 or gcs The evidence-pack backup you asked it to store, to your own bucket Your cloud-storage account

This table lists every host the tool contacts on its own initiative or by built-in default, plus the vendor hosts a shipped adapter kind resolves on its own. The full inventory of adapter kinds is the eleven adapter dispatchers in config.py: the eight named here (build_vector_store, build_cache, build_model, build_mcp, build_memory, build_rag, build_observability, build_agent) plus build_search_index (config.py:821), build_eval_set (config.py:850) and build_backup (config.py:869), whose network-reaching kinds are in the table above. The remaining build_* functions aggregate these or resolve detection providers. [project.optional-dependencies] in packages/adapters/pyproject.toml covers only the kinds needing a third-party package — datadog, helicone and otel are standard-library-only, as is every fake kind and the agent/RAG http kinds. The MCP kinds are the trap: stdio and http both need the third-party mcp package and appear in the extras block under the group name mcp, not under their kind names. Adding a network call means editing this table in the same change; treat a discrepancy as a defect and report it.

Six claims and how to break them

These are the claims a reviewer should be unwilling to take on faith. Each row is a test you can run against a clean Apache 2.0 checkout, in minutes, without our cooperation and without anything routing back through us.

Claim How to falsify it
Your source code is never an input. Put the CLI behind a logging proxy and run it. Nothing reads or transmits a repository, because no adapter addresses one — adapters address running endpoints.
The scaffolded configuration makes no network calls at all. Run sectum-ai init and then the run itself on a host with egress denied, or behind mitmproxy. Pass --config sectum-ai.yaml to seed and probe. Without it, probe uses the compiled-in _DEMO_CONFIG (app.py:725) and seed uses a bare SectumConfig() (app.py:655) — both all-fake and silent, but neither is the file you just generated. Cross-check the scaffold at cli/app.py:250-336.
There is no telemetry, analytics, or phone-home in the shipped code. Grep packages/ and scripts/ for analytics, telemetry, crash-reporting, licensing and version-check patterns. Grep every pyproject.toml for a telemetry dependency. Grep for atexit, __del__, and signal handlers.
Hosted model providers are opt-in, never implicit. Read probes/providers.py:193, :223 and :264. Those are the hosted detection call sites, each reachable only when the corresponding kind is set away from its default.
Evidence anchoring sends a hash, never content. Enable anchoring behind a proxy and read the request bodies, or read evidence/tsa.py:94 and evidence/rekor.py:170 and follow what is assembled into the payload.
Verifying an evidence pack requires nothing from us. Take a pack to a disconnected machine and run sectum-ai verify. It completes offline. Two practical notes: a pack from the scaffolded configuration is unanchored, so bare verify exits 4 by design (require_anchored defaults on, app.py:1568) — pass --allow-unanchored and you get INTEGRITY OK - UNANCHORED. Verifying an anchored pack needs the rfc3161/rekor extras, which live on sectum-ai-evidence, so stage them before you disconnect the machine.

The highest-yield test is the blunt one: run the CLI behind mitmproxy or a deny-all egress policy that logs. Everything on this page is ultimately a claim about what that proxy will and will not see. File and line references point into the OSS repository at the current release; where a citation has drifted, the surrounding code is the claim.

Why the default path needs no model

sectum-ai init scaffolds every adapter, embedder and judge to kind: fake, sets evidence.timestamper: local, and leaves rekor: false (cli/app.py:250-336; config.py:126, 128, 147, 163). Concretely:

This is possible because the default detection problem is not a judgement call. A marker Sectum AI planted in tenant A either appears in a response to tenant B or it does not — a string-and-vector question that deterministic matching answers with no model in the loop. Hosted models are available to operators who want semantic detection of paraphrased or partially reconstructed markers. They are an upgrade, not a dependency, and what they cost is described below.

Nothing reaches us, under any configuration

This is the one unconditional claim on this page. Nothing in this repository — not the CLI on its own initiative, and not any script it ships — contacts a host Sectum AI controls, in any configuration, at any point in a run. Grep the whole clone for a hostname Sectum AI controls in an executable position and you will find none. The commercial Snapshot tier does include an operator-run uploader that submits a finished pack to Sectum Cloud, but that lives in the separate Cloud product, not here, and it moves data only by your explicit action with your own subscription token.

An exhaustive search of the shipped tree finds no analytics, telemetry, crash or error reporting, usage metrics, license check, activation call, or version and update ping anywhere in packages/ or scripts/. No pyproject.toml declares a telemetry dependency. The runtime packages make no such call. (The release scripts do reach the network: scripts/generate_sbom.sh and scripts/generate_package_sboms.sh run uvx, which fetches its tool from PyPI. That is build-time tooling, it ships in no distribution, and it still goes nowhere near a host we control.) There are no atexit, signal, or __del__ hooks that transmit, and no base64, exec, or eval indirection that could hide one.

The practical consequence is worth stating plainly: we cannot tell whether you have ever run the tool, how often, or against what. There is no run record on our side to hold, subpoena, breach, or sell. Subscribing to Sectum Cloud is the one way data reaches us, and it is a separate product you send to deliberately — the open-source tool on your machine never does.

What you will find when you grep for “sectum.ai”

You should grep for it, and you will get hits. They are worth explaining before you find them, because they look like the thing this section denies. Each is a static identifier written into a generated document:

They make the output documents self-describing to downstream tooling. Those four modules import no network library and never dereference the identifiers. A grep of the whole clone returns more — about 51 hits across 42 files — because the repository also carries examples, docs and README links; every one of them is an inert identifier or a piece of prose, none an executable call. Grepping packages/ and scripts/ turns up 34 hits across 29 files, in four shapes, so expect more than the list above: the evidence-format identifiers, fourteen committed JSON-Schema $ids in spec/schemas/*.json, package metadata (Homepage/Documentation entries and README links), and one comment inside the config template init writes (cli/app.py:251) — that last one sits in a module that does reach network code elsewhere, so do not read the hit as a call. Nothing is fetched from schemas.sectum.ai or docs.sectum.ai at any point in a run. A URL used as a namespace is an identifier, not a request, in the same sense that an XML namespace URI is not a fetch.

The four opt-in egress paths

Everything Sectum AI can put on a network falls into four paths. All four are operator-initiated and none of them terminate at us. Three of them carry content: hosted detection sends the marker plaintext and the observed window, the sweep sends the synthetic corpus, and the adapters send probe traffic to endpoints you named. Only the evidence anchors are hash-only. Path 1 is the one that can send a tenant's content to a third party that tenant never chose.

1. Hosted detection providers — the path that sends content to a model vendor

This is the path that matters most, and it is the one most likely to be glossed over in a vendor document. It is stated here without hedging.

Three optional providers reach a hosted model API:

Why this path exists at all

The default detector is deterministic: it decides whether a marker planted in tenant A came back in a response to tenant B by matching the marker against the text. That settles the literal case, which is most of them, and it needs no model.

It does not settle the semantic case. A RAG pipeline can summarize or paraphrase another tenant's document without reproducing the marker string anywhere in the response. The boundary has still failed, and deterministic matching will score it clean. Adjudicating that is what a judge model is for, and it is the only reason these providers exist.

That question cannot be asked about a hash. The judge is handed the entity and the response and asked whether the second reveals the first — verbatim, Entity (owned by another tenant): … then Observed text: … (probes/providers.py:153-160). Hashing either side destroys exactly the meaning the model is being asked to weigh. So the content requirement is intrinsic to the job, not an implementation shortcut we could tidy away, which is precisely why the path is off by default and why the base_url override exists: point it at a local or in-VPC model and you keep semantic detection without the transfer.

What is transmitted, and what that means against production

What is transmitted is the observation text plus the foreign-marker plaintext being tested for. It is content, not a hash. Against a purely synthetic target that content is synthetic. Against a live, commingled multi-tenant system, the observation text can contain real tenant content — that is inherent to the probe, since the observation is what the system returned when asked for something it should not have returned.

So: enabling a hosted embedder or judge for a run against production is a deliberate data-transfer decision, subject to whatever DPA, residency and processor constraints apply to you. It should be made in advance, not discovered afterwards. Two ways to keep the capability without the transfer:

Detection provider credentials are always resolved from an environment variable when the provider is constructed, and cannot be given inline: EmbedderConfig and JudgeConfig are extra="forbid" and expose only api_key_env (config.py:142-155, :158-169), resolved from the environment when the provider is constructed (config.py:1239-1257). Adapters are looser. AdapterConfig is extra="allow" (config.py:116) and its resolver accepts an inline value while discouraging it (config.py:444-468), so a pgvector dsn or a Datadog key can be pasted into the file. The enforceable rule, better than our reassurance: reject any config containing a bare dsn:, api_key:, application_key:, secret_key:, public_key:, token:, password:, access_key_id: or secret_access_key: in CI — those are the names the resolver accepts inline as credentials (config.py:444-468 and its call sites; the same helper also accepts a bare base_url: for the otel adapter, which is an endpoint rather than a secret and is safe to commit). Do that and configs stay safe to commit and to attach to a ticket.

2. The embedding-strength sweep

The sweep measures how cross-tenant retrieval leakage varies with embedding-model strength. Its OpenAI backend (core/embeddings.py:160) sends the synthetic corpus to OpenAI through the vendor SDK. Its own docstring says it is not BYOC-safe; we have left that wording in the source rather than softening it, because it is the correct warning.

To reach that call site through the sweep, two conditions must both hold: a scenario embedding_models entry prefixed openai:, and two or more models — the sweep short-circuits below that (app.py:530). The default is a single fake-deterministic entry, which satisfies neither. The sweep is not the only route to this call site, though: sectum-ai calibrate --embedder openai:… resolves the same provider directly (app.py:2084-2109) with no scenario and a single model, so treat the command as the trigger rather than the config alone. Note that unlike the detection providers, this path takes no base_url: the way to avoid it is not to configure a hosted model into the sweep.

3. Evidence anchors — a hash of a hash

Anchoring proves when a pack existed and that it has not changed since. It never carries what the pack says. Both anchors are off in the scaffolded configuration.

sectum-ai verify is fully offline. A recipient can re-check a pack on an air-gapped machine, with no call to a log and no call to us. See the evidence chain for what verification actually checks.

4. Adapters — mostly the systems you named

agent/http.py, rag/http.py and the vector-database adapters connect only to the endpoints in your configuration: the system under test, your own vector store, your own backends. Where a default exists it is local — host falls back to localhost for chroma, weaviate, qdrant and redis (config.py:536, :545, :582, :654), never to a vendor.

One vector kind is different. pinecone has no local backend and no host default (config.py:565), so with host unset the index name is resolved through Pinecone's control plane (vector/pinecone.py:64-83). That is the one discovery step in the adapter surface, and it is why the table above lists Pinecone separately.

Alongside pinecone, three more adapter kinds are exceptions, and together they are the reason this heading says “mostly”. Selecting them points the tool at a vendor rather than at something you host, so they are listed separately in the table above:

The observability adapters are worth calling out, because they carry a vendor hostname you did not type. During Class-11 erasure scans they query your own accounts to check whether planted markers survived a deletion request in your logging and tracing tier: Datadog at api.datadoghq.com (datadog.py:98) and Helicone at api.helicone.ai (helicone.py), and LangSmith at api.smith.langchain.com when its optional api_url is left unset (langsmith.py:46). Those hosts are defaults and all are overridable, for other regions or a proxy; the phoenix, langfuse and otel adapters have no default at all and require you to supply the endpoint. For Datadog, Helicone, LangSmith, Langfuse and Phoenix the adapter pulls recent records and matches locally, so the marker is never sent — the scan does not plant the thing it is looking for. The generic otel adapter is the exception: it POSTs {tenant, marker} to the OTLP-JSON query endpoint you supply (otel.py:169) and re-scans the returned spans client-side. That endpoint has no default and is one you name, but the marker plaintext does cross it. The query itself still runs against your telemetry, under your own credentials, and is subject to whatever access controls you already apply there.

Defaults are not a guard, and the gate is not global

This section exists because a reviewer who reads the source and found this themselves would be right to discount everything above.

detection.mode defaults to hosted, which is permissive. The zero-egress property of a scaffolded run is therefore a consequence of the shipped default values — fake providers, local timestamper, Rekor off — not of an always-on block. If someone edits the config and sets an embedder to openai, egress begins, and in the default mode nothing in the tool stops it. Safe defaults under a permissive mode are not a guard, and we do not describe them as one.

detection.mode: local (config.py:206-233) is the hard gate, and its predicate is narrower than the name suggests. It rejects any detection embedder or judge whose kind is not fake unless a base_url override is set. It checks that an override exists, not that the URL is local — so mode: local with base_url: https://api.openai.com/v1 loads cleanly and egresses. The locality of that target is your control, not ours; the source says so at config.py:202-204. Note too that once base_url is set a missing key falls back to the placeholder sk-local-no-auth (config.py:1256), so a typo pointing at a reachable public host does not fail closed. This is exactly why the network-level allowlist below is not optional.

Its scope is also narrow: it does not gate the embedding-strength sweep, the evidence anchors, or the adapters — those are governed by their own settings, listed in the table above. A no-egress configuration therefore has to pin all four: detection.mode: local, scenario.embedding_models to ["fake-deterministic"], evidence.timestamper: local with rekor: false, and every adapters.*.kind left at fake. It is a configuration check, not a network sandbox.

The accurate summary of the posture is: the content-bearing paths are off by default, and the detection path is hard-enforceable with detection.mode: local. We will not claim more than that, because the source does not support more than that.

What the evidence pack carries

The pack never carries a raw target response. Observation.raw_response is discarded in the runner before pack assembly and the pack schema has no observation field, so a whole response cannot be reintroduced by a configuration change. The pack holds the run ID, the scenario hash, the manifest hash, finding records, and control mappings.

One field is observation-derived and you should know about it before you grep for it. Finding.evidence_span (spec/models.py:295) carries the verbatim slice the judge cited as proof, and the SARIF, OSCAL and auditor PDF renderings quote it too. The codebase treats it as sensitive on the same footing as a raw response: it is listed in _TENANT_CONTENT_KEYS (spec/_logging.py:47), which is what keeps it out of logs above DEBUG.

On the scaffolded configuration that field holds only data Sectum AI minted. The deterministic judge cites the synthetic marker itself (evidence_span=marker.plaintext, detection.py:386), as does the erasure probe. The LoRA, tenant-boundary and KV-cache probes write a fixed tool-authored sentence instead. Either way the field holds only text Sectum AI minted. A span copied out of a real response is reachable only along the opt-in hosted-judge path above — the same decision, with the same consequence, surfacing in a second place.

That the pack can stay this thin is also why a finding stands up on very little — a finding is “synthetic marker X, provably planted in tenant A, was observed by tenant B”, which needs the marker identity rather than the surrounding text. See the evidence chain for how those hashes are bound into a tamper-evident artifact.

Running it with no egress at all

If your requirement is an enforced control rather than a good default, pin it and layer it.

detection:
  mode: local
  embedder:
    kind: fake
  judge:
    kind: fake
scenario:
  embedding_models: ["fake-deterministic"]
evidence:
  timestamper: local
  rekor: false
adapters:            # every configured family (search_index, eval_set
                     # and backup default to fake when omitted)
  vector_store:
    kind: fake
  cache:
    kind: fake
  agent:
    kind: fake
  rag:
    kind: fake
  observability:
    kind: fake
  model:
    kind: fake
  mcp:
    kind: fake
  memory:
    kind: fake

One scoping caveat, so nobody is surprised at deployment: a fully air-gapped run is a run against the synthetic in-memory substrate. Once you point the tool at a real system, the adapters must reach that system. “No egress” here means nothing leaves the network you already control — not that probing a live target requires no packets.

Confirming this without trusting us

If you are working a vendor review, this is the sequence. None of it requires our participation, and any step that contradicts this page is a finding we want.

  1. Clone the Apache 2.0 repository. Grep packages/ and scripts/ for telemetry, analytics, licensing and update-check patterns, and grep every pyproject.toml for a telemetry dependency.
  2. Grep for sectum.ai. For each hit, open the module and confirm it imports no network library: intoto.py, oscal.py, sarif.py, spec/schema.py.
  3. Grep for HTTP client imports, then read the result against the destination table — and expect a mismatch, because a top-level import is only one of two mechanisms. Nine modules hold a stdlib urllib or httpx client at import time: agent/http.py, rag/http.py, the four observability adapters (datadog.py, helicone.py, otel.py, phoenix.py), evidence/tsa.py, evidence/rekor.py and probes/providers.py. Vendor SDKs are the other mechanism, and where each is imported varies: at module level in vector/chroma.py, vector/pgvector.py, vector/weaviate.py, cache/redis.py, both mcp/ adapters and phoenix.py; inside connect() in pinecone, langfuse, langsmith and the model adapters; and inside __init__ with no connect() at all in qdrant (qdrant.py:57) and the two live agent adapters (_openai_assistants_live.py:47, _anthropic_tooluse_live.py:48). So grep for an import inside any function body, not just connect(.
  4. Read probes/providers.py:193, :223, :264 and core/embeddings.py:160. Those are the content-bearing sites in detection. Then read the adapter surface, which also carries content: agent/http.py, rag/http.py, the vector-database adapters, agent/_openai_assistants_live.py:53, agent/_anthropic_tooluse_live.py:54 and observability/otel.py:169. Satisfy yourself that no site outside the destination table sends content anywhere.
  5. Read evidence/tsa.py:94 and evidence/rekor.py:170 and confirm that only a digest is assembled into the payload.
  6. Run sectum-ai init and a full run behind mitmproxy or a logging deny-all egress policy. Confirm the proxy stays silent.
  7. Set an embedder to openai with detection.mode: local and confirm the run fails fast rather than reaching the network.
  8. Take an evidence pack to a disconnected machine and run sectum-ai verify. An artifact that only verifies when it can reach the vendor is not an audit artifact.

If any step produces a result that contradicts this page, report it through the process in the OSS repository's SECURITY.md. Undocumented outbound behaviour is a security issue, and we treat it as one.

Scope, and where the threat model picks up

This page covers the open-source sectum-ai CLI running on infrastructure you control. It describes the behaviour of the software: what it can dial out to, and under which settings.

The threat model answers the deployment-level question instead — under a commercial engagement, where the trust boundary sits between BYOC and Hosted, how the ground-truth manifest is protected, and what is out of scope for the product as a whole. The two are complementary: the threat model draws the boundary around an engagement, this page describes the behaviour of the binary inside it. Under the OSS CLI there is no runner we operate in the picture at all, which is why the strongest claim here can be unconditional in a way a hosted engagement's claims cannot be.

Both documents rest on the same commitment. Sectum AI's deliverable is tamper-evident evidence that an auditor can re-check independently, without trusting us. A product built on that premise has no business asking you to trust an opaque data path to produce it.