Decisions
Tenant = wallet: one Durable Object per wallet; the sign-in is a wallet signature that the DO verifies (D47).
- Tenant = wallet. One Durable Object per wallet (name
chainId:address), one Harness, the pi-durable tables as today. A conversation is (wallet, conversationId). - Sign-in: the wallet signs a challenge (domain, nonce, expiry); the Worker issues a short-lived credential bound to the address; the DO accepts only its own address.
D47 · D1's note "the Worker issues a short-lived credential bound to the address" → D47 (the DO verifies the sign-in; the Worker only forwards).
A conversation's agent is derived from a per-wallet root the runner keeps in its DO; the agent holds no balance and pays no gas.
- A conversation's identity is derived from a per-wallet root that the runner generates and keeps in its DO storage. Formulas unchanged:
keccak(root, "conversation", n), subagent and schedule slots as today. - The identity has no balance and pays no gas. Contract A, gateways, directory and Portal statistics see the same client and identity shapes as today.
Service kinds in the descriptor: runner, store (now memory), model, tool; one gateway may declare several kinds.
- Service kinds in the descriptor:
runner (hosts conversations, derives identities, speaks to the Console), store (durable conversation storage; reusable by other services), model (chat API usable as the agent's model), tool. One gateway may have several kinds. - The Services page shows the kinds as badges.
D51a, D52a · D3 and D34 kind lists → D51a and D52a: the kinds are runner, memory, model, tool, host.
D3a · D3 and D34 kind lists → D3a and D34a: the kinds are runner, memory, model, tool, host, store.
- The kinds are runner, memory, model, tool, host and **
store** (D74): a store keeps an extension's large data, paid per MiB started, reads free for the owner; it is chosen at install, with no default. One gateway may declare several kinds, as steven@68df/datavault declares store and tool. - 2026-11-03, with D74.
The runner is a service the wallet chooses; the New conversation dialog requires one.
- The runner is a service chosen by the wallet. The New conversation dialog starts with a required Runner select; without a runner in My services it refuses to create and links to Services.
- Resolves "who pays for the Worker + DO": the runner's operator is paid in the runner's token.
A descriptor may declare uses with bundled or client payment (deferred by D48).
- A descriptor may declare
uses: the services it depends on, each with payment: "bundled" (in my price, invisible to the client) or payment: "client" (the client pays it; the runner signs with the conversation identity). - Recommended default (not yet decided): metered dependencies
client, the provider's own infrastructure bundled.
Two token kinds: direct (one gateway's unit) and proxy (a basket of child tokens); proxy tokens are deferred (D48).
- Two kinds of service token. Direct: one gateway's unit, as today. Proxy: a composite service's unit backed by a basket of child tokens, with
covers(child), rate(child) (proxy units charged per 1 child unit; rate 2 means 1 tDurable costs 2 tAcmeRun) and exchange(child, amount, to). - A direct token implements the three functions as "covers nothing". Rate definition fixed 2026-10-22 after the mock found D6 and D7 inverted; D7's arithmetic is the reference.
The Intent gets an explicit fund field for nested billing (deferred by D48).
- The Intent gets an explicit
fund field: the token the client authorized. Direct billing: fund == token. Nested billing: token = the child's token, fund = the proxy. New typehash; every gateway and the runner upgrade together. - Contract A:
reserve checks the authorization of (client, identity, fund) and fund.covers(token), reserving max × rate; settle pulls actual × rate of the proxy from the client to the proxy's owner and calls exchange(token, actual, payTo) from the basket. An empty basket fails the settle; the gateway releases and does not answer.
One hop only: the fund must cover the token directly.
- One hop only:
fund must cover token directly. A proxy of a proxy is not supported. - Depth-2 nesting (a dependency spending further on the client's behalf) would need sub-authorization on A; not in scope.
The proxy owner buys child tokens and deposits them into the basket (deferred by D48).
- The basket: the proxy's owner buys child tokens from each child's owner and deposits them into the proxy contract. No minting rights are granted to proxies.
- Portal shows basket levels; the owner refills.
wpCredit is an independent credit token, not a proxy and not a fund; ≈ wp is an exchange-rate view of caps, balances and spends.
- wpCredit is not a proxy and not a fund. It is an independent credit token that exchanges with every ServiceToken (each token publishes its rate).
- The
≈ wp figures in the mock are this exchange-rate view of caps, balances and spends. Purchase and exchange flows: open.
D54, D57 · D10's note "Purchase and exchange flows: open" → D54–D57 (issued by Woodpecker; credits to the token's owner; paths A and B; changeable rate). Still open: how a wallet buys wpCredit from Woodpecker; the starter pack's wpCredit amount.
The agent's model is a gateway with an agent-chat route; the runner holds no provider credentials.
- The agent's model is a gateway too: a chat gateway exposes an
agent-chat route (tools, tool_choice, streaming, tool messages pass through; metered by usage). The runner registers such gateways as models. No provider credentials in the runner. - Precedent: today's
llm-gateway provider in the Engine (gpt-luna's lane, tLLM). The round-2 template gateways need the route.
The first mock UI: three menu items, authorizations grouped per identity, caps entered inline.
- Mock UI (done): three menu items (Wallet & Settings, Services, Conversations); Authorizations grouped per conversation identity with
≈ wp; caps entered inline next to the Model select and on each checked tool, one value per token, total in wp and the signature count; conversation view with the Identity card on top and one card with the tabs Authorization / Spends. - Commits ffec0aa, b375fc5, 4149420; live at https://woodpecker-console-mock.pages.dev
D60 · D12 / D33 "the total ≈ wp in the footer" → D60 (the total is the editable wpCredit cap in the budget block).
D12a · D12 “three menu items” → D12a and D69 (Extensions, and one item per installed extension).
- Four menu items (Conversations, Services, Extensions, Settings) plus one per installed and enabled extension, in the person's order (D69). The rest of D12 stays superseded as noted.
- 2026-11-03, with D69.
Order of work: record the decisions, finish the mock, write the v2 documents, then rewrite the modules.
- Order: record now (this file); finish the mock; write the v2 architecture documents in this repository; then rewrite every module. The current system stays as it is until then.
Decisions of 2026-10-22 (the Deploy flow in the Console)
The Console deploys services itself; the Console wallet is the token owner; the v1 Portal is not merged.
- The Console deploys services itself: a "Deploy a service" entry on the Services page replaces the
/devportal skill and the Portal's deploy pages. No Claude Code. The Console wallet is the token owner and the paying client; there is no separate Portal wallet. - The Portal is not merged into the Console; the deploy flow is mocked on its own.
Deploy prerequisites: a runner, a model and a host service (kind host, D52).
- Prerequisites of a deployment: My services must hold a runner, a model and a cloud service (kind
cloud: a wrapped Cloudflare account that deploys Workers, writes secrets, binds DOs, reads logs; priced per operation). Missing ones are listed with an Add link. - First cloud service: Steven's own account. Other clouds later, same kind.
D52a · D15 "kind cloud" → D52a (host); the service woodpecker@0a1b/cf keeps its name and tCloud.
A deployment runs as an agent conversation on the runner, with its own agent, caps and spends.
- The deployment runs as an agent conversation on the runner (model + tools), not as a fixed task. The agent asks what the skill asked (preset, service id, symbol, models, limits, mapping), calls the cloud service, deploys the token with the wallet's signatures, binds, verifies. The deployment is a conversation like any other: identity, caps, spends.
- The transcript shows every tool call with its price and every signature.
The upstream API key goes from the browser straight to the host service's secret store; the agent sees only its fingerprint.
- The upstream API key is entered in a dedicated field above the agent conversation, outside the chat. It goes from the browser straight to the cloud service's secret store. It never enters the conversation; the agent sees only its fingerprint.
- Rule carried over from the skill (never print, never pass a value).
Directory rows have an Add button; a host service is listed like any other service.
- Directory rows get an explicit Add (to My services) button; a cloud service appears in the directory like any service.
Verify is a real paid call from the Console wallet to its own new gateway.
- Verify stays a real paid call, from the Console wallet to its own new gateway.
Direct services first; the bundled type is reasoned again later.
- First the direct (non-bundled) service type. The bundled (proxy) type and its templates are reasoned again later.
Decisions of 2026-10-23 (kinds and pricing, from the discussion with Steven)
A kind is a contract of interfaces (runner, memory, model, tool, host); a label is only a display name.
kind is a convention, not a label. A service that declares a kind promises the interfaces of that kind, and the Console relies on them without reading anything else. A service may declare several kinds. label is only the developer's display name in the descriptor.- Required interfaces per kind: model — an
agent-chat route (OpenAI-style chat with tools, tool messages and streaming, metered by usage) plus /pricing; tool — one or more tool routes with a JSON-Schema parameter block and a pricing rule each, plus /pricing; runner — create / submit / watch / abort a conversation, derive identities, pay dependencies with the conversation identity; store — the durable conversation storage API (commit, read, scan); cloud — deploy a Worker, put a secret, bind a Durable Object, read logs, delete. A kind comes from the template or preset the service was deployed with; it is never typed by hand. Whether the directory contract records kinds is the open item above.
D51a, D52a · D3 and D34 kind lists → D51a and D52a: the kinds are runner, memory, model, tool, host.
D34a · D3 and D34 kind lists → D3a and D34a: the kinds are runner, memory, model, tool, host, store.
- Kind **
store** joins the list of D34: its interfaces are put, list and delete per owner (the store methods of the extension API, D76), priced per MiB started, reads free for the owner. Kinds: runner, memory, model, tool, host, store. - 2026-11-03, with D74.
Pricing is one of fixed, tiered or metered, then numbers; the contract only ever sees (max, actual).
- Pricing is a choice among fixed forms, then numbers. Each tool route (or the model route) carries one pricing rule:
fixed (one price per call), tiered (a measure of the request — a number at a path, a string length, code points or body bytes — and [limit, price] tiers), or metered (a max rule for the quote and an actual rule from the response, or a shipped preset such as openai-usage, apify-usage, bytes-tiers), plus maxPerCall. The developer picks the form in the deploy form and sets the numbers; the preset proposes both. The contract only ever sees (max, actual); the Console shows the rule as fixed · 1 tR2 per call, tiered · ≤ 6 tVoice, metered · ≤ 100 tGpt, with the ≈ wp view. - Same three forms as the v1 gateway spec §3.1;
period and conditional stay reserved. In the mock's deploy form the pricing block is implied by the preset; a later change may expose form + numbers as fields.
Each kind and preset has a default pricing; the preset other is discussed with the agent and confirmed on a card.
- Pricing comes with the kind and the preset;
other is discussed with the agent. Every kind has a default pricing form (model: metered by usage; tool: fixed per call, or tiered when the preset defines a measure; runner: metered per turn; store: fixed per commit; cloud: metered per operation), and every known preset carries its numbers. The deploy form shows the resulting rule in one editable line. The Preset select gets an other entry for an API no preset knows: then, and only then, the agent asks in the chat (what the API is, its routes and parameters, a free check path, the pricing form and numbers), proposes a summary card, and continues only after the person confirms that card. - The one exception to D30 (no questions in the chat). The run after the confirmation is the same as for a preset.
label is a free search tag, never part of the service name.
label is a user-defined tag for search. It is free text the developer chooses, shown as a small tag next to the display name and matched by the search box; it is not an identifier and never part of the name.- Several tags may come later; one text field for now.
Decisions of 2026-10-23, later (provider, starter tokens, wording, effort)
Own services live only in My services, with Manage; the Directory lists other owners' services, with Add.
- Own services live only in My services. A service this wallet deployed never appears in the Directory; it is in My services from deployment on, with Manage. The Directory lists other owners' services; Add copies one into My services.
The provider woodpecker@0a1b publishes the runner, the llm, the memory and the host service (cf); a new wallet starts with them.
- The provider
woodpecker@0a1b publishes a runner (Worker + DO bundled in one service, no store dependency, metered per turn), an llm model and a cf cloud service. A new wallet starts with the runner and the llm in My services; it adds the cloud service before its first deployment. - Solves the chicken-and-egg of the first deployment without a bundled-deployment flow.
D43 "no store dependency" still holds: the runner commits to the memory service the wallet chose, paid by the conversation's agent, not as a runner dependency.
D63, D64 · D43 "metered per turn" on the map → D63 + D64 (one round = 1 tRunner; receipts; batch settle).
Starter tokens for a new wallet: 50 tRunner, 50 tLLM, 50 tMemory and 20 tCloud.
- Starter tokens: the provider gives a new wallet 50 tRunner, 50 tLLM and 20 tCloud (and the wpCredit balance), enough to try conversations and deploy a first service.
- Shown in the wallet as "Starter pack from woodpecker@0a1b".
Signing requests speak plainly: the purpose and the wp amount first; the conversation identity is called the agent.
- Signing requests speak plainly. The panel leads with the purpose and the wp amount; the transactions sit under a collapsed Details; the conversation identity is called the agent (e.g. "the agent of ‹title›"); the authorization contract is "Woodpecker's authorization contract"; one network-fee line.
- Change 17.
Effort is a conversation setting (off … max): a default in Settings, changed from the conversation header, no signature.
- Effort (thinking level) is a conversation setting: off, minimal, low, medium, high, xhigh, max; chosen in the New conversation dialog next to the model, changeable from the conversation header, with a default in Settings. No signature: it is a runner parameter, not a payment.
- Change 18.
D53 · D46 "chosen in the New conversation dialog" → D53 (the dialog has a Memory select instead; effort starts from the Settings default and changes from the conversation header).
Decisions of 2026-10-23, evening (sign-in, the map's scope and form)
The runner's Worker only forwards by wallet address; the DO verifies the sign-in itself: challenge, signature, session credential.
- The runner's Worker only forwards; the DO verifies the sign-in itself. The Worker reads the claimed wallet name
chainId:address (lower-case) from the request, checks only its format, takes the Durable Object by that name (getByName) and forwards the request. The DO issues the challenge (domain, nonce, expiry; the nonce kept in memory), verifies that the signature recovers to its own address (ctx.id.name), stores a session credential with an expiry in its own storage and checks it on every request; sign-out deletes it. No global secret in the Worker, no database, no registry of DOs; a junk request writes nothing. - Replaces the D1 note "the Worker issues a short-lived credential". Payment is not a sign-in concern: any wallet may sign in; the runner checks the agent's tRunner remaining on contract A before every turn. Alternative recorded, not chosen: the embedded wallet key signs every request (no challenge, no session); it would prompt on every request once external wallets are supported.
D47a · D47 “the signature recovers to its own address” → D47a (or to an identity contract A shows authorized by the wallet, D73).
- A service accepts a sign-in by the wallet's signature, or by an identity that contract A shows authorized by the wallet: the challenge is the same; the signature recovers to an identity, and the service accepts it when
tokensOf(wallet, identity) on contract A is not empty (D73). A brand-new wallet signs in with the wallet itself the first time. The Worker still only forwards; the DO still verifies. - 2026-11-03, with D73.
Current scope is direct tokens only: proxy tokens, nested billing, the Intent fund field and the bundled deployment are deferred.
- Current scope: direct tokens only. Proxy tokens, the store service, nested billing, the Intent
fund field and the bundled deployment (D6–D9, D20–D24, D27) are deferred. They stay in this log; the architecture map and the first v2 documents do not show them. The runner bundles its Worker and DO in one direct service (D43); the directory's uses field is deferred with them. - Steven, 2026-10-23.
D51, D51a · D48 "the store service is deferred" → D51 and D51a (the memory service, a direct service, is in scope); proxy tokens, nested billing, the Intent fund field and the bundled deployment stay deferred.
D56a · D48 "the Intent fund field is deferred" → D56a (needed by path B; proxy tokens, nested billing and the bundled deployment stay deferred).
The architecture map shows numbered steps with a narration panel; response arrows are allowed; the runner is one frame around its DO.
- The architecture map shows numbered steps. Each flow is an ordered list of steps. The step number sits on the edge; the info panel lists the steps as sentences when a flow is selected. A response arrow is allowed when it carries a step (thin, open arrowhead; legend entry "response"). The Worker router is not a box: the runner is one frame
woodpecker@0a1b/runner that contains the DO, with a note that the Worker is the entry and forwards by wallet address. - Change 21.
- A flow step that happens inside one party (for example "import the key" in the Console) has no edge; its number badge sits in the top-right corner of that party's box, and the box lights up. Everything else of D49 stands.
- Change 35, WoodpeckerMock5.
Decisions of 2026-10-23, late (where state lives; the store returns; the hosting kind)
Where state lives: the chain (balances, authorizations, directory, own services), the memory service (conversations, settings record), the runner's DO (identity root, sessions, running turns), the browser (key, language, theme, last-used runner and memory).
- Where each piece of state lives. The wallet is a key and holds nothing. On chain: token balances; authorizations (cap, used, reserved per wallet, agent, token); the service directory; which services are the wallet's own (directory owner). In the wallet's store (D51): the conversations (transcript, title, status, spends) and the wallet's settings record (My services and their default caps; default model, effort, runner and store). In the runner's DO: the identity root, the sessions, the running turns and a cache of the conversations it works on. In the browser only: the wallet key (exported and imported by the person), language, theme, and the last-used runner and store, with the provider's as the defaults (D43).
- Steven, 2026-10-23: "add it; it does not work without this". The Conversations list and My services are read from the store; the Directory and the balances from the chain; a new browser finds the settings record in the default store and the preferred runner in it.
D50a · D50's lists → D50a (the identity root, the install records, the configuration namespaces, the store service, caps for every identity; D74).
- The browser keeps the identity root (and the wallet key until the wallet is external), the language, the theme, the menu order, and the last-used runner and memory; the memory service keeps the conversations, the settings record with the install records, and the extensions' configuration namespaces; a store service keeps an extension's large data; the chain: caps for every identity (the agents, the Console's, the extensions', the devices') (D74).
- 2026-11-03, with D74.
The memory service: a direct service (woodpecker@0a1b/memory, tMemory, fixed 1 per commit) that keeps the conversations and the settings record; the runner commits each completed turn; the owner signs in to it as to the runner.
- The store service returns, as a direct service. Kind
store (D34): durable conversation storage with commit, read and scan, plus the wallet's settings record. Default woodpecker@0a1b/store, token tStore, fixed 1 tStore per commit (D36), in My services from the start with the runner and the llm (D43), 50 tStore in the starter pack (D44). The runner commits each completed turn to the conversation's store with an Intent signed by the conversation's agent (token = tStore); the store's submitter reserves and settles on contract A. The owner wallet signs in to the store as to the runner (D47) and reads its conversations and writes its settings record free of charge; the agent that committed a conversation may read it. The store is chosen per wallet in Settings and per conversation in the New conversation dialog, next to the runner. Any wallet may deploy its own store from the store preset (a Worker + Durable Object storage, no upstream key). - Revises D48 for the store only: proxy tokens, nested billing, the Intent
fund field and the bundled deployment stay deferred. Consequence: the runner is replaceable; the conversations stay in the store when the runner changes.
D51b · D51 “the conversations and the settings record” → D51b (and the extensions' configuration namespaces; D74).
- The service of D51 is called
memory: kind memory, default woodpecker@0a1b/memory, token tMemory, fixed 1 tMemory per commit. Nothing else is renamed: the "settings record" keeps its name, conversations keep theirs. - Steven, 2026-10-23: "memory, then: change store to memory and do not change the other names".
- The memory service also keeps the configuration namespaces of extensions (
ext:<id>), bound to their identities at the first write; 1 tMemory per commit, within the manifest's quota (D74). The settings record gains the install records (D72). - 2026-11-03, with D74.
The kind cloud is renamed host; woodpecker@0a1b/cf keeps its name and its token tCloud.
- The kind
cloud is renamed hosting. A hosting service deploys and runs the Workers of gateways, runners and stores: deploy a Worker, put a secret, bind a Durable Object, read logs, delete (the D34 interfaces, unchanged). The service woodpecker@0a1b/cf keeps its name and its token tCloud; only its kind changes. Kinds are now runner, store, model, tool, hosting. - Proposed word
hosting (alternatives: host, deployer); Steven to confirm the word.
- The kind of D52 is
host (not hosting). woodpecker@0a1b/cf and tCloud unchanged. Kinds: runner, memory, model, tool, host. - Steven, 2026-10-23.
Decisions of 2026-10-24, later (hybrid settlement; Steven's review of the New conversation dialog)
Hybrid settlement: token caps are per-token ceilings, one wpCredit cap is the budget; settle takes the wallet's tokens first and pays only the shortfall in wpCredit.
- Hybrid settlement. Two kinds of authorization work together on one identity: a cap per service token (the ceiling for that token — "tGpt at most 10") and one cap in wpCredit (the budget). For a call priced in tX, reserve needs
remaining(tX) ≥ max; if the identity also holds a wpCredit authorization, contract A reserves max × rate on it as well (worst case) and stores the rate. Settle takes the tokens first: transferFrom(client → payTo) for what the wallet holds, up to actual; for the shortfall it pulls shortfall × rate wpCredit to the token's owner and mints shortfall tokens to payTo (D56b). used(tX) += actual; used(wpCredit) += shortfall × rate. Without a wpCredit authorization, settle fails when the tokens are short, as today. The Intent keeps its v1 fields: no fund is needed, A decides the funding from the authorizations it holds. Path A (buy first) is unchanged. - Steven, 2026-10-24: "add hybrid settlement … a service token's cap is there to limit that service token, for example tGpt allows at most that much". Feasible because reserve only books and settle moves the assets (v1 §3). Replaces D56a's "one record covers every service": the token caps stay, the wpCredit cap is added to them.
D59 / D59a / D59b / D60 (hybrid settlement with the budget booked at reserve; off → revoke) → D67 (the shortfall is funded at settle at the current rate; off → authorize with the budget = used).
D67 · The /wpcredit note "D59–D61 will be re-stated on D64" → D67.
- Switch off = exact mode. Without a wpCredit authorization the token caps are the only limits: a call stops when its token runs out, settle fails as today, and nothing is bought on the person's behalf. This is the mode for precise limits; the Console says so in one line next to the switch and warns when a cap exceeds what the wallet holds.
- Steven, 2026-10-24: "this is right: with the switch off, it is computed from the caps that were set; for an exact limit, this is the way". No auto-buy anywhere.
D59 / D59a / D59b / D60 (hybrid settlement with the budget booked at reserve; off → revoke) → D67 (the shortfall is funded at settle at the current rate; off → authorize with the budget = used).
- Hybrid settlement, the details. One reservation record per call: token, max, the stored rate, and what it booked on the budget. At reserve, the budget booking is
min(max × rate, the budget's remaining): a call the tokens can pay is never blocked by a small budget. At settle, the shortfall's credits must fit the budget booking, else settle fails, as a token-short settle fails in exact mode (D59a). Settle and release close the one record and free both bookings. The Spent event gains a last field credits (the wpCredit paid, 0 when the tokens covered the call). - Design seat, 2026-10-24, answering the gaps WoodpeckerMock3 found in Change 42.
D59b "one reservation record per call … the budget booking at reserve" and D60 "off → revoke(identity, wpCredit)" → pending re-statement on D64 (wpCredit is the next step; there is no reserve and no revoke).
D59 / D59a / D59b / D60 (hybrid settlement with the budget booked at reserve; off → revoke) → D67 (the shortfall is funded at settle at the current rate; off → authorize with the budget = used).
The Pay-in-wpCredit switch sits with the editable wpCredit cap in the dialog's budget block and in the conversation's authorization bar (authorize / revoke on the identity).
- The wpCredit switch lives in two places. In the New conversation dialog, a budget block at the top holds the switch "Pay in wpCredit", the editable cap in wpCredit (default: the sum of the token caps at today's rates — the former "≈ wp" footer total) and the signature count; the per-token rows keep their caps. On the conversation page, the same switch sits in the authorization bar: on → the wallet signs
authorize(identity, wpCredit, cap) with an editable cap, one transaction added to the identity; off → revoke(identity, wpCredit); the budget meter shows used / cap in wpCredit. - Steven, 2026-10-24, points 1 and 3: the total and the switch "they are too far apart, I did not understand it"; the conversation page "there must also be a pay in wpCredit switch". Contract A needs nothing new for the toggle: authorize / revoke on an existing identity exist in v1.
D59b "one reservation record per call … the budget booking at reserve" and D60 "off → revoke(identity, wpCredit)" → pending re-statement on D64 (wpCredit is the next step; there is no reserve and no revoke).
D59 / D59a / D59b / D60 (hybrid settlement with the budget booked at reserve; off → revoke) → D67 (the shortfall is funded at settle at the current rate; off → authorize with the budget = used).
Each per-token cap row reads cap → what you hold → the rate (1 wpCredit = N tX) → the ≈ wp value, in plain layout.
- The cap cell says what it means. Each per-token row shows the cap input, what the wallet holds, the rate as "1 wpCredit = N tX" and the ≈ wp value, laid out so the eye reads cap → holds → rate → value; no monospace run of five facts in one line.
- Steven, 2026-10-24, point 5: "there is already a rate here, only the UI design is wrong, not friendly at all". Design by WoodpeckerMock2 (Change 41).
D67 · The /wpcredit note "D59–D61 will be re-stated on D64" → D67.
Decisions of 2026-11-03 (extensions)
Extensions: a page that runs in the Console in its own tab, an iframe from the extension's own origin, reaching the Console only through the extension API over postMessage; it never holds the wallet, a credential or a key.
- An extension is a page that runs in the Console in its own tab. The tab is an iframe served from the extension's own origin: one subdomain per extension on the host service (D71). Extensions are independent of services: an extension may use several services, several extensions may serve one service, and an extension may use none. The whole tab is the extension's: its own DOM, any framework. It reaches the Console only through the extension API over postMessage (D76); it never holds the wallet, a session credential or an identity's key, and it never touches the Console's DOM. Only two things go through the Console: signatures, through the wallet's signing panel (D32, D45), and paid writes, signed by the extension's identity (D73).
- Designed with Steven, 2026-11-02/03; built into the Console mock as Change 52 (four sample extensions: the Data Vault manager, Devices, Notes and the Film editor). The Console keeps the frame around the tab: the identity, the caps, the actions and the log of the extension API; the page inside is the extension's own.
The menu: a fourth item, Extensions (My extensions, the Directory, Publish an extension), and one item per installed and enabled extension; the person may unlock, drag and lock the order, which lives in the browser.
- A fourth menu item, Extensions. Its page has three parts: My extensions (the installed ones, and this wallet's own with Manage), the Directory (other owners' published extensions, with Install) and Publish an extension. Each installed and enabled extension is a menu item at the same level as the others: its tab. The person may unlock the menu, drag the items into an order and lock it; the order lives in the browser (D50). D12 is amended (D12a).
- The default order is Conversations, Services, Extensions, the extensions' tabs, Settings. A disabled extension leaves the menu and keeps its caps (D72). Mock routes:
#/extensions, #/extensions/publish, #/extensions/<id>, #/extensions/<id>/manage.
ExtensionDirectory: a second minimal contract, publish(name, url, hash) and unpublish(name) by the owner wallet; an entry holds owner, name, url, hash, updatedAt and active; the handle comes from the ServiceDirectory. Decided, not on chain yet.
- A second minimal contract, the ExtensionDirectory: no admin, no fee, no upgrade. Writes by the owner wallet:
publish(name, url, hash) and unpublish(name). An entry holds the owner, the name, the url, the hash (sha256 of manifest.json), updatedAt and active. Reads: entry(owner, name), entriesOf(owner), count() with enumeration (entryAt(index)). The handle is read from the ServiceDirectory (handleOf), so an extension's id is handle/name, for example steven@68df/film-editor; a wallet without a handle claims it there first. Events Published and Unpublished. Decided, not on chain yet. - Sample address in the mock:
0xe3d7…40b2. The chain holds only the url and the hash; the code is at the url (D74). Kinds, prices and permissions are not in the contract: they are in the manifest (D77). The owner wallet pays the gas of publish and unpublish.
Publish: a zip with manifest.json, read and hashed in the browser, stored and served by the host service (one paid operation by the Console's identity), then publish(name, url, hash) signed by the wallet; a new version is a new publish and installed copies keep their pinned hash.
- Publishing is a form, not an agent conversation. The package is a zip with manifest.json at its root. The Console reads it in the browser: the manifest, each file's sha256 against the manifest's
files, and the manifest's own sha256. The host service (kind host) stores the package and serves it from one subdomain per extension and one path per version (https://<name>.<handle>.ext.<host>/v/<hash>/), with the response headers the frame needs (frame-ancestors the Console, a CSP baseline, CORS on the manifest); the upload is one paid operation in tCloud, paid by the Console's identity (D75). Then the wallet signs publish(name, url, hash) (claim the handle in the ServiceDirectory first when the wallet has none). A new version is a new publish: the chain holds the new hash; installed copies keep the hash they pinned and show that an update exists; a changed permission asks for consent again. The host keeps old versions (how long: in its price document, open). - Sample:
steven@68df/film-editor 0.2.0, manifest hash 0x2949…f843, url https://film-editor.steven-68df.ext.woodpecker-0a1b.workers.dev/v/2949f843/; the upload costs 5 tCloud on woodpecker@0a1b/cf (metered, ≤ 5 per operation). The signing panel reads “Publish steven@68df/film-editor 0.2.0 · 1 transaction”. Walkthrough: publish.html.
Install: the card shows what the manifest declares, a Store select when it needs one and one cap per token for the extension's identity; one wallet request (approve on first use, one multicall of authorize), then the install record is committed to the settings record; uninstall sets every cap to used.
- Install, from the Directory or from My extensions. The install card shows what the manifest declares (configuration quota, store need, services called, wallet permission, reads, notifications), a Store select when the manifest needs one (a service of kind store from My services; there is no default store), and one cap per token the extension's identity may spend: its configuration's tMemory, its store's token, the token of each service it calls, with the balance ≥ cap gate of D64. Install asks the wallet once:
approve(A) on each first-used token, then one multicall of authorize for the extension's identity; then the install record (id, version, hash, identity, store, caps, enabled) is committed to the settings record, a commit by the Console's identity (D75). Disable keeps the caps and does not load the tab; uninstall sets every cap to used (one multicall) and removes the install record; the data in the store stays. An extension's calls to a service are paid by the extension's identity within these caps. - Signing panel: “Allow the extension Film editor to spend up to ≈ 10.5 wp · 10 tMemory · 50 tDatavault” (approve tDatavault on its first use, then authorize ×2 in one transaction); “Uninstall Film editor · 2 authorize with cap = used · one transaction”. Walkthrough: extension.html.
Identities in the browser: the Console's identity and one identity per installed extension are derived from a root the wallet signs once; they sign Intents and sign-in challenges in the Console; a service accepts an identity that contract A shows authorized by the wallet (D47a).
- The Console's own identity (one per wallet) and one identity per installed extension are derived in the browser from a root the wallet signs once: a deterministic signature over a fixed message (EOAs qualify; smart-contract wallets are an open item). The Console signs their Intents and the services' sign-in challenges with these identities; the keys never leave the Console's origin. A service accepts a sign-in by an identity that contract A shows authorized by this wallet (D47 amended: D47a); a brand-new wallet signs in with the wallet itself the first time, as D47 says today. The runner keeps its root and the agents: D2 unchanged; no DO interface signs for the Console or an extension; signing costs nothing. No conversation stands behind an extension: an extension that needs an agent is a v2 item (conversation methods on the extension API).
- Sample identities: the Console's
0x0c71…a3e9; the Data Vault manager's 0xe7a1…4c09; Devices' 0xe8d2…51b3; the film editor's 0xe9f3…6e27. A conversation's identity stays the agent, derived by the runner's DO (D2). The wallet's Authorizations tab shows one group per identity: the conversations, the Console, each extension, each device (D78).
Where extension data lives: code at the url and the hash on chain; install records in the settings record; configuration in the memory service, one namespace per extension; large data in a store service chosen at install; cache in the browser; on chain only the caps.
- Where extension data lives. Code and manifest: at the url the directory holds, the hash on chain. Install records: the settings record in the memory service. Configuration: the memory service, one namespace per extension (
ext:<id>), bound to the extension's identity at its first write, 1 tMemory per commit, within the manifest's quota. Large data: a store service, a new kind store (paid per MiB started, reads free for the owner), chosen by the person at install; no default store. Cache: the browser. On chain: only the caps. D3, D34, D50 and D51 are amended (D3a, D34a, D50a, D51b). - Sample store:
steven@68df/datavault, kinds store and tool, token tDatavault, fixed 1 per MiB started, 1 wpCredit = 100 tDatavault; the wallet holds 100. The install record pins version, hash, identity, store and caps, and carries the enabled flag (D72).
Paid writes outside a conversation: the settings record's commits and a publication's upload are paid by the Console's identity, whose cap is asked in the signing panel on first use and raised when it is used up; a write is confirmed only after its settle.
- Paid writes outside a conversation are paid by the Console's identity: the settings record's commits (every change of a default, every install record) and a publication's upload. A commit is 1 tMemory; the upload is one host operation in tCloud. The identity's cap for a token is asked in the signing panel the first time a write needs it (approve on first use, then authorize) and raised when it is used up. A write is confirmed only after its settle (D66); a failed settle stores nothing. Reads are free for the owner.
- Signing panel: “Allow the Console to spend up to ≈ 5 wp · 5 tCloud · for the upload of an extension package”; the other reasons read “for the commits of your settings record”, “for the install record of an extension”, “for the settings record after an uninstall”. Settings shows the record: the memory service, the Console's identity, “tMemory used 2 of 20”, the number of commits so far.
The extension API, v1: tabs only, over postMessage; ready, config, store, services.call, wallet.request, ui.toast and the reads, with permissions from the manifest and six error codes; a paid write settles on the extension's identity before the reply.
- The extension API, v1: tabs only; slots inside the Console's own pages are v2 on the same API. Over postMessage: requests
{ wp: 1, id, method, params }, replies { wp: 1, id, ok, result | error }, events { wp: 1, event, data } (theme, caps, lang). Methods: ready; config.get, config.put; store.put, store.list, store.delete; services.call(service, tool, args); wallet.request(kind: authorize, identity, label, token, cap, reason); ui.toast; read.balances; read.authorizations. Permissions come from the manifest (storage.config, storage.store, services, permissions.wallet, permissions.read, permissions.notify); error codes permission_denied, cap_exceeded, balance, no_store, rejected, unknown_method. A paid write settles on the extension's identity before the reply. wallet.request goes through the signing panel, which names the extension, the identity to authorize and the reason; the record on contract A is for the named identity (a device), not for the extension. The Console keeps a log of the calls on the tab. - SDK
woodpecker.js: Woodpecker.connect() sends ready and keeps its result; wp.call(method, params); wp.on(event, handler). ready answers with the wallet, the extension (id, name, version, identity), the language, the theme, the permissions, the caps, the store and the services with their prices. A reply to a paid write carries the receipt (token, actual, tx) and the caps after it.
The manifest: id (handle/name), name, version, description, icon, api, entry, files with sha256, services, storage quotas, permissions (wallet, read, notify) and locales; no content scripts, no access to the Console's DOM.
- **The manifest,
manifest.json**: id (handle/name), name, version (major.minor.patch), description, icon (a lucide name, D39), api (1), entry, files (path, sha256, size), services (names), storage (config quota, store quota), permissions (wallet with authorize, read with balances and authorizations, notify), locales (en, zh-CN, es-ES; identifiers never translate). No content scripts and no access to the Console's DOM. - The film editor's manifest: entry index.html, storage config 8 KiB and store 256 MiB, permissions notify, no services. The Devices manifest: wallet authorize, read authorizations, notify. The Data Vault manager's: services
steven@68df/datavault, config 4 KiB, read balances, notify.
Devices: a device has its own identity; an extension with the wallet permission asks the person, through the signing panel, for a cap on it; the device signs its own Intents and services settle against that record; Stop on chain (cap = used) revokes it.
- A device has its own identity: its own key, or a key a devices service keeps for it (open). An extension with the wallet permission asks the person, through the signing panel, for a cap on that identity; from then on the device (or its service) signs its own Intents and services settle against that record; Stop on chain (cap = used) revokes it. Where a device's logic runs is open: the device itself calling a model service; a devices service running a fixed pipeline per device with the device's key; or an agent conversation the device posts into (which needs D47's amendment and a rule on which conversation a device may post to).
- Sample: the Devices extension (
woodpecker@0a1b/devices, identity 0xe8d2…51b3) asks for kitchen-cam, 0xd3c1…ce01: “Give kitchen-cam a cap of 20 tVision · asked by the extension woodpecker@0a1b/devices · stream clips to the agent”; approve tVision on first use, then authorize(0xd3c1…ce01, tVision, 20); the wallet's Authorizations tab gains the group Device · kitchen-cam.
Security of the extension model: EIP-4361 challenges, short-lived per-service credentials, no credential or key in an extension, the hash pinned at install and checked against the chain, namespaces bound to one identity, the signing panel names the extension; open: file verification at load, encryption, smart-wallet signatures, quota enforcement.
- Security of the extension model. Challenges follow EIP-4361 (domain, nonce, expiry); a session credential is per service and short-lived; an extension never holds a credential or a key; the install record pins the hash and the Console checks the manifest's hash against the chain at install and at restore; the memory service binds a namespace to one identity; the signing panel shows the extension's id and the reason on every request it asks. Open: verifying the files at load (a service worker in the extension's origin); encryption of the settings record and the configuration namespaces (the memory operator reads plaintext today); deterministic signatures for smart-contract wallets; quota enforcement by the memory service.
- Open items of this section, collected: who runs a device's logic (D78); file verification at load; encryption; smart-wallet signatures (D73); the memory quota rule; the host's retention of packages (D71); the first wallet's gas and the starter pack trigger (already open, D44).