diff --git a/README.md b/README.md index 056d560..80659e4 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,16 @@ encoding/decoding and signing against the IC via [`ic-agent`](https://github.com/dfinity/agent-rs). The MCP layer is the [official Rust SDK](https://github.com/modelcontextprotocol/rust-sdk) (`rmcp`). +**Writes go only to apps that opted in.** Reading the Internet Computer is open — +every read tool here works on any canister. A *state-changing* call runs against +someone's live application, so `canister_update_call` is made **only** to a +canister the owning app declares in its +[service-discoverability](https://docs.internetcomputer.org/guides/frontends/service-discoverability/) +manifest at `/.well-known/ic-architecture`. Publishing that manifest is how an +app's operators opt in; an app that publishes none stays fully readable and +discoverable, but cannot be written to through this server. See +[Writes are gated on the discoverability manifest](#writes-are-gated-on-the-discoverability-manifest). + **Not for financial operations.** IMCP2 is infrastructure tooling for reading, building, and operating canisters — it is not a wallet or trading tool, and financial operations (token transfers, spending approvals, payments, trades) @@ -147,14 +157,14 @@ results). | Tool | Args | Returns | |------|------|---------| | `open_app` | `app` (name **or** URL) | **One-call entry point** when a user names/links an app: resolves the Internet Identity `derivation_origin` *and* discovers the canisters behind it, together. A name or bare host is matched to the known-app registry first (so a wrong-TLD guess repairs to the canonical URL); an explicit `https://` URL is resolved as given. An unknown bare name is *refused*, and so is a URL that would need its own origin assumed as the derivation origin while showing no IC evidence (never guessed). Also probes the app's own canisters and reports per-canister `oql`/`api_doc_available` capability flags — for **up to eight** eligible canisters, with both fields *omitted* (not false) on any beyond that — plus a data-access note (which canister is read through the OQL path, and the origin that path requires). Wraps `resolve_app` + `discover_app_canisters`; no auth | -| `discover_app_canisters` | `domain` | Canister ids behind a web domain — app-declared App Connect metadata first (`/ai-connect.html`'s `ic:canister-id` meta, `/.well-known/ic-app.json` manifest), then the frontend via `x-ic-canister-id` and backend candidates via `/env.json` + JS-bundle mining — each with provenance, its IC dashboard label/type where known, and (for the app's own canisters) `oql`/`api_doc_available` capability flags from a one-shot Candid probe | +| `discover_app_canisters` | `domain` | Canister ids behind a web domain — app-declared metadata first (the `/.well-known/ic-architecture` service-discoverability manifest, and the same manifest at the legacy `/.well-known/ic-app.json` path), then the frontend via `x-ic-canister-id` and backend candidates via `/env.json` + JS-bundle mining — each with provenance, its IC dashboard label/type where known, and (for the app's own canisters) `oql`/`api_doc_available` capability flags from a one-shot Candid probe | | `get_canister_candid` | `canister_id` | 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` | 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), `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) 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 the financial-service canisters the guard carries — 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 policy is stated in the server-level instructions, deliberately not in any tool description. `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), `app_url?`, `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`). **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 [Writes are gated on the discoverability manifest](#writes-are-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; and **every** update call is refused on the financial-service canisters the guard carries — 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 policy is stated in the server-level instructions, deliberately not in any tool description. `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` | | `get_app_principal` | `derivation_origin`, `account?` | The principal you act as at an app, without a call. Identify the app by its `derivation_origin` (from `open_app`/`resolve_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, last-used, and the derivation origin they were listed for. Identify the app by its `derivation_origin` (from `open_app`/`resolve_app`) | -| `resolve_app` | `app_url` | Resolve an app URL to its Internet Identity derivation context: `application_origin`, the `derivation_origin` to use (declared in `/.well-known/ic-app.json`, else a built-in known-app value, else assumed = app origin — flagged via `derivation_origin_source`: `declared`/`known`/`app_url_default`, with `application_is_ic` echoing the gateway evidence), and the app's `alternative_origins` (informational). An origin with **no IC evidence** that would need the `app_url_default` assumption is **refused** (guessed-domain guard, with a "did you mean" repair when the host resembles a well-known app). Does not return a principal (no account chosen) or require auth — pass the `derivation_origin` to `get_app_principal`/`list_app_accounts` | +| `resolve_app` | `app_url` | Resolve an app URL to its Internet Identity derivation context: `application_origin`, the `derivation_origin` to use (declared by the app in `/.well-known/ii-derivation-origin`, else in the legacy `/.well-known/ic-app.json`, else a built-in known-app value, else assumed = app origin — flagged via `derivation_origin_source`: `declared`/`known`/`app_url_default`, with `application_is_ic` echoing the gateway evidence), and the app's `alternative_origins` (informational). An origin with **no IC evidence** that would need the `app_url_default` assumption is **refused** (guessed-domain guard, with a "did you mean" repair when the host resembles a well-known app). Does not return a principal (no account chosen) or require auth — pass the `derivation_origin` to `get_app_principal`/`list_app_accounts` | | `icp_oql_guide` | — | The OQL query-surface dialect guide (for canisters where `get_canister_candid` reports `oql: true`): the JSON query object, predicate grammar, edges, and paged result shape. The entity/field names come from `get_canister_oql_schema` and queries run through `canister_query` (the `oql` argument) | | `get_canister_oql_schema` | `canister_id`, `derivation_origin`, `account?` | The canister's OQL schema catalogue (entities, primary keys, fields, edges) as JSON — wraps its `schema` method — plus a ready-to-run `canister_query` example per entity. **`derivation_origin` is required**: this server rejects an anonymous read (for now) with guidance — its own rule, not an inference about the canister — rather than calling `schema` anonymously and returning an empty list | @@ -182,7 +192,7 @@ Acting **for the user** at an app: unrelated or squatted site. The tool enforces this: a bare *unknown* name is refused (find the real URL — web-search or ask the user), and a URL that resolves to `app_url_default` while showing **no IC evidence** (no valid `x-ic-canister-id` - gateway header, no `ic-app.json` derivation origin) is refused too; when the host + gateway header, no declared derivation origin) is refused too; when the host resembles a known app the error names it and gives the real URL (a "did you mean" repair). For a single step, the narrower tools remain: **`resolve_app(url)`** (origin only), **`discover_app_canisters(url)`** @@ -199,8 +209,9 @@ Acting **for the user** at an app: passing the `derivation_origin` — an OQL read **requires** it (an anonymous per-app read is rejected for now, and a Candid `method` query is rejected on an OQL canister). Otherwise pass a Candid `method`. -7. **Act** with `canister_update_call`, passing `derivation_origin` + `account` - to act as the user. +7. **Act** with `canister_update_call`, passing the `app_url` from step 0–2 (it + gates the write on the app's declared manifest) plus `derivation_origin` + + `account` to act as the user. Genuinely public reads via a `canister_query` Candid `method` query or the public-metadata tools (`get_canister_candid`, `discover_app_canisters`) skip steps @@ -209,42 +220,131 @@ independent of the identity steps (3/4), so they can run in parallel. Managing y **own** canisters is not part of this connector: create and manage them with the [`icp` CLI](https://github.com/dfinity/icp-cli) in your own terminal. -### App-declared canister metadata (App Connect) +### App-declared canister metadata -Apps that adopt **Internet Computer App Connect** serve a bridge page at -`/ai-connect.html` whose `` declares the app's -**main backend** canister (spec §4.7/§6.1). Discovery reads that meta from the -raw served markup (no JavaScript is executed) and reports it as the -top-priority finding, labelled `main backend (App Connect)`. +The authoritative statement about which canisters an app comprises is the one +the app publishes itself. Two such statements are read, both from bytes the app +serves: -The App Connect spec **defers** multi-canister enumeration (§6.3: how an app -lists *all* the canisters it comprises, with roles). To fill that gap, this -server also reads a proposed convention: a `/.well-known/ic-app.json` manifest -the app serves itself — +**The service-discoverability manifest** (`/.well-known/ic-architecture`) — +[Layer 1 of the protocol](https://docs.internetcomputer.org/guides/frontends/service-discoverability/) +— enumerates *every* canister the app comprises, with roles: ```json { - "derivation_origin": "https://.icp0.io", + "version": "1.0.0", "canisters": [ - { "id": "aaaaa-…-cai", "role": "backend", "description": "orders + inventory API" }, - { "id": "bbbbb-…-cai", "role": "ledger" } + { "id": "aaaaa-…-cai", "name": "backend", "role": "the backend", + "description": "orders + inventory API; call getApiDoc() first" }, + { "id": "bbbbb-…-cai", "name": "frontend", "role": "the frontend" } ] } ``` -Each entry needs an `id` (a canister principal); `role` and `description` are -optional and become the finding's label (`role — description`). Unknown fields -are ignored, so the format can grow. Both sources are the app's own claim about -its composition — stronger than anything mined from client code — but an -SPA catch-all serving HTML at these paths simply yields no findings (no meta -tag; JSON parse fails), and every id is still validated as a principal. +Each entry needs an `id` (a canister principal); `name`, `role`, and +`description` are optional and become the finding's label (`role — description`, +falling back to `name`). Unknown fields are ignored, so the format can grow. +The write gate holds `id` to the protocol's own type rule — an entry must be a +**canister** principal (a 10-byte opaque id) to authorize anything, so a manifest +cannot declare a user principal, the anonymous principal, or the management +canister `aaaaa-aa` (all of which parse as principals) and have a write follow. +This manifest is also the **only** thing that permits a write — see +[Writes are gated on the discoverability manifest](#writes-are-gated-on-the-discoverability-manifest). + +Before the protocol was published this server proposed the same document at +`/.well-known/ic-app.json`, with an extra top-level `derivation_origin` field. +That path is still read for discovery, at lower authority, so those apps stay +legible — but it does not authorize a write (see below); new apps should publish +`/.well-known/ic-architecture` +and, if they pin a custom derivation origin, `/.well-known/ii-derivation-origin` +(the protocol's Layer 5 — one canonical `https://host` on a single line, which +takes precedence over the legacy field). + +Both are the app's own claim about its composition — stronger than anything +mined from client code. Both also **fail closed** on the most common +misconfiguration, an SPA catch-all serving `index.html` at these paths: the JSON +parse fails, and the derivation-origin file's first line is not an origin. Every +id is validated as a principal before it is kept. + +### Writes are gated on the discoverability manifest + +Reading is open; writing is not. A state-changing call runs against someone's +live application, and a canister being publicly callable is not a statement by +its operators that they want an agent driving it. So `canister_update_call` is +restricted to canisters an app **declares** in its `/.well-known/ic-architecture` +manifest. Publishing that manifest is a deliberate act, and per the protocol guide +it is exactly how an app's operators say "these are my canisters; an agent handed +my URL may work them out and use them". + +**Only the standard path authorizes.** The legacy `/.well-known/ic-app.json` +document is still read during discovery, but it does not permit a write: the apps +serving it adopted a proposal this server made before the protocol existed, under +different terms, and never agreed to the ones publishing the standard manifest now +signifies. Consent that was never given cannot be inherited from a path this server +invented. An origin serving only the older document gets a refusal of its own, +naming the document it *does* publish — reporting it as publishing nothing would +send its operators hunting for a file that is already there — and saying that +serving the same JSON at `/.well-known/ic-architecture` is the whole fix. + +**The manifest and the identity are bound.** `app_url` picks which manifest is +read; `derivation_origin` picks whose principal signs. Left unbound those are +separable, and separable is exploitable: publishing a manifest is free and the +gate deliberately does not prove ownership, so an attacker's origin could declare +someone else's canister while the call went out under the principal the user holds +at an app they actually trust. So when a call carries both, the app at `app_url` +is resolved to the derivation origin Internet Identity derives its users from, and +a call naming a different one is refused. The comparison is against what the app +*resolves to* — its declared Layer 5 origin, else a known-app value, else its own +origin — so the many apps whose derivation origin differs from their website (13 +of 17 in the built-in registry) still pass, and Internet Identity's own +`ii-alternative-origins` rule is enforced on the way, so an app cannot simply +claim another's identity to satisfy the check. An attacker's manifest can then +only authorize writes made as the attacker's own app identity, which is worth +nothing to them. `open_app` returns a matching pair, so the normal flow never +trips this. + +The manifest lives at the app's origin, not on chain, so the tool has to be told +which app owns the target: that is the `app_url` argument (`open_app` returns it), +falling back to `derivation_origin` when the app serves its manifest at that same +origin. The gate then fetches the manifest — under the same SSRF hardening and +size caps as the rest of discovery — and refuses unless the target is listed. +Every refusal names the standard path, links the guide, distinguishes its cause +(no origin given / origin unreachable / no manifest published / only the legacy +document published / published but this canister is not in it — that last one lists +what the app *does* declare), and says +explicitly that reads are unaffected, so an agent answers what it can rather than +concluding the whole app is off limits. A successful call echoes `declared_by` and +`declared_at`, so a write's provenance is visible in the reply. + +Three things this gate is not: + +- **It is not proof of ownership.** Whoever controls a domain controls what its + manifest says, so a manifest can name a canister its publisher does not own. + What the gate establishes is that *someone* published a document, at an origin + the caller named, claiming that canister as part of their app — and that a + write can be traced back to that claim. Two limits worth stating outright: an + anonymous write skips the identity binding (there is no app identity to + protect), and an authenticated one binds to the *caller's* app while still + reaching any victim method that accepts an arbitrary principal. Neither grants + a capability an attacker lacked — the IC accepts ingress from anywhere, so both + calls can be sent with an ordinary agent — so what the gate withholds is this + connector's willingness to make them **on a user's behalf**, and the binding is + what keeps the user's own app principals out of it. Closing the rest needs an + association the *target* attests to, which the protocol does not define today. +- **It is not a substitute for the canister's own authorization.** The IC still + decides what the calling principal may do; this only decides what this server + is willing to send. +- **It is not a read restriction.** `canister_query`, `get_canister_candid`, + `get_canister_api_doc`, the OQL surface and every discovery tool work on any + canister, exactly as before. Discovery fetches are **SSRF-hardened** (CWE-918). Only `https` URLs with a real host are fetched, and every outbound fetch runs under a redirect guard (a 3xx may only go to a **globally-routable** IP or the same host, never a different private target; capped at 10 hops) with per-body and aggregate size caps, so a hostile or accidental large body can't exhaust memory. The untrusted **user-supplied** site -fetches (an app origin from `discover_app_canisters`, `open_app`, or `resolve_app`) +fetches (an app origin from `discover_app_canisters`, `open_app`, `resolve_app`, or +the `app_url` the write gate checks in `canister_update_call`) additionally resolve the target host up front and **pin** the connection to that validated globally-routable address, so a name resolving to a private/loopback/link-local address is refused and re-resolution can't rebind @@ -252,12 +352,23 @@ mid-flight. Fixed public-host enrichment (the IC dashboard) uses the redirect guard but is not separately address-pinned. No JavaScript is executed, and every extracted id is validated as a principal. -The optional top-level **`derivation_origin`** is the app's declaration of the -Internet Identity derivation origin its frontends pin (see the identity section -above). It is the only authoritative way for `open_app` / `resolve_app` to learn a -**custom** derivation origin from an app URL — there is no reverse lookup from an app URL to it — -so an app that uses one should declare it here; otherwise the connector assumes -the derivation origin equals the application origin and flags that assumption. +Note that the write gate makes `canister_update_call` an **outbound-fetching** +tool: acting at an app now contacts that app's website (once per write) to read +its manifest, where previously only the discovery/resolution tools did. A +manifest is only honoured when the origin the connector **probed** is the origin +that **answered** — a redirect can't let one origin borrow another's declaration +and have `declared_by` name the wrong app. + +The optional top-level **`derivation_origin`** is this document's own declaration +of the Internet Identity derivation origin an app's frontends pin (see the +identity section above). The protocol moved that declaration to its own file, so +**a new app should publish `/.well-known/ii-derivation-origin`** instead — one +canonical `https://host` on a single line, which takes precedence over this field. +The field is still read for the apps that shipped against it. Either is the only +authoritative way for `open_app` / `resolve_app` to learn a **custom** derivation +origin from an app URL — there is no reverse lookup from an app URL to it — so an +app that pins one should declare it; otherwise the connector assumes the +derivation origin equals the application origin and flags that assumption. When the user names a **token, project, or service** rather than a website or id, web search the canister id or ask the user for it. @@ -266,7 +377,9 @@ inline.) `canister_query` and `canister_update_call` run anonymously by default; pass a `derivation_origin` to call as -your account at that app. The server mints a **short-lived account delegation on +your account at that app. (Anonymous still means anonymous *identity* — a write +additionally needs an app origin, via `app_url` or `derivation_origin`, for the +manifest check above; there is no origin-less write.) The server mints a **short-lived account delegation on demand** using the connection's registered Internet Identity session key (see [Domain identities](#domain-identities-on-demand)) — there is no per-app sign-in step. `get_app_principal` returns that account's principal @@ -291,7 +404,8 @@ token (see Auth). > URL. A derivation origin is a *stable per-app value*, so you **resolve it once** > and reuse it: `open_app` (or `resolve_app`) turns an app name/URL into it and > reports how — `derivation_origin_source`: **declared** -> (`/.well-known/ic-app.json` → `derivation_origin`), else a built-in **known-app** +> (`/.well-known/ii-derivation-origin`, else the legacy `/.well-known/ic-app.json` +> → `derivation_origin`), else a built-in **known-app** > value for a few apps that pin a custom origin without declaring it (an app's > own declaration always overrides this), else the app origin *assumed* > (**app_url_default**). Feeding that resolved origin to an identity tool records @@ -913,7 +1027,8 @@ mcp_get_delegation : *domain-based* derivation: a raw `derivation_origin` is canonicalized and used verbatim, with no recovery of a custom derivation origin from it. When an `app_url` is passed instead, `resolve_app` resolves the derivation origin by - precedence **declared** (`/.well-known/ic-app.json` `derivation_origin`) > + precedence **declared** (`/.well-known/ii-derivation-origin`, else the legacy + `/.well-known/ic-app.json` `derivation_origin`) > built-in **known-app** registry > application origin, so a custom origin an app declares (or that ships in the registry, e.g. `oisy.com`) **is** honoured, and the app's `/.well-known/ii-alternative-origins` list is fetched and surfaced by diff --git a/crates/imcp2-core/src/calls.rs b/crates/imcp2-core/src/calls.rs index 353c846..91591d0 100644 --- a/crates/imcp2-core/src/calls.rs +++ b/crates/imcp2-core/src/calls.rs @@ -190,6 +190,18 @@ pub struct CanisterUpdateCallArgs { /// Arguments in textual Candid syntax, e.g. `()` or `(record { owner = principal "..." })`. #[serde(default = "default_args")] pub args: String, + /// The website URL of the app that owns `canister_id` (e.g. + /// "https://app.example.com"), which open_app returns as `app_url`. An update + /// call is made only to a canister the app declares in its + /// service-discoverability manifest (`/.well-known/ic-architecture`), and this + /// is the origin that manifest is read from. Omitted, `derivation_origin` is + /// used as that origin instead, which is the same value when the app serves + /// its manifest at the origin it derives identities from. Given together with + /// `derivation_origin`, the two must belong to the same app: this app is + /// resolved to the derivation origin Internet Identity derives its users from, + /// and a mismatch is refused rather than signed. + #[serde(default)] + pub app_url: Option, /// Call as the user's account at an app, identified by its exact canonical /// Internet Identity derivation origin — not necessarily the visible URL, and /// not an alternativeOrigins entry. open_app and resolve_app resolve an app @@ -244,6 +256,16 @@ pub struct CanisterUpdateCallOutput { /// Always present so a text-only client can tell an anonymous call from an /// authenticated one. pub is_anonymous: bool, + /// The app origin whose service-discoverability manifest DECLARES this + /// canister — the app whose published manifest authorized this write. Always + /// present on a successful call: without a declaration the call is refused. + pub declared_by: String, + /// The well-known path that declaration was read from. Always + /// `/.well-known/ic-architecture`: it is the only document that authorizes a + /// write, so no other value can appear on a successful call. Echoed anyway, so + /// a reply says where the authorization came from rather than leaving it + /// implied. + pub declared_at: String, } /// Arguments for `canister_query` — a READ that runs EITHER a Candid `query` method diff --git a/crates/imcp2-core/src/discover.rs b/crates/imcp2-core/src/discover.rs index b225342..12e9085 100644 --- a/crates/imcp2-core/src/discover.rs +++ b/crates/imcp2-core/src/discover.rs @@ -2,11 +2,13 @@ //! Internet Computer, folding together the patterns we've seen across apps: //! //! 1. **App-declared metadata** (most authoritative — the app says so): -//! the `ic:canister-id` `` on `/ai-connect.html` (the App Connect -//! bridge page, spec §4.7/§6.1 — the app's MAIN backend), and the -//! `/.well-known/ic-app.json` manifest enumerating ALL the app's -//! canisters with roles (our proposed convention for the spec's deferred -//! §6.3 "multi-canister applications" — see README). +//! the `/.well-known/ic-architecture` manifest enumerating ALL the app's +//! canisters with roles — Layer 1 of the IC service-discoverability +//! protocol ([`SERVICE_DISCOVERABILITY_GUIDE`]) and the ONE signal the +//! update-call gate in `discoverability` keys on; and the legacy +//! `/.well-known/ic-app.json` manifest of the same shape (this server's +//! pre-protocol proposal, still read for DISCOVERY so the apps that adopted +//! it stay legible, though it no longer authorizes a write). //! 2. `x-ic-canister-id` response header — the frontend/asset canister. This //! is the one universal signal (the HTTP gateway sets it). //! 3. a runtime config asset (`/env.json`) carrying `*canister_id*` keys — @@ -36,10 +38,10 @@ use tokio::task::JoinSet; #[derive(Serialize, Clone, Debug)] pub struct Found { pub canister_id: String, - /// A human label if one was attached (App Connect role, env.json key, + /// A human label if one was attached (manifest role, env.json key, /// bundle constant name, or "frontend"); None for a bare bundle literal. pub label: Option, - /// Where it was found: "ai-connect.html", "ic-app.json", "header", + /// Where it was found: "ic-architecture", "ic-app.json", "header", /// "env.json", "bundle: