Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions evaluations/service-discoverability.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"skill": "service-discoverability",
"description": "Evaluation cases for the service-discoverability skill. Tests whether agents make a canister app discoverable to an AI agent: generate the /.well-known/ic-architecture manifest at deploy time (presync, not hard-coded IDs), serve it correctly as JSON, expose candid:service and a discoverable getApiDoc, understand the OQL data surface, and publish/read ii-derivation-origin without confusing it with ii-alternative-origins.",

"output_evals": [
{
"name": "Generate ic-architecture at deploy time (not hard-coded IDs)",
Expand Down Expand Up @@ -69,9 +68,29 @@
"Uses `icp canister metadata <canister-id> candid:service --network ic` (a raw canister ID has no project environment, so --network targets mainnet directly rather than -e)",
"Uses icp-cli, NOT dfx"
]
},
{
"name": "Adversarial: manifest entries need role labels, not just IDs",
"prompt": "My /.well-known/ic-architecture manifest lists all four of my canisters with just their \"id\" fields, since id is the only required field. Is that enough for an AI agent handed my app's URL? Just the key point.",
"expected_behaviors": [
"Says bare IDs are schema-valid but defeat the manifest's purpose: the agent cannot tell which canister is the backend",
"Explains the agent would fall back to fetching candid:service for every canister and guessing from method names",
"Recommends adding a role for every entry, plus a description where the purpose is not obvious from the name",
Comment thread
aterga marked this conversation as resolved.
Outdated
"Does NOT claim the manifest is fine as-is because id is the only required field"
]
},
{
"name": "Discovery traversal order from a bare URL",
"prompt": "An AI agent is handed only https://myapp.com and nothing else. What sequence of requests gets it to a correctly-encoded call against my backend canister? Just the ordered steps, no config.",
"expected_behaviors": [
"Step 1: GET /.well-known/ic-architecture at that origin to enumerate canister IDs and pick the backend by its role/description",
"Step 2: fetch candid:service metadata on the chosen canister ID for exact method signatures and types",
"Step 3: call the getApiDoc query method for behavior the Candid types cannot express (units, auth, polling, irreversibility)",
"Mentions the derivation origin (/.well-known/ii-derivation-origin, defaulting to the visible origin when absent) for acting as the signed-in user",
"Does NOT require a human to supply a canister ID out of band"
]
}
],

"trigger_evals": {
"description": "Queries to test whether the skill activates correctly.",
"should_trigger": [
Expand All @@ -82,7 +101,8 @@
"How do agents discover which canisters make up my app?",
"Expose a getApiDoc method so agents understand my canister's behavior",
"How do I publish my app's ii-derivation-origin so agents use the right principal?",
"Serve a service discovery manifest for my IC app"
"Serve a service discovery manifest for my IC app",
"What sequence of requests does an agent make to discover my IC app from just its URL?"
],
"should_not_trigger": [
"Sign an AI agent in to oisy.com and act as me",
Expand Down
24 changes: 20 additions & 4 deletions skills/service-discoverability/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Field rules:
- `version` — the manifest schema version.
- `id` — **required**, a canister principal.
- `name`, `role` — human-readable labels; `description` is optional. These fields are untrusted, so a consumer sanitizes them before use.
- Unknown fields must be ignored, so the format can grow without breaking older readers.
- Unknown fields must be ignored, so the format can grow (e.g. per-canister network hints, or an api-doc pointer) without breaking older readers.

### Generate it at deploy time (the `presync` pattern)

Expand Down Expand Up @@ -215,12 +215,25 @@ https://hcv4s-uaaaa-aaabq-qaaba-cai.icp.net

- If the derivation origin is just the app's own visible origin, you may omit the file; its absence means "derive for the visible / requested origin itself."
- Generate it at deploy time with the same `presync` pattern when the origin is a per-network canister URL; for a custom domain it is a one-line static file (e.g. `public/.well-known/ii-derivation-origin`).
- **Do not confuse it with `ii-alternative-origins`.** A custom origin is enabled by two coupled files: the app pins `derivationOrigin` in its II configuration, and the derivation origin itself publishes `/.well-known/ii-alternative-origins` listing the origins allowed to derive against it. That list answers "who may point here," not "where does this app point" — there is no reverse lookup from an app URL to its derivation origin, and reading it backwards silently produces the wrong principal. Note that on the default `*.icp0.io` / `ic0.app` canister origins you do **not** set a custom `derivationOrigin` at all (the `internet-identity` skill's Mistake #8 explains why adding it there breaks auth); a custom `derivationOrigin` goes hand in hand with a custom domain — see the `custom-domains` skill.
- **Do not confuse it with `ii-alternative-origins`.** A custom origin is enabled by two coupled files: the app pins `derivationOrigin` in its II configuration, and the derivation origin itself publishes `/.well-known/ii-alternative-origins` listing the origins allowed to derive against it. That list is the inverse relation — "who may point here," not "where does this app point" (Pitfall 8). On the default `*.icp0.io` / `ic0.app` canister origins, do **not** set a custom `derivationOrigin` at all (the `internet-identity` skill's Mistake #8 explains why it breaks auth); a custom one goes hand in hand with a custom domain — see the `custom-domains` skill.

## How an Agent Traverses This

Published independently, the layers are consumed in one order — each step exists to let the agent skip work at the next:

1. `GET /.well-known/ic-architecture` → every canister ID, and which one to call (from `role`/`description`).
Comment thread
aterga marked this conversation as resolved.
Outdated
2. `candid:service` on that canister → exact signatures and types, plus the names `getApiDoc` / `schema` / `execute`, which is why those must live in the interface and not a side channel.
3. `getApiDoc()` → the semantics the types cannot carry.
4. `schema()` once, then `execute(...)` per question, filtered and aggregated server-side.
5. `/.well-known/ii-derivation-origin`, or the visible origin when absent → derive the user's delegation and act as the signed-in user, so existing access control applies unchanged.
Comment thread
aterga marked this conversation as resolved.
Outdated

Steps 1-3 take an agent from a bare URL to a correctly-encoded, correctly-understood call in three round trips. Each layer left unpublished replaces one of them with guessing — unlabeled manifest entries alone cost a `candid:service` fetch per canister (Pitfall 10).

## Deployment Checklist

- [ ] **Composition:** the deploy pipeline emits `/.well-known/ic-architecture` (real JSON, extensionless path) with real per-environment canister IDs.
- [ ] **Content type:** a `_headers` rule serves the manifest as `application/json`.
- [ ] **Routing (non-static-site hosts only):** `/.well-known/*` is exempt from the SPA catch-all rewrite. Automatic on the static-site (certified-assets) canister, where a real file wins over the rewrite; needed on the legacy `@dfinity/asset-canister` or any non-IC host.
- [ ] **Interface:** `candid:service` metadata is present (not stripped).
- [ ] **Behavior:** the backend exposes `getApiDoc` / `get_api_doc` returning markdown.
- [ ] **Data (if applicable):** data-rich canisters expose OQL `schema` + `execute`.
Expand Down Expand Up @@ -262,11 +275,13 @@ Locally, the static-site recipe serves the same paths — e.g. `curl http://fron

7. **A missing `tmpl.<env>.json`, or templates in the wrong directory.** `$ICP_CLI_ENVIRONMENT` selects the template by name; if the file for the current environment is absent — or lives at the repo root while `presync` runs from the canister directory — `envsubst` reads nothing and writes an empty manifest. Keep one template per environment, under the frontend canister directory.

8. **Reading `ii-alternative-origins` to find the derivation origin.** It is the inverse relation (who may derive against this origin), not a pointer to it. There is no reverse lookup; using it backwards yields the wrong principal. Publish and read `ii-derivation-origin` for the forward fact.
8. **Reading `ii-alternative-origins` to find the derivation origin.** It is the inverse relation (who may derive against this origin), not a pointer to it. There is no reverse lookup; using it backwards **silently** yields the wrong principal — a plausible wrong answer, not an error. Publish and read `ii-derivation-origin` for the forward fact.

9. **Naming the behavior method undiscoverably.** The name must appear in `candid:service`, so use `getApiDoc` / `get_api_doc`. A method reachable only via an out-of-band hint defeats zero-knowledge discovery.

10. **Stripping `candid:service`.** Some minified/size-optimized builds drop wasm metadata. Keep it — it is what makes the interface fetchable. Verify with `icp canister metadata <id> candid:service --network ic`.
10. **Listing canisters without labels that identify them.** `id` is the only required field, so a manifest of bare IDs is valid — and useless: an agent cannot tell the backend from the frontend without fetching `candid:service` for every entry and inferring from method names, which is the exact work the manifest exists to avoid. Give every entry a `role`, and a `description` wherever the purpose is not obvious from the name.
Comment thread
aterga marked this conversation as resolved.
Outdated

11. **Stripping `candid:service`.** Some minified/size-optimized builds drop wasm metadata. Keep it — it is what makes the interface fetchable. Verify with `icp canister metadata <id> candid:service --network ic`.

## Additional References

Expand All @@ -277,4 +292,5 @@ Locally, the static-site recipe serves the same paths — e.g. `curl http://fron
- Load `icp-cli` for `icp.yaml` / `canister.yaml`, environments, and the recipe system.
- Load `canister-security` for access control on the methods agents call.
- Authoritative human guide: [Service discoverability](https://docs.internetcomputer.org/guides/frontends/service-discoverability/).
- Candid interface reference for the typed interface agents read: [Candid interface](https://docs.internetcomputer.org/guides/canister-calls/candid/).
- Community example of the Layer 1 generation (a personal repo — illustrative, not a stable dependency): [`raymondk/demo-ic-architecture`](https://github.com/raymondk/demo-ic-architecture/tree/main/frontend/ic-architecture).
Loading