You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
How to call a canister is explained in pieces across about 20 skills, and the pieces have drifted apart. Agents are the consumers of these skills, and an agent only sees the copy in the skill it loaded. This issue proposes one owner, canister-calls, with JS/TS as the main entry, the CLI and inter-canister calls as references, and every other skill linking to it instead of carrying its own copy.
host:internet-identity:150 sets host: window.location.origin (fixed in docs(internet-identity): cover local II minting with auth 10 #404). icp-cli, custom-domains and binding-generation.md say never to. encrypted-maps:105 requires a host parameter without saying what to pass.
rootKey:encrypted-maps:107 says it is "undefined on mainnet". dfx-migration.md:26 says the ic_env cookie always carries it. certified-variables:380 typed it as ArrayBuffer (fixed in fix(certified-variables): update to certificate-verification 4 and fix broken examples #407). static-site/references/legacy-asset-canister.md:104 still uses shouldFetchRootKey, which three skills ban.
Agent construction: there are three patterns and no rule for choosing between them:
bindgen createActor({ agentOptions });
HttpAgent.create + Actor.createActor;
a pre-built agent handed to a library client (encrypted-maps, IcrcLedgerCanister, AssetManager).
Sibling IDs:multi-canister:243 uses the dfx idiom import "canister:user_service", and :66 names the removed ic_cdk::call. icp-cli pitfall 22 says to read PUBLIC_CANISTER_ID:<name>.
Inter-canister calls:bounded_wait and unbounded_wait are used across skills with no rule for when to use which.
Frontend ledger calls:icrc-ledger:50 says transfers should not originate from the frontend, while wallet-integration is built on frontend transfers through a signer.
Proposed shape
canister-calls/
├── SKILL.md # JS/TS (browser + Node): HttpAgent vs createActor vs library clients,
│ # host/rootKey decision table, ic_env cookie, identity wiring,
│ # Candid↔TS types, Node scripts/tests against a local network
└── references/
├── cli.md # icp canister call: Candid text args, the '()' rule, --query/--identity/-o
└── inter-canister.md # Rust Call::bounded_wait vs unbounded_wait (with a rule), Motoko actor refs,
# (with cycles)/(with timeout), IDs via PUBLIC_CANISTER_ID
Candid↔TS types. Checked against bindgen 0.4.1. The mapping follows one principle: a native JavaScript form where one exists, and a tagged object only where none does.
Candid
TypeScript
opt T (argument, return value, vec element, variant payload)
today a Variant_<tags> enum, or a borrowed enum of a named type with the same tags; a string-literal union if dfinity/icp-js-bindgen#172 is adopted
variant with payload
{ __kind__: "Ok", Ok: … } ("Ok" in r works)
blob
Uint8Array
principal
imported from @icp-sdk/core/principal
Check "empty" with == null or ??: it is right in every position, so an agent does not need to know which one it is in. The icp-cli skill's "always use T | null" is wrong for record fields and is fixed in place first (#409).
Canister IDs stay in icp-cli. It owns ID resolution (PUBLIC_CANISTER_ID:<name> injection, canister-env-vars.md, --id-only, mappings) and the tooling side (bindgen setup, candid:, dev server, dfx migration mapping). canister-calls links to it for IDs.
Browser page on <canister-id>.icp.net (the default mainnet URL) or a custom domain
mainnet
leave unset → resolves to https://icp-api.io, because neither is a known gateway domain; this is what makes custom domains work, since a custom domain serves only the HTTP gateway, not /api/v2
IC_ROOT_KEY from the ic_env cookie
Browser page on a legacy <canister-id>.icp0.io or ic0.app URL
mainnet
leave unset → resolves to https://icp0.io / https://ic0.app
IC_ROOT_KEY from the ic_env cookie
Browser page
mainnet, but the page is served by a local network (e.g. mainnet ledger calls from a local dev server)
"https://icp-api.io"
omit — defaults to the mainnet key; do not pass the page's local IC_ROOT_KEY
Node script or test
a local network
api_url from icp network status --json
root_key from the same output, hex-decoded to bytes
Node script or test
mainnet
leave unset → resolves to https://icp-api.io
omit — defaults to the mainnet key
When writing the table into the skill, note that the ic_env cookie's root key is only as trustworthy as the page. On a verifying hostname the gateway verifies it along with the page: certified-assets certifies set-cookie: ic_env, and the legacy asset canister keeps it in the HTML asset's certified headers. A page from raw can carry a forged key (developer-docs#408 review).
Skills to update
The action for each skill is to move its generic calling content out, keep what is specific to its domain, and link with "Load the canister-calls skill …".
Skill
Move to canister-calls / link
Keep
Also fix
icp-cli
pitfall 13; binding-generation.md actor, host and opt sections
bindgen setup, ID resolution, dev-server.md, dfx migration mapping
Other agents (Python, Rust ic-agent, Go): leave them out for now. No skill covers them and there is no demand yet; add a reference when there is.
Candid discovery: leave it with canhelp (mainnet interfaces, with scripts) and service-discoverability. canister-calls links to them instead of adding candid-discovery.md.
Inter-canister calls:canister-calls owns the mechanics. multi-canister keeps the architecture, and its description drops "bounded vs unbounded wait" so the two skills don't compete on triggers.
Descriptions and triggers: the canister-calls description names what it owns (HttpAgent, createActor, agentOptions, host, rootKey, ic_env, icp canister call, Candid arguments, inter-canister calls). It also says what belongs elsewhere: deploying and IDs go to icp-cli, token flows to the domain skills, interface lookup to canhelp. Run the repo-wide trigger evals for icp-cli, multi-canister and canister-calls together.
A SKILL.md summary stops the model from opening references. With a host table in icp-cli's SKILL.md, eval case 28 stopped reading binding-generation.md and scored 0/6 against main's 6/6. The rule, its reason and a worked example must sit together in SKILL.md; references should carry only material an agent can safely skip.
The pointer's wording steers the reading. A main bisect in fix(icp-cli): drop false claim that createActor ignores { agent } #405 showed that the pitfall text alone, not the reference, moved case 28. A pointer naming rootKey/identity produced explanations about rootKey. Naming host correctly sent the model back to the host guidance. So the pointer should name the topics the reader will need.
Every cross-reference says when to load the other skill, not just that it exists.
Migrate the related evals too:icp-cli cases 17, 28 and 30, the internet-identity agent-setup case, and the adversarial host/rootKey cases.
How to call a canister is explained in pieces across about 20 skills, and the pieces have drifted apart. Agents are the consumers of these skills, and an agent only sees the copy in the skill it loaded. This issue proposes one owner,
canister-calls, with JS/TS as the main entry, the CLI and inter-canister calls as references, and every other skill linking to it instead of carrying its own copy.Drift found while working on #405
host:internet-identity:150setshost: window.location.origin(fixed in docs(internet-identity): cover local II minting with auth 10 #404).icp-cli,custom-domainsandbinding-generation.mdsay never to.encrypted-maps:105requires ahostparameter without saying what to pass.rootKey:encrypted-maps:107says it is "undefined on mainnet".dfx-migration.md:26says theic_envcookie always carries it.certified-variables:380typed it asArrayBuffer(fixed in fix(certified-variables): update to certificate-verification 4 and fix broken examples #407).static-site/references/legacy-asset-canister.md:104still usesshouldFetchRootKey, which three skills ban.createActor({ agentOptions });HttpAgent.create+Actor.createActor;encrypted-maps,IcrcLedgerCanister,AssetManager).multi-canister:243uses the dfx idiomimport "canister:user_service", and:66names the removedic_cdk::call.icp-clipitfall 22 says to readPUBLIC_CANISTER_ID:<name>.bounded_waitandunbounded_waitare used across skills with no rule for when to use which.icrc-ledger:50says transfers should not originate from the frontend, whilewallet-integrationis built on frontend transfers through a signer.Proposed shape
Candid↔TS types. Checked against bindgen 0.4.1. The mapping follows one principle: a native JavaScript form where one exists, and a tagged object only where none does.
opt T(argument, return value,vecelement, variant payload)T | nullopt Tf?: T,undefinedwhen emptyopt opt Tf?: T | null, three states: omitted,null, a value (after dfinity/icp-js-bindgen#198)opt opt Telsewhere, or a third levelSome<…> | NoneVariant_<tags>enum, or a borrowed enum of a named type with the same tags; a string-literal union if dfinity/icp-js-bindgen#172 is adopted{ __kind__: "Ok", Ok: … }("Ok" in rworks)blobUint8Arrayprincipal@icp-sdk/core/principalCheck "empty" with
== nullor??: it is right in every position, so an agent does not need to know which one it is in. Theicp-cliskill's "always useT | null" is wrong for record fields and is fixed in place first (#409).These fill the type gaps left over from #156.
Canister IDs stay in
icp-cli. It owns ID resolution (PUBLIC_CANISTER_ID:<name>injection,canister-env-vars.md,--id-only, mappings) and the tooling side (bindgen setup,candid:, dev server, dfx migration mapping).canister-callslinks to it for IDs.Draft host/rootKey decision table (from #405; resolution rules per
@icp-sdk/core6.1.0determineHost, Node row verified locally)hostrootKeyhttp://localhost:<port>IC_ROOT_KEYfrom theic_envcookie<canister-id>.icp.net(the default mainnet URL) or a custom domainhttps://icp-api.io, because neither is a known gateway domain; this is what makes custom domains work, since a custom domain serves only the HTTP gateway, not/api/v2IC_ROOT_KEYfrom theic_envcookie<canister-id>.icp0.iooric0.appURLhttps://icp0.io/https://ic0.appIC_ROOT_KEYfrom theic_envcookie"https://icp-api.io"IC_ROOT_KEYapi_urlfromicp network status --jsonroot_keyfrom the same output, hex-decoded to byteshttps://icp-api.ioWhen writing the table into the skill, note that the
ic_envcookie's root key is only as trustworthy as the page. On a verifying hostname the gateway verifies it along with the page: certified-assets certifiesset-cookie: ic_env, and the legacy asset canister keeps it in the HTML asset's certified headers. A page fromrawcan carry a forged key (developer-docs#408 review).Skills to update
The action for each skill is to move its generic calling content out, keep what is specific to its domain, and link with "Load the
canister-callsskill …".canister-calls/ linkicp-clibinding-generation.mdactor,hostandoptsectionsdev-server.md, dfx migration mappinginternet-identity:101-165,:447-489)AuthClient,agentOptionsfor II mintingwallet-integrationSignerAgentflows; the cross-networkhostrow becomes a table rowicrc-ledgerpitfall 7custom-domainshostsection (:179-193)encrypted-maps:93-112,:152)hostparameter, "undefined on mainnet"vetkeys:23,:203)certified-variablesUint8Array,LookupResultstatuses,--query)static-site'()'rule (:206)ic_envcookie contents (it sets the cookie)legacy-asset-canister.mdshouldFetchRootKeymulti-canisterimport "canister:…",ic_cdk::call, Rust init-arg IDs, descriptioncanister-securityfetchRootKeyand reentrancy pitfallsicrc-ledger,ckbtc,evm-rpcunbounded_waitis used; link for mechanicscloud-engine-canisters,deploy-to-cloud-enginebounded_wait,--proxy)agent-web-identity--identityweb-link flow (pitfall 9)canhelp,service-discoverabilitywriting-motoko(upstream-tracked)caffeine-appuseActor/env.jsonThese skills contain CLI call examples only and need an optional link at most:
https-outcalls,cycles-management,sns-launch,stable-memory.Recommendations for the open questions
ckbtc,icrc-ledgerandevm-rpcas standalone skills, since Proposal: canister-calls skill — Candid discovery + consolidated canister workflows #88's trigger evals showed the dedicated skills win, and have them link tocanister-callsfor mechanics.ic-agent, Go): leave them out for now. No skill covers them and there is no demand yet; add a reference when there is.canhelp(mainnet interfaces, with scripts) andservice-discoverability.canister-callslinks to them instead of addingcandid-discovery.md.canister-callsowns the mechanics.multi-canisterkeeps the architecture, and its description drops "bounded vs unbounded wait" so the two skills don't compete on triggers.canister-callsdescription names what it owns (HttpAgent,createActor,agentOptions,host,rootKey,ic_env,icp canister call, Candid arguments, inter-canister calls). It also says what belongs elsewhere: deploying and IDs go toicp-cli, token flows to the domain skills, interface lookup tocanhelp. Run the repo-wide trigger evals foricp-cli,multi-canisterandcanister-callstogether.Writing it for agents (lesson from #405)
SKILL.mdsummary stops the model from opening references. With ahosttable inicp-cli'sSKILL.md, eval case 28 stopped readingbinding-generation.mdand scored 0/6 againstmain's 6/6. The rule, its reason and a worked example must sit together inSKILL.md; references should carry only material an agent can safely skip.mainbisect in fix(icp-cli): drop false claim that createActor ignores { agent } #405 showed that the pitfall text alone, not the reference, moved case 28. A pointer namingrootKey/identityproduced explanations aboutrootKey. Naminghostcorrectly sent the model back to thehostguidance. So the pointer should name the topics the reader will need.icp-clicases 17, 28 and 30, theinternet-identityagent-setup case, and the adversarialhost/rootKeycases.Refs #88, #156, #404, #405