CrawlCheck
Specification · assurance case

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.

1 · Claim

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.

2 · Scope

Exactly what is compared, where and when

  • Transport: MCP servers reached through the official client SDKs (JS withPreflight, Python with_preflight / McpPreflight), the framework adapters for the OpenAI Agents SDK, LangChain MCP adapters and CrewAI (at every tool-list fetch), or any stack that calls continuityCheck itself.
  • 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 first tools/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.
3 · Assumptions

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 lock and continuity set.
4 · Policy

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.

ObservationDefault effectMeaning
unchangedproceedthe offered tools match the approved baseline
tool_addedpause_toola tool the baseline does not list is offered: unavailable until approved
tool_removedproceeda tool in the baseline is no longer offered
description_changedpause_toolthat tool waits for re-approval; the others stay usable
schema_changedpause_toolthat tool waits for re-approval
annotations_changedpause_toolthat tool waits for re-approval
tool_changedpause_toolchanged, and the baseline has no per-part hashes to say which part
evidence_expiredrefreshask again; if that fails, pause
signature_invaliddenyintegrity class: stays deny unless the operator overrides it on the record
key_unknowndenyintegrity class
key_retireddenyintegrity class
server_identity_mismatchdenyintegrity class: the server answering is not the one approved
server_unavailablepausenothing was compared
tool_list_truncatedpausea further page of tools/list was not read
response_malformedpausethe tool list could not be parsed
baseline_stalepausethe approval is older than the policy allows (90 days by default)
policy_version_mismatchpausethe policy in force is not the one the deployment expects
check_timeoutpausethe check did not finish
service_unavailablepauseCrawlCheck unreachable: the local fallback applies
decision_refuseddenyCrawlCheck's signed decision for the host did not allow the connection
baseline_untrusteddenythe 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.

5 · Pilot

Observe, then review, then enforce

Do not jump from a demo to broad blocking.

ModeWhat happensWhat to measure
observeNothing 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.
reviewEvery change pauses the session for a person; integrity failures still deny.Appropriate pauses, unnecessary interruptions, review time, exception reasons, whether operators bypass it.
enforceThe 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.
6 · Evidence

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_sha256 included: 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 nextCursor is 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.
7 · Acceptance

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.

8 · Frameworks

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.

Baseline

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.

Free pilot

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.

9 · Limitations

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.
10 · Correction

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.