Tool-interface continuity
One promise, stated so it can be tested: your agent will not silently proceed with a changed tool definition when your policy requires renewed approval. Before an agent uses a third-party MCP server, the check compares the tools offered now with the version you approved, applies your policy, and keeps a record of what was compared, which policy decided, and what the runtime was told to do.
What is claimed
An integrated runtime detects a difference between an approved tool interface and the interface observed at the check point, then follows the configured policy: proceed, pause the changed tool, pause the session, or deny. Every check leaves a crawlcheck-continuity-record whose record_sha256 covers every field in it.
Exactly what is compared, where and when
- Transport: MCP servers reached through the official client SDKs (JS
withPreflight, Pythonwith_preflight/McpPreflight), the framework adapters for the OpenAI Agents SDK, LangChain MCP adapters and CrewAI (at every tool-list fetch), or any stack that callscontinuityCheckitself. - Fields: each tool’s name, description, input schema and annotations, hashed per part (
lock.parts) so a change is named; the list as a whole (tools_sha256); the server URL the baseline was approved for; the baseline’s age. - Check point: after
initialize, before the session’s firsttools/call. Before a call only when you call it there. This is not a check of every executed call, and not of the code behind a tool. - Versions: @crawlcheck/sdk 1.11.0 and crawlcheck (Python) 1.12.0 and later; the framework adapters from @crawlcheck/sdk 1.12.0 and crawlcheck 1.13.0.
What has to be true for the claim to hold
- The baseline was reviewed and approved by a person you trust, and stored where the agent cannot rewrite it.
- The issuer is configured: the runtime checks receipts against CrawlCheck’s published key directory (or keys you pinned).
- Clocks agree to within 5 minutes (the receipts’
clock_skew_s). - The integration actually invokes the check: the wrapper is on the client the agent uses, with
lockandcontinuityset.
What each observation leads to by default
The operator’s policy decides; every value can be changed, and the record names the policy’s sha256.
| Observation | Default effect | Meaning |
|---|---|---|
unchanged | proceed | the offered tools match the approved baseline |
tool_added | pause_tool | a tool the baseline does not list is offered: unavailable until approved |
tool_removed | proceed | a tool in the baseline is no longer offered |
description_changed | pause_tool | that tool waits for re-approval; the others stay usable |
schema_changed | pause_tool | that tool waits for re-approval |
annotations_changed | pause_tool | that tool waits for re-approval |
tool_changed | pause_tool | changed, and the baseline has no per-part hashes to say which part |
evidence_expired | refresh | ask again; if that fails, pause |
signature_invalid | deny | integrity class: stays deny unless the operator overrides it on the record |
key_unknown | deny | integrity class |
key_retired | deny | integrity class |
server_identity_mismatch | deny | integrity class: the server answering is not the one approved |
server_unavailable | pause | nothing was compared |
tool_list_truncated | pause | a further page of tools/list was not read |
response_malformed | pause | the tool list could not be parsed |
baseline_stale | pause | the approval is older than the policy allows (90 days by default) |
policy_version_mismatch | pause | the policy in force is not the one the deployment expects |
check_timeout | pause | the check did not finish |
service_unavailable | pause | CrawlCheck unreachable: the local fallback applies |
decision_refused | deny | CrawlCheck's signed decision for the host did not allow the connection |
baseline_untrusted | deny | the approved lockfile's signature does not verify, or its key is neither CrawlCheck's nor yours (an unsigned lockfile only when you require a signed one) |
Effects, weakest to strongest: proceed, proceed_recorded (an approved exception, with who approved it and until when), refresh, pause_tool, pause, deny. An exception is never turned into an ordinary pass: the record keeps it. Nothing here reads a description for intent; a changed description is a reason to review, not evidence of malice.
Observe, then review, then enforce
Do not jump from a demo to broad blocking.
| Mode | What happens | What to measure |
|---|---|---|
| observe | Nothing is blocked. Every check is recorded with what the policy would have done (would). | How often tools change; whether the differences are understandable; how many records the operator calls actionable; added latency. |
| review | Every change pauses the session for a person; integrity failures still deny. | Appropriate pauses, unnecessary interruptions, review time, exception reasons, whether operators bypass it. |
| enforce | The policy applies as written: a changed or new tool is paused on its own while the rest stay usable. | Only the classes the review stage showed to be reliable. |
How the claim is tested
- Golden records. 17 interface cases (unchanged, legitimate description update, schema, annotations, added, removed, legacy baseline, truncated list, malformed response, old baseline, policy-version mismatch, different server, observe, review, approved exception, expired exception, operator policy) and all 14 failure classes in all 3 modes, plus the signed-baseline cases (CrawlCheck-signed, edited after signing, re-signed with a foreign key, operator-signed, wrong operator key, unsigned), in one file both SDKs must reproduce byte for byte,
record_sha256included: continuity.json. - End to end. Each SDK wraps a test MCP client and shows: only the changed tool pauses; a person can re-approve it; observe never blocks; review pauses the session; an unfollowed
nextCursoris a truncated list, not a pass; an unreadable list is malformed; CrawlCheck unreachable applies the fallback. - Receipts. The 64-case guard acceptance suite (expired, unknown key, retired key, foreign domain, replayed nonce, other operation, redirects).
- Pilot traces. None yet. This page will list them, with the operators’ permission, as they exist.
Signature valid is not safe to proceed
guard() returns the questions separately: integrity_valid, issuer_trusted, key_policy_valid, freshness_valid, subject_matches, operation_matches, replay_safe, baseline_matches, assessment_complete, policy_source, evidence_level, and policy_result (proceed, pause or deny). Each decision receipt names its authority (which policy produced it; the deployment’s own policy decides whether the agent acts) and its evidence_assurance: A record integrity, B reproducible decision, C corroborated reading, each reached or not with the reason. POST /api/v1/preflight/replay re-runs any receipt’s decision from the archived answer and the inputs it carries; /api/v1/preflight/rules serves the rule tables that hash to engine.rules_sha256.
In your agent framework
Observe blocks nothing. No new dependency: each adapter uses the framework you already have.
Each adapter runs the check where the framework fetches the server’s tool list, against your approved lockfile. observe records and passes everything through; review pauses the server on any change; enforce withholds a changed or new tool until confirm re-approves it. A refusal raises ContinuityRefused with the record.
OpenAI Agents SDK (Python, pip install crawlcheck openai-agents)
from crawlcheck.openai_agents import with_continuity
async with MCPServerStreamableHttp(params={"url": url}) as server:
agent = Agent(name="a", instructions="...",
mcp_servers=[with_continuity(server, lock, mode="observe")])OpenAI Agents SDK (JS, npm i @crawlcheck/sdk @openai/agents)
import { withContinuity } from "@crawlcheck/sdk";
await server.connect();
const agent = new Agent({ name: "a", instructions: "...",
mcpServers: [withContinuity(server, lock, { mode: "observe" })] });LangChain (Python, pip install crawlcheck langchain-mcp-adapters)
from crawlcheck.langchain import load_mcp_tools_with_continuity
client = MultiServerMCPClient({"shop": {"transport": "streamable_http", "url": url}})
tools = await load_mcp_tools_with_continuity(client, lock, server_name="shop", mode="observe")LangChain (JS, npm i @crawlcheck/sdk @langchain/mcp-adapters)
import { loadMcpToolsWithContinuity } from "@crawlcheck/sdk";
const tools = await loadMcpToolsWithContinuity("shop", client, lock, { mode: "observe" });CrewAI (Python, pip install crawlcheck "crewai-tools[mcp]")
from crawlcheck.crewai import with_continuity
with with_continuity(MCPServerAdapter(server_params), lock, mode="observe") as tools:
agent = Agent(role="...", goal="...", backstory="...", tools=tools)Records go to on_record / onRecord, a JSON-lines file (log_path, Python) and .gate.records / .continuity.records. Move to mode="review", then "enforce", once the observe records look right. Full options: SDK docs.
Check the baseline before trusting it
Once, at startup. Not on every call.
Whoever can edit the approved lockfile can approve their own tools. So the gate checks the lockfile’s signature when it is created, before it compares anything against it. A lockfile from /api/v1/mcp/lock is signed by CrawlCheck and must verify with a CrawlCheck key: verified. The SDKs ship the published key ids; pass the live key directory (keyPolicy) to follow a rotation. A lockfile you changed yourself, for example to drop a tool you do not want, you sign with your own Ed25519 key (signLock / sign_lock) and give the gate the public key (operatorKeys / operator_keys): operator_signed. Anything else is unsigned.
A signature that does not verify, a lockfile edited after signing, or a key that is neither CrawlCheck’s nor yours is baseline_untrusted: enforce and review refuse at startup, observe records it and goes on. An unsigned lockfile is refused only when you require a signed one (requireSignedBaseline / require_signed_baseline, on by default once you give an operator key). Every record names the result in baseline.trust. Signed lockfiles now carry the per-part hashes (parts) inside the signature; parts added to a signed lockfile afterwards are not used.
Run it on your agent, in observe mode
Nothing in your agent changes. Every check is recorded, and a weekly report shows what changed on your servers.
You get a pilot id and a token (shown once), a two-line SDK change, and a weekly report joining your own records with what our registry sweep saw change on your servers. Records are checked on arrival: one edited after it was made is refused.
What this does not establish
- That an unchanged description means unchanged code behind it.
- That a tool, or its publisher, is harmless or honest.
- That a user authorised a particular call, or that the call was carried out as assessed.
- That no prompt injection exists, in the tool definitions or in what the tools return.
- Anything between check points: a server can change after the check.
When an assessment is wrong
Report it at /corrections with the record or receipt. A correction is versioned: the original record stays as it was, the correction names it by record_sha256 or receipt sha256, the rule or default that changes is published with its new hash, and the SDK golden file gains the case so both implementations hold the corrected behaviour.