{"criteria_id":"mcp-manifest-drift/v1","service":"manifest-claim-audit","version":"1.1.0","what_is_checked":"Whether a submitted current MCP tool manifest differs from a customer-held baseline, tool by tool, in the fields the digest rules cover (name, description, inputSchema, annotations). The baseline is either anchored by this service's signature (a paid audit report, or its independently signed digest block) or asserted by the customer (a second submitted manifest, labelled as such). The report also re-grades the current manifest under mcp-manifest-claims/v1 and screens the new descriptions of changed tools under prompt-injection-rules/v1, reported separately as a pattern screen.","scope":"This report compares a submitted current manifest against a customer-held baseline and states, per tool, whether the fields covered by the published digest rules differ. It does not execute the server, observe its runtime behaviour, or grade the intent behind any difference.","retention":"Drift inputs — the current manifest, any baseline report, digest block, or baseline manifest, and any submitted URL — are processed in memory only and are never stored: not in KV, not in logs, not in the ledger. The payments ledger records only the payer, the time, the price, and the aggregate outcome counts. Because nothing is kept, a paid retry re-sends the baseline material and the current manifest (or its URL).","canonicalisation":{"digest_fields":["name","description","inputSchema","annotations"],"form":"Each tool's digest input is the JSON object holding exactly the digest_fields that are present on the tool, canonicalised as recursively sorted-key JSON with no whitespace (the same canonicalJson used for report signatures).","missing_fields":"A field contributes only when it is an own property of the tool object and its value is not undefined. An absent key is omitted from the digest input; an explicit null is present and is included as null — absent and null hash differently.","non_object_entries":"A tool entry that is not a JSON object has none of the four fields; its digest input is the object { \"__non_object_tool__\": <the raw entry> }, canonicalised the same way, so distinct non-object entries get distinct digests and cannot collide with object tools. Its pairing name is null.","pairing_name":"A tool's name for pairing and for the digest list: its name when that is a non-empty string after trimming, otherwise null.","per_tool_digest":"sha256, lowercase hex, of the UTF-8 bytes of the canonical digest input.","field_digests":"The digest block also carries, per tool, the sha256 hex of the canonical form of each of description, inputSchema, and annotations when present (null when absent). These name which fields changed in a comparison against a signed baseline; they are not part of the aggregate.","aggregate":"manifest aggregate digest = sha256, lowercase hex, of the canonical JSON of the ordered list [{ name, digest }, ...] — one entry per tool, in manifest order, exactly those two keys per entry.","formatting_independence":"Key order and whitespace in a submitted manifest do not affect any digest: a reformatted copy of the same manifest produces the same digests and compares as unchanged."},"digest_block":{"fields":{"criteria_id":"mcp-manifest-drift/v1","algorithm":"sha256","digest_fields":["name","description","inputSchema","annotations"],"tool_count":"number of tools in the issuing manifest","as_of":"issue time of the audit that produced the block","tools":"[{ name, digest, field_digests }]","aggregate_digest":"the aggregate defined above"},"independently_signed":"The full paid audit report carries the block as manifest_digest plus manifest_digest_signature — an ed25519 signature over the canonical JSON of the block alone — so the block and its signature can be submitted on their own, without the full report. The report-level signature also covers the block as part of the report.","internal_consistency":"A submitted block is accepted only if its aggregate recomputes from its tool digests, its tool_count matches, and every digest is a 64-character lowercase hex string; a block altered after signing fails its signature and is rejected before any payment is verified."},"input_contract":{"format":"JSON POST body","baseline_side":{"baseline_report":"the full paid audit report object from a previous audit of the baseline manifest (signed mode; its signatures are verified).","baseline_digests_plus_baseline_signature":"the manifest_digest block exactly as issued, plus the manifest_digest_signature (the signature object or its base64 value). Both fields are required together (signed mode).","baseline_manifest":"a manifest in any accepted shape ({ tools: [...] }, { result: { tools: [...] } }, or a bare array) — fallback mode; the baseline is customer-asserted and the report says so, in the words of the fallback honesty text below.","exclusivity":"Exactly one baseline mode per call; more than one, an incomplete digest pair, or none at all is a 400 error, returned before any payment is verified."},"current_side":{"manifest":"the current tools/list document, in any accepted shape. Mutually exclusive with url.","url":"the URL of a tools/list JSON document to fetch under the fetch contract and compare as the current manifest. Mutually exclusive with manifest.","caller":"Solana base58 wallet of the operator (required for the free drift pre-check; keys its allowance). On the paid drift call the payer is read from the payment itself."},"caps":{"max_body_bytes":524288},"validation_before_payment":"Everything is validated before any payment is verified: body shape, baseline exclusivity, baseline signature verification, digest-block consistency, current-manifest shape, URL validation, the URL fetch and parse, and the zero-tools checks on both sides. No failure at this stage consumes a payment signature.","zero_tools":"Zero tools on either side returns the signed could-not-compare shape (verdict 'COULD NOT COMPARE — the baseline or the current manifest contains no tools, or (in URL mode) the current manifest document could not be fetched and parsed as a tools/list document, so no comparison could be made. This is not a statement that a manifest is unchanged.'). On the paid endpoint this is a 422 returned before any payment is verified, so it never consumes a payment.","fallback_honesty":"The baseline for this comparison was asserted by the customer (a submitted manifest), not anchored by a service-signed report; the report states the difference between the two submitted documents only.","signed_baseline_note":"The baseline for this comparison is anchored by a service-signed audit report or by that report's independently signed digest block; baseline_as_of names when the baseline was issued.","origin_values":{"signed_report":"baseline_report mode, or baseline_digests mode (the digest block is part of a signed report, submitted detached — the anchor is the same signature).","customer_asserted":"baseline_manifest mode."}},"comparison":{"pairing":"Tools are grouped by pairing name, preserving manifest order. Occurrences pair in order: the i-th baseline occurrence of a name pairs with the i-th current occurrence. Unpaired baseline occurrences are REMOVED; unpaired current occurrences are ADDED.","statuses":{"UNCHANGED":"paired occurrences with equal per-tool digests","CHANGED":"paired occurrences with different per-tool digests; the report carries the field-level diff","ADDED":"a current occurrence with no baseline counterpart","REMOVED":"a baseline occurrence with no current counterpart"},"report_order":"Names in order of first appearance in the baseline, then names appearing only in the current manifest in order of first appearance; pairs in occurrence order within a name.","diff_fallback_mode":"With both manifests in hand: description { old, new }; inputSchema property lists — added and removed property names, changed properties with { property, old, new }, and a flag (with the full old/new schema) when the schema differs outside its properties; annotations as per-key { old, new } over the union of keys. Only differing fields appear.","diff_signed_mode":"Against a signed baseline the old field values are not held anywhere — the baseline is digests only — so the diff names the changed fields (from the per-field digests), shows the new field values and the current schema's property names, and marks the old values unavailable (old_available: false). The report never reconstructs old values it does not hold.","counts":"{ unchanged, changed, added, removed } — counts of tool occurrences in each status."},"verdict_vocabulary":{"unchanged":"MANIFEST UNCHANGED","changed":"MANIFEST CHANGED (n tools) — n is the number of differing tool occurrences: changed + added + removed.","separation":"A status of CHANGED is arithmetic: two digests differ. A match in the pattern screen is a screen result under a published rulebook. Neither states why a difference exists, and intent is not graded anywhere in a drift output."},"regrade":"Every full drift report re-grades the current manifest under mcp-manifest-claims/v1 (the manifest-claim criteria, GET /criteria) and carries that output in full: score, counts, per-tool checks, findings, and fix list, under its own criteria id.","changed_description_scan":{"rulebook":"prompt-injection-rules/v1","scope":"The new descriptions of CHANGED tools only. Added tools are not screened: first-sight content is graded by the manifest audit and by the injection verdict service, not by a drift comparison. The scan section of every report states this scope.","labelling":"The scan is a pattern screen, reported in its own section, visibly separate from the drift findings. Each match is reported as { id, category, start, end } (plus the normalization view when the match came from one), positions being JavaScript string indices into the tool's new description. No scan verdict is issued by a drift report.","categories":{"instruction_override":"Commands telling the agent to ignore, discard, or replace its existing instructions.","role_hijack":"Text that impersonates a system/developer role or tries to switch the agent into an unrestricted persona.","delimiter_escape":"Chat-template or context-boundary markers smuggled into untrusted text to fake a new system turn or escape the current context.","exfiltration":"Instructions to send, reveal, or leak the conversation, the system prompt, credentials, or user data to a third party.","tool_coercion":"Fake tool/function-call markup, or instructions to invoke tools without the operator's normal confirmation.","wallet_drain":"Instructions to move funds, sign draining transactions, or hand over seed phrases / private keys.","concealment":"Instructions to hide the text's existence or the agent's actions from the user or operator.","obfuscation":"Invisible characters and other structural tricks used to hide content from a reader or a scanner."},"rules":[{"id":"IO-1","category":"instruction_override","severity":"malicious","description":"A command to ignore, disregard, forget, override, or bypass earlier instructions, prompts, rules, directions, or guidelines.","pattern":"\\b(ignore|disregard|forget|override|bypass)\\b[^.\\n]{0,40}\\b(previous|prior|above|earlier|all|any|the)\\b[^.\\n]{0,24}\\b(instructions?|prompts?|rules?|directions?|guidelines?|messages?)\\b","flags":"i"},{"id":"IO-2","category":"instruction_override","severity":"malicious","description":"A replacement-instruction header: 'new instructions:' or 'here are your new instructions'.","pattern":"\\b(new instructions?\\s*:|here are your new instructions?\\b)","flags":"i"},{"id":"IO-3","category":"instruction_override","severity":"malicious","description":"A statement releasing the agent from its instructions: 'you are no longer bound by', 'stop following', 'cease following', 'no longer follow'.","pattern":"\\b(you are no longer bound by|stop following|cease following|no longer follow)\\b","flags":"i"},{"id":"IO-4","category":"instruction_override","severity":"suspicious","description":"A persistent behaviour directive addressed to the agent: 'from now on, you ...'.","pattern":"\\bfrom now on,?\\s+you\\b","flags":"i"},{"id":"RH-1","category":"role_hijack","severity":"malicious","description":"Fake role markup in the text: <system>, </system>, <developer>, [system], [developer], or <<SYS>> tags.","pattern":"<\\s*\\/?\\s*(system|developer)\\s*>|\\[\\s*(system|developer)\\s*\\]|<<\\s*SYS\\s*>>","flags":"i"},{"id":"RH-2","category":"role_hijack","severity":"suspicious","description":"A bare role label at the start of a line: 'System:', 'Assistant:', or 'Developer:'.","pattern":"(^|\\n)\\s*(system|assistant|developer)\\s*:","flags":"i"},{"id":"RH-3","category":"role_hijack","severity":"malicious","description":"A jailbreak persona switch: 'DAN mode', 'jailbreak mode', 'developer mode enabled/on/active', or 'do anything now'.","pattern":"\\b(DAN mode|jailbreak mode|developer mode (is )?(now )?(enabled|on|active)|do anything now)\\b","flags":"i"},{"id":"RH-4","category":"role_hijack","severity":"malicious","description":"An unrestricted-persona claim or assignment: an 'unrestricted / unfiltered / uncensored mode, assistant, AI or model', or 'act as / pretend / roleplay ... without any restrictions, rules or limits'.","pattern":"\\b(unrestricted|unfiltered|uncensored)\\s+(mode|assistant|ai|model)\\b|\\b(act as|pretend|roleplay)\\b[^.\\n]{0,60}\\bwithout (any )?(restrictions|rules|limits)\\b","flags":"i"},{"id":"DE-1","category":"delimiter_escape","severity":"malicious","description":"A model special-token marker of the form <|...|> (for example <|im_start|>, <|im_end|>, <|endoftext|>).","pattern":"<\\|[^|>\\n]{0,40}\\|>","flags":"i"},{"id":"DE-2","category":"delimiter_escape","severity":"malicious","description":"A chat-template instruction marker: [INST], [/INST], or [SYS].","pattern":"\\[\\/?(INST|SYS)\\]","flags":"i"},{"id":"DE-3","category":"delimiter_escape","severity":"suspicious","description":"Context-boundary prose: 'end of the system prompt / instructions / context', or 'begin a new/fresh context, conversation or session'.","pattern":"\\b(end of (the )?(system )?(prompt|instructions|context)|begin (a )?(new|fresh) (context|conversation|session))\\b","flags":"i"},{"id":"EX-1","category":"exfiltration","severity":"malicious","description":"An instruction to send, post, upload, forward, transmit, email, leak, exfiltrate, reveal, or disclose the conversation (history), chat history, system prompt/message, the agent's instructions, API keys, credentials, passwords, secret/private keys, a seed phrase, or user/personal data.","pattern":"\\b(send|post|upload|forward|transmit|email|leak|exfiltrate|reveal|disclose)\\b[^.\\n]{0,50}\\b(conversation( history)?|chat history|system prompt|system message|your instructions|initial instructions|api keys?|credentials|passwords?|secret keys?|private keys?|seed phrase|user data|personal data)\\b","flags":"i"},{"id":"EX-2","category":"exfiltration","severity":"malicious","description":"An instruction to reveal, print, show, repeat, output, display, recite, or tell the system prompt, system message, initial/hidden/developer instructions, or an API key.","pattern":"\\b(reveal|print|show|repeat|output|display|recite|tell me)\\b[^.\\n]{0,40}\\b(your |the )?(system prompt|system message|initial instructions|hidden instructions|developer instructions|api key)\\b","flags":"i"},{"id":"EX-3","category":"exfiltration","severity":"suspicious","description":"A URL carrying a long encoded-looking value (24+ URL-safe characters) in a query parameter — the shape of data smuggled out inside a link.","pattern":"https?:\\/\\/[^\\s)\"']{8,}?[?&][a-z_]{1,12}=[A-Za-z0-9+/=_-]{24,}","flags":"i"},{"id":"TC-1","category":"tool_coercion","severity":"malicious","description":"Fake tool-call markup in the text: <function_calls>, <function_call>, <invoke>, or <tool_call> tags.","pattern":"<\\s*(function_calls|function_call|invoke|tool_call)\\b","flags":"i"},{"id":"TC-2","category":"tool_coercion","severity":"malicious","description":"An instruction to call, invoke, run, execute, or use a function/tool without (or before) asking, confirmation, permission, approval, or telling.","pattern":"\\b(call|invoke|run|execute|use)\\b[^.\\n]{0,30}\\b(function|tool)\\b[^.\\n]{0,50}\\b(without|before)\\b[^.\\n]{0,24}\\b(asking|confirm|confirmation|permission|approval|telling)\\b","flags":"i"},{"id":"TC-3","category":"tool_coercion","severity":"suspicious","description":"An instruction to run or execute 'this' or 'the following' command, script, shell, or code.","pattern":"\\b(run|execute)\\s+(this|the following)\\s+(command|script|shell|code)\\b","flags":"i"},{"id":"WD-1","category":"wallet_drain","severity":"malicious","description":"An instruction to transfer, send, move, drain, withdraw, or sweep funds, a balance, tokens, assets, SOL, USDC, or crypto to a destination.","pattern":"\\b(transfer|send|move|drain|withdraw|sweep)\\b[^.\\n]{0,40}\\b(all|the|my|your|his|her|their)?\\s*(funds|balance|tokens?|assets|sol|usdc|crypto(currency)?)\\b[^.\\n]{0,40}\\bto\\b","flags":"i"},{"id":"WD-2","category":"wallet_drain","severity":"malicious","description":"A request to enter, type, provide, share, give, input, or paste a seed phrase, private key, recovery phrase, or mnemonic.","pattern":"\\b(enter|type|provide|share|give|input|paste)\\b[^.\\n]{0,30}\\b(seed phrase|private key|recovery phrase|mnemonic)\\b","flags":"i"},{"id":"WD-3","category":"wallet_drain","severity":"malicious","description":"A seed phrase, private key, recovery phrase, or mnemonic named together with a demand to enter, type, provide, share, give, input, or paste it (or 'here' / 'below').","pattern":"\\b(seed phrase|private key|recovery phrase|mnemonic)\\b[^.\\n]{0,30}\\b(enter|type|provide|share|give|input|paste|here|below)\\b","flags":"i"},{"id":"WD-4","category":"wallet_drain","severity":"suspicious","description":"An instruction to 'sign this/the/a transaction'.","pattern":"\\bsign (this|the|a) transaction\\b","flags":"i"},{"id":"CC-1","category":"concealment","severity":"malicious","description":"An instruction not to tell, inform, notify, or alert the user, human, operator, owner, or anyone ('do not / don't / never tell ...').","pattern":"\\b(do not|don't|never)\\s+(tell|inform|notify|alert)\\b[^.\\n]{0,24}\\b(the )?(user|human|operator|owner|anyone)\\b","flags":"i"},{"id":"CC-2","category":"concealment","severity":"suspicious","description":"'Keep this secret / hidden / confidential from ...'.","pattern":"\\bkeep this (secret|hidden|confidential) from\\b","flags":"i"},{"id":"CC-3","category":"concealment","severity":"suspicious","description":"Output forcing: 'respond only with ...'.","pattern":"\\brespond only with\\b","flags":"i"},{"id":"CC-4","category":"concealment","severity":"suspicious","description":"'Never mention these/this/your/the instructions, prompt, or rules'.","pattern":"\\bnever mention (these|this|your|the) (instructions?|prompt|rules)\\b","flags":"i"},{"id":"OB-1","category":"obfuscation","severity":"suspicious","description":"Invisible characters embedded in the text: zero-width space/joiners (U+200B-U+200D), word joiner (U+2060), byte-order mark (U+FEFF), or Mongolian vowel separator (U+180E). Adjacent matches of this rule merge into one span.","pattern":"[\\u200B-\\u200D\\u2060\\uFEFF\\u180E]","flags":"i"}],"normalization_views":"As published by the rulebook of record: the text as submitted; a leetspeak-folded view (0->o, 1->i, 3->e, 4->a, 5->s, 7->t, @->a, $->s, reported only when a substitution occurred inside the span); a percent-decoded view (positions mapped back, reported only when the span contains a %XX sequence); and base64-decoded tokens of 16+ characters whose decoding is valid text, scanned with the same rules and reported at the token's span."},"fetch_contract":{"applies_to":"URL mode only ({ url } submissions). Manifest mode ({ manifest } submissions) fetches nothing.","method":"Read-only GET requests, the submitted URL fetched at most once per audit, following redirects; the fetched document is graded as the manifest.","user_agent":"ExchangeRebel-ManifestClaimAudit/1.1 (manifest-claim audit; read-only fetch of a submitted manifest document; contact: toll-api)","urls_fetched":["the submitted manifest URL only"],"never_fetched":"No crawling, no other paths, no form submission, no login, no POSTs to the submitted URL's origin, and the MCP server at any endpoint named in the manifest is never contacted or executed.","caps":{"per_document_bytes":524288,"per_document_timeout_ms":10000,"documents_truncated_at_cap":"a document larger than the cap is NOT graded on a truncated copy — the report is the could-not-grade shape naming the cap"},"url_rules":"http/https only; no embedded credentials; no localhost or .local/.internal names; IP literals in private, loopback, link-local, or reserved ranges are rejected; ports 80, 443, 8080, 8443 only. An invalid URL is rejected before any payment is verified and never consumes a payment."},"prices":{"drift_precheck":"free — headline only, 100/day per caller wallet, rate-limited (minimum interval between free drift pre-checks)","drift":{"usd":0.05,"includes":"$0.05 buys one drift comparison report for one submitted manifest against one customer-held baseline. This report compares a submitted current manifest against a customer-held baseline and states, per tool, whether the fields covered by the published digest rules differ. It does not execute the server, observe its runtime behaviour, or grade the intent behind any difference."}}}