@byokit/openclaw 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +45 -7
  3. package/SECURITY.md +3 -2
  4. package/dist/accounts.d.ts +45 -1
  5. package/dist/accounts.js +641 -46
  6. package/dist/activation.d.ts +17 -0
  7. package/dist/activation.js +9 -0
  8. package/dist/approvals.d.ts +2 -0
  9. package/dist/approvals.js +9 -9
  10. package/dist/attribution.d.ts +3 -0
  11. package/dist/attribution.js +25 -0
  12. package/dist/bridge.d.ts +3 -1
  13. package/dist/bridge.js +4 -6
  14. package/dist/config.js +10 -0
  15. package/dist/constants.d.ts +1 -0
  16. package/dist/constants.js +2 -0
  17. package/dist/device.d.ts +5 -2
  18. package/dist/device.js +11 -1
  19. package/dist/engine.js +1 -1
  20. package/dist/index.d.ts +1 -1
  21. package/dist/keys.d.ts +19 -5
  22. package/dist/keys.js +135 -23
  23. package/dist/kit.d.ts +38 -1
  24. package/dist/kit.js +153 -28
  25. package/dist/link.js +21 -11
  26. package/dist/members.d.ts +3 -0
  27. package/dist/members.js +13 -9
  28. package/dist/pick.d.ts +7 -3
  29. package/dist/pick.js +34 -5
  30. package/dist/routes.d.ts +1 -0
  31. package/dist/routes.js +5 -3
  32. package/dist/routes.json +31 -31
  33. package/dist/runs.d.ts +7 -0
  34. package/dist/runs.js +111 -11
  35. package/dist/signin.d.ts +3 -0
  36. package/dist/signin.js +51 -17
  37. package/dist/testing/fake-gateway.d.ts +18 -0
  38. package/dist/testing/fake-gateway.js +210 -16
  39. package/dist/testing/model-stub.d.ts +1 -0
  40. package/dist/testing/model-stub.js +2 -1
  41. package/dist/types.d.ts +19 -0
  42. package/dist/words.json +8 -1
  43. package/package.json +2 -2
  44. package/plugin/accounts.js +12 -3
  45. package/plugin/catalog.js +48 -0
  46. package/plugin/index.js +2 -1
  47. package/plugin/keys.js +22 -4
package/CHANGELOG.md CHANGED
@@ -2,6 +2,33 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.12.0 (2026-10-13)
6
+
7
+ - FIX: an unsealed API key that a failed `addKey` could not clear (the gateway refused the clear after a restart ran late) is now dropped at the kit's first ready after start, so it no longer stays in the member's `byokit-key-<member>` store until the next add. A key whose ready marker exists is never cleared, and a sweep the gateway interrupts runs again on the next ready.
8
+ - FEAT: An OpenCode Go plan key now runs every Go model, including `muse-spark-1.3-contributor` over the Responses API, with no model setup in the app. On each start and after `addKey`/`addAccount`, the kit asks the Go provider's own catalog hook (new plugin method `byokit.catalog`) for the live models. It registers each one with its `api` in `models.providers['opencode-go'].models`, so `models()` lists it and runs resolve it. It keeps each pinned Go model the live list omits, writes only when the list changed, never drops a registered model, and leaves the provider alone when `KitOptions.config` sets that list. Each model gets the endpoint the pinned engine routes, `https://opencode.ai/zen/go/v1`, not the live catalog's `inference/go/v1`, which the pinned engine refuses with `missing x-opencode-session`.
9
+ - CHANGE: `run({ auth: 'apiKey', model })` accepts any model of the saved key's provider, not only the model saved at activation. Another provider still ends `plan`, now with the message "Choose a model of the provider saved with this key."
10
+ - FIX: `signedIn(member, provider)` is true after `addKey` for that provider: it also counts a ready key whose saved model is that provider's. `providers(member)` still lists only the member's own sign-ins, the ones a run without `auth: 'apiKey'` uses.
11
+ - FIX: an OpenCode Go plan key now sets up and runs on the pinned engine. `addAccount(member, { authChoice: 'opencode-go' })` (and `signIn` for any key-entry choice) asks for `Plan key` and pastes it into typed `openclaw.setup.activate { kind: 'api-key' }` instead of the setup wizard, which refused it ("That provider setup is not available on this Gateway."); a key-entry failure shows only the kit's words, never the engine's text. The plan key is sealed on the member agent (`seal { planKey: true }`: subscription billing). With a spawned engine, when the activation changes config `auth`, `done` waits out the gateway restart that causes (within 60 s); past that the sign-in ends `why: 'failed'` ("the engine took too long to restart") and is cleaned up, so nothing half-set-up is kept; `addKey` does the same and returns `retry`. A run started on done uses the new sign-in. The kit now writes `agents.defaults.modelPolicy = {}` unless the app sets a policy, and drops a saved `allow: ['openai/*']`: left unset, the pin turned the kit's `models` map into that allowlist and every non-OpenAI model fell back to openai. Discovery follows the engine: a key-entry choice without `appGuidedSecret`, or a login choice without `appGuidedAuth`/`appGuidedDiscovery`, is `no_upstream_flow` and never ready (22 routes at 2026.8.1, 15 of them new here, among them `fal-api-key`, `vllm`, `sglang` and `custom-api-key`; the 15 new no-upstream-flow choices are `fal-api-key`, `vllm`, `sglang`, `custom-api-key`, `alibaba-model-studio-api-key`, `clawrouter-api-key`, `cloudflare-ai-gateway-api-key`, `comfy-cloud-api-key`, `copilot-proxy`, `longcat-api-key`, `microsoft-foundry-entra`, `microsoft-foundry-apikey`, `pixverse-api-key`, `runway-api-key` and `vydra-api-key`); the engine job checks every ready and absent choice against the real Gateway. The pinned engine already sends OpenCode Go's required `x-opencode-session` (stable per conversation) on the activation probe and every run (kits before 0.10.0 pinned an engine without it); the engine job asserts it. The README and spec say how an app registers a live-only Go model such as Muse (`config.models.providers['opencode-go']`, `api: 'openai-responses'`).
12
+ - FIX: Key-agent creation waits for the published roster before activation. `addKey` now waits for catalog synchronization and prepares the full-catalog owner once before success; preparation failures clear the attempted key and return a secret-safe `retry`/`engine`. A chosen live-catalog model must be in the engine's registered list before the key is sealed, or the add returns `retry`/`model`. Ordinary runs do not gain a cold refresh.
13
+ - FIX: `addKey` accepts a catalog `model` for the Go live-catalog route and saves it for key-lane runs; `models(member, { authChoice })` lists that route's live catalog once a key is saved (empty before, with no agent queried), including Go Muse. Only an available row from that key lane can be selected; ordinary-member discovery is never used.
14
+ - `addKey` now returns a typed `{ status, reason? }` result instead of a string, so callers compare `result.status === 'ok'` (migrate `result === 'ok'`). `invalid` now only means the provider refused the key (`auth`), its billing refused it (`billing`), or the key was blank (`empty`); unsupported routes and unknown models are `not_included` with `model`; every other failure is `retry` with `network`, `inference`, `engine` or `busy`. A member that is not your own now throws `Choose your own member.`, as `ensureMember` does, instead of returning a result. `model` is accepted only for the Go live-catalog route: on any other route it returns `not_included`/`model` before activation, and `models(member, { authChoice })` lists nothing for them. Failures never contain keys, key fragments, headers or raw engine diagnostics.
15
+ - FEAT: OpenClaw kit S7 `kit.addAccount` identity merge: a sign-in whose identity (`who`) matches another account of the same member and provider moves into that account and `done.id` names it, so signing in again as the same account refreshes its row instead of adding one, whatever email it now reports and whatever `again` names. `again` with another identity adds a row and leaves the named account billing as before. A sign-in with no identity never merges, even with the same email. When several older accounts share an identity, the earliest added one takes the sign-in. The identity never appears in `accounts()`, views, errors or logs.
16
+ - FEAT: OpenClaw kit S11 session binding: every run first finds the account its session is bound to (an account agent's key, or the agent of the member's that holds `agent:<that agent>:<tail>`, found with `sessions.resolve { allowMissing: true }`, member agent first), so a later run on one app key stays on that account. A key bound to an account agent resolves `'auto'`, `'default'` and no account to it, and refuses another id (`code: 'bound'`, words `account.bound`); when that account bills per use, anything but its own id is refused (`code: 'paid'`, words `account.paid`); a key bound to the member agent chooses only among its own accounts. Refusals end `kind: 'other'` after the binding read and before any room, model or agent request (one account listing when the index is cold); an `auth: 'apiKey'` run continues only an API-billed conversation (other bindings refuse with `account.bound`), and a conversation holding no account names `another account`. New `kit.pick(member, sel, { sessionKey })` returns exactly what `run` would decide with that key, `considered` included. `run` accepts a member's account-agent key; `steer`/`abort` map a live run's app key to its engine key. With no account agents, runs ask nothing more.
17
+ - FEAT: OpenClaw kit S12 `kit.move({ member, sessionKey }, to)`: a conversation moves to another account. Its whole transcript (user turns, tool calls and results) forks onto the target account's agent with `sessions.create { fork: true, parentSessionKey }`, never `chat.history` plus `chat.inject`, under both agents' exclusive locks; then the source session is deleted only if it is still the session read (`expectedSessionId`, `expectedSessionUpdatedAt`). It answers `{ ok: true, session: 'agent:<target>:<tail>' }`, and the next run on the app key continues there. `unsupported` when the conversation is found nowhere, `busy` while a run is live on it (the kit's own runs or the engine's `hasActiveRun`), `start_failed` when the target is not ready or the engine refuses the fork (the source untouched), `close_failed` with `live` = the new key when the source cannot be deleted (both kept). A move within one agent forks nothing and answers its own key. A session already under the target key refuses the move `start_failed` (`move.taken`), the source untouched. A run that waited for its agent's lock across a move binds again under the lock and ends `{ ok: false, kind: 'other' }` (`run.moved`) before any event, so it never runs on the deleted key; a member-path run on a bound key now holds the member agent's shared lock. New words `move.unsupported`, `move.busy`, `move.notReady`, `move.closeFailed`, `move.taken`, `run.moved`. The fake gateway keeps per-session transcripts and serves `sessions.list` (`hasActiveRun`), `sessions.create { fork }`, `sessions.delete` and `chat.history`.
18
+ - FEAT: OpenClaw kit O14m attribution and link/device account frames: one member-attribution rule, the kit's `memberOf`, maps an indexed `<member>--<hex6>` account agent and its session keys to `<member>`, and approvals, the bridge and the link adapter all use it instead of one copy per file. Link `oc.events`/`oc.sessions` carry an account agent's rows and events under its member, link `oc.run` accepts `account`/`needs` and streams the account frame first, and the device client sends `account`/`needs` and refuses a misshapen session key on `oc.run` before any request. An unindexed account-shaped agent maps nowhere, so it stays unreachable through events, sessions, approvals, steer/abort and bridge attribution, and `addAccount` refuses key material outright (keys stay host-only).
19
+ - FIX: a paired device can sign in to an offered plan key (OpenCode Go) over the link. `oc.signIn.start('opencode-go', via)` no longer refuses with `This device can't do that.`: `routeFor` serves the provider's offered plan-key route whichever `via` the device asks, the host's `kit.signIn` asks `{ prompt: 'Plan key' }`, and `oc.signIn.paste` carries the key only inside the sealed link frame to the host, which activates and keeps it. No view, state or run frame sent back holds the key; a refused key shows only the kit's words.
20
+
21
+ ## 0.11.1 (2026-10-12)
22
+
23
+ - Dependency update: pins @byokit/discover 0.8.1.
24
+ - FIX: (from @byokit/discover 0.8.1) `browse()` and `scan()` accept resolved services whose `fullName` carries the service type without a trailing dot (`host._type._tcp`, as Android's react-native-zeroconf delivers) or with a trailing `.local` domain suffix, case-insensitive, instead of only the `host.local._type._tcp.` shape. Android phones now discover computers over mDNS instead of always scanning empty.
25
+
26
+ - FIX: restoring an engine home no longer rewrites a live file whose bytes already match the sealed object. `restore()` compares each decrypted object against the live file at the restore mode (0600) — size first, then bytes — and skips the temp write, file fsync, atomic rename and directory fsync when they match, so a start over an untouched tree pays no per-object writes. A missing, changed, different-mode or unreadable file restores exactly as before, and a skipped file changes nothing on disk, so the crash-durability contract is unchanged; nothing new is written to disk.
27
+ - FEAT: OpenClaw kit S5 account spine: per-member `accounts/<member>.json` index (0600, atomic, no email/token/engine line) behind `kit.accounts`, `renameAccount` and `defaults`/`setDefaults`; a pre-O14 sign-in synthesizes an `addedAt: 0` entry, a default naming a removed account drops on read, and an unindexed `<member>--<hex6>` agent is never listed. `src/locks.ts` adds the per-agent shared/exclusive/live locks later steps use for runs, removal, moves and sealing.
28
+ - FEAT: OpenClaw kit S6 `kit.addAccount(member, { authChoice, via?, name?, again? }, on)`: a member's first sign-in of a provider stays on its own agent, and each further one (or `again`) gets a new sealed agent `<member>--<hex6>` (drawn again on a collision) with account id `<provider>.<hex6>`, its index entry written before the agent and IDENTITY.md kept byte-identical. The ChatGPT authorize URL carries `prompt=login` so a second account can be chosen; the row reads `signing` until the sign-in ends; `done` comes only after the agent is sealed to that one sign-in and its `who` hash is indexed; a sign-in that fails, ends busy or is cancelled removes the new agent and its entry. `authChoice: 'openai-codex'` (the engine's id for ChatGPT) signs in on the `openai` route.
29
+ - FEAT: OpenClaw kit S9 Auto input: `kit.models(member, id)` maps the account provider's `models.list { agentId, view: 'default' }` rows to `provider/model` (`missing-auth`/`auth-failed` → `why: 'signed_out'`, `cooldown` → `why: 'resting'` with `until = unavailableUntil`), and `kit.room(member, id, demand?)` reads the `models.authStatus { agentId }` row for the provider's tightest applying usage window (`left`, `span`, `resetsAt`; no `at`, no kit cache, unknown for API-key accounts and unanswered reads). Both hold the account agent's shared lock for the read. The fake gateway serves `models.list { agentId }` per agent and a cold per-provider usage cache (`setUsage`; no `usage` on the first answer carrying the provider).
30
+ - FEAT: OpenClaw kit S10 account runs: `kit.run({ member, sessionKey, account: 'auto' | 'default' | id, model?, needs? })` picks one account once, before any request (Choosing, 5.15), checks its own sealed sign-in with `byokit.accounts ready` and runs on its agent; an unbound key that picks an account agent runs as `agent:<account agent>:<tail>`. The first event is `{ type: 'account', account, model, sessionKey, how, why, considered }`, an ok end carries `account` and `model`, a chosen account that fails its ready check ends `signed-out` after its account event, with no agent request (Auto skips such an account for the next one), an empty pick ends `signed-out`, a missing model ends `plan`, and a resting end writes `until` (now + 5 min when the message names none) so the next Auto skips the account. `needs` alone resolves as `account: 'default'`; with `account` absent and `needs` absent or empty a run is unchanged. Account runs hold the agent's shared lock to their end, and index edits now take their own per-member lock, so a live run never blocks a rename or a listing. A model the engine reports unavailable only because its account is signed out or resting still counts as that account's plan. Session binding arrives in the next step.
31
+
5
32
  ## 0.11.0 (2026-10-12)
6
33
 
7
34
  - Dependency update: pins @byokit/ui 0.7.0.
package/README.md CHANGED
@@ -22,7 +22,7 @@ npm install @byokit/openclaw
22
22
 
23
23
  Node 22.18 or later. The kit brings the engine with it: the first `start()` installs the pinned `openclaw@2026.8.35`
24
24
  under your `stateDir` (a few minutes, needs the network once), and later starts run from there. For a whole app, a
25
- phone page that pairs, signs in with ChatGPT, runs and answers approvals, see
25
+ phone page that pairs, signs in to a plan (ChatGPT or a plan key), runs and answers approvals, see
26
26
  [`examples/openclaw-kit`](../../examples/openclaw-kit).
27
27
 
28
28
  ## Bundled engine provenance
@@ -124,15 +124,38 @@ engine text that could echo the token. It is sent only as this member's wizard a
124
124
  results or kit logs. Ordinary browser paste and typed `kit.call('wizard.next', …)` pass-through are unchanged.
125
125
  API key (billed per use) entry still requires explicit selection; there is no subscription-to-key fallback.
126
126
 
127
- To choose which of a member's accounts a run uses, pass `model: 'provider/model'` in the run spec (for example
127
+ To choose the model for a run, pass `model: 'provider/model'` in the run spec (for example
128
128
  `'openai/gpt-5.1'`, where `provider` is an id `kit.providers(member)` lists). That provider is the one called and billed for this run only, with no fallback to another
129
129
  provider or model. If the member isn't signed in to it, the run ends `{ ok: false, kind: 'signed-out' }` and the
130
130
  engine is never called. Leave `model` out to keep the engine's own choice. A specific sign-in (`@profile`) can't be
131
131
  picked per run: the pinned engine may still switch to another sign-in for the same provider.
132
132
 
133
+ To let the kit choose among a member's accounts, pass `account: 'auto'` (the account with the most room left, decided
134
+ once at run start), `'default'` (the saved default) or an id from `kit.accounts(member)`, with `needs: ['provider/model', …]`
135
+ for other models the run may call. The first event is `{ type: 'account', account, model, sessionKey, how, why, considered }`,
136
+ and an ok end carries `account` and `model`. A chosen account that isn't signed in ends `signed-out` with no agent request;
137
+ when every account is resting, the end is `resting` with `until`, and Auto skips a resting account until then.
138
+ A conversation keeps its account: a later run on the same `sessionKey` stays on the account that holds the session,
139
+ whatever `account` says. Naming another account ends `{ ok: false, kind: 'other' }` with words to show (`account.bound`).
140
+ A conversation on an API-key account continues only when that account is named (`account.paid`). Both end after the
141
+ binding read and before any room, model or agent request (a refusal reads the account listing once when the index is
142
+ cold); an `auth: 'apiKey'` run first reads its key (`byokit.keys ready`), continues only an API-billed conversation and
143
+ is refused other bindings the same way (`account.bound`). `kit.pick(member, { account, model?, needs? }, { sessionKey })`
144
+ returns what that run would choose now, refusals included, without running. `steer` and `abort` accept the same
145
+ `sessionKey` while its run is live.
146
+
133
147
 
134
148
  For an owner who explicitly offers keys, `kit.addKey('ana', { authChoice: 'openai-api-key', apiKey })` returns
135
- `ok`, `invalid` or `not_included`. Label the option **API key (billed per use)**. Select only `routes()` rows with
149
+ `{ status: 'ok' }` or `{ status: 'invalid' | 'not_included' | 'retry', reason }`.
150
+ `reason` is `auth` (provider refused the key) or `billing` (the provider's billing refused it; the kit words it and never passes
151
+ provider text): both are `invalid`, and an unchanged key must not be retried. `empty` is a blank key, rejected locally before any
152
+ provider call, also `invalid`. `model` is an unsupported route or unknown model (`not_included`), or a live model the engine did not
153
+ register (`retry`). `network` is a timeout or transport failure; `inference` is a provider reply in an unexpected format, an unknown
154
+ probe failure, or a typed request-validation refusal; `engine` is a control or preparation failure; `busy` is a rate limit or a
155
+ restarting engine. `not_included` covers an unsupported route, an unknown chosen model and a probe reporting the route unavailable;
156
+ every other failure is `retry`. Migrate from the old string result with `result.status === 'ok'`, not `result === 'ok'`.
157
+ A member that is not your own throws. `retry` means the engine may have taken the key but it was not kept
158
+ sealed (for example a gateway restart ran late); no usable key was kept, so add it again. Failures never contain keys, key fragments, headers or raw engine diagnostics. Label the option **API key (billed per use)**. Select only `routes()` rows with
136
159
  `keyEntry: true`; API rows remain `offer: false`. Each route also carries its engine `revision`, `checked` date,
137
160
  label and key error word names. Submit the input directly, clear the field afterwards, and never log or save it in
138
161
  app state. The kit never returns engine key-check errors or the key. [`@byokit/ui/kits`](../ui/README.md) supplies `keyStep`/`keyView`
@@ -140,12 +163,25 @@ for entry, checking, success and failure states, using the kit's `key.*` words.
140
163
 
141
164
  To use the saved key for one run, pass `auth: 'apiKey'`. The key lives in the member's separate
142
165
  `byokit-key-<member>` agent and workspace, with `copyToAgents: false` and only that key in its local auth order.
143
- Its selected model is used; omit `model` or pass that same model. Key runs have separate history. To steer or stop
166
+ List the route's models with `kit.models('ana', { authChoice: 'opencode-go' })` after a key is saved; this reads the
167
+ live catalog. Before a key exists it returns `[]` and queries no agent. Only available key-lane
168
+ models can be saved; the ordinary member's sign-ins never supply the list. Pass a full catalog id, e.g.
169
+ `model: 'opencode-go/muse-spark-1.3-contributor'`, to `addKey` to save it as the lane's default after activation.
170
+ Model listing and selection are Go-only: other key routes list no models, and a `model` on them returns `not_included`/`model`
171
+ before activation.
172
+ An unknown model returns `{ status: 'not_included', reason: 'model' }` and clears the replacement.
173
+ Before success, `addKey` waits for catalog registration and prepares the new key agent's full-catalog owner once.
174
+ For a live-catalog provider, a chosen model must also be in the engine's registered `models.providers[provider].models`
175
+ list before anything is sealed; otherwise the add clears the attempt and returns `{ status: 'retry', reason: 'model' }`.
176
+ Preparation failure clears the attempted key and returns `{ status: 'retry', reason: 'engine' }`; ordinary runs stay unchanged.
177
+ With no choice, today's starter stays.
178
+ The saved model is used when a run's `model` is omitted; any other model of the same provider (`provider/model`) runs too,
179
+ and another provider ends `plan`. Key runs have separate history. To steer or stop
144
180
  one, pass `{ auth: 'apiKey' }` to `steer` or `abort` too (the device helpers accept the same option).
145
181
  Ordinary runs never enter the key agent, even when the subscription is resting. This agent boundary is necessary
146
182
  because the engine's profile pins can rotate. The `byokit-key-` member prefix is reserved. Adding another key
147
183
  replaces the previous key; a failed replacement leaves this option unavailable. If the normal agent already has
148
- an API key added through pass-through calls, `addKey` returns `invalid` and leaves that key untouched.
184
+ an API key added through pass-through calls, `addKey` returns `{ status: 'retry', reason: 'engine' }` and leaves that key untouched.
149
185
 
150
186
  `OpenClawKit.abort` always returns a Promise. Await it or attach `.catch(...)`: an unavailable engine, transport
151
187
  failure or invalid API-key session key rejects with the original error, never throws synchronously, and is not
@@ -160,7 +196,9 @@ without a new accepted frame emit no `started`. Handle these frames explicitly r
160
196
  non-text/tool frame as an end.
161
197
  Live readiness reads (`providers`, `signedIn`, or device `state()`) prepare admission for the next run, avoiding
162
198
  redundant checks while authority remains reusable. `providerStatus(member)` returns `undefined` for unknown
163
- status rather than `[]`; `providers` maps unknown to `[]`, and `signedIn` to `false`.
199
+ status rather than `[]`; `providers` maps unknown to `[]`, and `signedIn` to `false`. `signedIn(member, provider)`
200
+ is also true when the member holds a ready API key for that provider (`addKey`), while `providers` lists only the
201
+ member's own sign-ins, the ones a run without `auth: 'apiKey'` uses; a key run passes `auth: 'apiKey'`.
164
202
  See [the admission contract](../../docs/runtime-kits.md#58-runs-members-and-streams) for lifetime, expiry,
165
203
  file witnesses, invalidation and remote-revocation limits.
166
204
  Tool events carry the engine's call `id`, the `input` on `start` and the `output` and `error` on `end`. A run that
@@ -328,7 +366,7 @@ streams, [`examples/openclaw-kit`](../../examples/openclaw-kit) for a whole app,
328
366
 
329
367
  Subscription sign-ins are offered by default. API-billed routes remain in `kit.routes()` with `offer: false`;
330
368
  apps can offer them when the app or person opts in, labelled API key (billed per use). Each `Route` carries `billing` (`subscription`, `api` or
331
- `local`) so the app can label what is billed. Proxy routes, compatibility aliases, local runtimes stay off. Native Claude Code sign-in is offered by default. The pinned Gateway guides only some routes; other choices need manual paste or key entry. Pass the accounts the app offers in `KitOptions.offered` (the provider, auth choice or alias each route carries, e.g. `'openrouter'`); the kit allow-lists the plugin of every route those name, so an opted-in sign-in is not blocked. An app can still name a plugin directly in `config.plugins.allow`, and pass the route's `choice` explicitly to `kit.signIn()`.
369
+ `local`) so the app can label what is billed. Proxy routes, compatibility aliases, local runtimes stay off. Native Claude Code sign-in is offered by default. A plan key (OpenCode Go: `kit.addAccount(member, { authChoice: 'opencode-go' }, on)`) asks for `Plan key`; `paste` it and the engine live-tests it before `done`. A paired device starts the same sign-in with either `via` and pastes the key over the sealed link, which keeps it on the host. A run reaches the pinned engine's static Go models (default `opencode-go/deepseek-v4-pro`); the kit keeps OpenCode Go's `x-opencode-session` header on every request, the setup probe included. A live-only Go model such as Muse needs no app config: the kit registers the plan's live catalog with each model's API, so `run({ model: 'opencode-go/muse-spark-1.3-contributor' })` runs on the account (or with `auth: 'apiKey'` on a Go key); an app that sets `config.models.providers['opencode-go'].models` keeps its own list. A choice the pinned Gateway cannot set up has readiness `no_upstream_flow` and is never offered. Pass the accounts the app offers in `KitOptions.offered` (the provider, auth choice or alias each route carries, e.g. `'openrouter'`); the kit allow-lists the plugin of every route those name, so an opted-in sign-in is not blocked. An app can still name a plugin directly in `config.plugins.allow`, and pass the route's `choice` explicitly to `kit.signIn()`.
332
370
  Offered provider plugins are allowed by default alongside the app's plugin ids.
333
371
 
334
372
  ## API at a glance
package/SECURITY.md CHANGED
@@ -147,8 +147,9 @@ asks expire, deny when the caller disconnects, and are removed on shutdown.
147
147
  Native exec/plugin decisions use `allow-once`, not a persistent allowlist.
148
148
  The engine remains authoritative for native approval expiration and resolution.
149
149
 
150
- `openclawLink` checks the app's member mapping and session prefix before member
151
- operations, checks pending approval ownership before deciding, and denies mutation
150
+ `openclawLink` checks the app's member mapping and each session key's owner (`kit.memberOf`,
151
+ which maps an indexed account agent's keys to its member) before member operations, checks
152
+ pending approval ownership before deciding, and denies mutation
152
153
  to view grants. Raw `oc.call` is denied unless the app's `passThrough` explicitly
153
154
  allows the method; that callback must also enforce parameter/member scope. The
154
155
  local `kit.call`/`kit.decide` surface has operator authority and is trusted host
@@ -1,5 +1,32 @@
1
1
  import { createLocks } from './locks.ts';
2
- import type { Account, AccountId, Defaults, GatewayTransport, Member } from './types.ts';
2
+ import type { Account, AccountId, AccountPick, Defaults, GatewayTransport, Member, ModelInfo, MoveResult, Room, RunSelection, SignInOptions, SignInView } from './types.ts';
3
+ export declare const modelsOf: (provider: string, answer: unknown) => ModelInfo[];
4
+ /** The agent a session lives on (5.15 Runs step 1); `id` absent: the member agent, which may hold several accounts. */
5
+ export type Binding = {
6
+ agentId: string;
7
+ id?: AccountId;
8
+ };
9
+ type AddOptions = {
10
+ authChoice: string;
11
+ via?: 'browser' | 'code';
12
+ name?: string;
13
+ again?: AccountId;
14
+ };
15
+ type AccountSignIn = {
16
+ id: AccountId;
17
+ paste(text: string): void;
18
+ cancel(): void;
19
+ done: Promise<{
20
+ view: SignInView;
21
+ id: AccountId;
22
+ }>;
23
+ };
24
+ type StartSignIn = (agentId: string, o: SignInOptions, on: (v: SignInView) => void, fresh: boolean) => {
25
+ paste(text: string): void;
26
+ cancel(): void;
27
+ done: Promise<SignInView>;
28
+ };
29
+ export declare function memberOfAccount(root: string, ref: string): Member | undefined;
3
30
  export declare function createAccounts(ctx: {
4
31
  request: GatewayTransport['request'];
5
32
  root: string;
@@ -8,9 +35,26 @@ export declare function createAccounts(ctx: {
8
35
  workspace: string;
9
36
  }>;
10
37
  locks: ReturnType<typeof createLocks>;
38
+ signIn: StartSignIn;
39
+ routes: ReadonlyMap<string, {
40
+ session: string;
41
+ }>;
42
+ log?: (line: string) => void;
11
43
  }): {
12
44
  list(member: Member): Promise<Account[]>;
45
+ add(member: Member, o: AddOptions, on: (v: SignInView) => void): Promise<AccountSignIn>;
13
46
  rename(member: Member, id: AccountId, name: string): Promise<Account>;
14
47
  defaults(member: Member): Promise<Defaults>;
15
48
  setDefaults(member: Member, d: Defaults): Promise<void>;
49
+ models(member: Member, id: AccountId): Promise<ModelInfo[]>;
50
+ room(member: Member, id: AccountId, demand?: string[]): Promise<Room>;
51
+ bind(member: Member, sessionKey: string): Promise<Binding | undefined>;
52
+ keyRefusal(member: Member, binding: Binding): Promise<string | undefined>;
53
+ pick(member: Member, sel: RunSelection, skip?: readonly AccountId[], binding?: Binding): Promise<{
54
+ pick: AccountPick;
55
+ agent?: string;
56
+ }>;
57
+ rest(member: Member, id: AccountId, until: number): Promise<void>;
58
+ move(member: Member, sessionKey: string, to: AccountId): Promise<MoveResult>;
16
59
  };
60
+ export {};