From 85c28ff7c76684133ea4c5d0611e72f06c21b830 Mon Sep 17 00:00:00 2001 From: Arshavir Ter-Gabrielyan Date: Wed, 28 Oct 2026 15:09:51 +0000 Subject: [PATCH 1/3] Describe the guard's three checks in the server instructions instead of calling it incomplete Directory review asked, alongside the guard extension that landed in #213 and #214, that the server instructions "no longer describe the guard as incomplete". The financial-transactions paragraph still carried the pre-extension clause "that guard is a safeguard, not a complete filter", and the unit test pinned it. The paragraph now describes what an update call is checked against: by method name (the ICRC ledger methods, neuron management, and financial keywords as whole words, naming transfer, withdraw and swap as examples), by destination (the known financial-service canisters), and by the canister's own published Candid interface (ICRC-1/ICRC-2 methods make it a financial service; a missing or unreadable interface means no update call at all). It states the one thing none of the checks judges, a bespoke value-moving method under an unrelated name on a non-ledger canister, so the limit stays stated rather than implied away, and keeps the redirect. The keyword list, interface markers and canister ids are still not restated, for the same drift reason as before. The test now pins the three checks and the stated limit, pins the old clause out, and checks the three example keywords against the guard so the paragraph cannot name a keyword the guard does not refuse. The two submission docs that described the instructions are aligned. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PruVfYuaS7ZAgNBMTsuRP6 --- crates/imcp2-core/src/tools.rs | 61 ++++++++++++++++++-------- docs/anthropic-directory-submission.md | 13 ++++-- docs/openai-directory-submission.md | 12 ++--- 3 files changed, 59 insertions(+), 27 deletions(-) diff --git a/crates/imcp2-core/src/tools.rs b/crates/imcp2-core/src/tools.rs index d0215a8..6a74dce 100644 --- a/crates/imcp2-core/src/tools.rs +++ b/crates/imcp2-core/src/tools.rs @@ -2214,12 +2214,16 @@ fn identity_annotation(target: &IdentityTarget) -> String { /// usable for financial transactions, which is the one thing it must not /// suggest. `financial_policy_is_a_server_instruction_not_a_description` /// holds that line across every served description, and the directive scan -/// covers the schemas too. What the paragraph does NOT do is restate -/// [`crate::compliance`]'s method families and canister scopes: that list would -/// have to be kept in sync forever, and a refused call already gets a refusal -/// accurate for its own scope. Neither surface names a venue for a refused -/// operation. -const SERVER_INSTRUCTIONS: &str = "Tools for the Internet Computer: read what a canister offers, read and write its data, and act as the user's own identity at an app.\n\nCANISTERS. Each canister publishes a Candid interface declaring its methods and their types. Method arguments and replies are textual Candid, the `(...)` syntax, e.g. `(record { owner = principal \"aaaaa-aa\"; amount = 5 : nat })`; candid_syntax_guide returns that syntax; the full type system is served as an MCP resource, `candid://reference`. Some canisters additionally declare an OQL surface. This server reads those through OQL and refuses a Candid `method` query on them; update calls are unaffected. icp_oql_guide documents the dialect. IC how-to guides are served as `skill://` resources.\n\nAPPS AND IDENTITY. An app is a website backed by canisters. Internet Identity gives the user a different principal at each app, derived from that app's derivation origin, which is not always the app's visible URL; open_app resolve it, and the tools that act as the user take the resolved value. Within one app the user may hold several accounts.\n\nDECLARED CANISTERS. Every tool that reaches a canister — get_canister_candid, get_canister_api_doc, get_canister_oql_schema, canister_query and canister_update_call — works only on a canister the app that owns it declares at /.well-known/ic-architecture (https://docs.internetcomputer.org/guides/frontends/service-discoverability/), which is how its operators opt in to being read and operated by agents. `app_url` names that app and open_app returns it, alongside which of the app's canisters carry that declaration (marked [declared]); where a call carries both an `app_url` and a derivation origin, the two must belong to the same app. An app that publishes no manifest is still resolved and discovered, but none of its canisters can be read or called here: say that, and what its operators would publish, rather than retrying.\n\nSESSIONS. Calls that act as the user are signed with the Internet Identity credential this connection was authorized with. Internet Identity offers two access levels at that point: \"Questions only\", where reads work and state-changing calls are rejected by the network, and \"Actions & questions\", which permits both.\n\nFINANCIAL TRANSACTIONS ARE NOT SUPPORTED, to protect the user: do not use canister_update_call to move assets. Value-moving methods, neuron management, and every update call to a known wallet, ledger, exchange or staking canister are refused before they reach the network; that guard is a safeguard, not a complete filter, so treat this policy, rather than the absence of a refusal, as the limit. For financial operations (token transfers, spending approvals, payments, trades), the user works outside this connector, in a trusted interface they control.\n\nBuilding and deploying canisters happens in the user's own environment with the icp CLI; no tool here creates, funds or deploys one."; +/// covers the schemas too. The paragraph describes the three checks an update +/// call goes through — by method name, by destination, and by the canister's +/// own published interface — and states the one thing none of them judges, so +/// a reader knows what the guard covers without being told it is incomplete +/// (per directory review). It does NOT restate [`crate::compliance`]'s keyword +/// list, markers and canister ids: that copy would have to be kept in sync +/// forever, and a refused call already gets a refusal accurate for its own +/// scope. The three example keywords it names are pinned to the guard by the +/// same test. Neither surface names a venue for a refused operation. +const SERVER_INSTRUCTIONS: &str = "Tools for the Internet Computer: read what a canister offers, read and write its data, and act as the user's own identity at an app.\n\nCANISTERS. Each canister publishes a Candid interface declaring its methods and their types. Method arguments and replies are textual Candid, the `(...)` syntax, e.g. `(record { owner = principal \"aaaaa-aa\"; amount = 5 : nat })`; candid_syntax_guide returns that syntax; the full type system is served as an MCP resource, `candid://reference`. Some canisters additionally declare an OQL surface. This server reads those through OQL and refuses a Candid `method` query on them; update calls are unaffected. icp_oql_guide documents the dialect. IC how-to guides are served as `skill://` resources.\n\nAPPS AND IDENTITY. An app is a website backed by canisters. Internet Identity gives the user a different principal at each app, derived from that app's derivation origin, which is not always the app's visible URL; open_app resolve it, and the tools that act as the user take the resolved value. Within one app the user may hold several accounts.\n\nDECLARED CANISTERS. Every tool that reaches a canister — get_canister_candid, get_canister_api_doc, get_canister_oql_schema, canister_query and canister_update_call — works only on a canister the app that owns it declares at /.well-known/ic-architecture (https://docs.internetcomputer.org/guides/frontends/service-discoverability/), which is how its operators opt in to being read and operated by agents. `app_url` names that app and open_app returns it, alongside which of the app's canisters carry that declaration (marked [declared]); where a call carries both an `app_url` and a derivation origin, the two must belong to the same app. An app that publishes no manifest is still resolved and discovered, but none of its canisters can be read or called here: say that, and what its operators would publish, rather than retrying.\n\nSESSIONS. Calls that act as the user are signed with the Internet Identity credential this connection was authorized with. Internet Identity offers two access levels at that point: \"Questions only\", where reads work and state-changing calls are rejected by the network, and \"Actions & questions\", which permits both.\n\nFINANCIAL TRANSACTIONS ARE NOT SUPPORTED, to protect the user: do not use canister_update_call to move assets. Before an update call is sent it is checked three ways. By method name, on every canister: the ICRC ledger methods, neuron management, and any name carrying a financial keyword as a whole word (transfer, withdraw, swap and the like) are refused. By destination: every update call to the known wallet, ledger, exchange and staking canisters is refused. By the canister's own published Candid interface, read before anything is encoded: a canister that declares ICRC-1 or ICRC-2 methods is a financial service and receives no update call, and a canister whose interface is missing or unreadable receives none either. A refusal says which check applied and claims nothing beyond it. Not judged: a bespoke value-moving method under an unrelated name on a canister whose interface is not a ledger's. For financial operations (token transfers, spending approvals, payments, trades), the user works outside this connector, in a trusted interface they control.\n\nBuilding and deploying canisters happens in the user's own environment with the icp CLI; no tool here creates, funds or deploys one."; impl ServerHandler for IcTools { async fn list_tools( @@ -2809,16 +2813,19 @@ mod tests { // 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. Pin both sides — no tool description carries financial - // language, and the instructions state the denial, the limit of the guard - // that backs it, and the redirect. The refused method families are - // deliberately NOT restated here or in the instructions: they were a copy - // of `compliance` that had to be kept in sync, and an attempted call is - // refused with a message accurate for its own scope. What the instructions - // must not do is promise more than the guard delivers, so the - // safeguard-not-a-filter clause is pinned in its place. Neither surface - // names a venue for a refused operation: metadata answering a refused - // financial operation with a specific transactional service would read as - // a redirect from one such route to another. + // language, and the instructions state the denial, the three checks that + // enforce it (by name, by destination, by the canister's own interface), + // the one thing those checks do not judge, and the redirect. Directory + // review asked that the instructions describe what the guard covers + // rather than call it incomplete, so the old "a safeguard, not a complete + // filter" clause is pinned OUT. The keyword list, markers and canister ids + // are deliberately NOT restated in the instructions: they were a copy of + // `compliance` that had to be kept in sync, and an attempted call is + // refused with a message accurate for its own scope — the three example + // keywords the paragraph does name are checked against the guard here. + // Neither surface names a venue for a refused operation: metadata + // answering a refused financial operation with a specific transactional + // service would read as a redirect from one such route to another. #[test] fn financial_policy_is_a_server_instruction_not_a_description() { for tool in super::IcTools::all_tools() { @@ -2828,10 +2835,28 @@ mod tests { } let ins = super::SERVER_INSTRUCTIONS; assert!(ins.contains("FINANCIAL TRANSACTIONS ARE NOT SUPPORTED, to protect the user")); + for check in ["By method name", "By destination", "published Candid interface"] { + assert!(ins.contains(check), "the instructions must describe the {check} check: {ins}"); + } + assert!(ins.contains("Not judged:"), "the instructions must state the limit: {ins}"); assert!( - ins.contains("a safeguard, not a complete filter"), - "the instructions must not present the guard as complete coverage: {ins}" + !ins.contains("not a complete filter") && !ins.contains("safeguard"), + "the instructions describe what the guard covers rather than calling it \ + incomplete (per directory review): {ins}" ); + // The example keywords the paragraph names are real: each is refused + // by the guard on an arbitrary canister. + for keyword in ["transfer", "withdraw", "swap"] { + assert!(ins.contains(keyword), "the paragraph names {keyword}: {ins}"); + assert!( + crate::compliance::disallowed_update_method( + &candid::Principal::management_canister(), + keyword + ) + .is_some(), + "{keyword} is named in the instructions but not refused by the guard" + ); + } assert!(ins.contains("outside this connector, in a trusted interface they control")); assert!(!ins.contains("oisy.com"), "the instructions name no venue: {ins}"); } diff --git a/docs/anthropic-directory-submission.md b/docs/anthropic-directory-submission.md index 9e9d055..9350c31 100644 --- a/docs/anthropic-directory-submission.md +++ b/docs/anthropic-directory-submission.md @@ -219,10 +219,15 @@ transactions:** venue (a refused canister-creation or funding-completion call points at the user-run icp CLI). The policy is stated in the server-level instructions — the field the directories scan — where it covers the whole surface at once, - and a unit test holds it there. What those instructions state is the policy - itself, not its implementation: the method families and canister scopes are - in the guard and in the refusal an attempted call receives, so the - instructions carry no copy of that list to keep in sync. + and a unit test holds it there. Those instructions state the policy and + describe the three checks that enforce it — by method name, by destination, + and by the canister's own published interface — plus the one thing none of + them judges, so a reader knows what the guard covers without being told it + is incomplete (the review asked for exactly that). The keyword list, the + interface markers and the canister ids are in the guard and in the refusal + an attempted call receives, so the instructions carry no copy of them to + keep in sync; the three example keywords they do name are pinned to the + guard by the same unit test. - The README and the server instructions both state explicitly that financial transactions are not supported. The landing page is no longer one of them: #165 moved it to , diff --git a/docs/openai-directory-submission.md b/docs/openai-directory-submission.md index 3700cea..32f617b 100644 --- a/docs/openai-directory-submission.md +++ b/docs/openai-directory-submission.md @@ -184,11 +184,13 @@ submission states, not from a narrower reading: canister, a URL with no Internet-Computer evidence). The financial-transactions policy is a separate, server-wide matter and is stated where the scan reads it: in the server-level instructions, covering - the whole surface at once. Those instructions state the policy, not its - implementation — the method families and canister scopes live in the guard - and in the refusal each attempted call receives, rather than in a copy that - would have to be kept in sync. So no description promises a behavior - the tool does not have, and no refusal is hidden. + the whole surface at once. Those instructions state the policy and describe + the three checks that enforce it (by method name, by destination, by the + canister's own published interface) and the one thing none of them judges; + the keyword list, the interface markers and the canister ids live in the + guard and in the refusal each attempted call receives, rather than in a + copy that would have to be kept in sync. So no description promises a + behavior the tool does not have, and no refusal is hidden. - **The commerce and speculation rules have nothing to attach to.** The plugin sells nothing (no physical goods, no digital goods or subscriptions, no checkout) and offers no speculation product: no trading, From ae2270c2447e7f66794223fe71829620bd6872d2 Mon Sep 17 00:00:00 2001 From: Arshavir Ter-Gabrielyan Date: Wed, 28 Oct 2026 15:15:02 +0000 Subject: [PATCH 2/3] Mention the control-handover exemption in the instructions' keyword check Review on #215 found the rewritten paragraph over-claimed: it said every method name carrying a financial keyword is refused, while the keyword check exempts the control-handover names (`transfer_ownership` and its camelCase spelling), which `exemptions_are_literal` pins. The paragraph now says so in general terms with the one example, and the test pins that example against the guard the same way it pins the three keywords, so the instructions cannot name an exemption the guard does not honour. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PruVfYuaS7ZAgNBMTsuRP6 --- crates/imcp2-core/src/tools.rs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/crates/imcp2-core/src/tools.rs b/crates/imcp2-core/src/tools.rs index 6a74dce..7b4f0da 100644 --- a/crates/imcp2-core/src/tools.rs +++ b/crates/imcp2-core/src/tools.rs @@ -2221,9 +2221,10 @@ fn identity_annotation(target: &IdentityTarget) -> String { /// (per directory review). It does NOT restate [`crate::compliance`]'s keyword /// list, markers and canister ids: that copy would have to be kept in sync /// forever, and a refused call already gets a refusal accurate for its own -/// scope. The three example keywords it names are pinned to the guard by the -/// same test. Neither surface names a venue for a refused operation. -const SERVER_INSTRUCTIONS: &str = "Tools for the Internet Computer: read what a canister offers, read and write its data, and act as the user's own identity at an app.\n\nCANISTERS. Each canister publishes a Candid interface declaring its methods and their types. Method arguments and replies are textual Candid, the `(...)` syntax, e.g. `(record { owner = principal \"aaaaa-aa\"; amount = 5 : nat })`; candid_syntax_guide returns that syntax; the full type system is served as an MCP resource, `candid://reference`. Some canisters additionally declare an OQL surface. This server reads those through OQL and refuses a Candid `method` query on them; update calls are unaffected. icp_oql_guide documents the dialect. IC how-to guides are served as `skill://` resources.\n\nAPPS AND IDENTITY. An app is a website backed by canisters. Internet Identity gives the user a different principal at each app, derived from that app's derivation origin, which is not always the app's visible URL; open_app resolve it, and the tools that act as the user take the resolved value. Within one app the user may hold several accounts.\n\nDECLARED CANISTERS. Every tool that reaches a canister — get_canister_candid, get_canister_api_doc, get_canister_oql_schema, canister_query and canister_update_call — works only on a canister the app that owns it declares at /.well-known/ic-architecture (https://docs.internetcomputer.org/guides/frontends/service-discoverability/), which is how its operators opt in to being read and operated by agents. `app_url` names that app and open_app returns it, alongside which of the app's canisters carry that declaration (marked [declared]); where a call carries both an `app_url` and a derivation origin, the two must belong to the same app. An app that publishes no manifest is still resolved and discovered, but none of its canisters can be read or called here: say that, and what its operators would publish, rather than retrying.\n\nSESSIONS. Calls that act as the user are signed with the Internet Identity credential this connection was authorized with. Internet Identity offers two access levels at that point: \"Questions only\", where reads work and state-changing calls are rejected by the network, and \"Actions & questions\", which permits both.\n\nFINANCIAL TRANSACTIONS ARE NOT SUPPORTED, to protect the user: do not use canister_update_call to move assets. Before an update call is sent it is checked three ways. By method name, on every canister: the ICRC ledger methods, neuron management, and any name carrying a financial keyword as a whole word (transfer, withdraw, swap and the like) are refused. By destination: every update call to the known wallet, ledger, exchange and staking canisters is refused. By the canister's own published Candid interface, read before anything is encoded: a canister that declares ICRC-1 or ICRC-2 methods is a financial service and receives no update call, and a canister whose interface is missing or unreadable receives none either. A refusal says which check applied and claims nothing beyond it. Not judged: a bespoke value-moving method under an unrelated name on a canister whose interface is not a ledger's. For financial operations (token transfers, spending approvals, payments, trades), the user works outside this connector, in a trusted interface they control.\n\nBuilding and deploying canisters happens in the user's own environment with the icp CLI; no tool here creates, funds or deploys one."; +/// scope. The three example keywords it names, and the control-handover +/// exemption it mentions, are pinned to the guard by the same test. Neither +/// surface names a venue for a refused operation. +const SERVER_INSTRUCTIONS: &str = "Tools for the Internet Computer: read what a canister offers, read and write its data, and act as the user's own identity at an app.\n\nCANISTERS. Each canister publishes a Candid interface declaring its methods and their types. Method arguments and replies are textual Candid, the `(...)` syntax, e.g. `(record { owner = principal \"aaaaa-aa\"; amount = 5 : nat })`; candid_syntax_guide returns that syntax; the full type system is served as an MCP resource, `candid://reference`. Some canisters additionally declare an OQL surface. This server reads those through OQL and refuses a Candid `method` query on them; update calls are unaffected. icp_oql_guide documents the dialect. IC how-to guides are served as `skill://` resources.\n\nAPPS AND IDENTITY. An app is a website backed by canisters. Internet Identity gives the user a different principal at each app, derived from that app's derivation origin, which is not always the app's visible URL; open_app resolve it, and the tools that act as the user take the resolved value. Within one app the user may hold several accounts.\n\nDECLARED CANISTERS. Every tool that reaches a canister — get_canister_candid, get_canister_api_doc, get_canister_oql_schema, canister_query and canister_update_call — works only on a canister the app that owns it declares at /.well-known/ic-architecture (https://docs.internetcomputer.org/guides/frontends/service-discoverability/), which is how its operators opt in to being read and operated by agents. `app_url` names that app and open_app returns it, alongside which of the app's canisters carry that declaration (marked [declared]); where a call carries both an `app_url` and a derivation origin, the two must belong to the same app. An app that publishes no manifest is still resolved and discovered, but none of its canisters can be read or called here: say that, and what its operators would publish, rather than retrying.\n\nSESSIONS. Calls that act as the user are signed with the Internet Identity credential this connection was authorized with. Internet Identity offers two access levels at that point: \"Questions only\", where reads work and state-changing calls are rejected by the network, and \"Actions & questions\", which permits both.\n\nFINANCIAL TRANSACTIONS ARE NOT SUPPORTED, to protect the user: do not use canister_update_call to move assets. Before an update call is sent it is checked three ways. By method name, on every canister: the ICRC ledger methods, neuron management, and any name carrying a financial keyword as a whole word (transfer, withdraw, swap and the like) are refused, bar a short list of exempt control-handover names such as transfer_ownership. By destination: every update call to the known wallet, ledger, exchange and staking canisters is refused. By the canister's own published Candid interface, read before anything is encoded: a canister that declares ICRC-1 or ICRC-2 methods is a financial service and receives no update call, and a canister whose interface is missing or unreadable receives none either. A refusal says which check applied and claims nothing beyond it. Not judged: a bespoke value-moving method under an unrelated name on a canister whose interface is not a ledger's. For financial operations (token transfers, spending approvals, payments, trades), the user works outside this connector, in a trusted interface they control.\n\nBuilding and deploying canisters happens in the user's own environment with the icp CLI; no tool here creates, funds or deploys one."; impl ServerHandler for IcTools { async fn list_tools( @@ -2822,7 +2823,9 @@ mod tests { // are deliberately NOT restated in the instructions: they were a copy of // `compliance` that had to be kept in sync, and an attempted call is // refused with a message accurate for its own scope — the three example - // keywords the paragraph does name are checked against the guard here. + // keywords the paragraph does name, and the control-handover exemption it + // mentions (per review: without it the paragraph over-claimed what the + // keyword check refuses), are checked against the guard here. // Neither surface names a venue for a refused operation: metadata // answering a refused financial operation with a specific transactional // service would read as a redirect from one such route to another. @@ -2857,6 +2860,17 @@ mod tests { "{keyword} is named in the instructions but not refused by the guard" ); } + // ...and the exemption the paragraph mentions is real too: the name it + // gives as an example passes the name gate. + assert!(ins.contains("control-handover names such as transfer_ownership"), "{ins}"); + assert!( + crate::compliance::disallowed_update_method( + &candid::Principal::management_canister(), + "transfer_ownership" + ) + .is_none(), + "transfer_ownership is named as exempt in the instructions but refused by the guard" + ); assert!(ins.contains("outside this connector, in a trusted interface they control")); assert!(!ins.contains("oisy.com"), "the instructions name no venue: {ins}"); } From b6bd5461b287efb9ebcb71cbf3694b09740f1486 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 15:21:52 +0000 Subject: [PATCH 3/3] Drop the keyword scope's exemption list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The list held one name in two spellings, `transfer_ownership` and `transferOwnership`, kept on the OpenZeppelin `Ownable` convention alone: no mainnet interface declaring either could be found when review asked for sources. The list's own rule was that a name with no source does not get in, so it goes (at the maintainer's direction), and a name that carries a keyword is now refused whatever the app means by it. `financial_keyword` no longer consults a list; the module doc records that the exemption was tried and why it went; the tests pin the ownership spellings as refused (`no_name_is_exempt`), the keyword list's well-formedness check loses its exemption half, and the server instructions, their pinning test, the README and both directory submission docs no longer mention an exemption — the test now pins that the instructions mention none and that `transfer_ownership` is refused. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01VP2vTGhAZfkTv1hwmKERA3 --- README.md | 5 +- crates/imcp2-core/src/compliance.rs | 102 +++++++++---------------- crates/imcp2-core/src/tools.rs | 25 +++--- docs/anthropic-directory-submission.md | 4 +- docs/openai-directory-submission.md | 3 +- 5 files changed, 53 insertions(+), 86 deletions(-) diff --git a/README.md b/README.md index 99b2188..80824d6 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,7 @@ are unsupported by policy. The ledger surface is refused mechanically: `canister_update_call` rejects the ICRC-1/ICRC-2 and related ledger-standard transfer/approval methods and any method whose name carries a financial keyword (`transfer`, `withdraw`, `swap`, `trade`, `exchange`, `pay`, `buy`, `deposit`, -… as whole words, bar a short list of exempt control-handover names such as -`transfer_ownership`) on every canister, and refuses every update call to a +… as whole words, with no exemptions) on every canister, and refuses every update call to a canister whose own Candid interface declares ICRC-1 or ICRC-2 methods — every ICRC ledger and the ICP ledger, detected from the canister itself — or that is one of the financial-service canisters it carries. An update call needs the @@ -173,7 +172,7 @@ results). | `get_canister_candid` | `canister_id`, `app_url?`, `derivation_origin?` | The canister's `candid:service` interface (`.did` text), plus two capability flags: `oql` (`true` when it exposes an OQL query surface — a `schema` + `execute` pair — with a pointer to `icp_oql_guide`) and `api_doc_available` (`true` when it declares a `getApiDoc`/`get_api_doc` method, gating `get_canister_api_doc`) | | `get_canister_api_doc` | `canister_id`, `app_url?`, `derivation_origin?` | 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 for every documentation outcome — `available` + the doc on success, else `available:false` with `expected`/`retry`/`next`, so "no compatible method was detected" is distinct from "no answer was obtained". An unusable `canister_id` is rejected before any lookup and is a plain error, not that shape; and `expected:true` is not proof of absence, since an interface the parser cannot read also comes up empty | | `canister_query` | `canister_id`, `method?` **or** `oql?`, `args?` (textual Candid), `app_url?`, `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` so an origin mismatch is visible (never the principal — `get_app_principal` returns it on request) | -| `canister_update_call` | `canister_id`, `method`, `args` (textual Candid), `app_url?`, `derivation_origin?`, `account?` | 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`). **The target must be declared by its app**: the call is made only when the app at `app_url` (from `open_app`; falling back to `derivation_origin` when no `app_url` is given) declares `canister_id` in its `/.well-known/ic-architecture` manifest — see [Reaching a canister is gated on the discoverability manifest](#reaching-a-canister-is-gated-on-the-discoverability-manifest). When `derivation_origin` is also given, the two must belong to the same app — the app at `app_url` is resolved to the derivation origin II derives its users from, and a mismatch is refused rather than signed. The reply echoes `declared_by` / `declared_at` (which origin authorized the write, and at which path). **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; any other `icrc1_*`/`icrc2_*` method is refused by name as well, as is any method whose name carries a financial keyword as a whole word (`transfer`, `withdraw`, `swap`, `trade`, `exchange`, `pay`, `buy`, `deposit`, `mint`, `stake`, … — `swapTokens` and `withdraw_all` match, `payload` does not — except a literal list of control-handover names such as `transfer_ownership`); and **every** update call is refused on a canister whose own Candid interface declares ICRC-1 or ICRC-2 methods — every ICRC ledger and the ICP ledger, detected from the interface the canister publishes — and on the financial-service canisters the guard carries — all to protect the user. **The canister must publish its interface**: an update call to a canister that publishes none, or whose interface cannot be read, is refused (there is no `candid` fallback here, unlike `canister_query`), so every update the server sends was checked against the canister's own interface. 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 policy is stated in the server-level instructions, deliberately not in any tool description. Echoes `derived_for_origin` / `requested` so an origin mismatch is visible (never the principal — `get_app_principal` returns it on request) | +| `canister_update_call` | `canister_id`, `method`, `args` (textual Candid), `app_url?`, `derivation_origin?`, `account?` | 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`). **The target must be declared by its app**: the call is made only when the app at `app_url` (from `open_app`; falling back to `derivation_origin` when no `app_url` is given) declares `canister_id` in its `/.well-known/ic-architecture` manifest — see [Reaching a canister is gated on the discoverability manifest](#reaching-a-canister-is-gated-on-the-discoverability-manifest). When `derivation_origin` is also given, the two must belong to the same app — the app at `app_url` is resolved to the derivation origin II derives its users from, and a mismatch is refused rather than signed. The reply echoes `declared_by` / `declared_at` (which origin authorized the write, and at which path). **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; any other `icrc1_*`/`icrc2_*` method is refused by name as well, as is any method whose name carries a financial keyword as a whole word (`transfer`, `withdraw`, `swap`, `trade`, `exchange`, `pay`, `buy`, `deposit`, `mint`, `stake`, … — `swapTokens` and `withdraw_all` match, `payload` does not — with no exemptions, so `transfer_ownership` is refused too); and **every** update call is refused on a canister whose own Candid interface declares ICRC-1 or ICRC-2 methods — every ICRC ledger and the ICP ledger, detected from the interface the canister publishes — and on the financial-service canisters the guard carries — all to protect the user. **The canister must publish its interface**: an update call to a canister that publishes none, or whose interface cannot be read, is refused (there is no `candid` fallback here, unlike `canister_query`), so every update the server sends was checked against the canister's own interface. 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 policy is stated in the server-level instructions, deliberately not in any tool description. Echoes `derived_for_origin` / `requested` so an origin mismatch is visible (never the principal — `get_app_principal` returns it on request) | | `get_app_principal` | `derivation_origin`, `account?` | The principal you act as at an app. Identify the app by its `derivation_origin` (from `open_app`). Echoes `derived_for_origin` / `requested` so an origin mismatch is visible | | `list_app_accounts` | `derivation_origin` | The user's Internet Identity accounts at an app — the default account plus any named ones — with name, number, and the derivation origin they were listed for (no principals and no last-used timestamps: the listing exists to pick an account). Identify the app by its `derivation_origin` (from `open_app`) | | `candid_syntax_guide` | — | The textual-Candid value syntax the `args` of `canister_query` / `canister_update_call` take and their replies come back in: the literal form for each Candid type, and when a value needs an explicit `: type` annotation. The full type system stays available as the `candid://reference` MCP resource | diff --git a/crates/imcp2-core/src/compliance.rs b/crates/imcp2-core/src/compliance.rs index e8f0fcc..56d2c80 100644 --- a/crates/imcp2-core/src/compliance.rs +++ b/crates/imcp2-core/src/compliance.rs @@ -70,9 +70,10 @@ //! A call whose method name contains a financial / crypto keyword //! (`transfer`, `withdraw`, `swap`, `trade`, `exchange`, `pay`, `buy`, //! `deposit`, … — [`FINANCIAL_KEYWORDS`]) is refused before any network -//! work, modulo a literal list of exempt control-handover names -//! ([`EXEMPT_METHODS`], `transfer_ownership` and its spellings). Matching -//! is by WHOLE WORD: the name is split on `_`, `-`, digits and camelCase +//! work, with no exemption list: a name that carries a keyword is refused +//! whatever the app means by it (the ownership-handover names exempted at +//! first had no verified canister behind them and went, per review). +//! Matching is by WHOLE WORD: the name is split on `_`, `-`, digits and camelCase //! humps and lowercased, so `swapTokens`, `withdraw_all` and `Transfer` //! match while `payload`, `buyer` and `swapped` do not — a substring test //! would refuse every `set_payload`. The guard cannot know what an @@ -145,8 +146,11 @@ //! refusing a real ledger's, pool's or DIP20 token's method is worth more //! than allowing an app's non-financial one, so such names are now //! refused on every canister by whole-word keyword, with a register that -//! says so rather than classifying the call, and a literal exemption list -//! for the control-handover names. Still unlisted, on purpose: `approve`, +//! says so rather than classifying the call, and no exemption list (a +//! literal list of control-handover names, `transfer_ownership` and its +//! camelCase spelling, was carried at first; no mainnet interface +//! declaring either could be found, so it went — a name with no source +//! does not get in). Still unlisted, on purpose: `approve`, //! `send`, `claim`, `reward`, `balance`, `token`, `wallet`, `ledger`, //! `order`, `fee`, `cycles` (ordinary app verbs and nouns far more often //! than value operations), and derived forms such as `payment` or @@ -546,29 +550,6 @@ const FINANCIAL_KEYWORDS: &[&str] = &[ "bridge", ]; -/// Method names exempt from the keyword scope although they contain a keyword -/// — a control handover (of a canister, a record, an app), not a value -/// movement — matched LITERALLY, by exact bytes: `Transfer_Ownership` or -/// `transfer_ownership_v2` are not exempt. Every entry records where the name -/// comes from, so the list can be audited (per review): a name with no source -/// does not get in, and an entry whose source goes away goes with it. -const EXEMPT_METHODS: &[&str] = &[ - // The `Ownable` access-control convention — OpenZeppelin's Solidity - // `transferOwnership(address)` — which app code ported from the EVM keeps - // when it hands a canister's or a record's admin role to a new principal; - // the snake_case form is the Rust (ic-cdk) spelling of the same method. - // Kept at the maintainer's direction as the one control-handover name - // worth exempting. No mainnet interface declaring either spelling has - // been verified yet (the IC token standards use `setOwner` — DIP20, - // Sonic — or `set_custodians` — DIP721 — which carry no keyword); should - // a canister whose `transfer_ownership` moves VALUE turn up (an NFT - // handover is one), the exemption goes. Controller handover has no entry: - // on the Internet Computer a canister's controllers change through the - // management canister's `update_settings`, never through an app method. - "transfer_ownership", - "transferOwnership", -]; - /// The words of a method name, for whole-word keyword matching: maximal runs /// of ASCII letters, split additionally at a lower→upper boundary and before /// the last upper of an upper run that a lower follows (`HTTPRequest` → @@ -604,12 +585,11 @@ fn name_words(method: &str) -> Vec { words } -/// The first financial keyword `method` carries as a whole word, unless the -/// name is one of the literal [`EXEMPT_METHODS`]. +/// The first financial keyword `method` carries as a whole word. There is no +/// exemption list: `transfer_ownership` is refused like `transfer_tokens`, +/// since the guard cannot tell a control handover from a value movement by +/// the name, and no canister has been found that needs the distinction. pub(crate) fn financial_keyword(method: &str) -> Option<&'static str> { - if EXEMPT_METHODS.contains(&method) { - return None; - } name_words(method) .into_iter() .find_map(|word| FINANCIAL_KEYWORDS.iter().copied().find(|keyword| *keyword == word)) @@ -1010,8 +990,8 @@ mod tests { // Reads and generic names on arbitrary canisters stay callable: a word // that merely contains a keyword (`payload`, `buyer`, `swapped`, `trader`) - // is not that keyword, `approve`/`send`/`claim` are deliberately not - // keywords, and the literal exemptions are control handovers. (An + // is not that keyword, and `approve`/`send`/`claim` are deliberately not + // keywords. (An // ICRC-named read is refused — it addresses the canister as a ledger — and // `transfer_fee` is the keyword scope's accepted false positive.) The // cycles-minting canister's non-keyword names are here too: refused on the @@ -1035,8 +1015,6 @@ mod tests { "get_buyer", "swapped", "trader", - "transfer_ownership", - "transferOwnership", ] { assert!( disallowed_update_method(&any_canister(), method).is_none(), @@ -1265,11 +1243,14 @@ mod tests { } // A keyword matches a whole word only — in any spelling convention — and - // the literal exemptions are the one way around it. + // nothing is exempt: an ownership handover is refused like any other + // `transfer`. #[test] fn financial_keyword_matches_whole_words_only() { for (method, keyword) in [ ("swapTokens", "swap"), + ("transfer_ownership", "transfer"), + ("transferOwnership", "transfer"), ("withdraw_all", "withdraw"), ("icrc1_transfer", "transfer"), ("Transfer", "transfer"), @@ -1301,8 +1282,6 @@ mod tests { "send", "claim", "process_payment", - "transfer_ownership", - "transferOwnership", "", ] { assert_eq!(financial_keyword(method), None, "{method}"); @@ -1333,29 +1312,30 @@ mod tests { } } - // Exemptions are literal: the exact spellings pass, anything else that - // contains the keyword does not. + // No name is exempt: the ownership-handover spellings once carried as + // exemptions are refused by the `transfer` keyword like every other + // spelling (per review — no canister declaring them could be found, and a + // name with no source does not get in). #[test] - fn exemptions_are_literal() { - for method in EXEMPT_METHODS { - assert!( - disallowed_update_method(&any_canister(), method).is_none(), - "{method} is a literal exemption" - ); - } - for method in ["Transfer_Ownership", "transfer_ownership_v2", "transfer_owner"] { + fn no_name_is_exempt() { + for method in [ + "transfer_ownership", + "transferOwnership", + "Transfer_Ownership", + "transfer_ownership_v2", + "transfer_owner", + ] { let msg = disallowed_update_method(&any_canister(), method) - .unwrap_or_else(|| panic!("{method} is not literally exempt")); + .unwrap_or_else(|| panic!("{method} must be refused: nothing is exempt")); assert!(msg.contains(NAME_RULE), "{msg}"); } } - // The lists stay well-formed: keywords unique, lowercase, alphabetic, and + // The keyword list stays well-formed: unique, lowercase, alphabetic, and // none of them a word of the mixed-purpose entry point (one register per - // name); every exemption unique and actually carrying a keyword, so no - // entry is dead. + // name). #[test] - fn keyword_and_exemption_lists_are_well_formed() { + fn keyword_list_is_well_formed() { let mut keywords: Vec<&str> = FINANCIAL_KEYWORDS.to_vec(); for keyword in &keywords { assert!( @@ -1370,18 +1350,6 @@ mod tests { for (method, _) in MIXED_PURPOSE_METHODS { assert_eq!(financial_keyword(method), None, "{method} has its own register"); } - let mut exempt: Vec<&str> = EXEMPT_METHODS.to_vec(); - for method in &exempt { - let words = name_words(method); - assert!( - words.iter().any(|w| FINANCIAL_KEYWORDS.contains(&w.as_str())), - "{method} carries no keyword, so the exemption is dead" - ); - } - let total = exempt.len(); - exempt.sort_unstable(); - exempt.dedup(); - assert_eq!(exempt.len(), total, "duplicate exemptions"); } // A canister that provides an ICRC-1 / ICRC-2 method is a financial canister: diff --git a/crates/imcp2-core/src/tools.rs b/crates/imcp2-core/src/tools.rs index 7b4f0da..50c774b 100644 --- a/crates/imcp2-core/src/tools.rs +++ b/crates/imcp2-core/src/tools.rs @@ -2221,10 +2221,10 @@ fn identity_annotation(target: &IdentityTarget) -> String { /// (per directory review). It does NOT restate [`crate::compliance`]'s keyword /// list, markers and canister ids: that copy would have to be kept in sync /// forever, and a refused call already gets a refusal accurate for its own -/// scope. The three example keywords it names, and the control-handover -/// exemption it mentions, are pinned to the guard by the same test. Neither -/// surface names a venue for a refused operation. -const SERVER_INSTRUCTIONS: &str = "Tools for the Internet Computer: read what a canister offers, read and write its data, and act as the user's own identity at an app.\n\nCANISTERS. Each canister publishes a Candid interface declaring its methods and their types. Method arguments and replies are textual Candid, the `(...)` syntax, e.g. `(record { owner = principal \"aaaaa-aa\"; amount = 5 : nat })`; candid_syntax_guide returns that syntax; the full type system is served as an MCP resource, `candid://reference`. Some canisters additionally declare an OQL surface. This server reads those through OQL and refuses a Candid `method` query on them; update calls are unaffected. icp_oql_guide documents the dialect. IC how-to guides are served as `skill://` resources.\n\nAPPS AND IDENTITY. An app is a website backed by canisters. Internet Identity gives the user a different principal at each app, derived from that app's derivation origin, which is not always the app's visible URL; open_app resolve it, and the tools that act as the user take the resolved value. Within one app the user may hold several accounts.\n\nDECLARED CANISTERS. Every tool that reaches a canister — get_canister_candid, get_canister_api_doc, get_canister_oql_schema, canister_query and canister_update_call — works only on a canister the app that owns it declares at /.well-known/ic-architecture (https://docs.internetcomputer.org/guides/frontends/service-discoverability/), which is how its operators opt in to being read and operated by agents. `app_url` names that app and open_app returns it, alongside which of the app's canisters carry that declaration (marked [declared]); where a call carries both an `app_url` and a derivation origin, the two must belong to the same app. An app that publishes no manifest is still resolved and discovered, but none of its canisters can be read or called here: say that, and what its operators would publish, rather than retrying.\n\nSESSIONS. Calls that act as the user are signed with the Internet Identity credential this connection was authorized with. Internet Identity offers two access levels at that point: \"Questions only\", where reads work and state-changing calls are rejected by the network, and \"Actions & questions\", which permits both.\n\nFINANCIAL TRANSACTIONS ARE NOT SUPPORTED, to protect the user: do not use canister_update_call to move assets. Before an update call is sent it is checked three ways. By method name, on every canister: the ICRC ledger methods, neuron management, and any name carrying a financial keyword as a whole word (transfer, withdraw, swap and the like) are refused, bar a short list of exempt control-handover names such as transfer_ownership. By destination: every update call to the known wallet, ledger, exchange and staking canisters is refused. By the canister's own published Candid interface, read before anything is encoded: a canister that declares ICRC-1 or ICRC-2 methods is a financial service and receives no update call, and a canister whose interface is missing or unreadable receives none either. A refusal says which check applied and claims nothing beyond it. Not judged: a bespoke value-moving method under an unrelated name on a canister whose interface is not a ledger's. For financial operations (token transfers, spending approvals, payments, trades), the user works outside this connector, in a trusted interface they control.\n\nBuilding and deploying canisters happens in the user's own environment with the icp CLI; no tool here creates, funds or deploys one."; +/// scope. The three example keywords it names are pinned to the guard by the +/// same test, which also pins that it mentions no exemption (the keyword check +/// has none). Neither surface names a venue for a refused operation. +const SERVER_INSTRUCTIONS: &str = "Tools for the Internet Computer: read what a canister offers, read and write its data, and act as the user's own identity at an app.\n\nCANISTERS. Each canister publishes a Candid interface declaring its methods and their types. Method arguments and replies are textual Candid, the `(...)` syntax, e.g. `(record { owner = principal \"aaaaa-aa\"; amount = 5 : nat })`; candid_syntax_guide returns that syntax; the full type system is served as an MCP resource, `candid://reference`. Some canisters additionally declare an OQL surface. This server reads those through OQL and refuses a Candid `method` query on them; update calls are unaffected. icp_oql_guide documents the dialect. IC how-to guides are served as `skill://` resources.\n\nAPPS AND IDENTITY. An app is a website backed by canisters. Internet Identity gives the user a different principal at each app, derived from that app's derivation origin, which is not always the app's visible URL; open_app resolve it, and the tools that act as the user take the resolved value. Within one app the user may hold several accounts.\n\nDECLARED CANISTERS. Every tool that reaches a canister — get_canister_candid, get_canister_api_doc, get_canister_oql_schema, canister_query and canister_update_call — works only on a canister the app that owns it declares at /.well-known/ic-architecture (https://docs.internetcomputer.org/guides/frontends/service-discoverability/), which is how its operators opt in to being read and operated by agents. `app_url` names that app and open_app returns it, alongside which of the app's canisters carry that declaration (marked [declared]); where a call carries both an `app_url` and a derivation origin, the two must belong to the same app. An app that publishes no manifest is still resolved and discovered, but none of its canisters can be read or called here: say that, and what its operators would publish, rather than retrying.\n\nSESSIONS. Calls that act as the user are signed with the Internet Identity credential this connection was authorized with. Internet Identity offers two access levels at that point: \"Questions only\", where reads work and state-changing calls are rejected by the network, and \"Actions & questions\", which permits both.\n\nFINANCIAL TRANSACTIONS ARE NOT SUPPORTED, to protect the user: do not use canister_update_call to move assets. Before an update call is sent it is checked three ways. By method name, on every canister: the ICRC ledger methods, neuron management, and any name carrying a financial keyword as a whole word (transfer, withdraw, swap and the like) are refused. By destination: every update call to the known wallet, ledger, exchange and staking canisters is refused. By the canister's own published Candid interface, read before anything is encoded: a canister that declares ICRC-1 or ICRC-2 methods is a financial service and receives no update call, and a canister whose interface is missing or unreadable receives none either. A refusal says which check applied and claims nothing beyond it. Not judged: a bespoke value-moving method under an unrelated name on a canister whose interface is not a ledger's. For financial operations (token transfers, spending approvals, payments, trades), the user works outside this connector, in a trusted interface they control.\n\nBuilding and deploying canisters happens in the user's own environment with the icp CLI; no tool here creates, funds or deploys one."; impl ServerHandler for IcTools { async fn list_tools( @@ -2823,9 +2823,10 @@ mod tests { // are deliberately NOT restated in the instructions: they were a copy of // `compliance` that had to be kept in sync, and an attempted call is // refused with a message accurate for its own scope — the three example - // keywords the paragraph does name, and the control-handover exemption it - // mentions (per review: without it the paragraph over-claimed what the - // keyword check refuses), are checked against the guard here. + // keywords the paragraph does name are checked against the guard here, and + // so is the absence of any exemption: the keyword check has none, so the + // paragraph must not mention one (an earlier version named + // `transfer_ownership` as exempt, which the guard no longer honours). // Neither surface names a venue for a refused operation: metadata // answering a refused financial operation with a specific transactional // service would read as a redirect from one such route to another. @@ -2860,16 +2861,16 @@ mod tests { "{keyword} is named in the instructions but not refused by the guard" ); } - // ...and the exemption the paragraph mentions is real too: the name it - // gives as an example passes the name gate. - assert!(ins.contains("control-handover names such as transfer_ownership"), "{ins}"); + // ...and the paragraph names no exemption, because the guard has none: + // the ownership-handover name once mentioned as exempt is refused. + assert!(!ins.contains("exempt"), "the keyword check has no exemption: {ins}"); assert!( crate::compliance::disallowed_update_method( &candid::Principal::management_canister(), "transfer_ownership" ) - .is_none(), - "transfer_ownership is named as exempt in the instructions but refused by the guard" + .is_some(), + "transfer_ownership must be refused: nothing is exempt from the keyword check" ); assert!(ins.contains("outside this connector, in a trusted interface they control")); assert!(!ins.contains("oisy.com"), "the instructions name no venue: {ins}"); diff --git a/docs/anthropic-directory-submission.md b/docs/anthropic-directory-submission.md index 9350c31..a361f74 100644 --- a/docs/anthropic-directory-submission.md +++ b/docs/anthropic-directory-submission.md @@ -206,8 +206,8 @@ transactions:** methods (`notify_top_up`, `notify_create_canister`, `notify_mint_cycles`, `create_canister`), refuses any method whose name carries a financial keyword as a whole word (`transfer`, `withdraw`, `swap`, `trade`, - `exchange`, `pay`, `buy`, `deposit`, …, bar a literal list of - control-handover names such as `transfer_ownership`) on every canister, + `exchange`, `pay`, `buy`, `deposit`, …, with no exemptions) on every + canister, and refuses **every** update call to a canister whose own Candid interface declares ICRC-1 or ICRC-2 methods — every ICRC ledger and the ICP ledger, detected from the canister itself; an update call needs diff --git a/docs/openai-directory-submission.md b/docs/openai-directory-submission.md index 32f617b..0ad2773 100644 --- a/docs/openai-directory-submission.md +++ b/docs/openai-directory-submission.md @@ -166,8 +166,7 @@ submission states, not from a narrower reading: the cycles-minting canister's funding-completion methods on those canisters, any method whose name carries a financial keyword as a whole word (`transfer`, `withdraw`, `swap`, `trade`, `exchange`, `pay`, `buy`, - `deposit`, …, bar a literal list of control-handover names such as - `transfer_ownership`) on every canister, every update call to a canister + `deposit`, …, with no exemptions) on every canister, every update call to a canister whose own Candid interface declares ICRC-1 or ICRC-2 methods (every ICRC ledger and the ICP ledger, detected from the canister itself; an update call needs that published interface, so a canister that publishes none, or