Commit 64c48be
Gate every canister-reaching tool on the discoverability manifest (#184)
* Gate every canister-reaching tool on the discoverability manifest
Reads were open on any canister; only writes were gated. They now run the
same gate: get_canister_candid, get_canister_api_doc,
get_canister_oql_schema and canister_query reach a canister only when the
app that owns it declares that canister at /.well-known/ic-architecture,
exactly as canister_update_call already did.
The arguments are the ones canister_update_call takes. Each read grows an
optional `app_url` naming the app whose manifest decides, falling back to
`derivation_origin` when the app serves its manifest there; where a call
carries both, the identity binding holds them to the same app, which
matters for an authenticated read as much as for a write, since a read
runs as the user's principal at whatever app they named. The two
anonymous metadata reads (get_canister_candid, get_canister_api_doc)
accept `derivation_origin` as an origin fallback only, and skip the
binding because they sign as nobody. Every successful reply echoes
`declared_by`/`declared_at`.
One helper, authorize_canister_call, now performs the whole check for all
five tools, so a read and a write cannot drift into checking different
things; canister_update_call moves onto it and loses its inline copy.
Refusals carry a CallKind so each names the operation actually attempted —
an agent told its read was refused as "a state-changing call" relays
something false — while the rule sentence stays kind-neutral, since a
caller who hits it with one operation should be told the whole policy.
The sentence promising that reading is unaffected is gone (it would now be
false); what keeps a refusal from reading as "this app is off limits" is
naming what still works: open_app resolves the app and lists what it
declares.
Discovery pays for this. open_app now probes only declared canisters for
their oql/api-doc flags, because probing an undeclared one would hand back
as a capability flag exactly what get_canister_candid would refuse to
return; a candidate mined from /env.json or the JS bundle keeps null flags
and is marked by the absence of [declared] in the listing. A manifest is
now the only route from an app to a usable canister. That cost, and the
fact that gating reads is stricter than the protocol asks for, are stated
in the module's scope notes rather than left for a reader to discover.
There is no manifest cache, so the fetch is paid per call: a read now
carries a DNS resolve, a TLS handshake and two HTTP GETs ahead of the
boundary-node call.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pKeV5VZvkfEgFR8RX9Niy
* Build the [declared] marker from the gate's own parse, and carry app_url into the OQL examples
Two findings from review, both real.
The `[declared]` marker read the `ic-architecture` provenance, which is not
the authorization set. The two manifest parsers deliberately disagree:
`canisters_from_app_manifest` keeps an id as spelled, drops blanks BEFORE
the 100-entry cap, and never checks what kind of principal it is, while
`manifest_canister_ids` caps first, parses, and keeps only canister
principals. So open_app could mark — and capability-probe — an id every
gated tool then refuses: `aaaaa-aa`, the anonymous principal, or a real id
sitting past the cap behind blank entries. The marker now comes from the
gate's own parse of the same document, carried on `Found`/
`DiscoveredCanister` as `declared` and surfaced in the structured output,
so it cannot over-claim. A test runs both parsers over one body that
exercises both divergences.
The OQL schema reply's ready-to-run examples carried `derivation_origin`
and `account` but not the `app_url` that authorized the schema read, so
copying one fell back to checking the manifest at the derivation origin —
which fails for the 13 of 17 registry apps whose website and derivation
origin differ, exactly the case the fallback was meant to cover. The
examples now carry both.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pKeV5VZvkfEgFR8RX9Niy
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent d33476f commit 64c48be
5 files changed
Lines changed: 992 additions & 299 deletions
0 commit comments