-
Notifications
You must be signed in to change notification settings - Fork 3
docs: add Service discoverability guide under Frontends #323
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
aterga
wants to merge
5
commits into
main
Choose a base branch
from
claude/service-discoverability-docs-lgxe00
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+184
−2
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
7abd543
docs: add Service discoverability guide under Frontends
claude a9cf001
docs: fix derivation-origin acceptance test to fall back on 404
claude c3a7683
docs: keep .well-known serving guidance implementation-neutral
claude ffa4ca7
docs: clarify who reads ii-derivation-origin and when
claude 27ea66a
docs: apply review feedback to Service discoverability
claude File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,182 @@ | ||
| --- | ||
| 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. | ||
|
|
||
| ## 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 | | ||
|
|
||
| ## Layer 1: Composition discovery | ||
|
|
||
| An app should declare the set of canisters it comprises, each labeled with its role. | ||
|
|
||
| ### The canister manifest | ||
|
|
||
| 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": "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 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. | ||
| - `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:** | ||
|
|
||
| - 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 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. | ||
|
|
||
| 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. | ||
|
|
||
| ## Layer 2: Interface discovery | ||
|
|
||
| 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 | ||
|
|
||
| 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 | ||
|
|
||
| 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 | ||
|
aterga marked this conversation as resolved.
|
||
| ``` | ||
|
|
||
| `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 a paged `Result`: | ||
|
|
||
| <!-- Needs human verification: OQL Result cell value type (text vs a typed variant) --> | ||
| ```candid | ||
| type Result = record { | ||
| hasMore : bool; | ||
| rows : vec vec record { name : text; value : text }; // 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. | ||
|
|
||
| ## Layer 5: Acting as the user | ||
|
|
||
| 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 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). | ||
|
|
||
| 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). | ||
|
|
||
| **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 | ||
| ``` | ||
|
|
||
| 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. | ||
|
|
||
| **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. | ||
| - [ ] **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). | ||
|
|
||
| ## 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; fetch it against the backend ID from step 1 | ||
| # (and confirm the interface declares getApiDoc, plus schema/execute if data-rich) | ||
| icp canister metadata <BACKEND_ID> 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). | ||
| # 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. | ||
|
|
||
| ## 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. | ||
| - [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. | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.