From 7abd54351715f666a537ae844f0d920721e88f83 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 17:34:34 +0000 Subject: [PATCH 1/6] docs: add Service discoverability guide under Frontends Add a guide describing what a canister app exposes so an AI agent can discover and use it from just its URL, organized as five layers: composition (/.well-known/ic-architecture manifest), interface (candid:service), behavior (getApiDoc), data (OQL), and identity (/.well-known/ii-derivation-origin). Placed after Custom domains; bump Response certification and Frontend frameworks sidebar order to follow. --- docs/guides/frontends/certification.md | 2 +- docs/guides/frontends/frameworks.md | 2 +- .../frontends/service-discoverability.md | 208 ++++++++++++++++++ 3 files changed, 210 insertions(+), 2 deletions(-) create mode 100644 docs/guides/frontends/service-discoverability.md diff --git a/docs/guides/frontends/certification.md b/docs/guides/frontends/certification.md index 4c8052f9..a94b3c3a 100644 --- a/docs/guides/frontends/certification.md +++ b/docs/guides/frontends/certification.md @@ -2,7 +2,7 @@ title: "Response certification" description: "Verify that frontend responses are authentic and untampered using IC certificates" sidebar: - order: 3 + order: 4 --- Query responses on ICP are answered by a single replica without going through consensus. A malicious or faulty replica could return fabricated data. **Response certification** solves this: canisters commit a cryptographic hash to the subnet's certified state, and query responses include a certificate signed by the subnet's threshold BLS key. [HTTP gateways](../../concepts/edge-infrastructure.md#http-gateways) ([boundary nodes](../../concepts/edge-infrastructure.md#api-boundary-nodes)) verify every response automatically, so users are protected without any extra client-side code: as long as the canister certifies its responses. diff --git a/docs/guides/frontends/frameworks.md b/docs/guides/frontends/frameworks.md index d8e537d8..3de3037d 100644 --- a/docs/guides/frontends/frameworks.md +++ b/docs/guides/frontends/frameworks.md @@ -2,7 +2,7 @@ title: "Frontend frameworks" description: "Integrate React, Vue, Svelte, Next.js, and game engines with ICP canisters using the asset canister and icp-cli" sidebar: - order: 4 + order: 5 --- ICP hosts frontend applications as asset [canisters](../../concepts/canisters.md): static files (HTML, CSS, JavaScript) deployed to the network and served with certified responses. Any framework that can produce a static build output works: React, Vue, Svelte, Next.js, and even game engines like Unity WebGL and Godot. diff --git a/docs/guides/frontends/service-discoverability.md b/docs/guides/frontends/service-discoverability.md new file mode 100644 index 00000000..183e8a79 --- /dev/null +++ b/docs/guides/frontends/service-discoverability.md @@ -0,0 +1,208 @@ +--- +title: "Service discoverability" +description: "What a canister app exposes so an AI agent can discover its canisters, interfaces, behavior, data, and identity from just its URL" +sidebar: + order: 3 +--- + +When an AI agent is handed only your app's URL (for example, `https://yourapp.com`), it should be able to work out the rest on its own: which canisters your app comprises, what each one does, how to call them, how to query their data, and how to act as the signed-in user. No human supplying canister IDs, no bespoke integration. + +This guide describes what a canister app exposes to make that possible, ordered by priority. Parts of it are established Internet Computer standards, and parts are proposed conventions this guidance builds on; each section is labeled so you know which is which. + +## The five layers + +An agent handed only your app's URL should be able to do five things, unattended: + +1. Enumerate every canister the app comprises, and each one's role. +2. Inspect each canister's typed interface. +3. Understand the behavior the types cannot convey. +4. Query the app's data efficiently, without a bespoke method per question. +5. Act as the signed-in user, with that user's own permissions. + +Each layer is independently adoptable and independently useful. Together they make an app agent-ready. + +| Layer | Question it answers | Mechanism | +|-------|---------------------|-----------| +| 1. Composition | Which canisters make up this app, and what is each for? | `/.well-known/ic-architecture` manifest | +| 2. Interface | What methods and types does a canister expose? | `candid:service` metadata | +| 3. Behavior | How does it actually behave (units, lifecycle, gotchas)? | `getApiDoc` query method | +| 4. Data | How do I query its data? | OQL: `schema` and `execute` query methods | +| 5. Identity | How do I act as the signed-in user, under the right principal? | `/.well-known/ii-derivation-origin` declaration | + +`candid:service` (Layer 2) and `/.well-known/ii-alternative-origins` are existing Internet Computer and Internet Identity standards. The `/.well-known/ic-architecture` manifest, the `/.well-known/ii-derivation-origin` declaration, the `getApiDoc` convention, and the OQL data surface are proposed conventions. Layer 1 (composition discovery) is the most important and, in practice, today's weakest link, so it gets the most detail below. + +## One way to expose each fact + +Everything an app declares for discovery is served over HTTP as a dedicated file at a well-known path: a JSON manifest, or a single-value text file, read like `curl`. Do not embed discovery metadata in an HTML asset (a meta tag, inline script, or runtime cookie config), and do not require it to be mined from JavaScript bundles. One documented file per fact means there is nothing to reconcile and nothing to guess. + +The IC gateway does stamp an automatic `x-ic-canister-id` response header, but it names only the frontend and, like any header, is trivially forgeable. This guidance does not rely on it (see the anti-patterns under Layer 1). + +## Layer 1: Composition discovery + +**Proposed.** The single most valuable thing an app can declare is the set of canisters it comprises, each labeled with its role. Everything downstream (interface, behavior, data, identity) is per-canister, so an agent that cannot enumerate the set is stuck asking a human for canister IDs. + +### The canister manifest + +Serve a small JSON document at the origin's `/.well-known/ic-architecture` that lists every canister and its role: + +```json +{ + "canisters": [ + { "id": "hmxr2-pqaaa-aaabq-qaaaa-cai", "role": "backend", + "description": "orders + inventory API; call getApiDoc() first" }, + { "id": "hcv4s-uaaaa-aaabq-qaaba-cai", "role": "frontend" } + ] +} +``` + +This is the way an app declares its composition. It expresses the full set (not just a single "main" canister), it is read like `curl` (independent of the login page and of any HTML or JavaScript), and it fails loudly at deploy time rather than silently. + +**Field rules:** + +- `id` is required and must be a canister principal. +- `role` and `description` are optional, human-readable, and untrusted. A consumer sanitizes them before use. +- Unknown fields must be ignored, so the format can grow (for example, per-canister network hints or an api-doc pointer) without breaking older readers. + +**Serving rules:** + +- Serve it at exactly `/.well-known/ic-architecture`, at the origin, with no file extension. The IC's `.well-known` discovery files omit extensions by convention (compare `ic-domains` and `ii-alternative-origins`), even when, as here, the content is JSON. +- Serve real JSON with `Content-Type: application/json`. The most common failure is a single-page-app catch-all returning `index.html` for unknown paths. Exempt `/.well-known/*` from the SPA rewrite in your asset canister configuration. +- Generate it at deploy time. Canister IDs differ per network (local, staging, mainnet), so the file must be produced by the deploy pipeline (which already knows the IDs) rather than committed with hard-coded values. + +To serve `/.well-known/*` from an asset canister, add a rule to `.ic-assets.json5` so the hidden directory is included, exactly as for the `ic-domains` file: + +```json5 +[ + { + "match": ".well-known", + "ignore": false + } +] +``` + +See [Asset canister](asset-canister.md#ic-assetsjson5) for SPA aliasing configuration, and [Custom domains](custom-domains.md#step-2-create-the-ic-domains-file) for the same `.well-known` pattern applied to domain ownership. + +:::note +This is a proposed convention. There is no standard way today for an app to enumerate the multiple canisters it comprises: existing signals identify only a single "main" canister. The goal is exactly one documented way to declare composition. +::: + +### Anti-patterns: do not rely on these for discovery + +- **Discovery metadata embedded in HTML** (a `` tag, inline script, or templated page). Markup readers do not execute JavaScript, HTML is not a machine-readable contract, and it couples discovery to a page's lifecycle. Declare every fact in its own `/.well-known` file instead. +- **The `x-ic-canister-id` gateway header, for composition.** The IC HTTP gateway sets it automatically, but it names only the frontend or asset canister (never backends) and, like any response header, is trivially echoed by any origin. It is a hint at best, not a declaration the app controls. The manifest already lists the frontend as a role, so treat the manifest as the one source. +- **Config delivered only at runtime** (a `Set-Cookie` config blob, a script that reads `document.cookie`, and similar). It is invisible to a file reader and is a private transport between your platform and your own JavaScript, not a discovery interface. +- **IDs present only in the JavaScript bundle.** Mining a minified bundle is a last-ditch heuristic, not a contract. +- **A SPA catch-all answering `/.well-known/*`** (including `ic-architecture` and `ii-derivation-origin`) with `index.html`. +- **Per-network IDs committed into source.** They are wrong on every network but the one you baked them for. + +## Layer 2: Interface discovery + +**Standard.** Expose your Candid interface as the canister's public `candid:service` metadata, the standard IC mechanism emitted by default by the common toolchains. This lets an agent fetch the exact method signatures and types and encode or decode calls correctly. If it is unreadable, every downstream layer degrades to guessing. Nothing app-specific is required beyond not stripping it. + +See [Candid interface](../canister-calls/candid.md) for how Candid describes a canister's methods and types. + +## Layer 3: Behavioral guidance + +**Convention.** Candid types describe shape, not behavior. Expose a query method that returns a prose (markdown) guide to the things an agent cannot infer from types: + +```candid +getApiDoc : () -> (text) query; // or the snake_case name get_api_doc +``` + +Cover the non-obvious semantics, for example: + +- **Units and encoding:** integer money scaled by `10^8`, fractions versus tenth-bps, timestamp units. +- **Authentication:** which calls need a signed principal, and how anonymous access differs from a signed-in user. +- **Lifecycle:** staged or asynchronous operations that return before completing, so the agent must poll. +- **Mutation safety:** what is irreversible, and any dead-man switches. +- **Polling rules** and the gotchas that routinely trip up new integrators. + +**Name it discoverably.** Because the method name itself appears in `candid:service`, an agent finds `getApiDoc` with zero out-of-band knowledge: no bootstrap hint, meta tag, or side channel required. + +## Layer 4: Queryable data surface + +**Convention.** For data-rich apps, expose a self-describing query surface so an agent can answer questions without you writing a bespoke method per question. OQL is one such convention, a pair of query methods: + +```candid +schema : () -> (text) query; // JSON catalogue: entities, fields, edges +execute : (text) -> (Result) query; // one JSON query object -> rows +``` + +`schema` returns a JSON catalogue of entities, their fields (with types and roles), and the edges between them. An agent fetches it once so it knows what is queryable. + +`execute` takes one JSON query object (filters, aggregation, ordering, projection, paging) and returns rows. The result is a paged record of `{ hasMore, rows }` where each cell is `{ name, value }`. Agents read cells by name, never by position, and page while `hasMore` is true. + +Prefer server-side filtering and aggregation so only the needed data crosses into the agent's context. OQL is a recommended default, not a requirement: any Candid interface still works, just less economically for open-ended questions. + +## Layer 5: Acting as the user + +**Proposed.** To let an agent act with the user's own principal and permissions, an app exposes exactly one thing: the [Internet Identity](../authentication/internet-identity.md) derivation origin its frontends pin. An agent that already holds the user's Internet Identity authorization derives a short-lived, per-app delegation for that origin on demand. This yields the same principal the user has when they use your app, so your existing access control applies unchanged. No login page, no AI-specific backend code, and no bot account are required. (How the agent obtains the user's authorization in the first place, through session, scope, or consent, is the connector's concern and out of scope here. All the app has to publish is the origin.) + +The principal a user gets is a function of one input: the derivation origin Internet Identity signs for. By default that is the visible origin (`https://yourapp.com`). But an app may pin a custom derivation origin, typically its canister origin, so that a branded domain and its canister URL resolve to the same principal. When that is the case, the visible URL is not the origin identities are derived for, and an agent that assumes otherwise derives a valid but wrong principal, one with none of the user's permissions. + +**The alternative-origins file is the inverse relation.** A custom origin is enabled by two coupled files: the app pins `derivationOrigin` in its Internet Identity configuration, and the derivation origin publishes `/.well-known/ii-alternative-origins` listing the origins permitted to derive against it. That list answers "who may point here," not "where does this app point." The two are not interchangeable, and there is no reverse lookup from an app URL to its custom derivation origin. Reading it the wrong way round silently produces the wrong principal. See [Internet Identity](../authentication/internet-identity.md#alternative-origins) for how to configure `derivationOrigin` and `ii-alternative-origins`. + +**So the app states the answer rather than leaving it to be guessed.** Publish the effective derivation origin in a dedicated file at `/.well-known/ii-derivation-origin`, whose body is the canonical `https://host` origin on a single line: + +```text +https://hcv4s-uaaaa-aaabq-qaaba-cai.icp.net +``` + +This dedicated file, not the composition manifest, is the authoritative forward answer to "which origin does this app derive against." Keeping it separate is deliberate: + +- It is an identity concern (Layer 5), orthogonal to composition (Layer 1). An app may pin a custom derivation origin independently of how it declares composition. +- It sits symmetrically beside `/.well-known/ii-alternative-origins`, hence the `ii-` prefix: domain-based derivation is an Internet Identity concept, not a general IC one. This file is the forward relation ("where does this app point"); that file is the inverse ("who may point here"). +- A single-value, plain-text file is trivial to serve and to verify (one `curl`, no parsing), and it follows the `.well-known` convention. + +If you use the default (the app's own origin), omit the file. Its absence means "derive for the app origin itself." Serve it with no file extension and exempt `/.well-known/*` from the SPA catch-all, exactly as for the manifest. Generate it at deploy time when the origin is a per-network canister URL. + +**What a well-behaved agent does with it.** It fetches `/.well-known/ii-derivation-origin` (falling back to the app origin itself when the file is absent), signs the delegation for that canonical origin, and echoes back both the origin it derived for and the app URL it was asked about with every identity result. An origin mismatch is then visible at a glance instead of hiding behind a plausible-looking principal. + +## Precedence and who produces what + +| Signal | Layer | Produced by | Trust | Status | +|--------|-------|-------------|-------|--------| +| `/.well-known/ic-architecture` | Composition (all) | Deploy pipeline | App-declared | Proposed | +| `x-ic-canister-id` header | Composition (frontend) | IC gateway | Automatic hint (frontend); forgeable, not relied on | Automatic | +| `candid:service` | Interface | Canister toolchain | Authoritative | Standard | +| `getApiDoc` method | Behavior | Canister code | App-declared | Convention | +| `schema` / `execute` | Data | Canister code | App-declared | Convention | +| `/.well-known/ii-derivation-origin` | Identity | Deploy pipeline or static | App-declared | Proposed | +| `/.well-known/ii-alternative-origins` | Identity (inverse) | App (static file) | Not a lookup source | Standard | + +A consumer treats each app-declared signal as the app's own claim, to confirm rather than trust blindly. It fails closed: a malformed, oversized, or catch-all response yields no finding rather than a wrong one, and every ID is validated as a real canister principal before use. + +## Deployment checklist + +- [ ] **Composition:** the deploy pipeline emits `/.well-known/ic-architecture` listing every canister with a role, served as real JSON at the extensionless path. +- [ ] **Routing:** `/.well-known/*` is exempt from the SPA catch-all rewrite, so it is served as itself, not `index.html`. +- [ ] **Interface:** `candid:service` metadata is exposed (do not strip it). +- [ ] **Behavior:** the backend exposes `getApiDoc` or `get_api_doc`, returning a markdown guide. +- [ ] **Data (if applicable):** data-rich canisters expose OQL `schema` and `execute`. +- [ ] **Identity (if custom):** publish the effective origin in `/.well-known/ii-derivation-origin` (canonical `https://host`, one line). Do not expect agents to infer it from `ii-alternative-origins`. + +## Acceptance tests + +An app is agent-discoverable when these pass against the deployed origin: + +```bash +# 1. Manifest is real JSON listing the canisters (not the SPA shell) +curl -s https://APP/.well-known/ic-architecture | jq '.canisters[].id' + +# 2. Backend exposes candid:service, getApiDoc, and (if data-rich) OQL, +# verified against the backend ID from step 1 via an IC agent: +# get_candid(backend) succeeds; the interface declares getApiDoc; schema and execute are present. + +# 3. If you pin a CUSTOM derivation origin, it is published in its own file as the +# canonical https://host. An absent file means the default (https://APP). +curl -s https://APP/.well-known/ii-derivation-origin || echo "default (https://APP)" +``` + +End to end: an agent given only `https://APP` resolves the backend ID first (labeled with its role), reads `getApiDoc` to learn behavior, queries data apps via OQL, and, to act as the user, derives the user's principal against the app's declared derivation origin. All of that happens without a human supplying an ID or guessing which origin the user's principal comes from. + +## Next steps + +- [Asset canister](asset-canister.md): serve `.well-known` files and configure SPA routing. +- [Custom domains](custom-domains.md): apply the same `.well-known` pattern to domain ownership. +- [Internet Identity](../authentication/internet-identity.md#alternative-origins): configure `derivationOrigin` and alternative origins. +- [Candid interface](../canister-calls/candid.md): define the typed interface agents read. From a9cf0012cc89e34db009fe64452092af99231482 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Jul 2026 15:29:18 +0000 Subject: [PATCH 2/6] docs: fix derivation-origin acceptance test to fall back on 404 curl -s exits 0 on an HTTP 404, so the '|| echo default' branch never ran when the ii-derivation-origin file is absent (the documented default case). Use curl -sf so a non-2xx response is treated as an error and the fallback fires. --- docs/guides/frontends/service-discoverability.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/guides/frontends/service-discoverability.md b/docs/guides/frontends/service-discoverability.md index 183e8a79..7fe530f8 100644 --- a/docs/guides/frontends/service-discoverability.md +++ b/docs/guides/frontends/service-discoverability.md @@ -195,7 +195,9 @@ curl -s https://APP/.well-known/ic-architecture | jq '.canisters[].id' # 3. If you pin a CUSTOM derivation origin, it is published in its own file as the # canonical https://host. An absent file means the default (https://APP). -curl -s https://APP/.well-known/ii-derivation-origin || echo "default (https://APP)" +# Use -f so a 404 is treated as an error and the fallback fires (curl -s alone +# exits 0 on 404, so the "default" branch would never run). +curl -sf https://APP/.well-known/ii-derivation-origin || echo "default (https://APP)" ``` End to end: an agent given only `https://APP` resolves the backend ID first (labeled with its role), reads `getApiDoc` to learn behavior, queries data apps via OQL, and, to act as the user, derives the user's principal against the app's declared derivation origin. All of that happens without a human supplying an ID or guessing which origin the user's principal comes from. From c3a7683cdaf5b59df56567b013dbf661d1219798 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 26 Jul 2026 18:21:33 +0000 Subject: [PATCH 3/6] docs: keep .well-known serving guidance implementation-neutral Address review feedback: the Layer 1 serving rules pinned the specific .ic-assets.json5 rule inline, which is asset-canister-specific. State the requirement generically (serve /.well-known/* as a static file, exempt it from the SPA rewrite) and defer the concrete config to the Asset canister and Custom domains pages already linked. --- docs/guides/frontends/service-discoverability.md | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/docs/guides/frontends/service-discoverability.md b/docs/guides/frontends/service-discoverability.md index 7fe530f8..225c274a 100644 --- a/docs/guides/frontends/service-discoverability.md +++ b/docs/guides/frontends/service-discoverability.md @@ -66,21 +66,10 @@ This is the way an app declares its composition. It expresses the full set (not **Serving rules:** - Serve it at exactly `/.well-known/ic-architecture`, at the origin, with no file extension. The IC's `.well-known` discovery files omit extensions by convention (compare `ic-domains` and `ii-alternative-origins`), even when, as here, the content is JSON. -- Serve real JSON with `Content-Type: application/json`. The most common failure is a single-page-app catch-all returning `index.html` for unknown paths. Exempt `/.well-known/*` from the SPA rewrite in your asset canister configuration. +- Serve real JSON with `Content-Type: application/json`. The most common failure is a single-page-app catch-all returning `index.html` for unknown paths. Exempt `/.well-known/*` from the SPA rewrite wherever your frontend is served. - Generate it at deploy time. Canister IDs differ per network (local, staging, mainnet), so the file must be produced by the deploy pipeline (which already knows the IDs) rather than committed with hard-coded values. -To serve `/.well-known/*` from an asset canister, add a rule to `.ic-assets.json5` so the hidden directory is included, exactly as for the `ic-domains` file: - -```json5 -[ - { - "match": ".well-known", - "ignore": false - } -] -``` - -See [Asset canister](asset-canister.md#ic-assetsjson5) for SPA aliasing configuration, and [Custom domains](custom-domains.md#step-2-create-the-ic-domains-file) for the same `.well-known` pattern applied to domain ownership. +The exact configuration depends on how you host the frontend; the requirement is only that `/.well-known/*` is served as a static file, not rewritten to `index.html`. If you serve assets from an asset canister, see [Asset canister](asset-canister.md#ic-assetsjson5) for including the hidden `.well-known` directory and configuring SPA aliasing, and [Custom domains](custom-domains.md#step-2-create-the-ic-domains-file) for the same `.well-known` pattern applied to domain ownership. :::note This is a proposed convention. There is no standard way today for an app to enumerate the multiple canisters it comprises: existing signals identify only a single "main" canister. The goal is exactly one documented way to declare composition. From ffa4ca7173261705f5a9bf7ef2400ff633df14d6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 26 Jul 2026 18:49:00 +0000 Subject: [PATCH 4/6] docs: clarify who reads ii-derivation-origin and when Address review feedback on Layer 5: split the dense 'well-behaved agent' paragraph into who reads the file (the agent/connector, out of band like curl, not the II frontend during login, so no CORS or special headers) and what it does with it (report both origins alongside the principal so a mismatch is visible; a report in the agent's result, not a console log). --- docs/guides/frontends/service-discoverability.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/guides/frontends/service-discoverability.md b/docs/guides/frontends/service-discoverability.md index 225c274a..0b199793 100644 --- a/docs/guides/frontends/service-discoverability.md +++ b/docs/guides/frontends/service-discoverability.md @@ -145,7 +145,9 @@ This dedicated file, not the composition manifest, is the authoritative forward If you use the default (the app's own origin), omit the file. Its absence means "derive for the app origin itself." Serve it with no file extension and exempt `/.well-known/*` from the SPA catch-all, exactly as for the manifest. Generate it at deploy time when the origin is a per-network canister URL. -**What a well-behaved agent does with it.** It fetches `/.well-known/ii-derivation-origin` (falling back to the app origin itself when the file is absent), signs the delegation for that canonical origin, and echoes back both the origin it derived for and the app URL it was asked about with every identity result. An origin mismatch is then visible at a glance instead of hiding behind a plausible-looking principal. +**Who reads this file, and when.** The agent (or the connector acting for it) reads it: the same consumer that reads the other `/.well-known` files in this guide, not the Internet Identity frontend. It fetches `/.well-known/ii-derivation-origin` directly, out of band, the way `curl` would, before it constructs the delegation. This is not a browser request made during an interactive login, so it does not depend on CORS or any special response headers: like every discovery file here, the file is public and served as plain text. If the file is absent, the agent falls back to the app origin itself. + +**What the agent does with it.** It signs the delegation for that canonical origin, then returns both the origin it derived for and the app URL it was asked about alongside the resulting principal. Surfacing both, rather than the principal alone, is what makes an origin mismatch visible at a glance instead of hiding behind a plausible-looking principal. (This is a report in the agent's own result, not a console log the app can see.) ## Precedence and who produces what From 27ea66ad9c715c2dfe8e2e62feb8a41b738946be Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 13:21:50 +0000 Subject: [PATCH 5/6] docs: apply review feedback to Service discoverability Revise per PR review: declarative voice throughout (drop the proposed/standard/convention status framing and the standards-vs-proposed overview paragraph); trim the intro, drop the 'One way to expose each fact' and 'Anti-patterns' sections, the proposed-convention note, and the precedence table. Reformat the manifest as multi-line JSON with version and name fields (matching the ic-architecture template) and link the demo repo. Give the OQL Result payload a Candid type. Restructure Layer 5 (three-input framing; move the alternative-origins note after the derivation-origin file). Provide the icp canister metadata command in the acceptance tests. Rename the closing section to Related documents. --- .../frontends/service-discoverability.md | 117 ++++++++---------- 1 file changed, 49 insertions(+), 68 deletions(-) diff --git a/docs/guides/frontends/service-discoverability.md b/docs/guides/frontends/service-discoverability.md index 0b199793..17f89fb3 100644 --- a/docs/guides/frontends/service-discoverability.md +++ b/docs/guides/frontends/service-discoverability.md @@ -7,7 +7,7 @@ sidebar: When an AI agent is handed only your app's URL (for example, `https://yourapp.com`), it should be able to work out the rest on its own: which canisters your app comprises, what each one does, how to call them, how to query their data, and how to act as the signed-in user. No human supplying canister IDs, no bespoke integration. -This guide describes what a canister app exposes to make that possible, ordered by priority. Parts of it are established Internet Computer standards, and parts are proposed conventions this guidance builds on; each section is labeled so you know which is which. +This guide describes what a canister app exposes to make that possible, ordered by priority. ## The five layers @@ -29,38 +29,40 @@ Each layer is independently adoptable and independently useful. Together they ma | 4. Data | How do I query its data? | OQL: `schema` and `execute` query methods | | 5. Identity | How do I act as the signed-in user, under the right principal? | `/.well-known/ii-derivation-origin` declaration | -`candid:service` (Layer 2) and `/.well-known/ii-alternative-origins` are existing Internet Computer and Internet Identity standards. The `/.well-known/ic-architecture` manifest, the `/.well-known/ii-derivation-origin` declaration, the `getApiDoc` convention, and the OQL data surface are proposed conventions. Layer 1 (composition discovery) is the most important and, in practice, today's weakest link, so it gets the most detail below. - -## One way to expose each fact - -Everything an app declares for discovery is served over HTTP as a dedicated file at a well-known path: a JSON manifest, or a single-value text file, read like `curl`. Do not embed discovery metadata in an HTML asset (a meta tag, inline script, or runtime cookie config), and do not require it to be mined from JavaScript bundles. One documented file per fact means there is nothing to reconcile and nothing to guess. - -The IC gateway does stamp an automatic `x-ic-canister-id` response header, but it names only the frontend and, like any header, is trivially forgeable. This guidance does not rely on it (see the anti-patterns under Layer 1). - ## Layer 1: Composition discovery -**Proposed.** The single most valuable thing an app can declare is the set of canisters it comprises, each labeled with its role. Everything downstream (interface, behavior, data, identity) is per-canister, so an agent that cannot enumerate the set is stuck asking a human for canister IDs. +An app should declare the set of canisters it comprises, each labeled with its role. ### The canister manifest -Serve a small JSON document at the origin's `/.well-known/ic-architecture` that lists every canister and its role: +Serve a JSON document at the origin's `/.well-known/ic-architecture` that lists every canister and its role: ```json { + "version": "1.0.0", "canisters": [ - { "id": "hmxr2-pqaaa-aaabq-qaaaa-cai", "role": "backend", - "description": "orders + inventory API; call getApiDoc() first" }, - { "id": "hcv4s-uaaaa-aaabq-qaaba-cai", "role": "frontend" } + { + "id": "hcv4s-uaaaa-aaabq-qaaba-cai", + "name": "frontend", + "role": "the frontend" + }, + { + "id": "hmxr2-pqaaa-aaabq-qaaaa-cai", + "name": "backend", + "role": "the backend", + "description": "orders + inventory API; call getApiDoc() first" + } ] } ``` -This is the way an app declares its composition. It expresses the full set (not just a single "main" canister), it is read like `curl` (independent of the login page and of any HTML or JavaScript), and it fails loudly at deploy time rather than silently. +This is the way an app declares its composition. It is recommended to create this file during your app's deployment, as opposed to updating it for an already-deployed app, as demonstrated [here](https://github.com/raymondk/demo-ic-architecture/tree/main/frontend/ic-architecture). **Field rules:** +- `version` identifies the manifest schema version. - `id` is required and must be a canister principal. -- `role` and `description` are optional, human-readable, and untrusted. A consumer sanitizes them before use. +- `name` and `role` label the canister, and `description` is optional. These human-readable fields are untrusted, so a consumer sanitizes them before use. - Unknown fields must be ignored, so the format can grow (for example, per-canister network hints or an api-doc pointer) without breaking older readers. **Serving rules:** @@ -71,28 +73,15 @@ This is the way an app declares its composition. It expresses the full set (not The exact configuration depends on how you host the frontend; the requirement is only that `/.well-known/*` is served as a static file, not rewritten to `index.html`. If you serve assets from an asset canister, see [Asset canister](asset-canister.md#ic-assetsjson5) for including the hidden `.well-known` directory and configuring SPA aliasing, and [Custom domains](custom-domains.md#step-2-create-the-ic-domains-file) for the same `.well-known` pattern applied to domain ownership. -:::note -This is a proposed convention. There is no standard way today for an app to enumerate the multiple canisters it comprises: existing signals identify only a single "main" canister. The goal is exactly one documented way to declare composition. -::: - -### Anti-patterns: do not rely on these for discovery - -- **Discovery metadata embedded in HTML** (a `` tag, inline script, or templated page). Markup readers do not execute JavaScript, HTML is not a machine-readable contract, and it couples discovery to a page's lifecycle. Declare every fact in its own `/.well-known` file instead. -- **The `x-ic-canister-id` gateway header, for composition.** The IC HTTP gateway sets it automatically, but it names only the frontend or asset canister (never backends) and, like any response header, is trivially echoed by any origin. It is a hint at best, not a declaration the app controls. The manifest already lists the frontend as a role, so treat the manifest as the one source. -- **Config delivered only at runtime** (a `Set-Cookie` config blob, a script that reads `document.cookie`, and similar). It is invisible to a file reader and is a private transport between your platform and your own JavaScript, not a discovery interface. -- **IDs present only in the JavaScript bundle.** Mining a minified bundle is a last-ditch heuristic, not a contract. -- **A SPA catch-all answering `/.well-known/*`** (including `ic-architecture` and `ii-derivation-origin`) with `index.html`. -- **Per-network IDs committed into source.** They are wrong on every network but the one you baked them for. - ## Layer 2: Interface discovery -**Standard.** Expose your Candid interface as the canister's public `candid:service` metadata, the standard IC mechanism emitted by default by the common toolchains. This lets an agent fetch the exact method signatures and types and encode or decode calls correctly. If it is unreadable, every downstream layer degrades to guessing. Nothing app-specific is required beyond not stripping it. +Expose your Candid interface as the canister's public `candid:service` metadata, the standard IC mechanism emitted by default by the common toolchains. This lets an agent fetch the exact method signatures and types and encode or decode calls correctly. See [Candid interface](../canister-calls/candid.md) for how Candid describes a canister's methods and types. ## Layer 3: Behavioral guidance -**Convention.** Candid types describe shape, not behavior. Expose a query method that returns a prose (markdown) guide to the things an agent cannot infer from types: +Candid types describe shape, not behavior. Expose a query method that returns a prose (markdown) guide to the things an agent cannot infer from types: ```candid getApiDoc : () -> (text) query; // or the snake_case name get_api_doc @@ -110,7 +99,7 @@ Cover the non-obvious semantics, for example: ## Layer 4: Queryable data surface -**Convention.** For data-rich apps, expose a self-describing query surface so an agent can answer questions without you writing a bespoke method per question. OQL is one such convention, a pair of query methods: +For data-rich apps, expose a self-describing query surface so an agent can answer questions without you writing a bespoke method per question. OQL is one such convention, a pair of query methods: ```candid schema : () -> (text) query; // JSON catalogue: entities, fields, edges @@ -119,58 +108,50 @@ execute : (text) -> (Result) query; // one JSON query object -> rows `schema` returns a JSON catalogue of entities, their fields (with types and roles), and the edges between them. An agent fetches it once so it knows what is queryable. -`execute` takes one JSON query object (filters, aggregation, ordering, projection, paging) and returns rows. The result is a paged record of `{ hasMore, rows }` where each cell is `{ name, value }`. Agents read cells by name, never by position, and page while `hasMore` is true. +`execute` takes one JSON query object (filters, aggregation, ordering, projection, paging) and returns a paged `Result`: + + +```candid +type Result = record { + hasMore : bool; + rows : vec vec record { name : text; value : text }; // each row is a list of named cells +}; +``` -Prefer server-side filtering and aggregation so only the needed data crosses into the agent's context. OQL is a recommended default, not a requirement: any Candid interface still works, just less economically for open-ended questions. +Agents read cells by name, never by position, and page while `hasMore` is true. Prefer server-side filtering and aggregation so only the needed data crosses into the agent's context. Any Candid interface works; OQL just makes open-ended questions more economical. ## Layer 5: Acting as the user -**Proposed.** To let an agent act with the user's own principal and permissions, an app exposes exactly one thing: the [Internet Identity](../authentication/internet-identity.md) derivation origin its frontends pin. An agent that already holds the user's Internet Identity authorization derives a short-lived, per-app delegation for that origin on demand. This yields the same principal the user has when they use your app, so your existing access control applies unchanged. No login page, no AI-specific backend code, and no bot account are required. (How the agent obtains the user's authorization in the first place, through session, scope, or consent, is the connector's concern and out of scope here. All the app has to publish is the origin.) +To let an agent act with the user's own principal and permissions, an app should expose the [Internet Identity](../authentication/internet-identity.md) **_derivation origin_** its frontends pin. An agent that already holds the user's Internet Identity authorization derives a short-lived, per-app delegation for that origin on demand. This yields the same principal the user has when they use your app in a web browser, so your existing access control applies unchanged. + +The principal a user gets is a function of three inputs: + +1. The user's Internet Identity +2. The _account_ within that Internet Identity +3. Your app's derivation origin (the only factor controlled by your app) -The principal a user gets is a function of one input: the derivation origin Internet Identity signs for. By default that is the visible origin (`https://yourapp.com`). But an app may pin a custom derivation origin, typically its canister origin, so that a branded domain and its canister URL resolve to the same principal. When that is the case, the visible URL is not the origin identities are derived for, and an agent that assumes otherwise derives a valid but wrong principal, one with none of the user's permissions. +The derivation origin defaults to the **_visible_** origin requested by the user (for agentic flows) or the origin a user sees in their web browser address line (for classical flows). -**The alternative-origins file is the inverse relation.** A custom origin is enabled by two coupled files: the app pins `derivationOrigin` in its Internet Identity configuration, and the derivation origin publishes `/.well-known/ii-alternative-origins` listing the origins permitted to derive against it. That list answers "who may point here," not "where does this app point." The two are not interchangeable, and there is no reverse lookup from an app URL to its custom derivation origin. Reading it the wrong way round silently produces the wrong principal. See [Internet Identity](../authentication/internet-identity.md#alternative-origins) for how to configure `derivationOrigin` and `ii-alternative-origins`. +If the app has multiple frontends (e.g., due to migrating to a new brand name) the visible URL is not necessarily the origin identities are derived for. Providing the well-known file below tells an agent which origin to request Internet Identity derivations for when your users prompt that agent to access the app from any of its supported origins (e.g., starting from a new or secondary frontend). -**So the app states the answer rather than leaving it to be guessed.** Publish the effective derivation origin in a dedicated file at `/.well-known/ii-derivation-origin`, whose body is the canonical `https://host` origin on a single line: +**Instructions.** Each of the frontend origins your app supports should publish the app's derivation origin in a dedicated file at `/.well-known/ii-derivation-origin`, whose body is the canonical `https://host` origin on a single line: ```text https://hcv4s-uaaaa-aaabq-qaaba-cai.icp.net ``` -This dedicated file, not the composition manifest, is the authoritative forward answer to "which origin does this app derive against." Keeping it separate is deliberate: - -- It is an identity concern (Layer 5), orthogonal to composition (Layer 1). An app may pin a custom derivation origin independently of how it declares composition. -- It sits symmetrically beside `/.well-known/ii-alternative-origins`, hence the `ii-` prefix: domain-based derivation is an Internet Identity concept, not a general IC one. This file is the forward relation ("where does this app point"); that file is the inverse ("who may point here"). -- A single-value, plain-text file is trivial to serve and to verify (one `curl`, no parsing), and it follows the `.well-known` convention. - -If you use the default (the app's own origin), omit the file. Its absence means "derive for the app origin itself." Serve it with no file extension and exempt `/.well-known/*` from the SPA catch-all, exactly as for the manifest. Generate it at deploy time when the origin is a per-network canister URL. - -**Who reads this file, and when.** The agent (or the connector acting for it) reads it: the same consumer that reads the other `/.well-known` files in this guide, not the Internet Identity frontend. It fetches `/.well-known/ii-derivation-origin` directly, out of band, the way `curl` would, before it constructs the delegation. This is not a browser request made during an interactive login, so it does not depend on CORS or any special response headers: like every discovery file here, the file is public and served as plain text. If the file is absent, the agent falls back to the app origin itself. - -**What the agent does with it.** It signs the delegation for that canonical origin, then returns both the origin it derived for and the app URL it was asked about alongside the resulting principal. Surfacing both, rather than the principal alone, is what makes an origin mismatch visible at a glance instead of hiding behind a plausible-looking principal. (This is a report in the agent's own result, not a console log the app can see.) - -## Precedence and who produces what - -| Signal | Layer | Produced by | Trust | Status | -|--------|-------|-------------|-------|--------| -| `/.well-known/ic-architecture` | Composition (all) | Deploy pipeline | App-declared | Proposed | -| `x-ic-canister-id` header | Composition (frontend) | IC gateway | Automatic hint (frontend); forgeable, not relied on | Automatic | -| `candid:service` | Interface | Canister toolchain | Authoritative | Standard | -| `getApiDoc` method | Behavior | Canister code | App-declared | Convention | -| `schema` / `execute` | Data | Canister code | App-declared | Convention | -| `/.well-known/ii-derivation-origin` | Identity | Deploy pipeline or static | App-declared | Proposed | -| `/.well-known/ii-alternative-origins` | Identity (inverse) | App (static file) | Not a lookup source | Standard | +If you use the default (the app's own origin), you may omit the file. Its absence means "derive for the visible / requested origin itself." Serve it with no file extension and exempt `/.well-known/*` from the SPA catch-all, exactly as for the manifest. Generate it at deploy time when the origin is a per-network canister URL. -A consumer treats each app-declared signal as the app's own claim, to confirm rather than trust blindly. It fails closed: a malformed, oversized, or catch-all response yields no finding rather than a wrong one, and every ID is validated as a real canister principal before use. +**Relationship between derivation origin and alternative-origins.** A custom origin is enabled by two coupled files: the app pins `derivationOrigin` in its Internet Identity configuration, and the derivation origin publishes `/.well-known/ii-alternative-origins` listing the origins permitted to derive against it. That list answers "who may point here," not "where does this app point." The two are not interchangeable, and there is no reverse lookup from an app URL to its custom derivation origin. Reading it the wrong way round silently produces the wrong principal. See [Internet Identity](../authentication/internet-identity.md#alternative-origins) for how to configure `derivationOrigin` and `ii-alternative-origins`. ## Deployment checklist - [ ] **Composition:** the deploy pipeline emits `/.well-known/ic-architecture` listing every canister with a role, served as real JSON at the extensionless path. -- [ ] **Routing:** `/.well-known/*` is exempt from the SPA catch-all rewrite, so it is served as itself, not `index.html`. +- [ ] **Routing:** `/.well-known/*` is exempt from the SPA catch-all rewrite. - [ ] **Interface:** `candid:service` metadata is exposed (do not strip it). - [ ] **Behavior:** the backend exposes `getApiDoc` or `get_api_doc`, returning a markdown guide. - [ ] **Data (if applicable):** data-rich canisters expose OQL `schema` and `execute`. -- [ ] **Identity (if custom):** publish the effective origin in `/.well-known/ii-derivation-origin` (canonical `https://host`, one line). Do not expect agents to infer it from `ii-alternative-origins`. +- [ ] **Identity (if custom):** publish the effective origin in `/.well-known/ii-derivation-origin` (canonical `https://host`, one line). ## Acceptance tests @@ -180,9 +161,9 @@ An app is agent-discoverable when these pass against the deployed origin: # 1. Manifest is real JSON listing the canisters (not the SPA shell) curl -s https://APP/.well-known/ic-architecture | jq '.canisters[].id' -# 2. Backend exposes candid:service, getApiDoc, and (if data-rich) OQL, -# verified against the backend ID from step 1 via an IC agent: -# get_candid(backend) succeeds; the interface declares getApiDoc; schema and execute are present. +# 2. Backend exposes candid:service; fetch it against the backend ID from step 1 +# (and confirm the interface declares getApiDoc, plus schema/execute if data-rich) +icp canister metadata candid:service -e ic # 3. If you pin a CUSTOM derivation origin, it is published in its own file as the # canonical https://host. An absent file means the default (https://APP). @@ -193,7 +174,7 @@ curl -sf https://APP/.well-known/ii-derivation-origin || echo "default (https:// End to end: an agent given only `https://APP` resolves the backend ID first (labeled with its role), reads `getApiDoc` to learn behavior, queries data apps via OQL, and, to act as the user, derives the user's principal against the app's declared derivation origin. All of that happens without a human supplying an ID or guessing which origin the user's principal comes from. -## Next steps +## Related documents - [Asset canister](asset-canister.md): serve `.well-known` files and configure SPA routing. - [Custom domains](custom-domains.md): apply the same `.well-known` pattern to domain ownership. From cfed19bc9f4df91d57dd83ea38322c44b98f5c35 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 19:10:57 +0000 Subject: [PATCH 6/6] docs: fix OQL Result cell value type (variant, not text) The execute Result cells carry a type-tagged variant value, not text. Verified against the imcp2 OQL primer (static/oql-primer.md): Cell = record { name : text; value : variant }. Drop the verification note. --- docs/guides/frontends/service-discoverability.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/guides/frontends/service-discoverability.md b/docs/guides/frontends/service-discoverability.md index 17f89fb3..56dddc36 100644 --- a/docs/guides/frontends/service-discoverability.md +++ b/docs/guides/frontends/service-discoverability.md @@ -110,15 +110,12 @@ execute : (text) -> (Result) query; // one JSON query object -> rows `execute` takes one JSON query object (filters, aggregation, ordering, projection, paging) and returns a paged `Result`: - ```candid -type Result = record { - hasMore : bool; - rows : vec vec record { name : text; value : text }; // each row is a list of named cells -}; +type Cell = record { name : text; value : variant { ... } }; // value tagged by its scalar type +type Result = record { hasMore : bool; rows : vec vec Cell }; // each row is a list of named cells ``` -Agents read cells by name, never by position, and page while `hasMore` is true. Prefer server-side filtering and aggregation so only the needed data crosses into the agent's context. Any Candid interface works; OQL just makes open-ended questions more economical. +Each cell carries its column `name` and a `value` that is a type-tagged variant (text, integer, and so on), so agents read cells by name, never by position, and page while `hasMore` is true. Prefer server-side filtering and aggregation so only the needed data crosses into the agent's context. Any Candid interface works; OQL just makes open-ended questions more economical. ## Layer 5: Acting as the user