Refuse update calls to known finance-related canisters - #163
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
There was a problem hiding this comment.
Pull request overview
Adds a finance-canister denylist so all update calls to curated financial services are refused, while retaining method-specific precedence.
Changes:
- Adds and tests 45 labeled finance-canister restrictions.
- Removes NNS from name-based app discovery while preserving explicit URL identity derivation.
- Updates policy documentation and landing-page messaging.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
crates/imcp2-core/src/compliance.rs |
Implements and tests the denylist. |
crates/imcp2-core/src/tools.rs |
Updates server policy instructions. |
crates/imcp2-core/src/discover.rs |
Removes NNS name resolution. |
README.md |
Documents update-call restrictions. |
docs/openai-directory-submission.md |
Updates OpenAI policy explanation. |
docs/anthropic-directory-submission.md |
Updates Anthropic policy explanation. |
src/assets/index.html |
Updates landing-page policy text. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
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
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
crates/imcp2-core/src/compliance.rs:138
- The PR description calls this a 41-entry canister list, but the static contains 46 entries (44 inline IDs plus
ICP_LEDGERandCYCLES_LEDGER). Update the stated count, or remove any unintended entries, so the documented review scope matches the implemented guard.
const DISALLOWED_FINANCE_CANISTERS: &[(&str, &str, &str)] = &[
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
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 (3)
Previously missed (1) — in code that hasn't changed since the last review.
crates/imcp2-core/src/compliance.rs:138
- The PR description identifies this as a 47-entry list, but the constant contains 52 entries (6 infrastructure + 3 minters + 14 ledgers + 5 wallets + 3 WaterNeuron + 3 MULTI/DEX + 5 ICPSwap + 5 Sonic + 5 ICDex/ICLighthouse + 3 ICPEx). Reconcile the five-entry discrepancy or update the PR description so the audited inventory count is accurate.
const DISALLOWED_FINANCE_CANISTERS: &[(&str, &str, &str)] = &[
docs/openai-directory-submission.md:120
- The directory submission's exhaustive guard description omits the new global
manage_neuronrule. Because per-SNS governance canisters are intentionally not in the curated list, the current wording understates the protection added by this PR. Includemanage_neuronamong the standardized methods refused on every canister.
`manage_neuron` — neuron staking and disbursement — on every canister,
plus the ICP and cycles ledgers' own
value-moving methods on those ledgers, plus every update call on a curated
crates/imcp2-core/src/tools.rs:2009
- The documented refusal scopes omit the new global
manage_neuronrule. Per-SNS governance canisters are deliberately absent from the curated list, so these server instructions currently do not disclose the restriction that provides coverage for every SNS. Include the NNS/SNSmanage_neuronmethod alongside the globally refused ICRC methods.
equivalents) AND the governance method manage_neuron (neuron staking/disbursement, \
on the NNS and every SNS) on every canister, the ICP and cycles ledgers' own \
transfer/withdrawal/creation methods on those ledgers, and EVERY update call on a \
curated list of known financial-service canisters (token ledgers and minters, \
…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
|
All three requested changes are in 1. OpenAI policy analysis — corrected. You're right, and the old section was wrong in a way that mattered: it read the restriction as narrower. Section 3 of 2. CMC — blocked, and I extended it to four methods. You named Recovery is preserved, just not through the connector: these refusals point at the icp CLI, with the install pointer and the 3. Description now matches behavior.
The test that prohibited the word "financial" there is replaced by 4. Venue names removed (the risk-reduction change). Adopted. Every refusal, the server instructions, the README, the landing page, and the submission docs now say the operation belongs outside the connector, in a trusted interface the user controls. Nothing names a wallet. Two related cleanups: the Canister creation and funding keep the icp CLI pointer, on the reasoning that it's the user's own tooling rather than a transactional venue — and your point 2 asks for exactly that pointer for the recovery path. Say the word if you'd rather those went neutral too. Tests: 153 core (up from 151) + 52/10/9/20/1, all green; clippy at the repo's 10-warning baseline. Generated by Claude Code |
| the cycles-minting canister's funding-completion methods on those | ||
| canisters, and every update call on a curated list of known | ||
| financial-service canisters (token ledgers and minters, exchanges, wallet | ||
| backends, staking/governance). The refusal tells the user to perform the |
There was a problem hiding this comment.
Same as above, let's omit " and every update call on a curated list of known
financial-service canisters (token ledgers and minters, exchanges, wallet
backends, staking/governance)."
There was a problem hiding this comment.
Omitted in 1e8baa0, same as the Anthropic one: the bullet now says "every update call on the financial-service canisters it carries".
Generated by Claude Code
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
Read as the two funding tools — Both tools 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 behavior change. What changed is that the crate no longer carries a funding-shaped tool at all — which is what the submission docs have been claiming ("the connector has no funding or management tools"); until now that was true of the served surface rather than of the code. Their instruction builders, argument and output types, and the ICP decimal helpers that existed only to validate the echoed amounts went with them, so nothing is left dead behind. Tool-surface counts move 26 → 24 across both halves, 15 → 13 on the protocol half. Creating and funding a canister is still refused at the guard, unchanged: the cycles ledger's The same commit drops the financial sentence from Green locally: 143 core + 20 local tests, clippy at its 10-warning baseline. Generated by Claude Code |
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
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.
Suppressed comments (1)
crates/imcp2-core/src/tools.rs:531
- This integration comment still describes only the ICRC and system-canister method rules, omitting both new scopes: global
manage_neuronrefusal and blanket refusal of every update on listed finance canisters. Update it so future changes do not treat this call site as a narrower method-only guard.
// system ledgers'/cycles-minting canister's own value-moving methods
// on those canisters, before any network work, pointing the user
// outside this connector. Queries need no gate — a query cannot
// commit state, so it cannot move funds.
| /// financial-transactions policy is stated HERE in full, server-wide, because | ||
| /// it governs the whole surface rather than one tool; `canister_update_call`'s | ||
| /// own description carries a one-sentence disclosure of it, so the description | ||
| /// matches the tool's actual behavior (both directories require that, and a | ||
| /// refusal is a side effect a caller must be able to see from the description | ||
| /// alone). Neither surface names a venue for a refused operation. |
There was a problem hiding this comment.
Correct — that comment described the design that had just been reversed. Fixed in 2d8e5e6: it now says the policy is stated server-wide and in no tool description, with the reason (a policy paragraph in that description reads as a hint the tool is usable for financial transactions) and the name of the test that holds the line across every served description.
The three documentation claims you flagged in the same review — README, and both submission docs — were the same staleness and went out in 1e8baa0, which your review raced. None of them now asserts a tool-level disclosure.
Generated by Claude Code
| | `get_canister_api_doc` | `canister_id` | The canister's own prose API guide ("how this app behaves" — units, auth, lifecycle, mutation safety, polling, gotchas), from its `getApiDoc`/`get_api_doc` method. Call **only** when `get_canister_candid`/`open_app` report `api_doc_available`. Returns a **structured** result in every case — `available` + the doc on success, else `available:false` with `expected`/`retry`/`next` so an expected absence is distinct from an unreachable canister | | ||
| | `canister_query` | `canister_id`, `method?` **or** `oql?`, `args?` (textual Candid), `derivation_origin?`, `account?`, `candid?` | READ a canister — provide EITHER a Candid `query` `method` (with `args`) OR an `oql` query (a JSON object string, run against `execute`). A Candid `method` query may be anonymous or as your account and returns textual Candid; an `oql` query **requires** `derivation_origin` and returns `columns` + `rows` (a table) with `has_more`, validating `start` against the schema on an empty result. On an OQL canister a Candid `method` query is rejected — use `oql`. `candid` is a fallback: the `.did` interface text to encode/decode against when the canister exposes no `candid:service` metadata. Echoes `derived_for_origin` / `requested` / `acted_as_principal` | | ||
| | `canister_update_call` | `canister_id`, `method`, `args` (textual Candid), `derivation_origin?`, `account?`, `candid?` | Make an UPDATE (state-changing) call; reply as textual Candid; anonymous, or as your account at an app (identified by its canonical II `derivation_origin`, obtained once from `open_app`/`resolve_app`). **Financial transactions are refused**: the ICRC-standard transfer/approval methods (ICRC-1/ICRC-2 and the ICRC-4/-7/-37 equivalents) are disallowed on every canister, and the ICP and cycles ledgers' own value-moving methods (the legacy `transfer`, `withdraw`, the `create_canister` spends) on those ledgers, to protect the user — the refusal directs the user to act themselves — in a wallet they control (e.g. [oisy.com](https://oisy.com)), or, for canister creation, with the [icp CLI](https://github.com/dfinity/icp-cli) in their own terminal. `candid` is the same `.did` fallback as on `canister_query`, used when the interface isn't published on-chain. Echoes `derived_for_origin` / `requested` / `acted_as_principal` | | ||
| | `canister_update_call` | `canister_id`, `method`, `args` (textual Candid), `derivation_origin?`, `account?`, `candid?` | Make an UPDATE (state-changing) call; reply as textual Candid; anonymous, or as your account at an app (identified by its canonical II `derivation_origin`, obtained once from `open_app`/`resolve_app`). **Financial transactions are refused**: the ICRC-standard transfer/approval methods (ICRC-1/ICRC-2 and the ICRC-4/-7/-37 equivalents) and the NNS/SNS governance method `manage_neuron` (neuron staking and disbursement) are disallowed on every canister, and the ICP and cycles ledgers' own value-moving methods (the legacy `transfer`, `withdraw`, the `create_canister` spends) and the cycles-minting canister's funding-completion methods (`notify_top_up`, `notify_create_canister`, `notify_mint_cycles`, `create_canister`) on those canisters; and **every** update call is refused on a curated list of known financial-service canisters (token ledgers and minters, exchanges, wallet backends, staking/governance) — all to protect the user. The refusal directs the user to perform the operation outside the connector, in a trusted interface they control — or, for canister creation and funding, with the [icp CLI](https://github.com/dfinity/icp-cli) in their own terminal. The tool's own description states that financial operations and update calls to known financial-service canisters are not supported and return an error. `candid` is the same `.did` fallback as on `canister_query`, used when the interface isn't published on-chain. Echoes `derived_for_origin` / `requested` / `acted_as_principal` | |
There was a problem hiding this comment.
Fixed in 1e8baa0 (your review raced the push). The row now ends "The policy is stated in the server-level instructions, deliberately not in any tool description." — matching the test at tools.rs:2514-2521, which is the direction the maintainers settled on.
Generated by Claude Code
| user-run icp CLI). The policy is stated in full in the server-level | ||
| instructions, and `canister_update_call`'s own description discloses that | ||
| financial operations and update calls to known financial-service canisters | ||
| are not supported and return an error — so the description matches the | ||
| behavior. |
There was a problem hiding this comment.
Fixed in 1e8baa0. The passage now states the observable 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. Nothing in the document claims a disclosure a reviewer would not find.
The equivalent claim in the OpenAI doc went out in the same commit.
Generated by Claude Code
| - **The descriptions match the behavior**, as the guidelines require ("tools | ||
| should behave exactly as their names, descriptions, and inputs indicate"; | ||
| "side effects should never be hidden or implicit"): | ||
| `canister_update_call`'s description states that financial operations and | ||
| update calls to known financial-service canisters are not supported and | ||
| return an error, and the full policy — refused method families and the | ||
| reason — is in the server-level instructions. |
There was a problem hiding this comment.
Fixed in 1e8baa0. The attestation bullet no longer claims a tool-level disclosure: it says every description states what its tool does, returns, and rejects, and that the financial-transactions policy is stated in full in the server-level instructions — the field the scan reads — and kept out of descriptions deliberately.
Generated by Claude Code
| @@ -533,7 +538,6 @@ struct KnownApp { | |||
| } | |||
|
|
|||
| const KNOWN_APPS: &[KnownApp] = &[ | |||
There was a problem hiding this comment.
The inconsistency is real, so I've documented the actual criterion rather than invent a principled one — but the choice of which way to make it consistent isn't mine, so flagging it rather than acting.
What's true: name resolution is a read convenience. It turns a name into an app URL and derivation origin so open_app can read interfaces, discover canisters, and derive the user's per-app principal. It confers no ability to act — this PR's guard refuses every update call to all three of those apps' canisters, Oisy and MULTI/DEX and ICPSwap alike — so an entry here routes to reads, not to a transaction. That holds equally for NNS, which is why keeping its derivation origins was safe and correct.
So there is no safety distinction, and I won't pretend otherwise: NNS came out at the author's request, an editorial choice about which names the server advertises. 2d8e5e6 says exactly that at the list.
The consistent alternative you're pointing at — apply the financial-app criterion to the rest — would empty KNOWN_APPS entirely, since all three remaining entries are financial services. That deletes name resolution as a feature and makes open_app("oisy") fail for a legitimate read. That's a product decision for @aterga, not something to slip into a compliance PR.
Generated by Claude Code
…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
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Suppressed comments (3)
Previously missed (2) — in code that hasn't changed since the last review.
crates/imcp2-core/src/tools.rs:1957
- This says
canister_update_callcontains a financial-policy disclosure, but its description at line 506 contains none, andfinancial_policy_is_a_server_instruction_not_a_descriptionexplicitly enforces that omission. Correct this comment so it does not document the opposite contract.
/// governs the whole surface rather than one tool — and deliberately in NO
/// tool description (per review): a policy paragraph inside
/// `canister_update_call`'s description reads as a hint that the tool is
/// usable for financial transactions, which is the one thing it must not
/// suggest. `financial_policy_is_a_server_instruction_not_a_description`
crates/imcp2-core/src/compliance.rs:69
- The
canister_update_calldescription does not disclose this policy;tools.rs:2513-2522tests that no served tool description contains financial wording. Align this module documentation with the enforced server-instructions-only design.
//! instructions (get_info) and disclosed in `canister_update_call`'s own
//! description; this guard enforces it for the standardized
crates/imcp2-core/src/tools.rs:531
- This integration comment still describes only the ICRC and system-canister rules, omitting the new global
manage_neuronrule and the finance-canister blanket. That makes the guard look narrower than the call directly below actually is. Document all three scopes here.
// system ledgers'/cycles-minting canister's own value-moving methods
// on those canisters, before any network work, pointing the user
// outside this connector. Queries need no gate — a query cannot
// commit state, so it cannot move funds.
| - **The descriptions match the behavior**, as the guidelines require ("tools | ||
| should behave exactly as their names, descriptions, and inputs indicate"; | ||
| "side effects should never be hidden or implicit"): | ||
| every tool description says what its tool does, what it returns, and what | ||
| it rejects. The financial-transactions policy is not restated there: it is | ||
| stated in full in the server-level instructions, which is the field the | ||
| scan reads, and kept out of the descriptions deliberately — a policy | ||
| paragraph inside `canister_update_call`'s description would read as a hint | ||
| that the tool is usable for financial transactions. |
There was a problem hiding this comment.
Fair hit on my own replacement text — as written, the bullet asserted "every description says what it rejects" and then conceded the financial rejection isn't among them, which reads as a contradiction rather than a distinction. Fixed in dedf749.
It now separates the two claims, because they are about different things:
- each description states what its tool does, what it returns, and which inputs it rejects (an anonymous OQL read, a Candid data query on an OQL canister, a URL with no Internet-Computer evidence);
- the financial-transactions policy is server-wide, not per-tool, and is stated once in the server-level instructions — the field the scan reads — and deliberately in no description.
So nothing promises a behavior the tool lacks, and no refusal is hidden; it is stated for the whole surface in one place. The same commit also cleared the two remaining stale claims your suppressed comments named: the module doc in compliance.rs and the gate's call-site comment (which additionally now documents all three scopes rather than just the method rules).
On the third suppressed comment — tools.rs:1957 — that one is a misread: the text it quotes is the corrected version, which says the policy is stated "deliberately in NO tool description". Nothing to change there.
Generated by Claude Code
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
crates/imcp2-core/src/compliance.rs:69
- This scope note contradicts the implemented metadata contract:
canister_update_callhas no financial-policy disclosure, andtools.rs:2504-2530explicitly tests that the policy exists only in server instructions. Remove the tool-description claim so the module documentation matches the enforced design.
//! instructions (get_info) and disclosed in `canister_update_call`'s own
//! description; this guard enforces it for the standardized
crates/imcp2-core/src/tools.rs:531
- The updated guard comment still describes only the ICRC and system-canister branches, omitting both new behaviors: global
manage_neuronrejection and blanket rejection for listed finance canisters. Document all three scopes here so future changes at this call site do not rely on an incomplete description of the gate.
// system ledgers'/cycles-minting canister's own value-moving methods
// on those canisters, before any network work, pointing the user
// outside this connector. Queries need no gate — a query cannot
// commit state, so it cannot move funds.
…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
| "{}: {desc}", | ||
| tool.name | ||
| ); | ||
| assert!(!desc.contains(".com"), "{} names a venue: {desc}", tool.name); |
There was a problem hiding this comment.
Checked this one rather than taking it at face value, and it doesn't hold: icp_find_app_by_name is not a served tool, so the assertion never sees that description.
- It is declared at line 1327, inside
impl IcProtocolTools(which opens at line 1272) — the deferred half. - The test iterates
IcTools::all_tools(), which isIcCanisterTools::tool_router().list_all()(line 134) — the served half only, lines 157–1269. the_default_composition_defers_the_protocol_toolspins that boundary: 11 served tools, noneicp_-prefixed excepticp_oql_guide.
Empirically: cargo test -p imcp2-core --lib financial_policy_is_a_server_instruction_not_a_description passes on dedf749, and CI's test job is green on that head — so the workspace-green report stands.
The <name>.com/.app placeholder is also not a venue in the sense the check targets: the rule exists so metadata answering a refused financial operation doesn't hand the model another transactional service, and that string is a lookalike-domain example inside an unserved description.
One real note from this, which I'm not acting on inside this PR: if IcProtocolTools is ever wired back into the served surface, this check won't extend to it automatically, and that placeholder would then need rewording. Worth handling in whichever change re-serves that half.
Generated by Claude Code
| stated where the scan reads it: in full in the server-level instructions, | ||
| and deliberately in no description — a policy paragraph inside | ||
| `canister_update_call`'s description would read as a hint that the tool is | ||
| usable for financial transactions. So no description promises a behavior |
There was a problem hiding this comment.
and deliberately in no description — a policy paragraph inside
canister_update_call's description would read as a hint that the tool is
usable for financial transactions
Why did you mention this here 😭😭😭
Please omit this for the love of god!
There was a problem hiding this comment.
Fair, and the reason is worth stating plainly: explaining why the policy is kept out of descriptions invites the reading it exists to prevent. A reviewer needs the posture, not our deliberation.
Omitted in a747610, from both documents — I took the Anthropic one out in the same commit, before your second comment arrived. Both now say the policy is stated in the server-level instructions, covering the whole surface at once, held there by a test, and nothing further.
I did keep the rationale in the two code comments (tools.rs, compliance.rs), since that is what stops the sentence being added back a third time and directory review never reads them. Say so if you want those trimmed as well.
Generated by Claude Code
| outside the connector, in a trusted interface they control, and names no | ||
| venue (a refused canister-creation or funding-completion call points at the | ||
| user-run icp CLI). The policy is stated in full in the server-level | ||
| instructions — the field the directories scan — and deliberately in no tool |
There was a problem hiding this comment.
— and deliberately in no tool
description: a policy paragraph insidecanister_update_call's description
would read as a hint that the tool is usable for financial transactions.
Same as in my other comment, please omit for the love of god!
There was a problem hiding this comment.
Already out — a747610 removed it from this document and the OpenAI one together, since the sentence was the same in both. This bullet now ends: the policy is stated in the server-level instructions, the field the directories scan, where it covers the whole surface at once, held there by a test.
Generated by Claude Code
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
| // canisters, and derive the user's per-app principal. It confers no ability to | ||
| // act — every update call to these apps' canisters is refused by | ||
| // [`crate::compliance`], which carries all three of them — so an entry here is | ||
| // discovery-neutral rather than a route to a transaction. NNS was dropped from | ||
| // this list at the author's request; that was an editorial choice about which | ||
| // names the server advertises, not a safety criterion, and its derivation | ||
| // origins are deliberately kept below so reads still resolve to the right | ||
| // identity. |
There was a problem hiding this comment.
Correct, and it stings a little: that was the same overclaim this PR keeps removing from other people's sentences, in a comment I wrote two commits ago. The list covers each service's central canisters, and compliance.rs:73-78 says plainly that a static list cannot cover an exchange's dynamically created pool canisters — so "every update call to these apps' canisters is refused" was wrong.
Fixed in 27a2cc8. It now says 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. No claim of complete canister coverage.
Generated by Claude Code
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
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
crates/imcp2-core/src/management.rs:37
- Removing
E8S_PER_ICPleaves its doc comment attached toMAX_SINGLE_SHOT_WASM, so the generated documentation now incorrectly describes the Wasm-size threshold as an ICP conversion constant. Remove the orphaned line.
/// 1 ICP = 100_000_000 e8s.
crates/imcp2-core/src/tools.rs:2478
- The instruction-only tools were removed from this array, but their explanatory comment remains and now annotates no entries. Remove it so the read-only classification test does not imply that any listed tool is instruction-only.
// Instructions-only since the marketplace-compliance changes: they
// execute nothing and move no funds, so they are pure reads.
Extends the financial-transactions guard in two ways. First,
manage_neuron— the one standardized method through which NNS and SNS governance neurons are disbursed, split, and spawned — joins the always-refused method names alongside the ICRC transfer/approval surface, covering every SNS DAO's governance (54 exist today, more launch by NNS proposal) and every future one with no list to go stale. Second, a curated, labeled disallow list of finance-related canisters on whichcanister_update_callrefuses every update method — closing the gap where value could still move through non-standardized methods (a DEX swap, a minter withdrawal) that no method-name list can enumerate. Each canister entry carries two labels — what service it provides, and how it is finance-related — interpolated verbatim into the refusal, e.g.:The canister list (52 entries — count verified programmatically, per review)
Every id was verified against the IC dashboard's canister registry (
ic-api.internetcomputer.org, exact registered names) and the services' own published sources (theircanister_ids.json/ manifests / docs / on-chain SNS records) on 2026-08-28; the dashboard-unnamed entries are each pinned by a project-official source (DFINITY's own repos, apps' own/.well-known/ic-app.jsonmanifests, livex-ic-canister-idheaders, SNS proposal payloads).manage_neuron), both NNS dapp frontends (classicqoctq…and currentmc7vh…origins), the Genesis Token canister (claim_neurons), the ICP ledger, the cycles ledger.retrieve_btc), ckETH/ckERC20 (withdraw_eth/withdraw_erc20), ckDOGE (retrieve_doge).canister_ids.json./.well-known/ic-app.json); ICPSwap (SwapFactory, PasscodeManager, farm + staking-pool factories, frontend); Sonic (swap canister, LBP registry, vesting, plus the XTC and WICP DIP-20 ledgers — their legacytransfer/mintnames are exactly what the standardized-methods group deliberately does not match, so the canister entries close that documented gap for the two most prominent legacy ledgers); ICDex/ICLighthouse (router, DexAggregator, DAO trader, trading-mining, frontend); ICPEx (router, token-creation service, frontend).Deliberately not listed
manage_neuronmethod rule instead (per review): a static list of 54 DAOs (a third of them aborted swaps, some renamed or folded) would go stale the day the next SNS launches, while the method rule covers them all, present and future.notify_create_canister/notify_top_upare the documented recovery path for a user's own interrupted icp-CLI funding flow.Mechanics
manage_neuronrefused on any canister; every canister entry refuses arbitrary update names with both labels + the protective policy wording and no marketplace/compliance jargon; the list is well-formed (valid, unique principals; non-empty labels); precedence pinned. The literal-matching test demonstrates on an unlisted canister (on a listed one, everything is refused regardless of spelling).SERVER_INSTRUCTIONS, the README tool-table row, both directory docs' guard bullets, and the landing page's two "refused by design" strings — all four now name themanage_neuronrule alongside the ICRC names (per review). Per review, the directory docs state the posture present-tense only — no history of past functionality.Also in this PR
The NNS is dropped from the known-app registry (separate commit): the name registry exists to route agents to well-known apps, and routing them to a staking/funds frontend sits badly with the not-a-financial-tool posture — so
open_app("nns")no longer resolves, no repair guidance points at it, and the unknown-app note no longer enumerates it. TheKNOWN_DERIVATION_ORIGINSmappings 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 new guard.Verification
cargo testworkspace-green: 153 core + 20 local tests.cargo clippyat the existing 10-warning baseline.ic-api.internetcomputer.orgbefore curation (exact name matches, or unnamed-but-source-pinned; rejected/undeployed ids excluded).🤖 Generated with Claude Code
https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk