Validation report

The numbers we publish about our own proxy, each with the command that produced it and the commit it was measured at. The sections at the end are the gaps.

Measured at
87ccd47 on main, 12 September 2026
Measured by
Running each command in the repository, not by reading a document
Shelf life
Test counts and rule counts change with every merge. Re-run the commands rather than quoting this page back at us.

What the pattern layer catches

The deterministic deny list is the floor underneath everything else. This is how much of our own threat corpus it catches.

40 of 47 payloads in the in-repo agentic-threat corpus, 85.1% pattern-layer recall
  • Seven documented bypasses. Five of them are obfuscations of one destructive command.
  • Zero false positives on that corpus, and the corpus holds no safe-control payloads at all, so no figure about ordinary safe traffic can be derived from it. We derived one from our own production traffic instead, and it is the first gap below: our rules flag 15.47% of 4,764 governed calls on traffic we labelled benign.
  • The corpus is written by the defender, so this measures regression stability, not adversarial robustness. It tells you the rules still catch what they caught last week. It does not tell you what an attacker who has read them would do.
  • An earlier reading of this benchmark said 100% of a closed corpus. It was formally superseded on 2026-09-03, because the harness derived its labels from its own assertions and recall was 100% by construction.

python scripts/detection_bench.py, run at 87ccd47 on 12 September 2026. Sources: scripts/detection_bench.py, apps/proxy/tests/test_agentic_threats.py, and the decision record dated 2026-09-03 that supersedes the older reading.

Back to what is on this page

What runs before anything ships

One workflow, on every push to main: pytest at the repository root, then an npm ci and the dashboard suite.

Collected test cases at commit 87ccd47, 2026-09-12
Suite Count Command
Proxy test cases 3,802 pytest --collect-only -q apps/proxy/tests
Repository-level cases, ops and end-to-end 448 pytest --collect-only -q tests
Dashboard test files 67 find apps/dashboard -name "*.test.js*"
Dashboard test call sites 845 grep -hoE "(it|test)\s*\(" over those files

The 845 figure is an upper bound on files and a lower bound on executed cases: a parameterised .each block is one call site and many runs. The CI workflow is .github/workflows/deploy.yml, the only workflow in the directory. Cross-tenant isolation has its own file, apps/dashboard/__tests__/e2e/cross-tenant-isolation.test.js.

Back to what is on this page

What a deny actually does

A verdict is not an enforcement. Here is the response the agent gets instead, in the shape it arrives in.

When policy denies a tool call on the non-streaming Anthropic route, the proxy does not annotate the response and pass it on. It builds a synthetic Anthropic-shaped assistant message and returns that in place of the upstream one. The model's tool_use block is replaced by a single text block naming the policy, and stop_reason becomes end_turn. The agent runtime never receives a tool call, so it has nothing to execute. A verdict returned beside the action would leave the action in the response.

What the model asked for

"content": [
  { "type": "text",
    "text": "Clearing that directory." },
  { "type": "tool_use",
    "name": "bash",
    "input": { "command": "rm -rf ..." } }
],
"stop_reason": "tool_use"

What the agent receives

"content": [
  { "type": "text",
    "text": "Intercis blocked this action.
      Policy: shell-rm" }
],
"stop_reason": "end_turn"

Shape, not a capture: the two blocks above show the structural change, and the behaviour they illustrate is what _make_blocked_response in apps/proxy/main.py does. The real text starts with a red stop symbol and ends with a line saying the call was logged for audit review.

The record the decision leaves behind

One row per governed event in public.events, append-only for every client role, and chained per tenant, so a change that did not also recompute the rest of the chain shows up.

What a row carries
Tenant, agent, the tool name, a short excerpt of the command or path, the verdict, the policy, whether an injection was seen, the wire route it came off, a command fingerprint, the classifier status and reason, and the structured tool input. The verdict column is constrained to allow, deny, observe and pending. The conversation itself is not stored.
Who can change it
Row-level security is on from the first migration with no policies at all, so it fails closed until a later migration grants a tenant-scoped read. Migration 026 revokes TRUNCATE from the client roles and 027 revokes the unused DML, so no client role holds UPDATE or DELETE on events. Writes come from the service role only.
How the chain is built
A BEFORE INSERT trigger computes a SHA-256 over the previous row's hash plus every length-prefixed column of the event except the stored request body. That is the format from 2026-09-20, the day migration 054 was applied; events recorded before 2026-09-20 are hashed over ten named fields, and no earlier row is re-hashed, because re-hashing written rows is the act the chain exists to detect. One chain per tenant, with the genesis link derived from the tenant id, and same-tenant inserts serialised by an advisory transaction lock. Verify it yourself with scripts/verify_event_chain.py, which picks the formula from each row's own version marker and reports a row in the older format that follows a row in the newer one as a break.
The column outside the chain
The stored request body is deliberately not hashed, because a nightly job nulls it after 90 days and hashing it would break every chain the night the first row aged out. That job is events-raw-request-retention-90d, runs at 03:41 UTC, and is capped at 10,000 rows a night. It was confirmed live on the production database on 2026-09-12. The event row itself is never deleted.
What to call it
Tamper-evident, not tamper-proof. Anyone holding database-owner rights can rewrite rows and recompute the hashes, drop the newest rows, or append rows hashed correctly, and nothing outside the database anchors the chain to catch any of that. What it does catch is a change that left the hashes after it alone, including a row dropped from the middle. It does not stop that either.

Back to what is on this page

What none of this measures

The 8 things a security reviewer will ask about that the numbers above do not answer. They are here at the same size as the claims, because finding them later is worse for both of us. Each one is either a trade-off we intend to keep or an item on our roadmap without a date, and where there is a roadmap item the text under it says so.

We flag work that was fine, and here is how often

On 4,764 governed tool calls from our own Dogfood tenant, all of them labelled benign, today's rules flag 737. That is 15.47%, Wilson 95% 14.47% to 16.52%. 516 of the 737 are calls enforce mode would have taken out of the response and 221 are injection-scan rows, which are recorded and never remove anything. The window is 25 August to 4 September 2026 with 31 August excluded, the day a four-hour classifier outage left 25.6% of that day's calls unanswered and voided it under our own runbook. The full window including that day reads lower, 13.46%, and it is printed too. The layers, the count for every policy family and who applied the benign labels are on the benign-corpus page.

There is no latency figure, and the architecture argues against a small one

Nothing in the repository measures governed-request latency. No percentile, no load test, no benchmark. Worse for us: the streaming path forces the upstream stream, collects the whole thing, judges the assembled tool calls and only then replays it. The code's own docstring says that adds latency equal to the full stream duration. Measure it in your environment during the pilot; do not take a number from us, because we do not have one. A co-located latency benchmark is an item on our roadmap with no date.

Hosted MCP tool calls are observed, not governed

Intercis judges the tool calls that cross the Anthropic and OpenAI API wire. When a hosted MCP server runs a tool for the model, that call has already run by the time the response reaches us. At the commit above, measured by execution, a Responses body carrying mcp_call, mcp_list_tools and web_search_call extracted to an empty list and nothing was recorded. Corrected in place on 1 October 2026: since 28 September 2026 on both wire routes, and 29 September on the inference-hook path, each such call writes one observe row and is never judged or blocked. mcp_list_tools runs no tool and writes nothing.

The LLM classifier fails open

The classifier runs only on a deny-list miss. When it is unavailable the action is allowed. The event row records the classifier status as unavailable with a typed reason, and an audit-invariant signal fires carrying the provider and the upstream HTTP status. The deny list still enforces underneath, which is the point of having a deterministic floor. The proxy also watches itself: over a rolling window of the last 200 consultations, with at least 20 samples, it pages once when the unavailable share passes 5% and once more when it recovers below 2.5%.

Prompt injection is observed, not blocked

Thirteen regex patterns in four families: instruction override, prompt extraction, role hijack, delimiter escape. On a hit the proxy writes one row with the verdict observe, marks the request as injected, and stores the first 200 characters of the matching user message. It does not block, and we do not describe it as prevention.

Being in path is a configuration setting

Your agent reaches Intercis because its base URL points at Intercis. A process that can change its own base URL can call the provider directly. Out-of-process enforcement removes the in-process bypass; closing the network path is a deployment step you take with egress control, not a property you get for free.

The classifier call is billed to us and sent from our account

Traffic we forward upstream uses your own provider key; we do not store provider credentials at rest. The classifier is the exception. It uses an Intercis credential, never yours, and the call carries the tool name and the full tool input to the provider. That makes the model provider a subprocessor for that path. The subprocessor list in the repository is a draft, and the sentence you just read contradicts one of its statements. We have not published a current one. Attorney review of that draft is an item on our roadmap with no date.

Intercis is not SOC 2 certified

No audit is underway, no auditor is engaged, and there is no certification artefact of any kind. We do not generate SOC 2 or ISO 27001 evidence packages either. The audit trail above is evidence a reviewer can query and verify; it is not a report with an auditor's name on it. An evidence-package export is an item on our roadmap with no date.

Back to what is on this page

The 2026-04-02 run

The first end-to-end pass against the live proxy, written up at the time. It is kept here because it is dated evidence, not because it is current.

The written record is docs/validation-report-2026-04-02.md in the repository. It is dated 2026-04-02 and it lists 13 checks, all passed. The recording below is the file validation-report-2026-04-08.mp4, so the two artefacts carry different dates. We have not reconciled them and we are not going to pick one, so both are printed. Where anything here disagrees with a number higher up this page, the number higher up was measured this week and it wins; the suite sizes in particular have moved a long way since.

13 of 13 checks passed on 2026-04-02
The thirteen checks
  • Proxy reachable and healthy
  • Destructive action blocked
  • Block message returned to the agent
  • Safe action allowed through
  • Tool call preserved when allowed
  • Plain chat passed through untouched
  • Unauthenticated request rejected with HTTP 401
  • Blocked event visible in the dashboard
  • Allowed event visible in the dashboard
  • Deny verdict recorded in the audit log
  • Allow verdict recorded in the audit log
  • Anthropic SDK client path confirmed
  • Latency benchmark completed

Run against api.intercis.io with the agent's SDK client pointed at the proxy and no other change to the agent. These are the thirteen checks listed in docs/validation-report-2026-04-02.md, with the labels shortened. The suite behind them is demo/smoke_test.py.

The thirteenth check ran and we still publish no latency number

The benchmark sent the same request direct and through the proxy, five iterations. The proxy was in Railway's europe-west4 region and the client was on the US East Coast, so every proxied request crossed the Atlantic twice and the direct one crossed it once. That measures a route, not the cost of the control. The document also projects what a co-located deployment would cost. That projection has no measurement behind it, so neither number appears on this page. What we can tell you about latency is in the section above, and the answer is that we do not have a figure.

The recording has no captions and no transcript. It walks the same thirteen checks listed above, in that order, and it shows no result the list does not. That is a mitigation and not a substitute: this is a WCAG 1.2.2 and 1.2.3 failure, and nobody has timed captions for the file yet.

Back to what is on this page

Reproduce it

Each command here produced a number on this page. They are the same commands we run, in the same repository.

# pattern-layer recall on the in-repo threat corpus
python scripts/detection_bench.py

# collected test cases, per suite
python -m pytest --collect-only -q apps/proxy/tests
python -m pytest --collect-only -q tests

# the audit chain verifier, run on the published sample; its third row is altered on purpose, so this prints FIRST BREAK and exits 1
python scripts/verify_event_chain.py apps/site/evidence/sample-chain.json

# the end-to-end pass against a live proxy
export PROXY_URL=https://api.intercis.io
export ANTHROPIC_API_KEY=<your provider key>
export INTERCIS_PROXY_KEY=<your tenant key>
python3 demo/smoke_test.py

The tenant key is the ik_live_ credential for your own tenant, and it is issued with the pilot. Scripts: demo/smoke_test.py and demo/agent_test.py.

Intercis is a proxy that runs outside your agent process. Your agent's traffic to Anthropic or OpenAI goes through it, policy runs on the tool calls the model asks for, and once enforcement is on, a denied call is taken out of the response before your agent runtime can execute it. The pilot is 90 days on the Team tier.

Start a 90-day pilot