Skip to content

Commit 9b34a17

Browse files
authored
Merge pull request #43 from MaazAhmed47/codex/pre-hn-public-truth
fix: align public proof with enforced MCP behavior
2 parents 6cfedd3 + 36cce53 commit 9b34a17

41 files changed

Lines changed: 1079 additions & 165 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.mcp.json

Lines changed: 0 additions & 14 deletions
This file was deleted.

README.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
**MCP runtime trust layer for AI agents.**
1010

11-
Interlock detects material MCP tool drift after approval, including effective-permission expansion that static manifest comparison can miss, quarantines changed gateway-mediated calls before continued use, and emits hash-chained evidence.
11+
Interlock detects material MCP tool drift after approval, including effective-permission expansion that static manifest comparison can miss, quarantines the affected tool so that later gateway-mediated calls to that tool are held before upstream forwarding, and emits hash-chained evidence.
1212

1313
Current source metadata is `0.2.0-alpha.1`. This identifies the source tree; it is not a published GitHub release. See [GitHub Releases](https://github.com/MaazAhmed47/Interlock/releases) for published artifacts.
1414

@@ -36,7 +36,7 @@ docker compose run --rm demo-runner scenario-a
3636
docker compose run --rm demo-runner scenario-b
3737
```
3838

39-
This runs the real proof sequence: approved tool → material capability drift → quarantine before a changed gateway-mediated call continues → Security Receipt hash-chain verification. `scenario-b` adds the behavioral case that static manifest comparison misses: the same tool and schema move from expected `403 denied` to observed `200 allowed`.
39+
This runs the real proof sequence: an approved `read_file` boundary undergoes a material capability change, Interlock quarantines that tool, the next gateway-mediated call to `read_file` is held before any upstream `tools/call` is forwarded, and the decision produces a Security Receipt that passes hash-chain verification. `scenario-b` adds the behavioral case that static manifest comparison misses: the same tool and schema move from expected `403 denied` to observed `200 allowed`.
4040

4141
See the [offline demo instructions](demo/offline/README.md#quickstart) for the command reference, fixed ports, reset procedure, and exact proof limits.
4242

@@ -103,7 +103,7 @@ Scope note: this is behavioral verification, not provider-wide OAuth introspecti
103103

104104
Use Interlock when an AI agent can call MCP tools that touch real systems: files, internal documents, databases, Slack, GitHub, customer records, or deployment workflows — especially when those tools are vendor, community, forked, or separately operated surfaces your team does not fully control.
105105

106-
Example: you approved a read-only MCP tool for internal documents. Later, the same tool name gains external export behavior or starts exposing sensitive data fields. Interlock detects the drift, blocks or quarantines the tool before execution, and records a Security Receipt for review.
106+
Example: you approved a read-only MCP tool for internal documents. Later, the same tool name gains external export behavior or starts exposing sensitive data fields. Interlock can detect that material surface drift during gateway re-discovery, quarantine that tool, hold subsequent gateway-mediated calls to it before forwarding, and record a Security Receipt for review. Unrelated approved tools keep working. Undeclared behavioral drift may require a controlled probe or an observed response first.
107107

108108
---
109109

@@ -133,7 +133,7 @@ configuration is default-deny: the variable name must also appear in
133133
}
134134
```
135135

136-
Use a non-production workflow only. Baseline a safe read-only tool, then make or simulate one safe surface change and verify that Interlock detects the changed tool boundary before execution.
136+
Use a non-production workflow only. Baseline a safe read-only tool, then make or simulate one safe surface change and verify that Interlock detects the changed boundary at re-discovery and holds the subsequent gateway-mediated call to that tool before forwarding upstream.
137137

138138
---
139139

@@ -163,7 +163,7 @@ Interlock is built around that runtime control gap for MCP agents:
163163

164164
* baseline approved MCP tools
165165
* detect post-approval tool/schema drift
166-
* enforce quarantine before execution
166+
* enforce quarantine on the affected tool, holding subsequent gateway-mediated calls to it before upstream forwarding
167167
* produce audit evidence for runtime decisions
168168

169169
Interlock is not affiliated with or endorsed by OWASP. The mapping above describes alignment with public agentic security guidance.
@@ -293,7 +293,7 @@ Beyond pattern matching, Interlock enforces business-logic constraints on tool a
293293
}
294294
```
295295

296-
Now an agent calling `refund_user(amount=99999)` is denied before execution — even if the tool exists and the agent has permission. Regex can't catch business-logic violations like this; deterministic bounds can.
296+
Now an agent calling `refund_user(amount=99999)` through `/mcp/call` is denied at the gateway before Interlock forwards the upstream `tools/call` — even if the tool exists and the agent has permission. Regex can't catch business-logic violations like this; deterministic bounds can.
297297

298298
---
299299

@@ -516,7 +516,7 @@ Open an issue, start a discussion, or reach out from the links above.
516516
Interlock is strongest when agents are close to real systems: databases, Slack, files, ticketing, deployment tools, finance data, or internal APIs. A buyer should be able to prove value quickly by seeing:
517517

518518
- a clean MCP tool baseline recorded at discovery
519-
- a risky tool schema or capability drift quarantined before execution
519+
- a risky tool schema or capability drift detected at re-discovery, followed by a gateway-mediated call to that tool held before upstream forwarding
520520
- role-based policy blocking a dangerous call from the wrong agent
521521
- response scanning catching prompt injection, secrets, PII, or oversized output
522522
- audit evidence for every allow, deny, monitor, and quarantine decision
@@ -578,7 +578,7 @@ What to verify before production:
578578

579579
## What Interlock is
580580

581-
Interlock detects material MCP tool drift after approval, including effective-permission expansion that static manifest comparison can miss, quarantines changed gateway-mediated calls before continued use, and emits hash-chained evidence.
581+
Interlock detects material MCP tool drift after approval, including effective-permission expansion that static manifest comparison can miss, quarantines the affected tool so that later gateway-mediated calls to that tool are held before upstream forwarding, and emits hash-chained evidence.
582582

583583
It is built for the agent path, not just prompt filtering. The main security surface is `POST /mcp/call`, where Interlock checks server trust, tool whitelist rules, tool metadata, schema drift, provenance, RBAC, tool-call arguments, and MCP responses before returning anything to the agent.
584584

@@ -725,7 +725,7 @@ Readback effect evidence schema: [`readback-effect-drift-record.v1.json`](interl
725725

726726
`core/chain_drift.py` adds pre-execution analysis for planned MCP tool chains. This catches risks that no single tool call reveals alone: sensitive read -> external send, secret read -> shell execution, Terraform plan -> apply/destroy, or preview -> deploy/charge later in the same workflow. The analyzer does not call providers or execute tools. It hashes every step's arguments, builds an evidence-safe chain profile, and logs a chain-drift Security Receipt when the planned sequence crosses a material boundary.
727727

728-
Critical chain findings are denied before execution with types such as `chain_sensitive_read_to_external_effect`, `chain_secret_to_execution`, `chain_preview_to_deploy`, `chain_preview_to_destructive`, and `chain_preview_to_money_movement`. Read-only chains remain allowed. This is a prevention point for orchestrators that can submit a planned sequence before running it; it is not a claim that Interlock can infer every future agent step without seeing the plan.
728+
Critical chain findings are denied before the orchestrator forwards provider calls when it submits the planned sequence to Interlock, with types such as `chain_sensitive_read_to_external_effect`, `chain_secret_to_execution`, `chain_preview_to_deploy`, `chain_preview_to_destructive`, and `chain_preview_to_money_movement`. Read-only chains remain allowed. This is a prevention point for orchestrators that expose a plan before running it; it is not a claim that Interlock can infer every future agent step without seeing the plan.
729729

730730
Chain drift evidence schema: [`chain-drift-record.v1.json`](interlock-web/public/schemas/chain-drift-record.v1.json).
731731

RELEASE_NOTES.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
1-
# Interlock v0.1.0 — First Pilot-Ready Release
1+
# Interlock v0.1.0 — First Tagged Release
22

3-
First tagged release of Interlock for serious design-partner evaluation.
3+
Historical notes for the first published tag. The current source metadata and
4+
compatibility boundary are documented in `README.md`; this file does not
5+
describe the current tree.
46

5-
Interlock is a self-hosted runtime security gateway for AI agents and MCP servers. It sits between agents and tools, scans prompts and responses, enforces tool policy, detects MCP drift, and records audit evidence for allow, deny, monitor, and quarantine decisions.
7+
Interlock is an MCP runtime trust layer for AI agents. In this tag it sat between
8+
agents and tools, scanned prompts and responses, enforced tool policy, detected
9+
MCP drift, and recorded audit evidence for allow, deny, monitor, and quarantine
10+
decisions.
611

712
## What's included
813

914
### Runtime Security Controls
1015

1116
* OpenAI-compatible `/v1/chat/completions` gateway with prompt scanning before provider forwarding
12-
* MCP tool-call proxy with policy enforcement before execution
17+
* MCP tool-call proxy with policy enforcement at the gateway before upstream forwarding
1318
* Response scanning for prompt injection, PII, secrets, and oversized outputs
1419
* LLM judge path with configurable fail modes
1520

ROADMAP.md

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ they are actually done.
99

1010
## What's proven today
1111

12-
These paths are implemented, covered by the test suite (500+ tests run as a
13-
whole directory in CI), and reproducible end-to-end in the offline demo:
12+
These paths are implemented, covered by the full Python suite in CI, and the
13+
scoped default surface-drift path is reproducible end-to-end in the offline demo:
1414

1515
- **Capability drift detection.** A registered MCP tool's surface
1616
(description, input schema, annotations, derived effect metadata) is
@@ -21,8 +21,9 @@ whole directory in CI), and reproducible end-to-end in the offline demo:
2121
canary probes record what a tool's backing API actually permits. When a
2222
previously denied action starts succeeding (403 → 200) with the same
2323
identity and tool surface, the tool is quarantined with receipt evidence.
24-
- **Quarantine before execution.** Calls to a quarantined or drifted tool
25-
are blocked at the gateway before the upstream call is made, and the
24+
- **Gateway hold after detected material drift.** Subsequent calls to a
25+
quarantined or materially drifted tool are blocked at the gateway before an
26+
upstream `tools/call` is forwarded, and the
2627
denial is recorded with binding fields (call id, argument hash, surface
2728
hashes).
2829
- **Hash-chained Security Receipts.** Every allow/deny/quarantine decision
@@ -46,8 +47,8 @@ than find them in a pilot.
4647
- **Effect drift is detected post-execution for the first call.** Outcome
4748
drift (a "dry-run" tool that suddenly applies changes) is judged from the
4849
upstream response, so the first drifting call has already executed by the
49-
time it is caught. Subsequent calls are blocked by the resulting
50-
quarantine. Only surface drift and quarantine state block pre-execution.
50+
time it is caught. Subsequent calls to that tool are blocked by the
51+
resulting quarantine. Only surface drift and quarantine state block pre-execution.
5152
- **The audit chain is tamper-evident, not externally anchored.** The hash
5253
chain uses unkeyed SHA-256 and lives in the same database as the data it
5354
protects. It detects casual tampering; it does not resist an attacker
@@ -60,11 +61,12 @@ than find them in a pilot.
6061
`xfail` tests in `tests/test_drift_adversarial.py` (e.g. exfiltration
6162
verbs outside the heuristic keyword set, indirect auth-scope widening via
6263
an innocuous-looking parameter).
63-
- **Not yet protocol-complete against the official MCP SDK.** The gateway
64-
speaks the JSON-RPC tool-call subset it needs and is tested against mocks
65-
and mock servers, not certified against the official MCP SDK's transports
66-
and session semantics (Streamable HTTP, stdio, session lifecycle,
67-
notifications).
64+
- **Tested, pinned official-SDK interoperability; not full MCP conformance.**
65+
Official Python `mcp==2.0.0` and TypeScript client `2.0.0` probes cover the
66+
scoped stateless `2026-07-28` gateway path, and an official TypeScript server
67+
`2.0.0` probe covers pinned JSON/SSE upstream calls. These pins do not prove
68+
other SDK versions, stdio, subscriptions, sessionful transports, or full MCP
69+
conformance; see `docs/mcp-2026-compatibility.md`.
6870
- **Single-tenant assumptions.** Per-key data separation exists, but there
6971
is no hard tenant isolation story (separate schemas/databases,
7072
per-tenant encryption) for hosting mutually distrusting customers.
@@ -76,10 +78,10 @@ than find them in a pilot.
7678

7779
In rough priority order; each item closes a limitation above.
7880

79-
- **Official MCP SDK adoption and transport completeness.** Build the
80-
gateway's MCP surface on the official SDK; support Streamable HTTP and
81-
stdio transports and correct session lifecycle handling, verified against
82-
SDK-based reference servers.
81+
- **Broader transport and session interoperability.** Preserve the pinned
82+
official-SDK probes while adding explicitly scoped coverage for stdio,
83+
subscriptions, and session lifecycle behavior. This remains compatibility
84+
work, not a promise of full MCP conformance.
8385
- **Signed and externally anchored receipts.** Key-based signatures over
8486
receipt content and periodic anchoring of the chain head outside the
8587
primary database, so verification does not depend on trusting the

SECURITY.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ For enterprise pilots:
4040

4141
- Use Postgres through `DATABASE_URL`; do not rely on local SQLite for multi-instance deployments.
4242
- Use Redis through `REDIS_URL` before running multiple workers or pods.
43-
- Put the admin surface behind SSO, VPN, identity-aware proxy, or a private network until native OIDC/SAML is implemented.
43+
- OIDC admin authentication is implemented for issuer/audience/JWKS-validated JWTs and dashboard Authorization Code + PKCE. Configure and test it for the selected IdP, or keep the admin surface behind a VPN, identity-aware proxy, or private network.
44+
- SAML is not implemented. A SAML-only deployment needs an identity-aware proxy or another external bridge; do not describe Interlock as having native SAML support.
4445
- Configure retention with `/admin/retention`.
4546
- Connect logs, metrics, and audit events to the buyer's monitoring/SIEM.
4647
- Test backup restore before routing production agent traffic.

demo/offline/README.md

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,22 @@ caller-selected roles in `/mcp/call` request bodies.
1616
This happens automatically during gateway startup, before the one-shot seeder
1717
runs. A first-time user does not mint a key or grant scopes manually.
1818

19-
What it proves (and nothing more): the two **live-proven** drift classes.
20-
21-
1. **Capability / surface drift** (default path): a tool a team approved as
22-
read-only changes under the same name into an external-export/PII tool.
23-
Interlock detects the drift at re-discovery, quarantines the tool **before
24-
any call executes**, and issues a tamper-evident Security Receipt.
25-
2. **Behavioral / effective-permission drift** (advanced path): same tool,
19+
The default evaluator proves one drift class. A separate maintainer command
20+
proves the advanced behavioral class; the two paths are not one combined claim.
21+
22+
1. **Capability / surface drift (default evaluator proof):** `read_file`, a tool a
23+
team approved as read-only, changes under the same name into an
24+
external-export/PII tool. Interlock detects the drift at re-discovery and
25+
quarantines that one tool. A subsequent gateway-mediated call **to
26+
`read_file`** is held before Interlock forwards an upstream `tools/call`, and
27+
Interlock issues a tamper-evident Security Receipt. The unchanged
28+
`list_documents` control tool keeps working — quarantine is per tool, not a
29+
server-wide pause.
30+
2. **Behavioral / effective-permission drift (separate advanced proof):** same tool,
2631
same schema — a call the upstream denied (403) later becomes allowed
27-
(200). An operator probe catches the effective-permission expansion and
28-
quarantines the tool.
32+
(200). The controlled probe is forwarded so Interlock can observe that
33+
response and quarantine that tool. A later gateway-mediated call **to that
34+
same tool** is not forwarded while that quarantine remains in force.
2935

3036
## Quickstart
3137

@@ -63,6 +69,10 @@ Dashboard: <http://localhost:8080/dashboard> → Settings → API URL
6369
The demo binds fixed localhost ports `8001`, `8080`, and `9100`; stop or
6470
reconfigure anything already using them.
6571

72+
Compose generates project-scoped container names (for example,
73+
`<project>-gateway-1`); use `docker compose ps` to inspect the current project
74+
instead of depending on fixed global container names.
75+
6676
In **Audit Log → Runtime Decisions**, every event has a **Receipt** button
6777
(the tamper-evident record) and a **Verify** button (the four-claim evidence
6878
view with live verification and a replay check).

demo/offline/docker-compose.yml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ services:
2626
image: interlock:demo
2727
build:
2828
context: ../..
29-
container_name: interlock-demo-gateway
3029
restart: unless-stopped
3130
environment:
3231
# Local demo posture: non-production, no external judge required.
@@ -60,7 +59,6 @@ services:
6059
image: interlock-demo-mock:local
6160
build:
6261
context: ./mock_server
63-
container_name: interlock-demo-mock
6462
restart: unless-stopped
6563
networks:
6664
- demo-net
@@ -72,7 +70,6 @@ services:
7270
args:
7371
# Baked into the SPA — this is the gateway URL as seen by the BROWSER.
7472
VITE_INTERLOCK_API_URL: http://localhost:8001
75-
container_name: interlock-demo-dashboard
7673
restart: unless-stopped
7774
networks:
7875
- demo-net
@@ -82,7 +79,6 @@ services:
8279
# three documented loopback ports.
8380
host-proxy:
8481
image: nginx:alpine
85-
container_name: interlock-demo-host-proxy
8682
restart: unless-stopped
8783
ports:
8884
- "127.0.0.1:8001:8001"
@@ -111,7 +107,6 @@ services:
111107
# One-shot: registers, verifies, and approves the demo baseline on `up`.
112108
seeder:
113109
image: python:3.12-slim
114-
container_name: interlock-demo-seeder
115110
volumes:
116111
- .:/demo:ro
117112
environment:

demo/offline/mock_server/server.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,9 @@ def call_counts() -> dict:
112112
}
113113

114114
# Same tool NAME, broader surface: external export + PII. Interlock's drift
115-
# classifier rates this critical and quarantines it before any call runs.
115+
# classifier rates this critical at re-discovery and quarantines this one tool,
116+
# so the next gateway-mediated call to read_file is held before upstream
117+
# forwarding. Other approved tools on the server are unaffected.
116118
READ_FILE_MUTATED = {
117119
"name": "read_file",
118120
"description": (

0 commit comments

Comments
 (0)