From 7f62171d3c426aa8c844b426ef9d88fc8c9e0bc0 Mon Sep 17 00:00:00 2001 From: Soam Desai Date: Sun, 20 Sep 2026 20:49:15 -0700 Subject: [PATCH 1/9] docs(agents): fix 7 audit findings across AgentKit and Human-in-the-Loop - agents/human-in-the-loop/integrate.mdx: fix DurableAgent import to '@workflow/ai/agent' (it does not exist under 'workflow/ai'), add @workflow/ai to the install command, and pin ai to ^5 (the major actually supported) - agents/human-in-the-loop/integrate.mdx: add explicit prose + a second example tool showing that the action-performing tool must derive its own `action` string and independently re-verify the World ID proof, not just the approval tool - agents/agent-kit/ecosystem.mdx: replace the dead agentbook.world link (Vercel deployment not found) with the live AgentBook GitHub registry and the agentkit-cli status check - agents/hats/index.mdx: same dead-link fix for the AgentBook link used in the discount-unlock copy - agents/agent-kit/sdk-reference.mdx: document that omitting `uses` in `discount` mode defaults to unlimited uses (not a bounded trial) - agents/agent-kit/sdk-reference.mdx: document that AgentBook lookup failures are indistinguishable from "not registered" (both surface as `agent_not_verified` / a `null` return from `lookupHuman`) - agents/agent-kit/sdk-reference.mdx: document the `rpcUrls` per-chain override map on `createAgentkitHooks` and `verifyAgentkitSignature`, and the `resolveAgentkitSignatureRpcUrl` / `getDefaultPublicRpcUrl` helper exports --- agents/agent-kit/ecosystem.mdx | 2 +- agents/agent-kit/sdk-reference.mdx | 20 +++++++++++---- agents/hats/index.mdx | 2 +- agents/human-in-the-loop/integrate.mdx | 34 ++++++++++++++++++++------ 4 files changed, 43 insertions(+), 15 deletions(-) diff --git a/agents/agent-kit/ecosystem.mdx b/agents/agent-kit/ecosystem.mdx index d240983..108279f 100644 --- a/agents/agent-kit/ecosystem.mdx +++ b/agents/agent-kit/ecosystem.mdx @@ -5,7 +5,7 @@ description: "Projects and services that integrate AgentKit." "twitter:image": "https://raw.githubusercontent.com/worldcoin/developer-docs/main/images/docs/docs-meta.png" --- -Find places to use AgentKit at [agentbook.world](https://agentbook.world/). +Find places to use AgentKit in the [AgentBook registry](https://github.com/andy-t-wang/agentbook). (The agentbook.world frontend is currently down; the on-chain registry itself is live and can be queried with `npx @worldcoin/agentkit-cli status`.) To add your project, open a PR to the [AgentBook registry](https://github.com/andy-t-wang/agentbook). If you build an agent that calls x402 APIs, use [`createAgentkitClient`](/agents/agent-kit/sdk-reference#createagentkitclientoptions) and call `agentkit.fetch` so the agent tries AgentKit verification before paying. If you cannot change the agent's HTTP client, add the `agentkit-x402` skill: diff --git a/agents/agent-kit/sdk-reference.mdx b/agents/agent-kit/sdk-reference.mdx index 02980a4..9b10f0d 100644 --- a/agents/agent-kit/sdk-reference.mdx +++ b/agents/agent-kit/sdk-reference.mdx @@ -16,10 +16,12 @@ Usage counters are tracked per human per endpoint. Two agents backed by the same | ------------ | ------------------------------------------------------ | -------- | | `free` | `{ type: "free" }` | Registered human-backed agents always bypass payment. | | `free-trial` | `{ type: "free-trial"; uses?: number }` | Registered human-backed agents bypass payment the first `N` times. Default `uses` is `1`. | -| `discount` | `{ type: "discount"; percent: number; uses?: number }` | Registered human-backed agents can underpay by the configured percentage for the first `N` times. | +| `discount` | `{ type: "discount"; percent: number; uses?: number }` | Registered human-backed agents can underpay by the configured percentage for the first `N` times. If `uses` is omitted, the discount applies unlimited times, forever. | `discount` mode requires `verifyFailureHook` to be registered on the facilitator. Without it, discounted underpayments fail settlement verification. +Unlike `free-trial`, omitting `uses` in `discount` mode does **not** default to a single use — it defaults to unlimited uses. Always set `uses` explicitly for `discount` mode in production, or every verified human-backed agent gets a permanent discount on that endpoint. + ## Agent client APIs ### `createAgentkitClient(options)` @@ -103,6 +105,7 @@ Creates the request-time verification hooks used by the golden path integration. | `mode` | `AgentkitMode` | Access mode. Defaults to `{ type: "free" }`. | | `storage` | `AgentKitStorage` | Required for `free-trial` and `discount`. Optional for `free`. | | `rpcUrl` | `string` | Custom EVM RPC used during signature verification. | +| `rpcUrls` | `Record` | Custom RPC URLs keyed by CAIP-2 chain ID, e.g. `{ "eip155:8453": "https://base.example" }`. Use this instead of `rpcUrl` when verifying signatures across multiple chains (for example World Chain + Base). | | `onEvent` | `(event: AgentkitHookEvent) => void` | Optional logging/debug callback. | Returns: @@ -138,6 +141,8 @@ That is why Express and Next.js are compatible even though the docs use Hono for | `discount_applied` | `resource`, `address`, `humanId` | | `discount_exhausted` | `resource`, `address`, `humanId` | +`agent_not_verified` fires whenever the AgentBook lookup resolves to `null` — which includes both "this wallet is genuinely not a registered human-backed agent" **and** "the AgentBook lookup itself failed" (RPC timeout, dropped connection, or the public RPC being down; see [AgentBook lookup](#agentbook-lookup) below). There is no separate event for a failed lookup, so `agent_not_verified` alone is not proof that an agent is unregistered. If you need to tell these apart in production, wrap your `rpcUrl`/`client` with your own timeout and alerting (or a circuit breaker) so a lookup-dependency outage doesn't silently masquerade as routine rejections. + ## AgentBook lookup ### `createAgentBookVerifier(options?)` @@ -166,6 +171,8 @@ The returned object exposes: lookupHuman(address: string): Promise ``` +`lookupHuman` returns `null` both when the wallet is not registered **and** when the on-chain lookup itself fails (RPC timeout, network error, or the default public RPC being unavailable) — failures are swallowed internally and are not distinguishable from "not registered" by return value alone. If you need to alert on lookup-dependency failures separately from routine unregistered-agent rejections, wrap `rpcUrl`/`client` with your own timeout and circuit breaker. + ## Storage and replay protection ### `AgentKitStorage` @@ -206,13 +213,14 @@ Returns: { valid: boolean; error?: string } ``` -### `verifyAgentkitSignature(payload, rpcUrl?)` +### `verifyAgentkitSignature(payload, options?)` Verifies the cryptographic signature and returns the recovered address on success. -| Option | Type | Description | -| -------- | -------- | ----------- | -| `rpcUrl` | `string` | Optional custom RPC endpoint for EVM verification. | +| Option | Type | Description | +| --------- | ------------------------- | ----------- | +| `rpcUrl` | `string` | Optional custom RPC endpoint for EVM verification. | +| `rpcUrls` | `Record` | Custom RPC URLs keyed by CAIP-2 chain ID, e.g. `{ "eip155:8453": "https://base.example" }`. Use this instead of `rpcUrl` when verifying across multiple chains. | Behavior: @@ -238,6 +246,8 @@ Returns the JSON schema used in 402 challenge payloads. | `formatSIWEMessage` | Reconstruct the SIWE message used for EVM signing and verification. | | `verifyEVMSignature` | Verify an EVM signature for the reconstructed SIWE message. | | `extractEVMChainId` | Convert a CAIP-2 `eip155:*` chain ID to its numeric chain ID. | +| `resolveAgentkitSignatureRpcUrl` | Resolve the RPC URL to use for a given CAIP-2 chain ID from a `rpcUrl`/`rpcUrls` option pair, falling back to the chain's default public RPC. | +| `getDefaultPublicRpcUrl` | Return the default public RPC URL for a given CAIP-2 chain ID. | EVM verification uses viem's `verifyMessage`, which covers EOAs and ERC-1271 smart wallets. Counterfactual wallets can still represent their signature scheme with `signatureScheme: "eip6492"` in the payload schema. diff --git a/agents/hats/index.mdx b/agents/hats/index.mdx index 2fda60a..aef21f0 100644 --- a/agents/hats/index.mdx +++ b/agents/hats/index.mdx @@ -10,7 +10,7 @@ description: "Prove you're human-backed with AgentKit and claim an exclusive fre All hats have been claimed. Thanks for the incredible response — stay tuned for future drops. -The [Human Required](https://humanrequired.shop/) store is a Shopify store demo that only sells to agents verified as human-backed through [AgentKit](/agents/agent-kit/integrate). Agents registered in [AgentBook](https://agentbook.world/) can unlock a 100% discount and claim the hat for free. Discount codes are unique per human — each person can generate one. +The [Human Required](https://humanrequired.shop/) store is a Shopify store demo that only sells to agents verified as human-backed through [AgentKit](/agents/agent-kit/integrate). Agents registered in [AgentBook](https://github.com/andy-t-wang/agentbook) can unlock a 100% discount and claim the hat for free. (The agentbook.world frontend is currently down; check registration status on-chain with `npx @worldcoin/agentkit-cli status`.) Discount codes are unique per human — each person can generate one. Human in the Loop Hat diff --git a/agents/human-in-the-loop/integrate.mdx b/agents/human-in-the-loop/integrate.mdx index eaa859a..166ffac 100644 --- a/agents/human-in-the-loop/integrate.mdx +++ b/agents/human-in-the-loop/integrate.mdx @@ -19,11 +19,11 @@ Built on the [Workflow SDK](https://useworkflow.dev) and the [Vercel AI SDK](htt ## Install ```bash -# Server — human-in-the-loop + peer dependencies -npm install @worldcoin/human-in-the-loop ai workflow +# Server — human-in-the-loop + peer dependencies (ai@^5 is the supported major) +npm install @worldcoin/human-in-the-loop ai@^5 workflow @workflow/ai # Client — React bindings + peer dependencies -npm install @worldcoin/human-in-the-loop-react @worldcoin/idkit ai react +npm install @worldcoin/human-in-the-loop-react @worldcoin/idkit ai@^5 react ``` ## Environment variables @@ -43,7 +43,7 @@ Get these from the [World developer portal](https://developer.world.org) by crea ```ts // src/workflows/chat/index.ts -import { DurableAgent } from 'workflow/ai' +import { DurableAgent } from '@workflow/ai/agent' import { getWritable } from 'workflow' import { openai } from '@workflow/ai/openai' import { tools } from './steps/tools' @@ -66,6 +66,10 @@ export async function chatWorkflow(messages: ModelMessage[]) { ## Step 2: Register the approval tool + +The World ID binding only protects what you bind it to. If you leave `action` at its default (the tool call's `toolCallId`), the proof is bound to an opaque per-invocation ID, not to the action being approved — which defeats the "no replay" guarantee for any tool that has a real side effect. The tool that performs that side effect (not just the approval tool) must **derive its own `action` string from that action's specific parameters**, and **independently re-verify the returned `IDKitResult` against World ID** before trusting it — tool inputs are LLM-generated, so the agent could pass a different or tampered result to the executing tool. See the [flight booking example](https://github.com/worldcoin/human-in-the-loop/tree/main/examples/flight-booking) for a complete, end-to-end implementation of this pattern. + + ```ts // src/workflows/chat/steps/tools.ts import { requestHumanAuthorization } from '@worldcoin/human-in-the-loop/workflows' @@ -74,13 +78,27 @@ import { z } from 'zod' export const tools = { approveAction: { description: 'Request human approval via World ID before a sensitive action.', - inputSchema: z.object({ summary: z.string() }), + inputSchema: z.object({ summary: z.string(), flightNumber: z.string() }), // Pauses the workflow, streams approval context to the client, // waits for World ID proof, verifies it, then resumes. - // Action defaults to toolCallId; pass a function to bind to input fields: - // action: ({ input }) => `booking:${input.flightNumber}` - execute: requestHumanAuthorization(), + // Bind `action` to the specific action being approved — never leave + // this at the toolCallId default for a real side-effecting action: + execute: requestHumanAuthorization({ + action: ({ input }) => `booking:${input.flightNumber}`, + }), + }, + bookFlight: { + description: 'Book the flight after approval.', + inputSchema: z.object({ flightNumber: z.string() }), + // This tool performs the real side effect, so it must independently + // re-verify the World ID proof against the SAME `action` string above + // before booking — never trust that the approval happened just because + // the model called this tool. See the flight-booking example for the + // full bookingApproval() re-verification helper. + execute: async ({ flightNumber }) => { + // ...verify the proof for `booking:${flightNumber}`, then book + }, }, // ...your other tools } From f58e52af48cd9f95d6e83d798f3f739dedb105bf Mon Sep 17 00:00:00 2001 From: Soam Desai Date: Mon, 21 Sep 2026 10:20:45 -0700 Subject: [PATCH 2/9] polish: tighten wording, match house style De-duplicate the two "AgentBook registry" link mentions in ecosystem.mdx, and trim the audit-pass prose across agent-kit and human-in-the-loop docs to match the repo's terser Mintlify style. --- agents/agent-kit/ecosystem.mdx | 4 ++-- agents/agent-kit/sdk-reference.mdx | 10 +++++----- agents/hats/index.mdx | 2 +- agents/human-in-the-loop/integrate.mdx | 13 +++++-------- 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/agents/agent-kit/ecosystem.mdx b/agents/agent-kit/ecosystem.mdx index 108279f..10129d6 100644 --- a/agents/agent-kit/ecosystem.mdx +++ b/agents/agent-kit/ecosystem.mdx @@ -5,8 +5,8 @@ description: "Projects and services that integrate AgentKit." "twitter:image": "https://raw.githubusercontent.com/worldcoin/developer-docs/main/images/docs/docs-meta.png" --- -Find places to use AgentKit in the [AgentBook registry](https://github.com/andy-t-wang/agentbook). (The agentbook.world frontend is currently down; the on-chain registry itself is live and can be queried with `npx @worldcoin/agentkit-cli status`.) -To add your project, open a PR to the [AgentBook registry](https://github.com/andy-t-wang/agentbook). +Find places to use AgentKit in the [AgentBook registry](https://github.com/andy-t-wang/agentbook) (the agentbook.world frontend is down; query status on-chain with `npx @worldcoin/agentkit-cli status`). +To add your project, open a PR to the registry. If you build an agent that calls x402 APIs, use [`createAgentkitClient`](/agents/agent-kit/sdk-reference#createagentkitclientoptions) and call `agentkit.fetch` so the agent tries AgentKit verification before paying. If you cannot change the agent's HTTP client, add the `agentkit-x402` skill: diff --git a/agents/agent-kit/sdk-reference.mdx b/agents/agent-kit/sdk-reference.mdx index 9b10f0d..8660002 100644 --- a/agents/agent-kit/sdk-reference.mdx +++ b/agents/agent-kit/sdk-reference.mdx @@ -16,11 +16,11 @@ Usage counters are tracked per human per endpoint. Two agents backed by the same | ------------ | ------------------------------------------------------ | -------- | | `free` | `{ type: "free" }` | Registered human-backed agents always bypass payment. | | `free-trial` | `{ type: "free-trial"; uses?: number }` | Registered human-backed agents bypass payment the first `N` times. Default `uses` is `1`. | -| `discount` | `{ type: "discount"; percent: number; uses?: number }` | Registered human-backed agents can underpay by the configured percentage for the first `N` times. If `uses` is omitted, the discount applies unlimited times, forever. | +| `discount` | `{ type: "discount"; percent: number; uses?: number }` | Registered human-backed agents can underpay by the configured percentage for the first `N` times. Omitting `uses` applies the discount forever. | `discount` mode requires `verifyFailureHook` to be registered on the facilitator. Without it, discounted underpayments fail settlement verification. -Unlike `free-trial`, omitting `uses` in `discount` mode does **not** default to a single use — it defaults to unlimited uses. Always set `uses` explicitly for `discount` mode in production, or every verified human-backed agent gets a permanent discount on that endpoint. +Unlike `free-trial`, omitting `uses` doesn't default to a single use — always set it explicitly in production, or every verified human-backed agent gets a permanent discount on that endpoint. ## Agent client APIs @@ -105,7 +105,7 @@ Creates the request-time verification hooks used by the golden path integration. | `mode` | `AgentkitMode` | Access mode. Defaults to `{ type: "free" }`. | | `storage` | `AgentKitStorage` | Required for `free-trial` and `discount`. Optional for `free`. | | `rpcUrl` | `string` | Custom EVM RPC used during signature verification. | -| `rpcUrls` | `Record` | Custom RPC URLs keyed by CAIP-2 chain ID, e.g. `{ "eip155:8453": "https://base.example" }`. Use this instead of `rpcUrl` when verifying signatures across multiple chains (for example World Chain + Base). | +| `rpcUrls` | `Record` | Custom RPC URLs keyed by CAIP-2 chain ID, e.g. `{ "eip155:8453": "https://base.example" }`. Use this instead of `rpcUrl` when verifying signatures across multiple chains. | | `onEvent` | `(event: AgentkitHookEvent) => void` | Optional logging/debug callback. | Returns: @@ -141,7 +141,7 @@ That is why Express and Next.js are compatible even though the docs use Hono for | `discount_applied` | `resource`, `address`, `humanId` | | `discount_exhausted` | `resource`, `address`, `humanId` | -`agent_not_verified` fires whenever the AgentBook lookup resolves to `null` — which includes both "this wallet is genuinely not a registered human-backed agent" **and** "the AgentBook lookup itself failed" (RPC timeout, dropped connection, or the public RPC being down; see [AgentBook lookup](#agentbook-lookup) below). There is no separate event for a failed lookup, so `agent_not_verified` alone is not proof that an agent is unregistered. If you need to tell these apart in production, wrap your `rpcUrl`/`client` with your own timeout and alerting (or a circuit breaker) so a lookup-dependency outage doesn't silently masquerade as routine rejections. +`agent_not_verified` also fires when the AgentBook lookup itself fails (RPC timeout, dropped connection, public RPC outage) — not only when the wallet is genuinely unregistered. There's no separate event for a failed lookup; see [AgentBook lookup](#agentbook-lookup) for how to tell the two apart. ## AgentBook lookup @@ -171,7 +171,7 @@ The returned object exposes: lookupHuman(address: string): Promise ``` -`lookupHuman` returns `null` both when the wallet is not registered **and** when the on-chain lookup itself fails (RPC timeout, network error, or the default public RPC being unavailable) — failures are swallowed internally and are not distinguishable from "not registered" by return value alone. If you need to alert on lookup-dependency failures separately from routine unregistered-agent rejections, wrap `rpcUrl`/`client` with your own timeout and circuit breaker. +`lookupHuman` returns `null` both when the wallet is unregistered and when the lookup itself fails (RPC timeout, network error, public RPC outage) — failures are swallowed internally and indistinguishable from "not registered" by return value alone. To alert on lookup failures separately, wrap `rpcUrl`/`client` with your own timeout and circuit breaker. ## Storage and replay protection diff --git a/agents/hats/index.mdx b/agents/hats/index.mdx index aef21f0..1dae902 100644 --- a/agents/hats/index.mdx +++ b/agents/hats/index.mdx @@ -10,7 +10,7 @@ description: "Prove you're human-backed with AgentKit and claim an exclusive fre All hats have been claimed. Thanks for the incredible response — stay tuned for future drops. -The [Human Required](https://humanrequired.shop/) store is a Shopify store demo that only sells to agents verified as human-backed through [AgentKit](/agents/agent-kit/integrate). Agents registered in [AgentBook](https://github.com/andy-t-wang/agentbook) can unlock a 100% discount and claim the hat for free. (The agentbook.world frontend is currently down; check registration status on-chain with `npx @worldcoin/agentkit-cli status`.) Discount codes are unique per human — each person can generate one. +The [Human Required](https://humanrequired.shop/) store is a Shopify store demo that only sells to agents verified as human-backed through [AgentKit](/agents/agent-kit/integrate). Agents registered in [AgentBook](https://github.com/andy-t-wang/agentbook) (the agentbook.world frontend is down; check status with `npx @worldcoin/agentkit-cli status`) can unlock a 100% discount and claim the hat for free. Discount codes are unique per human — each person can generate one. Human in the Loop Hat diff --git a/agents/human-in-the-loop/integrate.mdx b/agents/human-in-the-loop/integrate.mdx index 166ffac..db72942 100644 --- a/agents/human-in-the-loop/integrate.mdx +++ b/agents/human-in-the-loop/integrate.mdx @@ -67,7 +67,7 @@ export async function chatWorkflow(messages: ModelMessage[]) { ## Step 2: Register the approval tool -The World ID binding only protects what you bind it to. If you leave `action` at its default (the tool call's `toolCallId`), the proof is bound to an opaque per-invocation ID, not to the action being approved — which defeats the "no replay" guarantee for any tool that has a real side effect. The tool that performs that side effect (not just the approval tool) must **derive its own `action` string from that action's specific parameters**, and **independently re-verify the returned `IDKitResult` against World ID** before trusting it — tool inputs are LLM-generated, so the agent could pass a different or tampered result to the executing tool. See the [flight booking example](https://github.com/worldcoin/human-in-the-loop/tree/main/examples/flight-booking) for a complete, end-to-end implementation of this pattern. +World ID only protects what you bind `action` to. Left at its default (`toolCallId`), the proof binds to an opaque per-call ID, not the action itself — breaking the replay guarantee for any side-effecting tool. The tool that performs the side effect must derive its own `action` from that action's parameters and independently re-verify `IDKitResult` before trusting it — tool inputs are LLM-generated and can be tampered with. See the [flight booking example](https://github.com/worldcoin/human-in-the-loop/tree/main/examples/flight-booking) for the full pattern. ```ts @@ -82,8 +82,8 @@ export const tools = { // Pauses the workflow, streams approval context to the client, // waits for World ID proof, verifies it, then resumes. - // Bind `action` to the specific action being approved — never leave - // this at the toolCallId default for a real side-effecting action: + // Bind `action` to the action's own parameters — never leave this + // at the toolCallId default for a side-effecting action: execute: requestHumanAuthorization({ action: ({ input }) => `booking:${input.flightNumber}`, }), @@ -91,11 +91,8 @@ export const tools = { bookFlight: { description: 'Book the flight after approval.', inputSchema: z.object({ flightNumber: z.string() }), - // This tool performs the real side effect, so it must independently - // re-verify the World ID proof against the SAME `action` string above - // before booking — never trust that the approval happened just because - // the model called this tool. See the flight-booking example for the - // full bookingApproval() re-verification helper. + // Must independently re-verify the proof for this same `action` + // before booking. See the flight-booking example's bookingApproval(). execute: async ({ flightNumber }) => { // ...verify the proof for `booking:${flightNumber}`, then book }, From ac7b6fb853d7112f0ffc19b57ea0c37f90351c79 Mon Sep 17 00:00:00 2001 From: Soam Desai Date: Mon, 21 Sep 2026 16:59:06 -0700 Subject: [PATCH 3/9] fix: bookFlight has no way to receive or check an approval proof MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses a Codex review comment on PR #193: bookFlight's inputSchema only took flightNumber, so there was nowhere to pass the approval proof the adjacent Warning says must be independently re-verified — a model could call bookFlight directly without ever calling approveAction, and the implementation had no way to detect that. Added a required `approval` field to bookFlight's schema and a stub check that its `action` matches the same derivation approveAction uses, matching the real pattern in the linked flight-booking example (github.com/worldcoin/human-in-the-loop/tree/main/examples/flight-booking). Updated the system prompt to tell the model to pass the approval through, since the tool now requires it. --- agents/human-in-the-loop/integrate.mdx | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/agents/human-in-the-loop/integrate.mdx b/agents/human-in-the-loop/integrate.mdx index db72942..af0f307 100644 --- a/agents/human-in-the-loop/integrate.mdx +++ b/agents/human-in-the-loop/integrate.mdx @@ -57,7 +57,7 @@ export async function chatWorkflow(messages: ModelMessage[]) { model: openai('gpt-5.4'), tools, system: - 'You are a helpful assistant. Before performing any sensitive action, use the approveAction tool.', + 'You are a helpful assistant. Before performing any sensitive action, call approveAction first, then pass its returned result as the `approval` argument to the action tool — never call the action tool without it.', }) await agent.stream({ messages, writable }) @@ -89,12 +89,22 @@ export const tools = { }), }, bookFlight: { - description: 'Book the flight after approval.', - inputSchema: z.object({ flightNumber: z.string() }), + description: 'Book the flight. Requires the IDKitResult from approveAction as `approval` — the model cannot call this without it.', + // `approval` is required input, not something execute reads from + // elsewhere: a model that skips approveAction has no value to pass here. + inputSchema: z.object({ + flightNumber: z.string(), + approval: z.object({ action: z.string() }).passthrough(), + }), // Must independently re-verify the proof for this same `action` - // before booking. See the flight-booking example's bookingApproval(). - execute: async ({ flightNumber }) => { - // ...verify the proof for `booking:${flightNumber}`, then book + // before booking — never trust that approveAction ran just because + // this tool was called. See the flight-booking example's bookFlight(). + execute: async ({ flightNumber, approval }) => { + const expectedAction = `booking:${flightNumber}` + if (approval.action !== expectedAction) { + throw new Error(`approval does not match this booking (expected action: ${expectedAction})`) + } + // ...re-verify `approval` against the World ID verify endpoint, then book }, }, // ...your other tools From 3b278cc206ef927711817868607cdf6c2d7d8e8c Mon Sep 17 00:00:00 2001 From: Soam Desai Date: Mon, 21 Sep 2026 17:02:56 -0700 Subject: [PATCH 4/9] chore: retrigger CI (previous CodeQL upload hit a transient GitHub server error) From a122894eb97646aaf290f8c648fac35c062f52fa Mon Sep 17 00:00:00 2001 From: Soam Desai Date: Tue, 22 Sep 2026 18:47:20 -0700 Subject: [PATCH 5/9] docs(agents): correct helper contracts and type approval examples --- agents/agent-kit/ecosystem.mdx | 2 +- agents/agent-kit/sdk-reference.mdx | 4 +-- agents/hats/index.mdx | 2 +- agents/human-in-the-loop/integrate.mdx | 36 ++++++++++++++++---------- 4 files changed, 26 insertions(+), 18 deletions(-) diff --git a/agents/agent-kit/ecosystem.mdx b/agents/agent-kit/ecosystem.mdx index 10129d6..d2c0ae7 100644 --- a/agents/agent-kit/ecosystem.mdx +++ b/agents/agent-kit/ecosystem.mdx @@ -5,7 +5,7 @@ description: "Projects and services that integrate AgentKit." "twitter:image": "https://raw.githubusercontent.com/worldcoin/developer-docs/main/images/docs/docs-meta.png" --- -Find places to use AgentKit in the [AgentBook registry](https://github.com/andy-t-wang/agentbook) (the agentbook.world frontend is down; query status on-chain with `npx @worldcoin/agentkit-cli status`). +Find places to use AgentKit in the [AgentBook registry](https://github.com/andy-t-wang/agentbook) (the agentbook.world frontend is down; query status on-chain with `npx @worldcoin/agentkit-cli status `). To add your project, open a PR to the registry. If you build an agent that calls x402 APIs, use [`createAgentkitClient`](/agents/agent-kit/sdk-reference#createagentkitclientoptions) and call `agentkit.fetch` so the agent tries AgentKit verification before paying. If you cannot change the agent's HTTP client, add the `agentkit-x402` skill: diff --git a/agents/agent-kit/sdk-reference.mdx b/agents/agent-kit/sdk-reference.mdx index 8660002..4aed7cc 100644 --- a/agents/agent-kit/sdk-reference.mdx +++ b/agents/agent-kit/sdk-reference.mdx @@ -246,8 +246,8 @@ Returns the JSON schema used in 402 challenge payloads. | `formatSIWEMessage` | Reconstruct the SIWE message used for EVM signing and verification. | | `verifyEVMSignature` | Verify an EVM signature for the reconstructed SIWE message. | | `extractEVMChainId` | Convert a CAIP-2 `eip155:*` chain ID to its numeric chain ID. | -| `resolveAgentkitSignatureRpcUrl` | Resolve the RPC URL to use for a given CAIP-2 chain ID from a `rpcUrl`/`rpcUrls` option pair, falling back to the chain's default public RPC. | -| `getDefaultPublicRpcUrl` | Return the default public RPC URL for a given CAIP-2 chain ID. | +| `resolveAgentkitSignatureRpcUrl` | Return the configured `rpcUrls[chainId]` or `rpcUrl` for a CAIP-2 chain ID, or `undefined`. The signature verifier selects the chain's default public RPC downstream. | +| `getDefaultPublicRpcUrl` | Return the default public RPC URL for a numeric chain ID, such as `480`, or `undefined` if no default is configured. | EVM verification uses viem's `verifyMessage`, which covers EOAs and ERC-1271 smart wallets. Counterfactual wallets can still represent their signature scheme with `signatureScheme: "eip6492"` in the payload schema. diff --git a/agents/hats/index.mdx b/agents/hats/index.mdx index 1dae902..e388a05 100644 --- a/agents/hats/index.mdx +++ b/agents/hats/index.mdx @@ -10,7 +10,7 @@ description: "Prove you're human-backed with AgentKit and claim an exclusive fre All hats have been claimed. Thanks for the incredible response — stay tuned for future drops. -The [Human Required](https://humanrequired.shop/) store is a Shopify store demo that only sells to agents verified as human-backed through [AgentKit](/agents/agent-kit/integrate). Agents registered in [AgentBook](https://github.com/andy-t-wang/agentbook) (the agentbook.world frontend is down; check status with `npx @worldcoin/agentkit-cli status`) can unlock a 100% discount and claim the hat for free. Discount codes are unique per human — each person can generate one. +The [Human Required](https://humanrequired.shop/) store is a Shopify store demo that only sells to agents verified as human-backed through [AgentKit](/agents/agent-kit/integrate). Agents registered in [AgentBook](https://github.com/andy-t-wang/agentbook) (the agentbook.world frontend is down; check status with `npx @worldcoin/agentkit-cli status `) can unlock a 100% discount and claim the hat for free. Discount codes are unique per human — each person can generate one. Human in the Loop Hat diff --git a/agents/human-in-the-loop/integrate.mdx b/agents/human-in-the-loop/integrate.mdx index af0f307..ed37b6e 100644 --- a/agents/human-in-the-loop/integrate.mdx +++ b/agents/human-in-the-loop/integrate.mdx @@ -67,7 +67,7 @@ export async function chatWorkflow(messages: ModelMessage[]) { ## Step 2: Register the approval tool -World ID only protects what you bind `action` to. Left at its default (`toolCallId`), the proof binds to an opaque per-call ID, not the action itself — breaking the replay guarantee for any side-effecting tool. The tool that performs the side effect must derive its own `action` from that action's parameters and independently re-verify `IDKitResult` before trusting it — tool inputs are LLM-generated and can be tampered with. See the [flight booking example](https://github.com/worldcoin/human-in-the-loop/tree/main/examples/flight-booking) for the full pattern. +The default `action` is the unique `toolCallId`. For a sensitive operation, your backend must bind the approval to the intended operation and its parameters, independently verify the proof, and consume the approval once before performing the side effect. A required `approval` input is not proof of authorization — tool inputs are LLM-generated. See the [flight booking example](https://github.com/worldcoin/human-in-the-loop/tree/main/examples/flight-booking) for proof verification and parameter binding. ```ts @@ -75,36 +75,44 @@ World ID only protects what you bind `action` to. Left at its default (`toolCall import { requestHumanAuthorization } from '@worldcoin/human-in-the-loop/workflows' import { z } from 'zod' +const approvalInputSchema = z.object({ + summary: z.string(), + flightNumber: z.string(), +}) + +const bookingInputSchema = z.object({ + flightNumber: z.string(), + approval: z.object({ action: z.string() }).passthrough(), +}) + export const tools = { approveAction: { description: 'Request human approval via World ID before a sensitive action.', - inputSchema: z.object({ summary: z.string(), flightNumber: z.string() }), + inputSchema: approvalInputSchema, // Pauses the workflow, streams approval context to the client, // waits for World ID proof, verifies it, then resumes. - // Bind `action` to the action's own parameters — never leave this - // at the toolCallId default for a side-effecting action: - execute: requestHumanAuthorization({ + // Bind this example's approval to the flight number. + execute: requestHumanAuthorization>({ action: ({ input }) => `booking:${input.flightNumber}`, }), }, bookFlight: { - description: 'Book the flight. Requires the IDKitResult from approveAction as `approval` — the model cannot call this without it.', - // `approval` is required input, not something execute reads from - // elsewhere: a model that skips approveAction has no value to pass here. - inputSchema: z.object({ - flightNumber: z.string(), - approval: z.object({ action: z.string() }).passthrough(), - }), + description: 'Book the flight using the IDKitResult from approveAction as `approval`.', + // Requiring an object does not establish that its proof is authentic. + inputSchema: bookingInputSchema, // Must independently re-verify the proof for this same `action` // before booking — never trust that approveAction ran just because // this tool was called. See the flight-booking example's bookFlight(). - execute: async ({ flightNumber, approval }) => { + execute: async ({ flightNumber, approval }: z.infer) => { + 'use step' + const expectedAction = `booking:${flightNumber}` if (approval.action !== expectedAction) { throw new Error(`approval does not match this booking (expected action: ${expectedAction})`) } - // ...re-verify `approval` against the World ID verify endpoint, then book + // ...verify the complete proof against the World ID verify endpoint, + // enforce one-time use for this pending booking, then book the flight }, }, // ...your other tools From a647fbc5dd262ed685b1d4fcc8d4ab263ff8af97 Mon Sep 17 00:00:00 2001 From: Soam Desai Date: Wed, 23 Sep 2026 15:02:48 -0700 Subject: [PATCH 6/9] docs(agents): fix install peers and enforce approval checks - integrate: pin ai@^6 on both install lines (current @workflow/ai requires ai@^6 as a peer) and add zod - integrate: import the ModelMessage/UIMessageChunk types; use instructions, since @workflow/ai 4.2.1 deprecates system - integrate: bookFlight re-verifies the proof with the World ID verify endpoint and consumes each approval once, keyed on the 4.0 nonce - sdk-reference: suggest a custom viem client with a timeout to surface AgentBook lookup failures - ecosystem, hats: replace outage wording with the CLI status check --- agents/agent-kit/ecosystem.mdx | 2 +- agents/agent-kit/sdk-reference.mdx | 2 +- agents/hats/index.mdx | 2 +- agents/human-in-the-loop/integrate.mdx | 41 +++++++++++++++++++------- 4 files changed, 34 insertions(+), 13 deletions(-) diff --git a/agents/agent-kit/ecosystem.mdx b/agents/agent-kit/ecosystem.mdx index d2c0ae7..f607b8d 100644 --- a/agents/agent-kit/ecosystem.mdx +++ b/agents/agent-kit/ecosystem.mdx @@ -5,7 +5,7 @@ description: "Projects and services that integrate AgentKit." "twitter:image": "https://raw.githubusercontent.com/worldcoin/developer-docs/main/images/docs/docs-meta.png" --- -Find places to use AgentKit in the [AgentBook registry](https://github.com/andy-t-wang/agentbook) (the agentbook.world frontend is down; query status on-chain with `npx @worldcoin/agentkit-cli status `). +Find places to use AgentKit in the [AgentBook registry](https://github.com/andy-t-wang/agentbook). To check whether an agent wallet is registered, run `npx @worldcoin/agentkit-cli status `. To add your project, open a PR to the registry. If you build an agent that calls x402 APIs, use [`createAgentkitClient`](/agents/agent-kit/sdk-reference#createagentkitclientoptions) and call `agentkit.fetch` so the agent tries AgentKit verification before paying. If you cannot change the agent's HTTP client, add the `agentkit-x402` skill: diff --git a/agents/agent-kit/sdk-reference.mdx b/agents/agent-kit/sdk-reference.mdx index 4aed7cc..f099668 100644 --- a/agents/agent-kit/sdk-reference.mdx +++ b/agents/agent-kit/sdk-reference.mdx @@ -171,7 +171,7 @@ The returned object exposes: lookupHuman(address: string): Promise ``` -`lookupHuman` returns `null` both when the wallet is unregistered and when the lookup itself fails (RPC timeout, network error, public RPC outage) — failures are swallowed internally and indistinguishable from "not registered" by return value alone. To alert on lookup failures separately, wrap `rpcUrl`/`client` with your own timeout and circuit breaker. +`lookupHuman` returns `null` both when the wallet is unregistered and when the lookup itself fails (RPC timeout, network error, public RPC outage) — failures are swallowed internally and indistinguishable from "not registered" by return value alone. To bound lookups and surface RPC failures separately, pass a custom viem `client` with a timeout, e.g. `createPublicClient({ transport: http(rpcUrl, { timeout: 5_000 }) })`, and monitor that RPC endpoint. ## Storage and replay protection diff --git a/agents/hats/index.mdx b/agents/hats/index.mdx index e388a05..c950640 100644 --- a/agents/hats/index.mdx +++ b/agents/hats/index.mdx @@ -10,7 +10,7 @@ description: "Prove you're human-backed with AgentKit and claim an exclusive fre All hats have been claimed. Thanks for the incredible response — stay tuned for future drops. -The [Human Required](https://humanrequired.shop/) store is a Shopify store demo that only sells to agents verified as human-backed through [AgentKit](/agents/agent-kit/integrate). Agents registered in [AgentBook](https://github.com/andy-t-wang/agentbook) (the agentbook.world frontend is down; check status with `npx @worldcoin/agentkit-cli status `) can unlock a 100% discount and claim the hat for free. Discount codes are unique per human — each person can generate one. +The [Human Required](https://humanrequired.shop/) store is a Shopify store demo that only sells to agents verified as human-backed through [AgentKit](/agents/agent-kit/integrate). Agents registered in [AgentBook](https://github.com/andy-t-wang/agentbook) (check with `npx @worldcoin/agentkit-cli status `) can unlock a 100% discount and claim the hat for free. Discount codes are unique per human — each person can generate one. Human in the Loop Hat diff --git a/agents/human-in-the-loop/integrate.mdx b/agents/human-in-the-loop/integrate.mdx index ed37b6e..ed08e84 100644 --- a/agents/human-in-the-loop/integrate.mdx +++ b/agents/human-in-the-loop/integrate.mdx @@ -19,11 +19,11 @@ Built on the [Workflow SDK](https://useworkflow.dev) and the [Vercel AI SDK](htt ## Install ```bash -# Server — human-in-the-loop + peer dependencies (ai@^5 is the supported major) -npm install @worldcoin/human-in-the-loop ai@^5 workflow @workflow/ai +# Server — human-in-the-loop + peer dependencies +npm install @worldcoin/human-in-the-loop ai@^6 workflow @workflow/ai zod # Client — React bindings + peer dependencies -npm install @worldcoin/human-in-the-loop-react @worldcoin/idkit ai@^5 react +npm install @worldcoin/human-in-the-loop-react @worldcoin/idkit ai@^6 react ``` ## Environment variables @@ -46,6 +46,7 @@ Get these from the [World developer portal](https://developer.world.org) by crea import { DurableAgent } from '@workflow/ai/agent' import { getWritable } from 'workflow' import { openai } from '@workflow/ai/openai' +import type { ModelMessage, UIMessageChunk } from 'ai' import { tools } from './steps/tools' export async function chatWorkflow(messages: ModelMessage[]) { @@ -56,7 +57,7 @@ export async function chatWorkflow(messages: ModelMessage[]) { const agent = new DurableAgent({ model: openai('gpt-5.4'), tools, - system: + instructions: 'You are a helpful assistant. Before performing any sensitive action, call approveAction first, then pass its returned result as the `approval` argument to the action tool — never call the action tool without it.', }) @@ -82,9 +83,14 @@ const approvalInputSchema = z.object({ const bookingInputSchema = z.object({ flightNumber: z.string(), - approval: z.object({ action: z.string() }).passthrough(), + // World ID 4.0 proofs bind `nonce`, so it can key one-time use below. + approval: z.object({ protocol_version: z.literal('4.0'), action: z.string(), nonce: z.string() }).passthrough(), }) +// Atomically records an approval key; returns false if it was already used. +// Back it with durable storage (e.g. a unique database key), not memory. +declare function consumeApproval(key: string): Promise + export const tools = { approveAction: { description: 'Request human approval via World ID before a sensitive action.', @@ -101,9 +107,8 @@ export const tools = { description: 'Book the flight using the IDKitResult from approveAction as `approval`.', // Requiring an object does not establish that its proof is authentic. inputSchema: bookingInputSchema, - // Must independently re-verify the proof for this same `action` - // before booking — never trust that approveAction ran just because - // this tool was called. See the flight-booking example's bookFlight(). + // Never trust that approveAction ran just because this tool was called: + // check the binding, re-verify the proof, and consume it once. execute: async ({ flightNumber, approval }: z.infer) => { 'use step' @@ -111,8 +116,24 @@ export const tools = { if (approval.action !== expectedAction) { throw new Error(`approval does not match this booking (expected action: ${expectedAction})`) } - // ...verify the complete proof against the World ID verify endpoint, - // enforce one-time use for this pending booking, then book the flight + + const rpId = process.env.WORLD_RP_ID + if (!rpId) throw new Error('WORLD_RP_ID is required to verify approvals') + const res = await fetch(`https://developer.world.org/api/v4/verify/${rpId}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(approval), + signal: AbortSignal.timeout(10_000), + }) + if (!res.ok) { + throw new Error(`approval failed World ID verification (${res.status}): ${await res.text()}`) + } + + if (!(await consumeApproval(`${expectedAction}:${approval.nonce}`))) { + throw new Error('approval already used') + } + + // ...book the flight }, }, // ...your other tools From 56b00e5e8a5098750ac6ddee0de4f2ad980d409b Mon Sep 17 00:00:00 2001 From: Soam Desai Date: Wed, 23 Sep 2026 15:43:29 -0700 Subject: [PATCH 7/9] docs(agents): key approvals on the nullifier and pin the verify environment - bookFlight accepts the 3.0 proofs the approval UI produces (orbLegacy, the HumanApproval default) instead of requiring 4.0 - one-time use is keyed on the proof's nullifier, parsed with BigInt as the verifier does, so re-encodings like 0x01/0x1 can't mint new keys - pin environment: production on the verify call so an untrusted approval can't select staging/sandbox test proofs --- agents/human-in-the-loop/integrate.mdx | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/agents/human-in-the-loop/integrate.mdx b/agents/human-in-the-loop/integrate.mdx index ed08e84..ef58f1c 100644 --- a/agents/human-in-the-loop/integrate.mdx +++ b/agents/human-in-the-loop/integrate.mdx @@ -83,8 +83,13 @@ const approvalInputSchema = z.object({ const bookingInputSchema = z.object({ flightNumber: z.string(), - // World ID 4.0 proofs bind `nonce`, so it can key one-time use below. - approval: z.object({ protocol_version: z.literal('4.0'), action: z.string(), nonce: z.string() }).passthrough(), + // One credential, so its nullifier identifies the approval below. + approval: z + .object({ + action: z.string(), + responses: z.array(z.object({ nullifier: z.string() }).passthrough()).length(1), + }) + .passthrough(), }) // Atomically records an approval key; returns false if it was already used. @@ -122,14 +127,21 @@ export const tools = { const res = await fetch(`https://developer.world.org/api/v4/verify/${rpId}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(approval), + // The approval is untrusted input: pin the environment so it can't + // select "staging" or "sandbox", which accept test proofs. + body: JSON.stringify({ ...approval, environment: 'production' }), signal: AbortSignal.timeout(10_000), }) if (!res.ok) { throw new Error(`approval failed World ID verification (${res.status}): ${await res.text()}`) } - if (!(await consumeApproval(`${expectedAction}:${approval.nonce}`))) { + // One-time use, keyed on the proof's nullifier (bound by 3.0 and 4.0 + // proofs). Parse it as the verifier does, so "0x01" and "0x1" share a + // key. Nullifiers repeat per person and action, so each person can book + // a flight number once; add a unique booking ID to the action to allow more. + const nullifier = BigInt(approval.responses[0].nullifier).toString(16) + if (!(await consumeApproval(`${expectedAction}:${nullifier}`))) { throw new Error('approval already used') } From 38bed309e7247f4a0c2d3b933dc3c7a5cb7a5970 Mon Sep 17 00:00:00 2001 From: Soam Desai Date: Wed, 23 Sep 2026 15:45:30 -0700 Subject: [PATCH 8/9] chore: retrigger Mintlify deployment (preview revalidation failed on Mintlify's side) From 8e9521135d0c2d4b1934aab114b11a102c4f09e5 Mon Sep 17 00:00:00 2001 From: Soam Desai Date: Wed, 23 Sep 2026 16:00:32 -0700 Subject: [PATCH 9/9] docs(agents): cap the approval nullifier at 32 bytes The 3.0 verifier pads the nullifier to 64 hex chars and ABI-decodes a uint256, so bytes past the first 32 are ignored: N+"00" still verifies as N but BigInt() gives a new one-time-use key. Require a 0x-prefixed nullifier of at most 64 hex chars, matching the portal's v2 bound. --- agents/human-in-the-loop/integrate.mdx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/agents/human-in-the-loop/integrate.mdx b/agents/human-in-the-loop/integrate.mdx index ef58f1c..a7ec720 100644 --- a/agents/human-in-the-loop/integrate.mdx +++ b/agents/human-in-the-loop/integrate.mdx @@ -83,11 +83,15 @@ const approvalInputSchema = z.object({ const bookingInputSchema = z.object({ flightNumber: z.string(), - // One credential, so its nullifier identifies the approval below. + // One credential, so its nullifier identifies the approval below. Cap it at + // 32 bytes: the 3.0 verifier reads only the first 32, so a longer encoding + // would still verify but produce a new key. approval: z .object({ action: z.string(), - responses: z.array(z.object({ nullifier: z.string() }).passthrough()).length(1), + responses: z + .array(z.object({ nullifier: z.string().regex(/^0x[0-9a-fA-F]{1,64}$/) }).passthrough()) + .length(1), }) .passthrough(), })