Nothing leaves your network that you did not configure.
And here is how to prove that rather than believe it.
On this page
What crosses the boundary, and what does not.
Governed payloads leave your network only for the model and tool providers you configure, after policy and redaction
- Prompts, completions and tool arguments
- The trace database and the audit chain
- Your provider API keys
- The registry, the policies and every decision
- Approvals, and who granted them
- Governed payloads to your model providers, after policy and redaction
- Tool calls to the MCP servers you pinned
- Webhook events to your own receiver
- Evidence to the SIEM you connected
- Audit anchors to the destination you chose
No telemetry, no phone-home, no licence callback, no copy of your traces. The connection to your stack points inwards, so the worst a compromised deployment can do to your SIEM is stop sending to it.
It is verifiable rather than asserted: block egress to everything except the providers you configured and watch it keep working.
How to verify the vendor receives nothing.
A procedure rather than an assurance. It takes a few minutes, needs no cooperation from anybody here, and produces a result you can put in a file. What it establishes is not that a setting is switched off: there is no code in the product that could send your data, so this is how you confirm that for yourself rather than take it on trust.
- 01List every outbound call site. Run grep -rn "fetch(" packages/server/src and read each hit. What matters is not the count but where each destination comes from: the OpenAI, Anthropic, Google, Bedrock and Azure provider clients, the embeddings path, the MCP upstream client, the webhook publisher, the OIDC client in auth/oidc.ts, the anchor sink publisher in audit/anchoring.ts, and any vendor pull connector you have explicitly enabled, all take their URL from operator-supplied configuration — the issuer you named, the sink you named, the provider you registered. The single exception is the model-price catalogue, whose URL is openrouter.ai and which runs only when an admin invokes a price sync.
- 02Account for the one network client that is not a fetch. The event publisher contains an SMTP module for emailing approval notifications, and the composition root never configures it, so it opens no socket in this release. It is disclosed rather than left for you to find, because a reviewer auditing the source for network clients will find it, and because presence of a primitive is not presence of a feature.
- 03List every hard-coded URL. Run grep -rn "https\?://" packages/server/src packages/core/src. The non-example, non-comment hits are the provider base URLs used as seed and onboarding defaults, the Bedrock endpoint derived from a region, the price-catalogue URL, a local demo MCP URL, two schema identifiers embedded as literal strings in the Microsoft Teams card payload which are sent to your Teams receiver and never fetched, and the OpenRouter attribution header naming the project’s public repository. That header is the only vendor-owned string in the list: it is a header value rather than a destination, it goes to OpenRouter rather than to the vendor, and it is disclosed in the data-flow document. No vendor endpoint is contacted, because none exists.
- 04Watch it. Run Token Observe with egress allowed only to your model providers, your tool servers and your webhook receivers, and confirm nothing is blocked. A delivered image can run fully air-gapped and the dashboard bundles no CDN assets; building that image from source still requires access to the configured base image, the Debian archives and the npm registry.
- 05Check the dependency list. packages/core, which contains the entire governance domain, has zero runtime dependencies, so the code that decides allow or block can be read end to end without standing up any infrastructure. The server’s runtime dependencies are deliberately few, and the CycloneDX SBOM attached to each release lists the complete transitive set, alongside a SHA256SUMS file covering every release artifact.
Nothing leaves your network that you did not configure
Egress from Token Observe is, in total, six destinations, one of which is an operator-triggered lookup rather than anything the request path initiates on its own, and every one of them is an endpoint somebody in your organisation configured: the model providers you register, the MCP tool servers you register, the webhook receivers you register, the public model-price catalogue when an admin invokes a price sync, your OIDC issuer if you configure single sign-on, and your anchor sink if you switch audit anchoring on. There is no default egress, and the last two do not exist at all until the corresponding feature is switched on.
The consequence is testable rather than rhetorical. If your network policy allows outbound connections only to your model providers, your tool servers and your webhook receivers, Token Observe functions completely — adding your identity provider and your sink host only if you enabled them. A delivered image can run fully air-gapped and the dashboard bundles no CDN assets; an air-gapped install never calls the price catalogue at all and loads rates through POST /api/prices instead.
Egress is allowlisted, not merely configurable, and the reason is specific to what this process holds. A provider row names both a URL and an environment variable, and Token Observe resolves that variable and sends its value to that URL as a bearer credential — so an unconstrained registry write would be equivalent to reading every secret in the process, in the one process that deliberately concentrates every provider key in the estate. The MCP case is worse than the provider case and is guarded harder for that reason: an MCP server row names a variable whose value is sent raw as an Authorization header to a URL in the same row, it is writable at operator rather than admin rank, and the outbound call is triggered by any viewer listing that server’s tools. Unguarded, it would read any variable in the process and post it anywhere.
So there are four allowlist pairs rather than one — providers, MCP servers, webhook receivers, and vendor organisation-admin credentials — each pair naming which hosts may be registered and which environment-variable prefixes may be referenced. MCP hosts default to loopback only, because a sidecar is the one topology safe to assume and “somewhere on your network” and “somewhere on the internet” are the same string to this process. Webhook hosts have no safe default at all, because a webhook destination is always third-party, so an unstated policy resolves to the existing rows alone. The credential prefixes default to purpose-specific names such as AZURE_OPENAI_ and AWS_BEDROCK_, with bare AZURE_ and AWS_ deliberately absent so that a host’s ambient cloud credentials, AWS_SECRET_ACCESS_KEY included, stay unnameable from a provider row.
One primitive is disclosed here because it is not on the destination list and a reviewer auditing the source for network clients will find it: the event publisher contains an SMTP module for emailing approval notifications, and the composition root never configures it, so this release opens no mail socket. Presence of a primitive is not presence of a feature, and stating that is cheaper than being asked about it in week three of a security review.
- Model providers
- The governed request after unicode sanitisation and after the redaction plan has been applied, plus your API key read from the process environment. OpenAI, Anthropic, Google, OpenRouter, Bedrock, Azure, or any OpenAI-compatible endpoint you register, including a self-hosted one.
- MCP tool servers
- The tool name and its arguments, after policy evaluation and after inbound redaction, with the per-server auth header read from the environment. On every allowed tools/call, and on catalogue refresh.
- Webhook receivers
- A JSON event carrying identifiers, counts, policy names and a one-line human summary, signed with x-acp-signature when a secret is configured; Slack receivers get the summary text only. Sent best-effort on a later tick, because a webhook can never delay or fail the governed request that produced it. Treat receivers as inside your data boundary: an approval.requested event on the proposed-tool-call path carries the bounded excerpt described below.
- The model-price catalogue
- A GET for the public OpenRouter price list, sending no prompt, no trace and no identifier — with one caveat that is stated rather than rounded off: it does send your OpenRouter key if one is configured, for rate-limit purposes. It runs only when an admin calls POST /api/prices/sync. The call also carries HTTP-Referer and X-Title attribution headers, which identify the gateway software and name the project’s public repository; they do not identify the agent, the user or your organisation, and they reach OpenRouter rather than the vendor.
- Your identity provider
- The standard Authorization Code with PKCE exchange — the issuer’s discovery document and JWKS, then a POST to its token endpoint. No prompt, no trace, no agent, no payload; this flow is about one human signing in, and an install that never configures OIDC makes no such call.
- Your anchor sink
- One signed statement per anchoring interval about the shape of the audit chain: head sequence and hash, entries covered, the previous anchor’s hash, the key id and the signature. No payload, no identifier and no personal data. A file sink writes the same statement to local disk and makes no network call at all.
# The single highest-value hardening step for a deployment whose model
# endpoints are not the public vendor ones. Neither list may be empty:
# an empty allowlist would mean "any host" and "any variable".
ACP_ALLOWED_PROVIDER_HOSTS=llm.internal.example.com
ACP_ALLOWED_KEY_ENV_PREFIXES=ACP_PROVIDER_
ACP_ALLOWED_MCP_HOSTS=tools.internal.example.com # default: loopback only
ACP_ALLOWED_MCP_KEY_ENV_PREFIXES=ACP_MCP_
ACP_ALLOWED_WEBHOOK_HOSTS=siem.internal.example.com # no safe default exists
ACP_ALLOWED_WEBHOOK_KEY_ENV_PREFIXES=ACP_WEBHOOK_
# The credential buckets are disjoint by design: a name that a provider
# row may reference is not one an MCP row or a webhook row may reference.How to check that, rather than take it on trust
The claim that the vendor receives nothing takes about five minutes to confirm against source you are licensed to read: list every outbound call site, list every hard-coded URL, run it with egress allowed only to your own endpoints, and check the dependency list. The full procedure is set out below, and the licence exists to let you run it — section 3.3 says in terms that the thirty-day evaluation is there so that a prospective customer’s security team can read, run and attack the software before a purchase order is raised.
Section 9 of the licence goes further than permission. You, and anyone operating an evaluation, may inspect, test, fuzz, penetration-test and reverse-engineer the software as deployed on infrastructure you control, and may commission a third party to do it. You may publish performance results, provided the publication names the version and configuration tested, and no pre-approval of benchmark results is required. You may publish security assessment findings and name the software, after coordinated disclosure. There is no gag clause. A pre-purchase penetration test by a prospective customer is welcome, and the published defect list and threat model are the recommended starting point precisely so the test is not spent re-finding what is already known.
Three artefacts ship with every release so that verification does not depend on a conversation: a CycloneDX SBOM listing every dependency and version, a SHA256SUMS file covering every release artifact, and the full CI gate run again on the release tag — format check, lint, typecheck, the unit and integration suites, a full build, a boot-and-exercise smoke suite, npm audit at high, a gitleaks secret scan over the whole history, and a scan of the exact packaged image that refuses every HIGH or CRITICAL operating-system, runtime or library finding, fixed or unfixed, with no implicit allowlist.
The dependency position is a security position, not a taste. Runtime dependencies are kept deliberately few because every addition is supply-chain surface in a product that gets deployed air-gapped. The governance domain — the code that decides allow or block — lives in packages/core, which has zero runtime dependencies, so it can be read end to end without standing up any infrastructure at all.
- The evaluation licence
- Thirty days from first installation, for internal evaluation, security review and proof of concept. It must not be used to process live production traffic or regulated personal data, and it carries no support commitment, no warranty and no service level of any kind.
- What you may publish
- Benchmark results, naming the version and configuration, with no pre-approval. Security assessment findings, naming the software, after coordinated disclosure. The licence records that the maintainers already publish their own outstanding defects, and says section 9 is intended to be consistent with that practice rather than to suppress it.
- Where to start a review
- The published known-issues list and the STRIDE threat model. The first names the file, states the concrete cost and describes the attack that works; the second names the residual risk on every trust boundary and the role that has to accept it.
- The status of the licence itself
- Stated here because the page cites it repeatedly. The published licence carries its own banner: it was drafted by the engineering team rather than by qualified counsel, placeholders remain in it, and it asks to be treated as a statement of intended commercial terms rather than as an executed grant of rights until that review is complete. The clauses quoted on this page are quoted as that stated position and not as an executed one. The product documentation is likewise expressly not a warranty or a contractual specification unless an order form makes it one, so what binds is the agreement you sign rather than this page or the repository.
- Licence-compliance verification
- Self-certification only, and by construction: because the software reports nothing to the licensor, there is no visibility of your usage. One written certification per twelve months on thirty days’ notice is permitted; there is no right to inspect your systems, to require a metering component, or to receive your records.
What is kept, where it is kept, and what is deliberately not kept
One configured database holds the whole persistent state: normally the SQLite file at ACP_DB_PATH in WAL mode, or an explicitly configured PostgreSQL evaluation backend. There is no second datastore, no cache tier, no object store and no external message broker, so backing up the configured database backs up everything durable — and a SQLite deployment must also keep that backup on a different volume, because there is no vendor-side copy to fall back on.
The most sensitive table in the system is trace_events, because prompts and tool arguments are where unplanned personal data accumulates. What it actually holds is bounded: a post-redaction prompt excerpt capped at 4,000 characters, MCP tool arguments and results capped at 16,000, the policy decisions, the redaction records as kinds and counts rather than values, and response metadata — stop reason, token counts and content-block types. The excerpt is read back off the outbound payload rather than off the original, so the search index cannot contain what the redactor just removed.
One caveat is stated here rather than glossed, because it is the single place raw model output is persisted. When a policy requires approval for a tool call the model proposed, the approval’s action summary is built as the tool name plus up to 160 characters of that proposal’s arguments, and that happens before egress redaction runs on the response. So an approval record — and the approval.requested webhook event built from it — can contain up to 160 characters of unredacted text the model generated. It cannot contain unredacted text from the prompt, because the request was redacted before it reached the provider, and approvals raised on the MCP gateway and on the request path carry no arguments at all, only the tool name or the policy reason. Treat the approvals table and the approval webhook at the same sensitivity as trace content.
What is deliberately not persisted matters as much as what is. Model response text is not stored — the response event records the stop reason, upstream request id, token counts and content-block types, not the text. The full prompt is not stored, only the bounded post-redaction excerpt. No API key exists in the database in any form; provider keys, MCP auth headers and webhook signing secrets are referenced by environment-variable name and the value lives in the process environment and nowhere else. Agent tokens are held as SHA-256 with a display prefix, and the plaintext exists once, in the response that minted it. OIDC ID tokens are verified and read but not stored, and no offline scope is requested and no refresh token held, so nothing persists that could be replayed against your identity provider. Request bodies, query strings and headers never reach the logs, at any log level.
The most interesting of those refusals is the smallest. When a caller presents a credential Token Observe does not recognise, the rejection is counted in an hourly roll-up — the reason and the count — and the presented token is not stored, and neither is a digest of it. A digest would buy nothing defensively, because an unknown credential resolves to no key in the database by definition and there is nothing to compare it against. What it would create is an offline oracle against a secret that may well be live somewhere else in your estate: a mistyped production key from another system, a credential pasted into the wrong base URL. Anyone who could read that table could then confirm guesses at that secret at their leisure, against a copy that outlives the mistake. A governance product must not turn someone else’s typo into a durable cracking target.
Encryption is stated plainly, because this is where reviewers find surprises. In transit to providers, tool servers and webhooks it is HTTPS as configured by the endpoint URL. Inbound, Token Observe speaks plain HTTP and expects you to terminate TLS in front of it; it does not manage certificates. At rest, the database file is not encrypted by Token Observe — encryption at rest is whatever your volume or host provides. Field-level protection is real but partial: passwords are scrypt-hashed with a per-user salt, agent tokens are SHA-256, temporary Effect recovery envelopes are AES-256-GCM under a key derived from an external secret that is never stored, and prompt excerpts and ordinary tool arguments in trace_events remain plaintext JSON.
- seats
- The only table whose primary subject is a human other than an operator: a named employee and the subscription tool they hold a seat on. It supports a coverage census, so it is a list of who is and is not governed. Trace retention does not cover it and subject erasure does not reach it.
- user_idp_groups
- The directory groups your identity provider asserted about one named person at their last sign-in. That is an attribute of a person, held about a person, and a data protection officer should treat it exactly as they treat HR-adjacent data rather than as configuration. Capped at 500 keys per person, replaced wholesale at each sign-in, and aged out on its own window.
- radar_findings
- Detector output from evidence you supplied, which may include workstation hostnames, staff usernames, service-account and key identifiers, billing amounts, and — where an operator supplies per-user seat spend — the email addresses of named employees. Retained indefinitely; neither the retention window nor subject erasure reaches it.
- webhook_deliveries
- The exact bytes POSTed to each receiver, kept so a signature dispute can be settled, and not purged.
- audit_anchors
- No payload, no identifier and no personal data — a statement about the shape of the chain rather than about anything in it. This is the one table meant to be handed to an outside auditor.
What is enforced inline, and what that enforcement is worth
Token Observe fails closed for policy decisions: the governance evaluator returns allow only when every gate passes, and missing, unresolvable or erroring state denies the request. The kill switch is evaluated first and beats everything else, which is the literal implementation of the EU AI Act Article 14(4)(e) stop capability. The trade is stated rather than hidden — because governance is inline, Token Observe is a single point of failure in the agent request path, and if it is unavailable the fleet stops. That is intentional: a control you can bypass by turning it off is not a control.
Redaction runs inline, in-process, before the request leaves — not asynchronously and not after the fact. Detection covers credit card with Luhn validation, IBAN with mod-97, UK NHS number with mod-11, US Social Security number, UK national insurance number, email and phone, plus the secret kinds: JWTs, AWS access keys, prefixed vendor API keys and PEM private-key headers. Secret kinds are always masked irreversibly and never tokenised, whatever a policy’s mode says, because a reversible placeholder for a credential is a credential. Other kinds are masked or tokenised per policy, and a tokenised value keeps a stable placeholder across one conversation so the model can still reason about “the same customer”.
It runs in both directions: responses are scanned too, so a data-class rule fires on an identifier the model produced even though nobody sent one. Streaming is covered by a hold-back buffer with a floor of 64 characters and a separate buffer for each tool-call argument channel, so a value split across two chunks is still caught, and the response-side data-class decision is taken before the first byte, because a stream has no later enforcement point.
The limit sits beside the claim. Matching is regex plus checksum, so free-text personal data and non-UK/US identifier formats are not detected at all, and the confidence scores are published rather than hidden so a policy can set a threshold: checksum-validated kinds score 0.9 to 0.98, national insurance and Social Security numbers 0.85, and phone 0.7. Token Observe is a compensating control, not a complete DLP, and it should not be bought as one.
Authority is deny-by-default and narrows at every hop. Agent RBAC is explicit-deny-wins with action-level tool scoping; /v1/models is filtered against the registry, and tools/call re-checks grants independently of tools/list. Delegation intersects permissions at each hop, so a forged delegation chain can only narrow authority, never widen it. Approvals are bound to a payload hash, are single-use, and expire — with the honest residual that an approver who skims the action summary approves what they were shown.
Failover is typed, because the alternative launders a refusal into a success. A 429 or a timeout fails over; a content-policy refusal, an auth failure, an invalid request and a context-length error do not. Per-agent data policy — zero data retention, no training on payloads, a required serving region — is enforced on the fallback chain as well as on the primary route, and when no route satisfies it the request fails closed with ACP_ZDR_UNAVAILABLE rather than quietly downgrading. Those provider flags are operator-asserted and unverified: setting them records your assertion about your contract with that provider, which Token Observe cannot check, and the assertion itself is audited with the actor who set it.
Every policy can run in shadow mode first, so you learn your false-positive rate before you start blocking real work. A stricter option exists and is off by default: with backtest-before-enforce switched on, the transition into enforcement is refused until a backtest of that exact rule — digested over its trigger, action, scope and priority — has been run and acknowledged by a named person, whose name and the accepted figures are copied into the audit entry. It is a process control rather than a technical one, and it is described as such.
- Injection scanning on both channels
- Unicode is sanitised to a fixpoint before anything reads the payload, then prompts and tool results are scored, with tool-result findings weighted 1.25× because indirect injection actually arrives there. The residual is stated: these are regex heuristics, and paraphrase, translation or non-English phrasing defeats them. Sanitisation is a separate control with a separate limit — the unicode tag block, zero-width characters, bidirectional overrides and private-use ranges are stripped before storage and display, so ASCII smuggling cannot hide instructions from a human approver, but homoglyph substitution is not addressed at all.
- MCP tool integrity pinning
- A descriptor hash over the canonicalised name, description and input schema, re-verified at the 60-second catalogue TTL and on live-listener maintenance; drift quarantines the tool and emits an event. The residual is the window: a change may be served from the trusted snapshot until the next refresh, at most 60 seconds on access.
- The console is a trust boundary of its own
- Every response carries nosniff, X-Frame-Options DENY and Referrer-Policy no-referrer, and console responses add a same-origin content security policy. A test asserts that no http://, https:// or * can ever appear in it, because a well-meant font CDN is exactly how a narrow policy stops being one. The directive that matters most is frame-ancestors none: without it, a page on any origin can frame this console and land a click on Engage kill switch.
- Budgets and rate limits
- Per-request, hourly, daily and monthly ceilings checked pre-flight, with USD admission atomically reading projected spend and reserving per agent. The residual is arithmetic rather than architectural: a provider-reported final cost can exceed the pre-flight estimate, and the token and request windows are not distributed admission counters.
- Trace search never generates SQL
- The model emits a schema-validated filter object, never SQL; queries are parameterised; and the signed-in user’s team predicate is applied after translation, so it cannot be supplied or removed by the model.
403 ACP_KILL_SWITCH_ENGAGED evaluated first; beats every other gate
403 ACP_AGENT_NOT_ACTIVE lifecycle state; step 6, right after the switch
403 ACP_RBAC_DENIED deny-by-default; explicit deny wins
403 ACP_POLICY_BLOCKED a policy matched and its action was block
403 ACP_DELEGATION_DENIED the chain could only narrow, and it did
403 ACP_ZDR_UNAVAILABLE no route satisfies this agent's data policy
429 ACP_BUDGET_EXCEEDED pre-flight admission, not post-hoc billing
429 ACP_RATE_LIMITED carries Retry-After
503 ACP_AUDIT_UNAVAILABLE verification failed; writes latchedWho can read the evidence, and how that boundary is actually held
Control-plane access is a named human with a server-side session, deny by default, at one of four ranks — admin, operator, auditor, viewer — and evidence reads are additionally bounded by per-user team scopes that a caller cannot widen with a query parameter. There is no admin bearer token: an earlier revision of the API contract documented one that was never implemented, and the claim was removed rather than the feature added, because every control-plane action being attributable to a named person is what makes the audit log evidence.
The scope is derived from the signed-in user by the server and applied inside the trace and usage-store queries, then repeated on detail, search, export, compliance, spend, governance, approvals, savings and seat-registry paths — because scoping the list alone leaves the detail URL as a way around the boundary. Surfaces that cannot be honestly projected onto one team return 403 to a scoped user rather than a partial answer: the audit chain, the anchors, the retention controls, the shadow-AI radar, the seat census and the fleet summary. Sensitive trace list, search and detail reads append attributable audit events of their own, and demoting an admin or operator to auditor or viewer without an explicit scope drops any inherited wildcard to the empty scope, so a role reduction cannot accidentally preserve full-corpus access.
Two smaller decisions are worth a procurement reviewer’s attention. Authorisation is checked before existence, so probing identifiers cannot distinguish a real account from an absent one. And accounts are disabled rather than deleted — there is no delete endpoint for a user — so the audit trail keeps resolving the actor, while a patch that would leave the install with no enabled admin is refused outright.
Single sign-on is Authorization Code with PKCE, verified against the issuer’s JWKS, with the ID token read and discarded. Setting ACP_LOCAL_LOGIN_ENABLED=false makes OIDC the only login path and refuses to boot unless the OIDC configuration is complete, which is the mode the local technical-readiness check requires. The directory group snapshot is capped at 500 keys per person and replaced wholesale at each sign-in — a group the provider stopped sending has been revoked, and merging would hide that — and it ages out on its own window, 24 hours by default. Provider-specific caveats are surfaced by a status endpoint rather than discovered in production: Entra’s group overage, Okta’s extra scope, and the fact that Google Workspace emits no group claim at all.
SCIM is a bounded Users lifecycle, not a directory mirror, and it is off until a bearer token is configured. Every SCIM-created account is a viewer with no evidence scopes, and SCIM cannot set or change roles, evidence scopes or passwords. It may disable a higher-authority row, but only a named admin can rename or reactivate one — which is what stops email-first joining from turning the provisioning credential into the role or scopes already held by an unbound account. Setting a user inactive revokes all of their sessions before the disabled row is written, and disabling the final active administrator is refused. Bodies are capped at 64 KiB and 32 nesting levels; the token is at least 32 bearer characters, is never logged, and is compared through fixed-length SHA-256 digests with a timing-safe comparison. There are no Groups, no live directory read and no hard delete.
Local passwords are scrypt with a per-user salt behind two independent throttles — a per-IP budget as the CPU shield, deliberately generous so a shared office address is not locked out and not reset by a successful sign-in, and a per-account budget as the credential-stuffing shield, cleared on success so it counts consecutive failures. Local accounts have no MFA. That is the residual risk recorded below, and it is why the recommended posture is single sign-on with local login disabled.
- Two role universes, kept apart
- A user’s control-plane rank is only ever set by an admin. Roles mapped from identity-provider groups are a permission mask used on the agent path alone. No directory group can promote anyone in the console.
- The on-behalf-of mask
- Two of the three enforcement settings refuse nothing: off is the default and reads no principal at all, and shadow resolves and records what it would have refused while letting the request through. Only enforce is a control; the other two are instrumentation, and an install that has not reached enforce should not describe the intersection as a mitigation it holds.
- Sessions
- Server-side rows with a 24-hour expiry that survive a restart, so revocation is real: an admin can drop every session for a user, and a password change drops that user’s other sessions. Expired rows are removed by an hourly pass even if the cookie is never presented again.
- /metrics
- Unauthenticated by Prometheus convention, and it exposes agent identifiers and month-to-date spend, so it must be reachable only from your monitoring network. Only the matched route pattern is ever used as a label, so a caller cannot write arbitrary text into the scrape output and cardinality is bounded by construction.
The evidence chain, and precisely what each layer proves
The audit chain is a hash-chained record of governance-plane changes — actor, action, resource, detail — and it is tamper-evident, not tamper-proof. Which guarantee you actually hold depends on one setting, and every export says which: the protection field reads unkeyed or keyed, and the difference is not cosmetic.
Unkeyed is the default, and what every deployment that has not set an audit HMAC key gets. The chain is hashed with an unkeyed SHA-256 and anchored nowhere outside the database, so an operator with write access to the database file can rewrite an entry and recompute every downstream hash, and verification will report valid: true. That is not a theoretical concession — a test does exactly that on a default install, deliberately, so the limit is a tested fact rather than a caveat in prose, and the same test asserts that the forgery fails once a key is configured, so neither half of the claim can drift. What unkeyed offers is tamper-evidence against alteration that does not also recompute the chain, which is genuine integrity verification and should not be presented to an auditor as more.
Keyed protection makes entry digests HMAC-SHA256 under an external key, and seals the chain head at every boot with a checkpoint MAC. Rewriting an entry now needs the key as well as database access. Entries written before the key was configured cannot be re-MAC’d — doing so is the very act being prevented — so they are covered instead by the checkpoint over the head they reached, and altering any of them moves that head. First enablement is a deliberate two-boot ceremony under an external one-shot flag, and rotation is a second one; every ordinary keyed boot refuses a missing checkpoint schedule even when the rows form a valid plain-SHA chain, which is what stops a database writer deleting all the checkpoints, rehashing plain, and asking the product to seal the downgrade as though it were first enablement. The key ring is finite by design: keep the whole ordered set for as long as the live database or any backup may need to verify, to a maximum of 64 predecessors, and understand that dropping the oldest key is not rollover — it makes retained checkpoints unverifiable.
Four qualifications belong in front of an auditor with the keyed claim, and they are published rather than left to be discovered. It holds only if the key is injected from a KMS or secret manager the database administrator cannot read; a key in a file beside the database buys nothing. An attacker with host access reads the key out of the process environment and is out of scope, because that is a host compromise and no in-database scheme survives one. The checkpoint vouches for pre-key history only from the moment the key was introduced, and cannot say whether that history was already honest. And rotation needs the complete ordered predecessor ring plus the ceremony, because during an intentionally authorised transition the database alone cannot distinguish a real hand-off from selective checkpoint deletion plus a tail forged with the previous key.
Anchoring exists because a MAC has a structural problem as evidence: the key that verifies the chain is the key that can forge it. Hand it to an auditor and you have handed them the power to fabricate the very record they were given it to check — so they cannot verify independently, and you cannot prove you did not. Ed25519 splits those powers. With a signing key configured, Token Observe periodically signs a statement of the chain head and appends it to a table chained anchor-to-anchor, publishing each one to a file sink, a URL sink, or both, because an anchor that never leaves the database it attests is only as durable as that database. The private half never leaves the signer port.
The exact claim is one sentence, and it is the only sentence anchoring supports: any copy of the anchors you kept off-box beats any rewrite made after you took it. That is narrow and it is checkable. It does not say the history beneath the first anchor was honest, that the signer’s clock was truthful, or that someone holding the signing key could not have produced the same file. Anchoring is also off unless the key is set, so an install that has not configured it does not hold this property at all.
What anchoring refuses to do is the feature rather than a gap in it. It refuses to sign when the chain does not verify, when the head has moved backwards, and when an already-anchored entry no longer matches the hash that was attested — because signing over a forged head would launder the rewrite under a key the auditor was told to trust, whereas not signing leaves the previous anchor standing, and that anchor still contradicts the rewrite. Refusing is strictly better than proceeding. Signing-key rotation is handled the same way: the first anchor under a new key embeds a transition statement signed by both the retiring and the incoming key, binding the installation id, sequence, previous-anchor link, attested head, entries covered, protection state, asserted time, cadence and both key ids, so the hand-off is independently checkable without rewriting an old anchor or trusting a key carried beside the evidence.
Evidence exports are digest-sealed, not signed, and the distinction is load-bearing. A trace export and a compliance bundle carry a SHA-256 the recipient recomputes over the canonical JSON of the bundle, plus the embedded chain-verification verdict — valid, entries checked, the sequence any break was located at, the protection mode and the checkpoint it was verified against. The unkeyed export digest detects accidental or post-export edits but does not prove origin; provenance comes from authenticated delivery and, for the ledger head, from keyed audit plus an independently held Ed25519 anchor. Separately, an intrinsic verification failure at runtime latches readiness and audit and domain writes unavailable until restart, later governed requests receive ACP_AUDIT_UNAVAILABLE, and the detecting response does not append an audit row onto the chain it has just found unsafe. That latch is monotonic, restart does not clear it, and there is no online endpoint to clear it.
- Edit a row without re-hashing
- Detected either way, and located to an exact sequence number.
- Delete a middle row
- Detected either way.
- Delete the tail and stop logging
- Ordinary SQL deletion is detected, because SQLite does not lower the sequence high-water mark on DELETE — but a malicious database writer can lower that local row too, so only a previously recorded off-box head or anchor proves a stopped tail against that actor.
- Rewrite the entry and recompute the whole chain
- Undetected on the unkeyed default. Detected keyed, because the digests are MACs and cannot be recomputed without the key.
- Rewrite an entry written before the key existed
- Undetected unkeyed. Detected keyed, because altering it moves the head the boot checkpoint sealed.
- Read the key out of the host’s environment
- Undetected either way, and explicitly out of scope. That is a host compromise, and no in-database scheme survives one.
# Once, at key generation. The private half is what the deployment holds
# and belongs in your KMS; the public half is all an auditor ever needs.
openssl genpkey -algorithm ed25519 -out anchor.pem
openssl pkey -in anchor.pem -pubout -out anchor.pub
# Months later. No install, no database, no network. The verifier imports
# only the zero-dependency core and Node built-ins, loads nothing native
# and touches no database.
node scripts/verify-anchor.mjs --anchors anchors.jsonl --public-key @anchor.pub
# After one rotation, retired keys newest to oldest:
node scripts/verify-anchor.mjs --anchors anchors.jsonl \
--public-key @anchor-new.pub \
--previous-public-key @anchor-old.pub
# exit 0 trusted, 1 not trusted, 2 usage error. --json for a pipeline.
# --public-key is REQUIRED and must arrive out of band: a signature
# checked against a key read out of the artefact being verified proves
# nothing at all. --allow-partial is required before a file that does
# not start at anchor 1 is accepted, because a chain beginning at 4 is
# a deletion rather than a shorter file.The half of the posture you own, because you run it
Token Observe speaks plain HTTP inbound, does not manage certificates, does not encrypt the database file, and holds no custody of the keys that make its own evidence worth anything — so terminating TLS in front of it, encrypting the volume, keeping the audit HMAC key in a secret manager the database administrator cannot read, and putting the anchor sink somewhere the party running the database cannot rewrite are your controls rather than the product’s. Each of them is named here rather than assumed, because a shared-responsibility boundary discovered during an incident is the worst possible time to discover it.
The same property that removes the data processing agreement cuts both ways, and the vendor says so: if the database file is lost and there is no backup, the traces and the audit chain are gone, because there is no vendor-side copy. Back it up like evidence — to storage the operators of the database cannot rewrite — and check that the chain still verifies after a restore, before anything else.
Retention is implemented, and the default is to keep everything. That default is a decision you have to make rather than one you can inherit: the trace retention window is unset by default and unset means keep forever, which over-satisfies the six-month minimum in EU AI Act Article 26(6) and satisfies nothing in GDPR Article 5(1)(e), so a GDPR-regulated deployment must set it. The default is deliberate in both directions — retention has to be decided rather than assumed, and an upgrade that silently began deleting a customer’s evidence would be the worse failure. Once set, an hourly pass ages traces out in small batches, each its own short transaction, so a purge interleaves with gateway traffic instead of stalling the inline, fail-closed request path.
Read this before quoting a retention period to a data protection officer, because they will ask. The window and the erasure endpoint act on traces, trace events, the search index and trace scores, and nothing else. The audit log is never touched, by design, which is what lets the record of a deletion outlive the deleted data and why erasure is safe to offer at all. Approvals are not purged and cannot be erased by subject, which makes them the one place payload-adjacent text survives a purge that removed the trace it came from. Radar findings are not purged and cannot be erased by subject, and they may carry staff usernames and workstation hostnames. Webhook deliveries keep the exact bytes POSTed. Identity-provider group snapshots age out on their own authority-aligned window, and the rejected-caller roll-up purges on a 90-day window of its own, so quoting the trace window for it is wrong in both directions.
Erasure itself is audited on both halves. A dry run returns the count before anything is removed, and both the preview and the deletion write an audit entry — the preview because reading the prompt corpus for a named person is itself an act worth recording. The entry carries a SHA-256 digest of the subject identifier rather than the identifier, along with the trace ids removed: enough to tie the entry to the export that preceded it, without the erasure record becoming a fresh copy of the thing just erased.
And the backup boundary is stated precisely rather than rounded off. Those controls act on the live primary database only. A retained full-snapshot backup necessarily preserves whatever data existed when it was taken; restoring a pre-erasure snapshot can resurrect erased traces and can also lose the audit row that recorded the later erasure. There is no external deletion-tombstone ledger and no automatic reapplication of deletions after recovery. Until a separately approved backup-retention window, destruction evidence when that window closes, and a controlled isolated recovery procedure that reconciles every deletion in the lost interval are all in place, “erased from the live primary” is the precise claim — not “gone from every copy”.
- Terminate TLS in front of it
- And do not expose the control plane to the internet. HSTS is sent only when the request actually resolves as https, which behind a terminating proxy means telling Token Observe to trust that proxy — the same switch that decides whether the session cookie is marked Secure.
- Set the session secret from a secret manager
- The server refuses to start without it outside tests. Set the trust-proxy flag only when a trusted proxy really is in front, because it is what makes a client-supplied forwarded header believable.
- Set the four allowlist pairs to your own endpoints
- The defaults are not one policy and should not be read as one: provider hosts default to the public vendor endpoints, MCP hosts to loopback only, webhook hosts to nothing at all — an unstated webhook policy resolves to the rows that already exist — and the vendor-admin pair to the two vendor APIs its connectors need. Each credential-prefix list defaults to purpose-specific names. Narrowing all four to your own endpoints and your own variable names is what stops an admin-level registry write from turning into a read of every secret in the process environment.
- Keep /metrics on the monitoring network
- It is unauthenticated by Prometheus convention and exposes agent identifiers and month-to-date spend.
- Set NODE_ENV=production
- Which also makes the mock provider opt-in rather than opt-out.
- Treat the database file as evidence
- Restrict access to it, back it up to immutable storage, and check that the audit verification endpoint returns valid: true after a restore. Backup scheduling, off-box placement, legal holds, freshness alerting and proven deletion are operator-owned controls, and live deletion does not reach data already in a backup.
ACP_TRACE_RETENTION_DAYS=180 # unset means keep forever
ACP_CALLER_SIGHTING_RETENTION_DAYS=90 # a separate window; 0 keeps it all
GET /api/retention/status
reports the window, the exact cutoff instant, how many traces are
eligible now and the outcome of the last pass -- with the caller
roll-up reported as a sibling block rather than folded in, because
the two windows disagree by default and a reader who inherits one
number for the other question has been misled by the shape of the
answer.
POST /api/retention/run admin; runs one pass on demand
DELETE /api/retention/subject?subject=...&match=any&dryRun=true
returns the count before anything is removed. Both the preview and
the deletion are audited, under a digest of the subject rather than
the subject itself.What remains, and the named role that has to accept each of it
Nine residual risks are recorded with the role whose written acceptance is required, and none of them is evidence of approval until that role records a dated acceptance in the evidence pack. Anything not on that list and not mitigated elsewhere is an unrecorded gap, which is treated as a finding rather than as an absence.
The defect list is published for the same reason, and it is unusually specific: it names the file, states the concrete cost, describes the attack that works, and records which findings were refuted on verification as well as which were confirmed. Most vendors will not share that under NDA. Publishing it is a deliberate trade — it hands an attacker a starting point, and it also means a buyer’s security review is reading the same list the engineering team is working from, so the review can be about the substance rather than about what has been left out. The corollary is the part that matters more: if you find something that is not on that list, it is genuinely not known to the maintainers, and they want it.
The same discipline is enforced against the documentation. A documentation claim that materially overstates a security property and cannot be substantiated in the code is treated as a security defect rather than a typo, and three such gaps have already been found and corrected. That rule is why the limits on this page sit beside the claims in the same register: a sentence here that the source cannot support is a defect against the same list. It is also why an earlier description of an internal multi-pass review as “independent” was withdrawn rather than left standing — only reproducible tests and linked implementation count as evidence, and independent assurance remains an external gate that has not yet been passed.
Four items are expected to be accepted in writing before a deployment starts, and only one of them is on the list above. That one is the single-node SQLite topology. The other three are absences rather than mechanisms — no vendor-operated service level, no independent certification, and the preview limitations on endpoint-seat governance — so they are recorded under what is not claimed instead, and a reviewer looking for them on the residual list will not find them there. The nine themselves are grouped by the role best placed to weigh each one: six for the CISO, one for the data protection officer, one for the head of product and one for the engineering lead.
- CISO
- Six: heuristic injection detection with false negatives, the tamper-evident audit chain, the unauthenticated on-behalf-of header, the identity-provider group snapshot, no MFA on local control-plane accounts, and long-lived agent bearer tokens.
- Data Protection Officer
- One: regex personal-data detection with both false positives and false negatives, including free-text personal data that is not detected at all.
- Head of Product
- One: provider data-policy flags are unverified operator assertions about contracts Token Observe cannot read.
- Engineering Lead
- One: SQLite is a single writer on a single host, so sustained write load degrades governance latency and there is no replica.
What is deliberately not claimed.
Taken from the product’s own published limitations. If something is not on this list and not on the published defect list, it is genuinely not known rather than quietly omitted.
Token Observe has not had an independent penetration test.
No third-party security assessment, red-team engagement or code audit has been carried out on it. The security work done so far is internal: a five-dimension adversarial review with a three-verifier refutation panel per finding, a STRIDE threat model, targeted testing of specific attacks including a forged audit chain and adversarial ReDoS inputs, and the automated release gates. That is a genuine amount of work and it is not the same thing as an external test, so it is not presented as one. A pre-purchase penetration test by a prospective customer is welcome and expressly permitted by the licence.
There is no SOC 2, ISO 27001 or ISO/IEC 42001 certification.
None is held, none is in progress, and certification is deliberately not on the near-term roadmap. What exists instead is a control-by-control mapping to EU AI Act deployer articles, ISO/IEC 42001 Annex A, NIST AI RMF and the OWASP LLM and agentic lists, each framed as “this feature helps evidence that clause” rather than as compliance. A design-partner agreement can carry a contractual commitment, with a named date, to any certification milestone your procurement process requires.
The audit chain is tamper-evident, not tamper-proof.
Unkeyed is the default, and on a default install an operator with write access to the database file can rewrite an entry, recompute every downstream hash, and get valid: true. A test does exactly that, deliberately, so the limit is a tested fact. Keyed protection and off-box Ed25519 anchoring close most of it and both are off until you configure them; even then, key theft signs anything, history before the first anchor is covered by no anchor, a sink administered by the party that runs the database is not independent, and only an RFC 3161 authority or a public ledger proves when.
Evidence exports are digest-sealed, not signed.
A trace export and a compliance bundle carry a SHA-256 over the canonical JSON of the bundle and the embedded chain-verification verdict. That detects accidental or post-export edits; it does not prove origin. Durable origin evidence comes from authenticated delivery and, for the ledger head, from keyed audit plus an Ed25519 anchor retained independently of the database. WORM export, RFC 3161 timestamping and a Merkle tree for partial-log proofs are not built.
There is no availability service level and there are no service credits.
No uptime percentage is offered, and none should be accepted from any self-hosted vendor without asking what it could possibly mean. The vendor can commit to responding, to diagnosing and to fixing defects in the software; it cannot commit to the availability of your deployment, because it does not run it, cannot observe it and cannot restart it. There are no service credits because there is no service level to credit against, and there is no out-of-hours or 24/7 cover.
It is a single-writer SQLite process on one node, with no high availability, no point-in-time recovery and no proven recovery objectives.
There is no replica and no clustering by design at this scale, and no claim that process-local login throttles, connector buckets, OIDC transactions, provider circuit state or MCP sessions are safe to multiply across replicas. Recovery is from a retained full snapshot, not from point-in-time recovery. Recovery automation exists; a partner-proven recovery point or recovery time does not, and the twenty-four-hour and one-hour figures that appear in planning documents are unqualified planning objectives rather than evidence or commitments.
There is no throughput or capacity commitment.
The only published number is a thirty-second, mock-provider, single-laptop laboratory baseline, published with its own list of what it does not prove and an instruction not to turn it into a concurrency limit or a throughput commitment. No retained partner-shaped sustained-load result exists, and publishing a capacity figure before that would be a guess wearing a number.
There is no claim that the vendor is legally never a processor.
Runtime phone-home is zero and the published licence undertakes it, which removes the data processing agreement, sub-processor and transfer-assessment path from the ordinary supply of the software. It does not decide the legal roles created by evaluation terms, support access, incident handling or data you choose to share, and counsel must analyse those separately. Before sending a log excerpt or a diagnostic bundle, redact it; if it would contain personal data, execute a data processing agreement first.
Token Observe does not make you compliant with any law, regulation or standard.
It is a compensating control. It does not discharge any obligation you owe to a regulator, a data subject or a customer, and the vendor does not sign off your DPIA, FRIA, record of processing or risk register. Deciding risk tiers, running a fundamental-rights impact assessment and notifying regulators remain the deploying organisation’s duties; authoring your policy set and mapping it to your control framework is a professional-services engagement rather than support.
The policy, redaction, injection-detection and routing controls are heuristic, and are not warranted to identify every instance of what they are designed to detect.
Personal-data matching is regex plus checksum, so free-text personal data and non-UK/US identifier formats are not detected at all. Injection scoring is regex-based, and paraphrase, translation or encoding defeats it; homoglyph substitution is not addressed; markdown-image exfiltration is detected but outbound URLs are not rewritten. There is also no warranty that the published defect list is exhaustive.
There is no MFA on local control-plane accounts, and SCIM is a bounded Users push rather than a live directory.
Single sign-on moves MFA to your identity provider, and local login can be disabled entirely once it is rolled out — an install that configures neither retains local-password and manual-leaver risk. SCIM has no Groups, no live directory read and no hard delete, and group claims are a snapshot taken at sign-in rather than a live read, so a revoked group keeps granting authority until the person next signs in or the capture ages out.
Endpoint-seat governance is a preview and is not equivalent to an inline gateway.
The seat hook is marked preview and not production-eligible: it is an unsigned Node command rather than a signed native binary, its replay protection is bounded by a device clock the governed party controls, and its local evidence spool is unsigned. On an unenrolled device the files it relies on are ordinary files the user owns — device management is a prerequisite rather than a mitigation. Pre-1.0, only the latest published minor receives security patches; there is no long-term support branch and no backporting, because with one engineering team an LTS promise that cannot be kept is worse than none.
Residual risks, and the role that has to accept each one.
The question a reviewer actually has is not whether a risk exists but who is accountable for accepting it. Each row below names the role whose written acceptance the product’s own threat model requires.
Heuristic injection detection has false negatives.
Injection scoring is regex-based, and paraphrase, translation or encoding defeats it. The proposed rationale for accepting it is that blocking on a classifier with a meaningful false-positive rate would break legitimate traffic on the hot path, and that injection findings are one input to policy rather than the only control — RBAC, tool scoping and approvals bound the damage a successful injection can do.
Regex personal-data detection has both false positives and false negatives.
Checksum-validated kinds — card, IBAN, NHS number — score 0.9 to 0.98; national insurance and Social Security numbers score 0.85 and phone scores 0.7; free-text personal data is not detected at all. The proposed rationale is that Token Observe is a compensating control rather than your only DLP, and that confidence scores are exposed precisely so policies can set their own thresholds.
The audit chain is tamper-evident, not tamper-proof.
Off-box Ed25519 anchoring is built but off by default, and WORM export, RFC 3161 timestamping and a Merkle tree for partial-log proofs are not built. An anchor buys exactly one sentence and no more: any copy you kept off-box beats any rewrite made after you took it. Four things it does not close, none of which a signature can — key theft, because whoever holds the private half signs any history they like; pre-anchor history, because anchor one says nothing about whether the entries beneath it were already honest; sink collusion, because suppression is only detectable if a copy exists somewhere the install cannot reach; and time, because the creation instant is asserted by the signer and only a timestamp authority or a public ledger proves when. The proposed rationale is that detecting operator tampering already exceeds the append-only-table baseline, and that the remaining gap is closed by key custody — a control that belongs to your KMS, not to the product.
The on-behalf-of header is unauthenticated.
There is no signed actor or subject claim, and an agent that omits the header skips the mask entirely unless that agent is configured to require it. The proposed rationale is that the mask can only narrow authority, never grant it — a forged principal buys an attacker strictly less than sending no header at all — and that the agent’s own RBAC decision is computed before any principal field is read.
Identity-provider group claims are a snapshot, not a live directory read.
Token Observe holds no refresh token and requests no offline scope, so a revoked group keeps granting authority until the person next signs in or the capture ages past the claims window, 24 hours by default. An Entra group overage sends a Graph link instead of the groups, which is not followed, so those principals capture zero groups and are denied. The proposed rationale is that following a directory API would mean a new credential, a new egress host, a directory-wide read permission and a network dependency inside a login — all wrong for a product that ships air-gapped — and that both failure directions are closed rather than open.
Provider data-policy flags are unverified operator assertions.
Marking a provider as zero-data-retention, no-training or region-pinned records your assertion about your contract with that provider; Token Observe cannot verify it, and enforces routing on the assertion. The proposed rationale is that the assertion is itself auditable — it appears in the audit log with the actor who set it — and that automated verification of a contractual term is not achievable.
SQLite is a single writer on a single host.
Sustained write load degrades governance latency, and there is no replica. The proposed rationale for accepting it at the current target scale is that the swap remains mechanical behind the store ports.
There is no MFA on local control-plane accounts, and SCIM is an optional bounded Users push rather than a live directory.
Single sign-on supplies your identity provider’s MFA, and configured SCIM supplies username, name and active-state lifecycle with immediate session revocation. An install that configures neither retains local-password and manual-leaver risk, and Groups, hard delete and live directory reads remain absent. Acceptance is conditional on three things: TLS terminating in front of the deployment, local login being disabled after single-sign-on rollout, and the SCIM token staying in a secret manager when that realm is enabled.
Agent credentials are long-lived bearer tokens with manual rotation.
Holding one is being that agent, and a token is valid until it is revoked or expires; it is not workload-bound. The proposed rationale is that workload identity such as SPIFFE cannot be presented by the agent frameworks Token Observe must support today, and that revocation, optional expiry and last-used timestamps compensate.
Security research is permitted, and so is publishing it.
Report a vulnerability through a GitHub private security advisory, which is the default and gives you a private thread and a CVE path, or by email to security@tenhaw.com, or through your named escalation contact if you are a design partner — but do not open a public issue, pull request or discussion for a security report. A human confirms receipt within two business days and names a point of contact; it is not an automated reply. An advisory is published within seven days of a fix being released, with wording and timing coordinated with you, and the default disclosure deadline is 90 days from the triage verdict, whether or not a fix has shipped. A CVE is requested for Critical and High issues. There is no bug bounty and no monetary reward, which is a resourcing statement rather than a judgement of the work. Severity starts from CVSS v3.1 and is then adjusted for the context this product actually runs in: a flaw that defeats the governance decision or corrupts the evidence record is rated above what its base score alone would suggest, because those are the two things the product exists to provide.
No gag clause, no pre-approval
If you make a good-faith effort to comply with the policy, Tenhaw will not pursue or support legal action against you in relation to your research, and will treat your activity as authorised under the Computer Misuse Act 1990 and equivalent laws. Good faith means you test only against a deployment you own or are permitted to test, you do not access, modify or retain anyone else’s data, you stop as soon as you have established that a vulnerability exists, you do not degrade a live service, and you give a reasonable opportunity to fix the issue before disclosing it. Section 9 of the licence explicitly permits security testing and the publication of results: there is no gag clause and no pre-approval requirement for benchmark or assessment results. Two scope notes are worth knowing before you start. A new path that evades or weakens the keyed audit epochs, the boot checkpoints, the signed anchors or the off-box head comparison is expressly in scope, because the database writer is part of the evidence-integrity threat model. And algorithmic denial of service — a single small request that consumes disproportionate CPU — is in scope and has been found here before, while denial of service by volume is not.
Reports go to security@tenhaw.com, which is the address published in the product’s own security policy and is read by a person rather than a queue.
Commercial source-available
Source-available rather than open source, which is the harder of the two words and the accurate one. A product that calls itself open source and then forbids redistribution has told its most technical reader something untrue in the first sentence.
- Use, modify and self-host under a licence
- Read the entire governance domain before buying: the core package is pure functions with zero runtime dependencies
- Security research, and publication of the results, with no gag clause and no pre-approval
- Redistribution
- Offering Token Observe as a competing hosted service
The published licence is a template pending review by counsel rather than legal advice, and the final terms are the ones in your signed agreement.
The support severities, the response targets and the three published reasons there is no availability SLA are on the pricing page, and the framework mappings with an honest coverage level on every row are on the compliance page.
What does the Token Observe vendor actually receive from a running deployment?
Nothing. There is no product telemetry, usage-count feed, crash-report service, licence callback or hosted component, and no vendor-operated service anywhere in the request path. This is not a policy that could change with a configuration flag — there is no code in the product that could send it — and section 6.1 of the published licence undertakes it in terms as well, subject to that licence being the vendor’s stated commercial terms pending legal review rather than an executed grant. Confirming it takes about five minutes: grep the server source for outbound call sites and check that every destination resolves from operator-supplied configuration, grep for hard-coded URLs and find that the only vendor-owned string is an attribution header sent to OpenRouter, run the product with egress allowed only to your own endpoints, and read the dependency list. The one exception is anything you voluntarily send during sales, support or incident response, which is governed by your support agreement rather than by the architecture.
Do we need a data processing agreement or sub-processor terms?
For the ordinary supply of the software, the technical answer is that there is no vendor-side copy of your data to cover, so there is no vendor sub-processor path in the runtime: the model providers and tool servers Token Observe routes to are your processors under your agreements with them, and its role is to constrain which of them may receive a given payload and to record what was sent. That is a technical fact, not a legal conclusion, and it is stated as such. Counsel decides whether evaluation terms, support access, incident handling or data you choose to share require a data processing agreement, a transfer assessment or sub-processor terms — the licence explicitly carves out voluntary disclosure in support and professional-services engagements, and recommends executing a data processing agreement before any such disclosure of personal data.
Is the audit chain tamper-proof?
No, and the difference is stated rather than blurred: it is tamper-evident. On the unkeyed default, an operator with write access to the database file can rewrite an entry, recompute every downstream hash, and have verification report valid: true — a test does exactly that on a default install, deliberately, so the limit is a tested fact rather than a caveat in prose. Configuring an audit HMAC key from a secret manager the database administrator cannot read makes the digests MACs and seals the head at every boot, which defeats a full recompute. Configuring an Ed25519 anchor key adds one further sentence and only one: any copy of the anchors you kept off-box beats any rewrite made after you took it. It does not say the history beneath the first anchor was honest, that the signer’s clock was truthful, or that someone holding the key could not have produced the same file.
Are you SOC 2 or ISO 27001 certified, and has this been penetration-tested?
No to all three, and none of them is implied anywhere. There is no SOC 2, no ISO 27001 and no ISO/IEC 42001 certification, and there has been no independent penetration test, red-team engagement or third-party code audit. The security work that has been done is internal — a five-dimension adversarial review with a three-verifier refutation panel per finding, a STRIDE threat model, targeted testing of specific attacks including a forged audit chain and adversarial ReDoS inputs, and the automated release gates — which is a genuine amount of work and is not the same thing as an external test. A pre-purchase penetration test by a prospective customer is welcome and expressly permitted by section 9 of the licence, with no pre-approval requirement and no gag clause on publishing what you find.
Can our security team review and attack it before we buy?
Yes, and the licence exists partly to say so. Any person may install and operate Token Observe for internal evaluation, security review and proof of concept for thirty days from first installation, provided it is not used for live production traffic or regulated personal data. Section 9 permits you, or a third party you commission, to inspect, test, fuzz, penetration-test and reverse-engineer it as deployed on infrastructure you control, to publish performance results naming the version and configuration with no pre-approval, and to publish security assessment findings naming the software after coordinated disclosure. The published defect list and threat model are the recommended starting point, so a paid test is not spent re-finding what is already known.
What is the retention position, and can you erase one person’s data?
Trace retention and subject erasure are both implemented, and the default is to keep everything — the retention window is unset by default, and unset means keep forever. That over-satisfies the six-month minimum in EU AI Act Article 26(6) and satisfies nothing in GDPR Article 5(1)(e), so a regulated deployment must set it; the default is deliberate, because retention has to be decided rather than assumed and an upgrade that silently began deleting evidence would be the worse failure. Erasure targets one subject’s traces with a dry run first, and both the preview and the deletion are audited under a digest of the subject rather than the identifier. Two limits belong in the same breath. The window and the erasure endpoint reach traces, trace events, the search index and trace scores and nothing else — not the audit log, which is untouched by design so the record of a deletion outlives the deleted data, and not approvals, radar findings or webhook deliveries. And both act on the live primary database only: a retained snapshot preserves whatever existed when it was taken, so “erased from the live primary” is the precise claim rather than “gone from every copy”.
Can it run air-gapped?
Yes. A delivered image can run fully air-gapped, the dashboard bundles no CDN assets, and an air-gapped install never calls the model-price catalogue at all — prices are loaded through the API instead. Set the provider host allowlist to your own internal model endpoints and the credential-prefix allowlist to your own variable names, and egress is confined to endpoints you named. The build is a different question and is stated as one: building that image from source still requires access to the configured base image, the Debian archives and the npm registry. Runtime dependencies are kept deliberately few for exactly this reason — every addition is supply-chain surface in a product that gets deployed air-gapped — and the governance domain itself has zero runtime dependencies.
What happens to our agent fleet if Token Observe is unavailable?
It stops, and that is intentional rather than a limitation being explained away. Governance is inline, so Token Observe is a single point of failure in the agent request path; a control you can bypass by turning it off is not a control. The consequence is that its own availability is a governance property, which is why timeouts, provider circuit breakers and restore-tested backups are treated as security work rather than only as reliability work — and why an availability failure and an evidence failure are the same severity in the support model. A deployment that is serving traffic happily but has stopped recording it is an S1 here, because the product exists to produce that record; most support agreements would call that an S3. What is not offered is an availability service level, an uptime percentage or service credits, because the vendor does not run your deployment, cannot observe it and cannot restart it.
Send the questionnaire, and we will tell you what is still open.
Most of a standard vendor security questionnaire is answered by this page, the compliance mappings and the published defect list. Anything left goes to security@tenhaw.com and gets a written answer.
no form · no qualification step · no sales desk · the other three ways in