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:
- Your source code is not an input. There is no repository argument, no ingest step, no file walker, no upload stage anywhere in the tool. The questions a review normally puts to a code-scanning vendor — do you upload our repo, retain it, train on it — have no surface to attach to. No configuration setting changes this.
- The planted data is data Sectum AI generated. Detection is a search for markers Sectum AI minted, in tenants it created. It is not an analysis of your content.
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/embeddingsprobes/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/completionsprobes/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/messagesprobes/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 calibratecore/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/tsrevidence/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.devevidence/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 backendsagent/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.comagent/_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.iovector/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 weightsmodel/_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 configurebuild_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.combuild_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 Storagebuild_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:
-
The default embedder is
FakeEmbeddingProvider, a SHA-256 hashing-trick vector computed locally (probes/detection.py:331-354). No model, no API key, no network. -
The default judge is
FakeJudge, a deterministic token-order match (probes/detection.py:357-392). No model, no network. -
For
kind: fake,build_embedderandbuild_judgereturnNone(config.py:1299, 1309). No client object is constructed, so there is nothing that could dial out.
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:
- the in-toto
predicateTypeURI (intoto.py) - the OSCAL UUID namespace and
href(oscal.py) - the SARIF
informationUriandhelpUri(sarif.py) - the JSON Schema
$id(spec/schema.py)
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:
-
OpenAI embedder — POST
api.openai.com/v1/embeddings(probes/providers.py:193), only whendetection.embedder.kind: openai. -
OpenAI judge — POST
api.openai.com/v1/(chat/completions probes/providers.py:223), only whendetection.judge.kind: openai. -
Anthropic judge — POST
api.anthropic.com/v1/messages(probes/providers.py:264), only whendetection.judge.kind: anthropic.
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:
-
All three providers accept an operator-set
base_url, which redirects them to a local Ollama or vLLM instance or an in-VPC inference endpoint. That keeps model-backed detection inside your boundary. - Leave the defaults in place and use the deterministic local detection stack described above.
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.
- RFC 3161 timestamping —
evidence/tsa.py:94posts tohttps://freetsa.org/tsrby default, overridable with--tsaorevidence.tsa_urlif your auditor wants a specific authority or your policy forbids a public one. The payload is a DER timestamp query whose only meaningful content is the message imprint: a SHA-256 of theattested_digest, itself the canonical SHA-256 of the whole pack (chain.py:117-125). No evidence content, no findings, no canary plaintext, no manifest bytes. - Sigstore Rekor —
evidence/rekor.py:170submits the digest, an ephemeral ECDSA signature, and the corresponding public key, torekor.sigstore.devby default and overridable. Never content. Note the intent: a transparency log is a public, append-only record, so what it publishes is a commitment that a run with a given digest existed at a given time. That is precisely the property that lets a third party check your evidence without trusting either you or us.
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:
-
agent.kind: openai-assistantsandanthropic-toolusedrive the vendor's own agent as the system under test (agent/_openai_assistants_live.py:53,agent/_anthropic_tooluse_live.py:54). This carries probe traffic and planted marker plaintext to that vendor — the heaviest payload on this page after the hosted judge. -
model.kind: huggingface, and anst:embedding spec, fetch model and tokenizer weights fromhuggingface.cowhen they are not already cached (model/_huggingface_live.py:79,82,core/embeddings.py:127). Only the model identifier leaves, but this is the thing that breaks an otherwise air-gapped run.
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 -
Ship that configuration to your runners and treat a change to
detection.mode,scenario.embedding_models,evidence.*or anyadapters.*.kindas a reviewable event — and pin the CLI invocation too, sinceseed --embedding-model openai:<model>overridesscenario.embedding_modelsand never appears in a config diff (app.py:658-663). It is a control you can evidence to an auditor by diff. -
If you want model-backed detection quality, set
base_urlto in-VPC inference rather than relaxing the mode. - Enforce a host-level or network-level egress allowlist as well. A configuration check inside a tool is one control; it should not be your only one. Under the scaffolded configuration a deny-all egress policy changes nothing about the outcome, which is the cheapest way to confirm this page.
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.
-
Clone the Apache 2.0 repository. Grep
packages/andscripts/for telemetry, analytics, licensing and update-check patterns, and grep everypyproject.tomlfor a telemetry dependency. -
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. -
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
urlliborhttpxclient 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.pyandprobes/providers.py. Vendor SDKs are the other mechanism, and where each is imported varies: at module level invector/chroma.py,vector/pgvector.py,vector/weaviate.py,cache/redis.py, bothmcp/adapters andphoenix.py; insideconnect()inpinecone,langfuse,langsmithand the model adapters; and inside__init__with noconnect()at all inqdrant(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 justconnect(. -
Read
probes/providers.py:193,:223,:264andcore/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:54andobservability/otel.py:169. Satisfy yourself that no site outside the destination table sends content anywhere. -
Read
evidence/tsa.py:94andevidence/rekor.py:170and confirm that only a digest is assembled into the payload. -
Run
sectum-ai initand a full run behindmitmproxyor a logging deny-all egress policy. Confirm the proxy stays silent. -
Set an embedder to
openaiwithdetection.mode: localand confirm the run fails fast rather than reaching the network. -
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.