Describe the surface in model-readable metadata, don't direct the model - #167
Conversation
The name registry exists to route agents to well-known apps; routing them to a staking/funds frontend sits badly with the server's not-a-financial-tool posture, so the NNS entry is removed: no name resolution for "nns", no repair guidance toward it, and the unknown-app note no longer enumerates it. The KNOWN_DERIVATION_ORIGINS mappings stay: a user who brings an NNS URL themselves still derives the correct per-app identity for reads instead of a silently wrong (empty-looking) one, and update calls to the NNS canisters are refused by the finance-canister guard added alongside this change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk
Adds a third group to the financial-transactions guard: a curated, labeled disallow list of canisters whose purpose is holding, staking, exchanging, or moving value. On these, canister_update_call refuses EVERY update method; each entry carries what the service is and how it is financial, and the refusal interpolates both labels verbatim. The list (49 entries, every id verified against the IC dashboard's canister registry and the services' own published sources on 2026-08-28): - The network's own funds infrastructure: NNS Governance (manage_neuron disburses neuron-held ICP), both NNS dapp frontends, the Genesis Token canister, the ICP ledger, the cycles ledger. - Chain-key minters (they move REAL assets on other chains): ckBTC, ckETH/ckERC20, ckDOGE. - All 14 mainnet chain-key token ledgers. - Five prominent SNS governance canisters (OpenChat, Gold DAO, WaterNeuron, KongSwap, Dragginz) — the same disburse pattern. - Wallets: the Oisy backend, the Chain Fusion Signer (signs and broadcasts Bitcoin/Ethereum transactions), Oisy Trade, the Oisy rewards canister, and the wallet frontend. - Exchanges: ICPSwap (SwapFactory, PasscodeManager, farm and staking-pool factories, frontend), KongSwap (backend, treasury extension, frontend), Sonic (swap, LBP registry, vesting, plus the XTC and WICP DIP-20 ledgers whose legacy transfer names the ICRC group deliberately does not match), ICDex/ICLighthouse (router, DexAggregator, DAO trader, trading-mining, frontend), and ICPEx (router, token-creation service, frontend). Method-level refusals keep precedence, so the tailored messages survive (a creation spend still points at the icp CLI; an ICRC hit still names the standard method); the canister blanket catches everything else on listed canisters. The CMC stays deliberately unlisted (its notify_* calls are the documented recovery path), as do read-only indexes/archives and the exchange-rate canister. The module docs note the honest limit: exchanges create per-pair pool canisters dynamically, so the list covers each service's central canisters and the ICRC method group plus the stated policy cover the rest. Tests: every entry refuses arbitrary update names with both labels and the protective policy wording; the list is well-formed (valid, unique principals, non-empty labels); method-level precedence is pinned. The server instructions, README tool table, both directory docs, and the landing page now state the canister-level refusal alongside the method refusals. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk
Per maintainer review on #163: the service no longer exists (its site and repository are gone), so its backend, treasury extension, frontend, and SNS governance come off the list — a dead service does not meet the list's prominence bar. The list stands at 45 entries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk
Per maintainer review on #163: the submission docs read as if a gap had been closed at some point, rather than describing a product that simply does not support financial transactions. The financial sections of both directory docs now state the posture present-tense only — the "(resolved in code)" heading, the shipped-mitigations lead with its PR citations, the "execution paths that once moved funds are removed from the binary" sentences, the "per maintainer review" attributions, and the "Status: resolved in code" gap-closure paragraph are gone. The mcp-review email logistics stay (no reply needed; answer with the stated posture if one arrives), and the production-gap blocker keeps its operational fact (deploy a release from current main before submitting) without dwelling on what the old build serves. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk
Answering review on #163 ("why not list all SNS governance canisters?"): enumerating them can't stay current — 54 SNS DAOs exist today (a third of them aborted swaps, some renamed or folded) and new ones launch by NNS proposal. Their value-moving surface is one standardized method, so the guard now refuses `manage_neuron` on EVERY canister, the way it refuses the ICRC transfer/approval names: that covers each existing SNS DAO's governance, every future one, and the NNS's, with no list to go stale. The four per-DAO governance entries come off the canister list (the NNS Governance entry stays — the network's own governance keeps its whole update surface refused), and the standardized-methods group is renamed and re-documented to say it carries both the ICRC token surface and the governance interface. Also per review: the Chain Fusion Signer label had an accidental run of spaces (interpolated verbatim into refusals) — collapsed; and the gate function's doc now describes all three refusal scopes instead of the stale "both groups" wording. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk
Per review on #163. WaterNeuron (liquid staking): the water_neuron protocol canister (ICP deposits, nICP minting, unstaking withdrawals), the nICP ledger, and the frontend — ids pinned by the project's own canister_ids.json and the dashboard registry; the nICP index/archive (read-only) and the icrc_validator (proposal validator) stay off, as do the two SNS-registered dapp canisters with no documented role. MULTI/DEX (exchange): its backend, bridge, and frontend, self-declared by the app's own /.well-known/ic-app.json manifest (the frontend id also matches the live x-ic-canister-id header and the known-app registry). The list stands at 47 entries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk
Review on #163: the guard refuses manage_neuron everywhere, but the server instructions, README tool row, both directory docs, and the landing page still enumerated only the ICRC methods, the ledger-scoped methods, and the canister list — understating the guard and leaving SNS neuron coverage unexplained. All four surfaces now name the governance method alongside the ICRC names. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk
…names Three review points on the finance-canister disallow list, plus one risk-reduction change: - The cycles-minting canister's update surface is refused. Its four user-callable methods (notify_top_up, notify_create_canister, notify_mint_cycles, create_canister) each complete a funding operation: the ICP debit happened earlier, but the call is what finishes the flow, which makes it a concrete financial path rather than a theoretical one. Their refusal points at the icp CLI, which is also how an interrupted mint is recovered — so the recovery path is preserved, just not through the connector. The review named the two notify_* completions; the other two are the same interface and the same class of call, so all four are listed together. - canister_update_call's description discloses the refusal. Both directories require a description to match actual behavior, and OpenAI's guidelines say a side effect must never be implicit, so the description now carries one factual sentence: financial operations and update calls to known financial-service canisters are not supported and return an error. The full policy — refused method families and the reason — stays in the server-level instructions rather than being restated per tool. The test that prohibited the word "financial" in that description is replaced by one that pins the disclosure. - No refusal names a venue any more. Refusal messages, the server instructions, the README, the landing page, and the submission doc all now say the operation belongs outside the connector, in a trusted interface the user controls, instead of naming a wallet. Metadata that answered a refused financial operation with a specific transactional service would read as a redirect from one such route to another. Canister creation and funding keep the icp CLI pointer: that is the user's own tooling, and this connector already says that work happens there. - The OpenAI submission's policy section is corrected. It claimed that OpenAI's restriction was usefully narrower than Anthropic's; it is not — the app guidelines prohibit execution of money transfers, crypto transfers, and investment trades outright. The section is rewritten around the same non-financial posture the Anthropic submission states. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk
The server instructions and the 11 served tool descriptions told a model how to work — "START WITH open_app", "ALWAYS call this FIRST", "NEVER guess a domain", "web search the official URL", a numbered seven-step flow across tools, and a routing chain to run before answering. Both directories read these fields: OpenAI scans server instructions and prohibits model-readable fields that steer how the model works, and descriptions that tell Claude how to behave are a rejection trigger. Every surface now states what it is and how it behaves. The instructions go from 10005 to 3521 characters and cover the value encoding, what the two tool families act on, what a derivation origin is and which reads it gates, the two Internet Identity access levels, how canister values are stored, the financial-transactions policy, and where building and deploying happen. Each description says what its tool does, what it returns, and what it rejects. Behavior that a caller needs stays, stated as fact rather than instruction: an anonymous OQL read is rejected, a Candid data query on an OQL canister is rejected, a URL with no Internet Computer evidence is refused. Argument schemas follow the same rule. A unit test pins the property across the instructions and all 11 descriptions, so the phrasings that crept in before cannot come back. The submission docs record the posture against the guidelines each directory publishes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk
…ransfer The shared refusal template classified every matched method as "a financial transaction". That is right for icrc1_transfer, but manage_neuron is one entry point for the whole neuron surface — voting, following, and dissolve-delay changes go through it alongside disbursing, splitting, and spawning — so a user who tried to vote was told their call was a transfer. The template now states what the method is and lets the policy sentence that follows say what is not supported, and manage_neuron's label says the method is refused as a whole rather than per command, because its surface includes the operations that move staked tokens. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk
Keeps this stacked branch current with its base (#163), so the metadata diff shows only its own changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk
|
CI is red on The failing check is The IC dashboard API is down — that URL returns HTTP 524 and then times out on three consecutive direct attempts from here. This PR changes tool descriptions, the server instructions, and argument-schema documentation; it changes no request path. The same test failed at the same time on #164, an unrelated diff, and this branch's own previous head ( Nothing to port and nothing to weaken: the test asserts against a live service, and relaxing it to survive an outage would cost the assertion its value. The re-run waits for the dashboard; a check-in is scheduled and will report back here only if the result is anything other than green. For the record, the head also carries a merge of this PR's base branch (#163's Generated by Claude Code |
There was a problem hiding this comment.
Pull request overview
Reframes MCP metadata as factual capability descriptions rather than model directives.
Changes:
- Rewrites server, tool, and argument descriptions.
- Adds a regression test for directive wording.
- Updates directory-submission documentation.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
docs/openai-directory-submission.md |
Documents OpenAI metadata compliance. |
docs/anthropic-directory-submission.md |
Documents Anthropic metadata posture. |
crates/imcp2-core/src/tools.rs |
Rewrites served metadata and adds testing. |
crates/imcp2-core/src/identities.rs |
Revises resolver argument metadata. |
crates/imcp2-core/src/discover.rs |
Revises discovery argument metadata. |
crates/imcp2-core/src/calls.rs |
Revises call argument metadata. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Two review decisions from the PR author and sea-snake. icp_create_canister and icp_top_up_canister are gone. Both printed icp CLI steps for operations that spend the user's ICP or cycles, and both already sat on the unserved protocol router, so no client sees a change — but the crate no longer carries a funding-shaped tool at all, which is what the submission docs already claim. Their instruction builders, argument and output types, and the ICP decimal helpers that existed only to validate the amounts they echoed go with them; the two tool-surface counts move from 26/15 to 24/13. canister_update_call's description loses the financial sentence, per sea-snake: a policy paragraph inside that description reads as a hint that the tool is usable for financial transactions, which is the one thing it must not suggest. The test that required the sentence is replaced by the stronger property it displaced — NO tool description carries financial language, checked across the whole served surface, alongside the venue-naming ban. The policy stays where it belongs, in the server-wide instructions. Creating and funding canisters remains refused at the guard: the cycles ledger's creation spends and the cycles-minting canister's whole update surface, each pointing at the user's own icp CLI. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk
Resolves two conflicts in tools.rs against this branch's metadata work: - canister_update_call keeps this branch's factual description, minus the financial sentence #163 has now removed from every description. - The two instructions-only tool tests go with the tools they covered; this branch's model_readable_metadata_states_capabilities_not_directives test stays, and now runs over the 11-tool served surface. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk
…closes Two review points from sea-snake: the submission docs and the README described the finance-canister rule as covering "a curated list" and then enumerated the categories, which invites the reading that some services are outside it. They now say the guard refuses every update call on the financial-service canisters it carries, with no hedge. The same passages still claimed canister_update_call's description discloses the financial refusal, which stopped being true when that sentence came out. They now state the actual posture: the policy is in the server-level instructions — the field the directories scan — and deliberately in no tool description, held there by a test across the served surface. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk
…rion The SERVER_INSTRUCTIONS doc comment still described the design that was just reversed: it said canister_update_call's description carries a one-sentence disclosure. It now says the policy is server-wide and in no tool description, and names the test that holds that. KNOWN_APPS says what it is and why its contents are safe: name resolution is a read convenience — the guard refuses every update call to all three apps' canisters, so an entry routes to reads, not to a transaction. NNS came out at the author's request, which was a choice about which names the server advertises rather than a safety criterion, and its derivation origins stay so reads still resolve to the right identity. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk
The directives test read tool descriptions and the server instructions but not the schemas — and a directive in an argument or reply field's doc comment reaches the model exactly like one in a description. Review found the hole and a live violation through it: OpenAppOutput's alternative_origins said "never infer". The scan now covers every served tool's input and output schema, that field states the inverse relation as a fact instead, and a guard asserts the scan still sees field documentation so the new coverage cannot go vacuous. Three descriptions claimed things the server has no way to observe or enforce: - open_app and resolve_app said a domain "derived from an app's name" is not resolvable. The server cannot know how a caller arrived at a URL; what it actually does is refuse an unknown bare name, and refuse an assumed origin that shows no Internet-Computer evidence. They now say that. - discover_app_canisters promised every canister id found, while results are bounded at 50 (20 for unlabelled bundle literals) with the dropped count reported in `omitted`. The description now states the bound and the reporting. The OpenAI submission doc's descriptions-match row loses a garbled possessive and the claim that canister_update_call's description names its financial refusal, which is no longer the design. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk
…tale claims The comment above the gate in canister_update_call still described a method-only guard, which makes the call look narrower than it is. It now names all three scopes: the standardized value-moving names (the ICRC surface plus manage_neuron) on every canister, the system ledgers' and cycles-minting canister's own methods on those canisters, and every update method on a listed financial-service canister. The compliance module doc still said the policy is disclosed in canister_update_call's description. It now says the opposite, with the reason. The OpenAI submission's descriptions-match bullet claimed every description says what it rejects and then said the financial rejection is absent from them — a contradiction a reviewer would catch. It now distinguishes the two: descriptions state the inputs each tool rejects, and the financial policy is a server-wide statement made once, where the scan reads it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk
One conflict, in SERVER_INSTRUCTIONS: this branch's factual instruction text is kept, and its doc comment takes #163's correction — the policy is stated server-wide and in no tool description, held there by a test that now also covers the schemas. The instruction text itself loses the same hedge sea-snake asked to remove from the docs: it said "a curated list of known financial-service canisters", which invites the reading that some services are outside the list. It now says the financial-service canisters the guard carries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk
Both directory documents explained why the financial policy is kept out of tool descriptions. A reviewer does not need the deliberation, and spelling it out invites the reading it exists to prevent — that the tool might otherwise be usable for financial transactions. They now state the posture: the policy is in the server-level instructions, covering the whole surface at once, held there by a test. The same rationale stays in the code comments (tools.rs, compliance.rs), where it is the reason the test exists and the thing that stops the sentence being added back a third time. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk
The comment justifying KNOWN_APPS said every update call to those apps' canisters is refused. That is the overclaim this PR keeps removing elsewhere: the list covers each service's central canisters, and compliance.rs says in its own scope notes that a static list cannot cover an exchange's dynamically created pool canisters. What is true, and what it says now: name resolution writes nothing, so an entry is not itself a route to a transaction; a later update call is a separate request and goes through the guard under exactly the scope that module documents, including its note on what the list cannot cover. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk
…ster-disallow' into claude/icp-marketplace-compliance-vpawel-10-simplify-metadata
aterga
left a comment
There was a problem hiding this comment.
Requesting changes on three points:
-
P1 — Restore the no-fabricated-domain safeguard.
In[OpenAppArgs](https://github.com/dfinity/imcp2/blob/6c50d6a1219078c97cd64b3f13f0466dbc97fe99/crates/imcp2-core/src/discover.rs#L1827-L1837), add:If the user supplied only an app name, pass that name unchanged. Only pass a URL supplied by the user or obtained from a verified official source; do not construct a domain from the name.
Add the same constraint to
open_app’s description and a regression assertion. The runtime check proves that a domain uses ICP, not that an ICP-hosted lookalike belongs to the intended app. OpenAI specifically says required identifiers must not depend on model guessing. [OpenAI tool guidance](https://developers.openai.com/plugins/plan/tools) -
P1 — Replace the blanket “no directives” test with a narrower marketplace-policy test.
[model_readable_metadata_states_capabilities_not_directives](https://github.com/dfinity/imcp2/blob/6c50d6a1219078c97cd64b3f13f0466dbc97fe99/crates/imcp2-core/src/tools.rs#L2501-L2569)prohibits legitimate invocation and safety guidance. Both marketplaces expressly expect descriptions to explain when tools apply. [OpenAI](https://developers.openai.com/plugins/plan/tools), [Anthropic](https://claude.com/docs/connectors/building/review-criteria)Change it to reject only:
- Unrelated behavioral instructions.
- Overly broad triggering.
- Preference over or interference with other plugins/tools.
- Calls to unrelated external software.
- Hidden or obfuscated instructions.
Preserve tool-local prerequisites, selection criteria, and safety constraints. Update both submission documents accordingly; “must not manipulate how the model works” overstates OpenAI’s actual rule.
-
P2 — Correct the identity-mismatch diagnosis.
The claims in[get_app_principal](https://github.com/dfinity/imcp2/blob/6c50d6a1219078c97cd64b3f13f0466dbc97fe99/crates/imcp2-core/src/tools.rs#L908-L910)and[list_app_accounts](https://github.com/dfinity/imcp2/blob/6c50d6a1219078c97cd64b3f13f0466dbc97fe99/crates/imcp2-core/src/tools.rs#L964-L966)incorrectly say a browser mismatch means the derivation origin is wrong.Replace with:
A difference from the browser may indicate a different derivation origin, selected account, or Internet Identity; compare those inputs before retrying.
Review: the shared CLI redirect is also used for `notify_mint_cycles`, whose operation mints cycles into a cycles-ledger account rather than creating or funding a canister, so "Recommend that the user creates and funds canisters themselves with the icp CLI" gave that caller the wrong next step. Verified: `notify_mint_cycles` is in `CLI_REDIRECT_METHODS`, and that constant's own doc comment already said an interrupted mint is recovered in the CLI too — the intent was there, the redirect text just did not say it. It now covers all three operations, and the doc comment records why the wording is deliberately broader than canisters. The refusal still points only at the user's own terminal, names no connector tool, and keeps the install pointer the test asserts. Workspace green (52 + 10 + 10 + 146 + 21 + 1), clippy at the 10-warning baseline. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk
Review found the name-based-detection rule broken again at the places the last pass did not reach — including the note a caller actually receives. `has_oql` reports only that the interface declares `schema` and `execute`; neither data behaviour nor authorization follows from two method names. - The served data-access note said "the canister(s) flagged [oql] hold this app's data, gated by the CALLER's principal". This one was not flagged in review — a sweep for the same phrasing found it — and it is the worst instance, being text the caller reads. It now states the READ PATH, which is this server's own behaviour: those canisters are read through the OQL tools rather than a Candid data query, that path requires the origin, and the flag reports the declaration, not what the canister stores. - `DiscoveredCanister.oql` closed with "this is a caller-gated data backend", two lines below the correction that made the rest of the field accurate. - `open_app`'s description said a flagged canister "holds the app's data, gated by the caller's principal". - `DiscoveredCanister.api_doc_available` still collapsed the two outcomes the adjacent `oql` field now separates: only a failed FETCH gives null, while a fetched-but-unparsable interface gives false. - `ApiDocOutput`'s root doc, `expected`, and `retry` all treated "no compatible method detected" as proof of absence. `api_doc_method` returns None for an interface that cannot be parsed or exceeds the parser's limits, and that path sets `expected: true` — so true does not mean the interface read fine, and false on `retry` means the reading will not change, not that the canister declares nothing. - The README row and two internal comments carried the same claim. Also merges the base branch's cycles-minting redirect fix. Workspace green (52 + 10 + 10 + 149 + 21 + 1), clippy at the 10-warning baseline. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk
Review: `discover_app_canisters`'s description promised that a domain with no Internet-Computer evidence yields an empty `canisters` list, but the handler returns a plain error when discovery itself fails — `Err(e) => Ok(err(app_url_error_with_guidance(...)))` — so an unreachable domain (DNS, TLS, timeout) never reaches the empty-list path. The promise is now conditioned on discovery succeeding, and says outright that an empty list means no findings rather than a failed lookup, so a caller cannot read one as the other. The argument schema already said "reachable"; the tool description now agrees with it. Workspace green (52 + 10 + 10 + 149 + 21 + 1), clippy at the 10-warning baseline. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Suppressed comments (1)
docs/anthropic-directory-submission.md:368
canister_update_callcannot perform lifecycle management calls as claimed here. Its raw update path does not set an effective canister ID (calls.rs:1522-1532), while management-canister lifecycle calls explicitly require the target viawith_effective_canister_id(management.rs:324-328). Remove this parenthetical so reviewers are not told to expect an unsupported capability.
> connector, with the icp CLI. (The generic `canister_update_call` can
> still reach management methods on a canister your principal controls.)
main gained #165, which drops the landing site in favour of internetcomputer.org/icp-mcp and redirects the old paths. Two conflicts: - `src/assets/index.html` (modify/delete): accepted the deletion. This branch had edited that page's two "refused by design" strings, replacing "use a wallet you control, like oisy.com" with "use a trusted interface you control, outside the connector" — the no-named-venue rule review asked for. That file is gone, so the edit has no home here; the page is maintained in dfinity/internetcomputer-org and the change belongs there. - The Anthropic checklist: took main's privacy-policy line, which names the page's new single home, and kept this branch's financial-acknowledgment line. main's phrasing ("question resolved in code") is the past-gap framing review asked to remove — the posture is stated present-tense, not as a gap we closed. The move also falsified a claim in both submission documents: they said the README, the landing page, and the server instructions all state that financial transactions are not supported. The README does (its opening "Not for financial operations" section) and the instructions do, but the landing page is no longer in this repository, and the page committed in the other one carries no policy text at all. Both documents now claim only the two surfaces this repository controls, and say where the third lives and that adding the posture there is a separate change. Workspace green (52 + 7 + 10 + 146 + 21 + 1), clippy at the 10-warning baseline. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk
Review caught an inaccuracy I introduced two commits ago. Fixing the CLI-only claim, I wrote that the generic `canister_update_call` can still reach the management canister's lifecycle methods for a controller principal. That is wrong, and the repository says so in its own words. `raw_call` issues `agent.update(&canister, method)` with no `with_effective_canister_id`, so an ingress call to `aaaaa-aa` carries `aaaaa-aa` as its effective canister id. `management.rs`'s `mgmt_call` sets `.with_effective_canister_id(target)` explicitly, and its doc comment states why: "the boundary node requires this for lifecycle methods". So those calls are rejected before they reach a replica; the update-call path is not a route to canister management. All three sites that carried my claim now say that, with the mechanism, so a reviewer can check it rather than take it on trust: the Anthropic posture bullet, its reviewer step 4, and the OpenAI posture bullet. Worth recording what this cost: the original text said canister management is done by the user with the icp CLI, review called that an overclaim, and I widened it on a premise I had not verified against the agent call path. The claim is now no wider than the mechanism behind it. Workspace green (52 + 7 + 10 + 146 + 21 + 1), clippy at the 10-warning baseline; documentation-only change. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk
Review: the served instructions and the Anthropic reviewer procedure both said a call with no derivation origin "runs as the anonymous principal, and the canister decides whether to accept it". Verified against the handler: `canister_update_call` parses the canister id and then calls `compliance::disallowed_update_method` immediately — before `resolve_did`, before any identity resolution, before any network I/O. So for a refused method the SERVER decides, with or without an origin, and the canister never sees the call. Both texts now say the Internet Identity access levels do not govern such a call, that this server's own checks still do and the financial guard runs before any identity or network work, and that past those the canister decides. Three deciders, in the order they actually apply. Also merges the base branch: main's removal of the landing site, the two submission documents no longer claiming the landing page states the policy, and the correction that the update-call path cannot reach management-canister lifecycle methods. Workspace green (52 + 7 + 10 + 149 + 21 + 1), clippy at the 10-warning baseline. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk
|
Answering the suppressed finding from the latest review, which has no thread of its own — and it was the most valuable one in the round, because it caught an error I introduced myself.
Verified and correct. How it got there is worth stating plainly. Two rounds ago review flagged "creating, funding, and managing canisters is done by the user with the icp CLI" as an overclaim, because the generic update-call tool restricts nothing about the target canister. I checked the guard, saw no restriction on Fixed on the base branch in Generated by Claude Code |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
Suppressed comments (3)
crates/imcp2-core/src/tools.rs:1987
- “Per-app data is gated by the calling principal” is broader than the implementation and conflicts with the new
DiscoveredCanisterdocumentation, which correctly says method names establish neither storage nor canister-side gating. Only this connector's OQL paths unconditionally require a delegated principal; Candidmethodreads may remain anonymous. Attribute the gate to the OQL tool path rather than to all per-app data.
An app's derivation origin is the exact origin Internet Identity derives the user's principal from. It is not necessarily the app's visible URL, and an alternative-origins entry does not identify it; open_app and resolve_app resolve it, and the identity-bearing tools take the origin itself rather than a URL. There is no on-chain name-to-URL directory: open_app matches a name against a built-in registry of well-known apps, and where the derivation origin would have to be assumed from the URL itself, open_app and resolve_app refuse an origin with no evidence of being an Internet Computer app, while discover_app_canisters returns an empty result for such a domain. Per-app data is gated by the calling principal, so OQL reads require a derivation origin and reject an anonymous read. Account delegations are short-lived and derived on demand from this connection's standing Internet Identity credential, which is obtained at connect time and lasts for the chosen session duration (up to 30 days). Internet Identity's consent screen offers two access levels, and they govern the calls signed with that session's account delegation — the ones that carry a derivation origin: on a \"Questions only\" session those reads work and those update calls are rejected by the network, while \"Actions & questions\" permits both. A call made with no derivation origin is not signed with the delegation at all; it runs as the anonymous principal, so those access levels do not govern it. This server's own checks still do — the financial-transactions guard runs before any identity or network work, so a call it refuses is refused whether or not an origin was passed — and past that the canister decides whether to accept it.\n\n\
crates/imcp2-core/src/tools.rs:595
- This repeats the unsupported claim that per-app data itself is principal-gated. The implementation only establishes that this connector rejects anonymous OQL requests; the canister may expose public data, and the same tool allows anonymous Candid reads. Describe the enforced OQL prerequisite without inferring the canister's storage or authorization behavior from the
schema/executenames.
description = "Read from an Internet Computer canister with either a Candid `query` method or an OQL query — exactly one of the two. `method` is a query function from the canister's Candid interface, invoked with textual-Candid `args`; `oql` is an OQL query as a JSON object string, run against the canister's `execute` method as plain JSON with no Candid escaping. Canisters that expose OQL (get_canister_candid reports `oql: true`) reject a Candid `method` data query and are read through `oql`; a plain query canister such as a ledger takes `method`. The `oql` path requires `derivation_origin` — per-app data is gated by the caller's principal, and an anonymous OQL read is rejected — and returns `columns` and `rows` (a markdown table) with `has_more` for paging; on an empty result it re-reads the schema for this principal and, when that read returns entities and the query's `start` is not one of them, returns `valid_entities` plus a did-you-mean repair; a `start` that does exist, an empty schema, or a schema read that fails leave both out. The `method` path may be anonymous, or take `derivation_origin` and `account` to read as the user's account, and returns the decoded reply in textual Candid. Reading as the user's account — the whole `oql` path, and the `method` path when given a `derivation_origin` — requires an authenticated session; an anonymous `method` read does not. `derivation_origin` is resolved by open_app or resolve_app rather than being a raw URL, and the OQL entity and field names come from get_canister_oql_schema. State changes go through canister_update_call.",
crates/imcp2-core/src/tools.rs:248
- This also attributes the authentication requirement to the schema/canister, although the code only proves that this connector rejects a missing origin before making any call (
tools.rs:268-274). Keep the prerequisite, but state it as connector behavior so the metadata does not infer canister-side gating from the OQL method names.
description = "Fetch the OQL schema catalogue of a canister that exposes the OQL surface (get_canister_candid reports `oql: true`): its entities, their primary keys, fields, and edges, as JSON, plus a ready-to-run canister_query example per entity (each preserving this call's identity). Entity names are the schema's own, often plural and different from the Candid types and methods (e.g. `bookings`, not `Booking`/`getBookings`). `derivation_origin` is required: the schema is gated by the caller's principal, so a read with no origin is rejected with guidance rather than returning an empty entity list. Since every read here is made as the user's app account, it requires an authenticated session. The origin is the app's canonical Internet Identity derivation origin, which open_app and resolve_app resolve; the reply echoes `derived_for_origin` and `acted_as_principal`.",
The merge-base changed after approval.
…nisters #163 was squash-merged as 2279f91, so its commits are not ancestors of main and this stacked branch went un-mergeable against its new base. Merged main in rather than rewriting history. Five conflicts, all this branch's own rewrites against the pre-merge text of the same lines, resolved to this branch: `discover_app_canisters`'s description, SERVER_INSTRUCTIONS and its doc comment, the model-readable-metadata policy test (absent on the other side), the metadata paragraph in the Anthropic document, and its reviewer step 3. Review then found the same defect at seven sites, and it is one this branch created: the corrected `DiscoveredCanister` docs say method names establish neither what a canister stores nor how it authorizes callers, while other surfaces still asserted "per-app data is gated by the caller's principal" as fact. What is actually enforced is narrower and entirely ours: this server rejects an OQL read with no derivation origin. A canister may serve public data, and a Candid `method` read may be anonymous. Reattributed to the connector, with that distinction stated: the server instructions, `icp_oql_guide`'s served text, `canister_query`'s and `get_canister_oql_schema`'s descriptions, `OqlSchemaArgs.derivation_origin`, `CanisterQueryOutput.is_anonymous`, and both README passages. Three implementation comments keep the old shorthand deliberately — they are neither served nor public, so they are notes, not metadata. Two further findings, both real: - The declared-origin path is NOT uniformly fail-soft, and the exception is security-relevant. `decide_declared_origin` returns Err — refusing resolution outright — for a CROSS-origin declaration the declared origin does not authorize in its /.well-known/ii-alternative-origins, with "Refusing to derive an identity here rather than use a wrong one". My earlier wording lumped that in with the fetch failures that fall back to the assumed origin, so a caller could have expected an IC-evidence fallback where resolution actually fails. `open_app`'s and `resolve_app`'s descriptions and the OpenAI readiness row now name it as a hard refusal, so a declaration cannot read as a way around the identity checks. - The README's `get_canister_api_doc` row still promised a structured result "in every case" after the description and output schema had been corrected: an unusable canister id is a plain error, and `expected: true` is not proof of absence. This push resets the approval on this PR. That is the cost of the seven metadata corrections plus a base branch that had to be merged anyway — the merge alone would have reset it. Workspace green (52 + 7 + 10 + 149 + 21 + 1), clippy at the 10-warning baseline. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk
|
Answering the three suppressed findings from the latest review, which have no threads of their own. All three were right, and they name the same defect this branch created. The corrected What is actually enforced is narrower and entirely ours: this server rejects an OQL read with no derivation origin. A canister may serve public data, and a Candid Fixed in On the approval. @sea-snake, this push resets your approval, and I want to be straight about the trade. The base branch was squash-merged, so this PR had to merge Generated by Claude Code |
#167 rewrote the served metadata to describe the surface rather than direct the model, and added a test that scans the instructions, all 11 descriptions and every argument/reply schema for the manipulations the directories prohibit. This branch's metadata was written in the old register, so the resolution is main's text plus this branch's facts restated in the new one: - SERVER_INSTRUCTIONS: main's rewrite, with the write gate as a descriptive paragraph. The old "WRITES GO ONLY TO APPS THAT OPTED IN" block told the model what to do when refused ("do NOT keep retrying", "tell the user"); the replacement states what the gate is, that reads are not gated that way, and who can change it. The refusal texts still carry the actionable detail — they are tool results, not metadata. - canister_update_call: main's description plus the gate, `app_url`, and the `declared_by`/`declared_at` echo, as statements. - calls.rs `app_url`: same, in the schema doc ("Omitted, `derivation_origin` is used as that origin instead" rather than "omit it only when…"). - resolve_app: main's description with the declared source corrected to /.well-known/ii-derivation-origin, which this branch made take precedence over the legacy field. - discover_app_canisters + the README row: main's text with the ai-connect provenance replaced by the two manifest documents, per review. - open_app: main's description and reply text, noting that `app_url` is the origin the write gate reads the manifest from. Two of this branch's own assertions pinned the old wording (READABLE, "WRITES GO ONLY…"); they now pin the same facts in the new text. Build and the full suite pass on the merge: 259 tests, 0 failures, including main's new metadata scan over the text added here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JarhapgjytMcVSCW7d8sqB
Resolves the conflicts #163's follow-ups (#167) and the landing-site move (#165) created against the write gate: - compliance.rs: takes main's four-group guard wholesale (the CMC's funding-completion methods are listed there now, so the branch's "deliberately NOT listed" note went away) and re-applies only the Layer-2 framing plus the note that Layer 1, not this list, bounds the un-enumerable remainder. - tools.rs: the gate is now described in #167's factual register — no ordering rules, no imperatives — on canister_update_call, resolve_app, discover_app_canisters and in SERVER_INSTRUCTIONS, which keeps the surface inside the marketplace-policy scan. resolve_app's "declared" source and discovery's app-declared sources are corrected to ii-derivation-origin and ic-architecture. - The Developer Terms page follows every other human-facing page to dfinity/internetcomputer-org: its source text lives in docs/icp-mcp-developer-terms-draft.md (the privacy policy's convention), DEVELOPER_TERMS_URL points at internetcomputer.org/icp-mcp/developer-terms/, and this origin redirects /developer-terms there. The anti-drift test moves with it: the source text must name the revision the gate enforces, and the URL refusals carry must be the page this origin redirects to. - Refusal-text assertions follow main's guard, which no longer names a venue. cargo test --locked --workspace --all-targets: 263 tests green; clippy unchanged at the 6-warning pre-existing baseline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SzGAjX3k9utczAg2Jw8yQX
Summary
The server instructions and the served tool descriptions told a model how to work:
START WITH open_app,ALWAYS call this FIRST,NEVER guess a domain, "web search the official URL", a numbered seven-step flow across tools, and a routing chain to run before answering anything. Both directories read these fields, and both prohibit a description that steers the model rather than describing the tool — so every surface now describes what it is, what it returns, what it rejects, and what it requires.What a caller needs is kept, and after review the emphasis moved: both directories expect a description to say when a tool applies and what is unsafe to pass, so the rule this PR enforces is not "no guidance" but "none of the five manipulations the directories actually prohibit" — unrelated behavioral instructions, overly broad triggering, preference over or interference with other tools, calls to unrelated external software, and hidden or obfuscated instructions.
The second half of the PR became an accuracy pass. Review turned up a recurring defect — metadata claiming more than the code does — and every instance found is fixed and listed below.
Stacked on #163 (base is that branch, so the diff shows this change alone; GitHub retargets to
mainwhen #163 merges). It rewrites the same regions #163 touched — the update-call description and the instructions' policy paragraph — so branching offmainwould have lost that work.Related issues
None — follow-up in the directory-submission series.
Changes
compliance.rs's method families and canister scopes, and this branch takes that rewrite verbatim), and where building and deploying happen. Gone: theSTART WITHentry-point rule, the routing chain, the numbered "Typical flow", the locale-conversion directives, "web search or ask the user", and the caps imperatives.canister_queryloses its "start with open_app first" preamble;open_apploses itsROUTINGblock;resolve_appanddiscover_app_canisterslose the "NEVER guess / web search" rules.open_appcarries, per review, the one constraint the runtime cannot enforce: pass the user's app name unchanged, pass only a URL the user supplied or one from a verified official source, and do not construct a domain from the name — because the IC-evidence check shows a domain is served from the Internet Computer, not that it is the app the user meant.calls.rs,discover.rs,identities.rs): imperatives become plain statements, and "or ask the user for it" becomes what actually provides the value.discover_app_canistersclaimed a refusal it does not perform (it returns an empty result).get_canister_api_docpromised a structured result "in every case", in its description and again in its root schema.retry: truewas presented as a diagnosis, though an unreadable interface and a rejecting or trapping method both set it.api_doc_availablewas presented as "a guide is returned" when it reports a declaration — corrected onget_canister_candid,open_app, theDiscoveredCanisterschema, and the runtime notes.ResolveAppOutput.alternative_originswas attributed to the application origin, though it is read from the derivation origin.canister_querypromised a did-you-mean repair on any empty OQL result, when it needs an unknownstartand a schema read that returns entities.get_canister_oql_schema's whole schema documented an anonymous path the handler rejects. Three tools did not disclose their session prerequisite. The identity-mismatch note claimed a wrong derivation origin when a different account or Internet Identity produces the same difference. And the access levels govern only calls signed with the session's delegation, not every update call — which was also wrong in both submission documents' reviewer procedures, where a reviewer following the step would not have seen the promised behavior.model_readable_metadata_respects_marketplace_policyscans the server instructions, all 11 descriptions, and every argument and reply schema for those five prohibited categories. It checks each schema's decoded strings rather than its JSON serialization — escaping turns an invisible character into printable ASCII, which would split a banned phrase and pass the character check — matches phrases on collapsed whitespace, and allows only printable ASCII plus the three punctuation marks the metadata actually uses, so no invisible character can ride along. A second test,open_app_metadata_forbids_a_constructed_domain, pins the safeguard above on both surfaces a model reads before calling.Out of scope: the 13 deferred
IcProtocolToolsdescriptions are not served and are unchanged. Two behavior changes review raised are deliberately not here, since they are not metadata: classifying retryable agent errors, and extending the IC-evidence probe to declared derivation origins.Testing
cargo build --locked --all-targetscargo test --workspace— 146 core + 52 / 10 / 9 / 20 / 1, all greencargo clippy --workspace --all-targets— unchanged 10-warning baseline (this repo is not rustfmt-clean; no repo-widecargo fmt, matching prior PRs)npm test --prefix monitoring/mcp-status(dashboard unchanged)Checklist
Generated by Claude Code
Generated by Claude Code