Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
265 changes: 222 additions & 43 deletions README.md

Large diffs are not rendered by default.

485 changes: 485 additions & 0 deletions crates/imcp2-core/src/architecture.rs

Large diffs are not rendered by default.

1,219 changes: 1,219 additions & 0 deletions crates/imcp2-core/src/authorization.rs

Large diffs are not rendered by default.

37 changes: 37 additions & 0 deletions crates/imcp2-core/src/calls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,33 @@ pub struct CanisterUpdateCallArgs {
pub canister_id: String,
/// Update method name to invoke.
pub method: String,
/// REQUIRED. The https origin of the application this call belongs to —
/// scheme and host only, e.g. `https://example.com` (no path). This is
/// what AUTHORIZES the call: the origin must be a registered application
/// whose developer accepted the ICP MCP Developer Terms, its
/// `/.well-known/ic-architecture` manifest is read fresh on every call,
/// and the target canister must be one the manifest declares. A canister
/// id found any other way (a response header, an `/env.json`, a JS
/// bundle) cannot be written to. Get the value from open_app / resolve_app
/// (`application_origin`). NOT interchangeable with `derivation_origin`:
/// several frontends can share one derivation origin, and the manifest
/// lives at the application origin. Reads (canister_query) need no
/// application origin.
///
/// `Option` + `schemars(required)` deliberately: the SCHEMA marks it
/// required, so a client sends it rather than discovering the requirement
/// from an error, while the type still lets a client that omits it anyway
/// reach the gate's own refusal — which names the argument, says where to
/// get it, and distinguishes it from `derivation_origin` — instead of an
/// opaque invalid-params protocol error rmcp would raise for a missing
/// required `String`.
///
/// No `#[serde(default)]`: schemars treats a defaulted field as optional
/// regardless of `required` (schemars_derive `schema_exprs.rs`), and serde
/// already deserializes a missing `Option` field to `None` without it — so
/// the pair only works this way round. Pinned by a test on both halves.
#[schemars(required)]
pub application_origin: Option<String>,
/// Arguments in textual Candid syntax, e.g. `()` or `(record { owner = principal "..." })`.
#[serde(default = "default_args")]
pub args: String,
Expand Down Expand Up @@ -219,6 +246,16 @@ pub struct CanisterUpdateCallOutput {
pub canister_id: String,
/// The method that was invoked.
pub method: String,
/// The registered application origin the call was authorized against, in
/// canonical form. Compare it with what you passed to catch an origin that
/// canonicalized to a different application than you meant.
pub application_origin: String,
/// How that application's own `/.well-known/ic-architecture` manifest
/// describes the canister that was called (its name/role) — null when the
/// manifest declares the id with no labels. Read it as confirmation that
/// the canister you called is the one the application says it is.
#[serde(skip_serializing_if = "Option::is_none")]
pub declared_as: Option<String>,
/// The decoded reply in textual Candid.
pub reply: String,
/// The principal the call was signed as — null for an anonymous call.
Expand Down
24 changes: 22 additions & 2 deletions crates/imcp2-core/src/compliance.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,20 @@
//! The financial-transactions guard for the generic update-call tool.
//! **Layer 2 of the write gate: the financial-transactions guard.**
//!
//! `canister_update_call` is authorized in two layers, and this is the inner
//! one. [`crate::authorization`] decides *whether the application may be
//! written to at all* — it must be registered under the ICP
//! service-discoverability protocol, with its developer's acceptance of the
//! ICP MCP Developer Terms on file, and must declare the target canister in
//! its own `/.well-known/ic-architecture` manifest. This module then refuses
//! value-moving calls **inside** that authorized surface.
//!
//! The two layers answer different questions and neither substitutes for the
//! other. Layer 1 is what keeps arbitrary canisters — a ledger a user names by
//! hand, a canister mined out of a frontend bundle — out of reach entirely, so
//! this list is not the thing standing between an agent and the ICP ledger.
//! Layer 2 is what keeps a *registered* application from moving value through
//! its own authorized surface, which registration must never buy: it is
//! deliberately origin-blind, so no registration state can reach it.
//!
//! This server is not a financial tool: its purpose is reading, building, and
//! operating canisters, and the marketplace directories it is listed in
Expand Down Expand Up @@ -81,7 +97,11 @@
//! launch: entries cover each service's central canisters (verified
//! against the IC dashboard's registry and the services' own published
//! sources), and the standardized-methods group plus the stated policy
//! cover the rest.
//! cover the rest. What bounds the un-enumerable remainder is Layer 1, not
//! this list: an update call can only reach a canister a registered
//! application declares as its own, so a bespoke value-moving method is
//! reachable only inside an application whose developer accepted Terms
//! that forbid exposing one.
//! * Legacy pre-ICRC token standards (DIP20/EXT `transfer`/`transferFrom`/
//! `approve` on arbitrary canisters) are deliberately NOT matched: the
//! names are too abstract to block everywhere without breaking
Expand Down
Loading
Loading