@byokit/openclaw 0.12.0 → 0.14.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,30 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.14.0 (2026-10-15)
6
+
7
+ - Dependency update: pins @byokit/accounts 0.23.0.
8
+
9
+ - FEAT: index the member's single API-key lane (`byokit-key-<member>`) as `<provider>.key` on `addKey` (replace-on-add, no `<member>--<hex6>` per later key), so bind sees key-held conversations, an API-billed binding continues on that lane, and a key lane is never an Auto or default-fallback candidate. A member-bound conversation with no indexed account ends signed-out with plain words (`auto.noneBound`). The key never reaches the index, logs or link.
10
+ - FEAT: `removeKey(member, o?)` removes a member's saved key (the `byokit-key-<member>` lane): its credential store and ready marker are cleared through `byokit.keys clear`, so `signedIn` is false and the lane stays signed out after an engine restart; `{ provider }` scopes it to that provider's saved key and no saved key is a no-op. `signOut(member, provider)` now also removes that provider's saved key, which the member-agent logout never reached (a Crewhouse user signing out of OpenCode Go was signed back in after a restart).
11
+ - FIX: the shipped `byokit.keys` handler answers `clear` for a member with no key lane with `{ ok: false, absent: true }` instead of refusing, so removing an unsaved key is a no-op rather than an error.
12
+ - FIX: `signedIn` still recognises a sealed key on `byokit-key-<member>` after an engine restart (key readiness no longer depends on the member-agent admission read succeeding), and `addKey` returns a thenable cancel handle that stops a pending add and clears any partial profile (`retry`/`busy`; no wizard.cancel).
13
+ - FIX: After `removeAccount` on a member-agent account (`authLogout`), a late token refresh that rewrites
14
+ `state/agents/<member>` is dropped on the next `prepare`: the removal records the provider in the index's `dropped`
15
+ list (parallel to account-agent `gone`), and the orphan sweep removes that provider's member-store auth profiles
16
+ (JSON and native store) while no member-agent index entry names it again. A live subscription the index has not yet
17
+ named is left alone, so listing can still synthesize it.
18
+ - FIX: `removeAccount` on a member-agent account restores the member's pre-sign-in primary model (stored as `priorModel` on the index entry when `addAccount` pins it), or clears the pin with `agents.update { model: null }` when there was none, so default-model runs work again after removal. A damaged or missing stored value still completes removal and falls back to clearing the pin. Account-agent removal is unchanged.
19
+ - FIX: A damaged `accounts/<member>.json` index now quarantines only that member: its agents are held back (never restored, swept or resealed away) while the other members restore, start and persist normally, the kit reports words `account.indexDamaged`, and a re-add rewrites a good index. A removed account's data still never returns through the gap.
20
+ - Start native Claude Code plan sign-in in the engine-owned folder through the existing paste/cancel handle and phone link adapter, then verify engine readiness and live activation before reporting done. For native Claude, phone sign-in readiness waits for the active drive and never hides a failed attempt behind native credential detection; other providers keep their signed-in readiness. ChatGPT device-code sign-in remains callback-free. SECURITY: the native Claude login child receives only PATH, HOME, LANG, TMPDIR and XDG_* from the engine, never the gateway token or bridge socket. Native and key sign-ins have one commit point, the `setup.activate` call: a phone cancel or an engine shutdown before it declines with no activation, the native drive (including during detection or with a ready native row) and the key paste alike, and a key paste nobody answers still ends at the paste timeout; after it a Claude cancel no longer changes the result, which is done once the restart is confirmed or says the sign-in was saved but the engine did not restart in time (`signin.savedNotRestarted`); a key cancelled during activation still declines and is cleaned up. `cancel()` always settles. A paste into a native child that already closed its stdin fails plainly instead of crashing the host, and engine `stop()` still stops and seals when an in-flight Claude reset fails. An already-ready row proceeds to detect/activate; persistent engine disagreement clears the stale ready row through Accounts, which deletes that folder's Claude login files (`.credentials.json`, `.claude.json`) but keeps the folder, then starts one fresh login.
21
+ - FEAT: `run({ thinking })` maps an unsupported thinking level to the nearest catalog-supported one that still reasons (rounding up: `low`/`medium` → `high` when a model only lists `off`/`high`/`max`), never silently to `off` unless asked; levels come from the registered model catalog (`compat.supportedReasoningEfforts` or `thinkingLevelMap`), and a model with no levels passes the asked value through. An ok end reports `thinking: { asked, used }`; `words('run.thinkingClamped')` is the notice when they differ.
22
+
23
+ ## 0.13.0 (2026-10-14)
24
+
25
+ - FEAT: OpenClaw kit S8 `kit.removeAccount(member, id)`: a member-agent account goes through `models.authLogout` with its sealed profileIds, then an empty provider `list` (still listed stops with nothing dropped and says the account is still signing in), then its entry is dropped; an account agent goes through `byokit.accounts clear`, an empty `list` (a profile still listed after clear stops with nothing deleted and says the account is still signing in), `agents.delete` with `deleteFiles: false`, removal of its `<root>/state/agents/<agent>` dir, then the entry. Removal is refused `busy` while a run holds the agent and the member stays ready. On every `prepare` and after the sealed auth store restores, the kit removes each `<member>--<hex6>` state dir that a removal recorded as gone and no index names; a legacy member's own dir is never recorded and stays. `byokit.accounts clear` takes an optional `provider` that clears only that provider; a seal failure on a member-agent sign-in clears only that provider, and when that clear is refused the add reports failed with its credential still on the member agent.
26
+ - SECURITY: `kit.removeAccount` re-seals the live credential store at once while the engine runs, so `auth-store.sealed` and `auth-store.objects/` stop holding the removed profile immediately instead of keeping it until the next stop; a late engine write of the removed agent is swept on the next `prepare` or sealed restore. Durable removal tombstones also exclude late refreshes from stop-time sealing and prune removed-agent entries and objects from older sealed stores before restore.
27
+ - FIX: Changing the wrap mode (for example turning on dual wrapping) now reseals every engine store under the new mode on the next stop, including unchanged ones, so a later restore with the OS keyring locked opens them. Since 0.12.0 unchanged stores kept their old keyring-only wrap and that restore failed with `AuthStoreUnreadableError` (`invalid-snapshot`).
28
+
5
29
  ## 0.12.0 (2026-10-13)
6
30
 
7
31
  - 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.
package/README.md CHANGED
@@ -113,7 +113,9 @@ prints the code only into a note's text.
113
113
  Device approval can take longer than two minutes. The kit waits through the engine's advertised code lifetime
114
114
  (`expiresInMinutes` on the pin, or `expires_in` seconds when supplied), including progress pulls. If the engine
115
115
  supplies no lifetime, it owns the deadline. Ordinary wizard requests keep their 120-second timeout.
116
- `signIn.cancel()` or the optional caller `signal` cancels this sign-in and releases its wizard session.
116
+ `signIn.cancel()` or the optional caller `signal` cancels this sign-in and releases its wizard session. When the
117
+ engine answers that cancel `cancelled`, `done` also waits up to 10 s for it to free its setup admission, so a sign-in
118
+ started after `done` is admitted rather than ending `why: 'busy'`.
117
119
  `done` returns a typed `SignInView`: `why: 'expired'` for an expired code, `why: 'declined'` for cancellation,
118
120
  with a plain sentence in `error`. `SignInOptions` is exported from the host entry.
119
121
 
@@ -139,14 +141,15 @@ A conversation keeps its account: a later run on the same `sessionKey` stays on
139
141
  whatever `account` says. Naming another account ends `{ ok: false, kind: 'other' }` with words to show (`account.bound`).
140
142
  A conversation on an API-key account continues only when that account is named (`account.paid`). Both end after the
141
143
  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
+ cold); an `auth: 'apiKey'` run first reads its key (`byokit.keys ready`), continues only its own key-lane conversation
145
+ (`byokit-key-<member>`) and is refused other bindings the same way (`account.bound`). `kit.pick(member, { account, model?, needs? }, { sessionKey })`
144
146
  returns what that run would choose now, refusals included, without running. `steer` and `abort` accept the same
145
147
  `sessionKey` while its run is live.
146
148
 
147
149
 
148
- For an owner who explicitly offers keys, `kit.addKey('ana', { authChoice: 'openai-api-key', apiKey })` returns
149
- `{ status: 'ok' }` or `{ status: 'invalid' | 'not_included' | 'retry', reason }`.
150
+ For an owner who explicitly offers keys, `kit.addKey('ana', { authChoice: 'openai-api-key', apiKey })` is a thenable
151
+ handle: `await` it for `{ status: 'ok' }` or `{ status: 'invalid' | 'not_included' | 'retry', reason }`, and call
152
+ `cancel()` to stop a pending add (clears any partial key; settles `retry`/`busy`).
150
153
  `reason` is `auth` (provider refused the key) or `billing` (the provider's billing refused it; the kit words it and never passes
151
154
  provider text): both are `invalid`, and an unchanged key must not be retried. `empty` is a blank key, rejected locally before any
152
155
  provider call, also `invalid`. `model` is an unsupported route or unknown model (`not_included`), or a live model the engine did not
@@ -178,7 +181,15 @@ With no choice, today's starter stays.
178
181
  The saved model is used when a run's `model` is omitted; any other model of the same provider (`provider/model`) runs too,
179
182
  and another provider ends `plan`. Key runs have separate history. To steer or stop
180
183
  one, pass `{ auth: 'apiKey' }` to `steer` or `abort` too (the device helpers accept the same option).
181
- Ordinary runs never enter the key agent, even when the subscription is resting. This agent boundary is necessary
184
+ `kit.removeKey('ana')` removes that saved key: the key agent's credential store and its ready marker go, so
185
+ `signedIn('ana', 'opencode-go')` is false and stays false across an engine restart (a removed key is not a partial
186
+ state the next start revives). `removeKey('ana', { provider })` removes the saved key only when it is that provider's;
187
+ with no saved key the call resolves without doing anything. `signOut('ana', provider)` removes that provider's saved
188
+ key too, through the same call: the key lane is a separate agent, so the member-agent logout alone would leave it
189
+ sealed and ready and the member would read signed in again after a restart. A sign-out of another provider leaves
190
+ the saved key alone. [`@byokit/ui/kits`](../ui/README.md) redraws the key option from `signedIn`.
191
+ An ordinary run enters the key lane only when its conversation is bound to it or the run names its
192
+ `<provider>.key` account, even when the subscription is resting. This agent boundary is necessary
182
193
  because the engine's profile pins can rotate. The `byokit-key-` member prefix is reserved. Adding another key
183
194
  replaces the previous key; a failed replacement leaves this option unavailable. If the normal agent already has
184
195
  an API key added through pass-through calls, `addKey` returns `{ status: 'retry', reason: 'engine' }` and leaves that key untouched.
@@ -190,15 +201,18 @@ reported as successful cancellation. Healthy cancellation ends the run as define
190
201
 
191
202
  A run spec also takes `system`, `images` (`{ data, mimeType }[]`), `thinking` and `tools`, a subset of the app tools
192
203
  (`KitOptions.tools` names) this run may call; any other app tool is refused at the gate before `ToolHost.gate` sees
193
- it. Run events report `started` when the Gateway accepts the request (not when it finishes), and forward
204
+ it. An unsupported `thinking` level is mapped to the nearest catalog-supported one that still reasons (rounding up;
205
+ never silently to `off`), and an ok end reports `thinking: { asked, used }` — use `words('run.thinkingClamped')`
206
+ when they differ. Run events report `started` when the Gateway accepts the request (not when it finishes), and forward
194
207
  actual `thinking` progress as `{ type: 'thinking', tokens }`. Silent routes invent no progress; cached replays
195
208
  without a new accepted frame emit no `started`. Handle these frames explicitly rather than treating every
196
209
  non-text/tool frame as an end.
197
210
  Live readiness reads (`providers`, `signedIn`, or device `state()`) prepare admission for the next run, avoiding
198
211
  redundant checks while authority remains reusable. `providerStatus(member)` returns `undefined` for unknown
199
212
  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'`.
213
+ is also true when the member holds a ready API key for that provider (`addKey`), including after an engine restart
214
+ that retains the key profile on `byokit-key-<member>`, while `providers` lists only the member's own sign-ins, the
215
+ ones a run without `auth: 'apiKey'` uses; a key run passes `auth: 'apiKey'`.
202
216
  See [the admission contract](../../docs/runtime-kits.md#58-runs-members-and-streams) for lifetime, expiry,
203
217
  file witnesses, invalidation and remote-revocation limits.
204
218
  Tool events carry the engine's call `id`, the `input` on `start` and the `output` and `error` on `end`. A run that
@@ -380,7 +394,7 @@ Entries:
380
394
 
381
395
  | Export | What it does |
382
396
  | --- | --- |
383
- | `OpenClawKit` (`.`) | Prepares, starts, supervises and stops the engine. `call` / `callDynamic` pass through to the Gateway, `onEvent` listens. Helpers: `ensureMember`, `routes`, `providers`, `signedIn`, `signIn`, `signOut`, `migrateRetainedLogin`, `confirmRetainedLogin`, `toolNames`, `run`, `steer`, `abort`, `approvals`, `onApproval`, `decide`, `allowOnce`, `disallowOnce`, `patchConfig`, `getConfigKey`, `setConfigKey`, `memoryLimited`, `doctorContext` |
397
+ | `OpenClawKit` (`.`) | Prepares, starts, supervises and stops the engine. `call` / `callDynamic` pass through to the Gateway, `onEvent` listens. Helpers: `ensureMember`, `routes`, `providers`, `signedIn`, `signIn`, `signOut`, `removeKey`, `migrateRetainedLogin`, `confirmRetainedLogin`, `toolNames`, `run`, `steer`, `abort`, `approvals`, `onApproval`, `decide`, `allowOnce`, `disallowOnce`, `patchConfig`, `getConfigKey`, `setConfigKey`, `memoryLimited`, `doctorContext` |
384
398
  | `ENGINE_VERSION`, `PROTOCOL_VERSION`, `OPERATOR_SCOPES` (`.`) | The pinned engine version, its protocol and the operator scopes the kit connects with |
385
399
  | `KitOptions`, `RunSpec`, `RunEvent`, `RunEnd`, `RunUsage`, `PlanWindow`, `Route`, `Approval`, `Decision`, `ToolSpec`, `ToolHost`, `KitState`, ... (`.`) | Public types (docs/runtime-kits.md §5.2) |
386
400
  | `GatewayMethods`, `GatewayMethod`, `GatewayParams`, `GatewayResult`, `GatewayEventName`, `GatewayEventPayload` (`.`) | The generated pass-through tables, so an app can write its own generic wrapper over `call` / `onEvent` once |
@@ -684,4 +698,4 @@ frozen (docs/runtime-kits.md §5).
684
698
 
685
699
  Apache-2.0. See [LICENSE](LICENSE); the bundled engine's MIT text is [engine/OPENCLAW-LICENSE](engine/OPENCLAW-LICENSE).
686
700
 
687
- Claude: `anthropic-cli` uses your own unmodified Claude Code login on this machine (provider `claude-cli`, plugin `anthropic`; subscription billing; `kit.signedIn(member, 'claude-cli')` is true once Claude Code is signed in, and `'anthropic'` stays the API-billed provider a Claude Code login never satisfies. Before 0.7.0 this route reported provider `anthropic`; that value is deprecated, kept as `deprecatedProvider` until 0.9.0), with login kept in Claude Code under the kit’s isolated `HOME=<stateDir>/openclaw/home` and `CLAUDE_CONFIG_DIR=<stateDir>/openclaw/home/.claude`; sign in there with `claude auth login`, then call `signIn`. Activation live-tests the route; native sign-out is through Claude Code. `apiKey` is explicit API billing (“API key (billed per use)”, `offer: false`): `signIn` asks for the key through `paste`, then engine activation verifies it. The `setup-token` choice is listed but never offered: the pinned Gateway has no app-guided sign-in for it. The kit never imports Claude credentials or implements direct Claude.ai OAuth; [Anthropic’s terms](https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use) apply.
701
+ Claude: `anthropic-cli` uses your own unmodified Claude Code login on this machine (provider `claude-cli`, plugin `anthropic`; subscription billing; `kit.signedIn(member, 'claude-cli')` is true once Claude Code is signed in, and `'anthropic'` stays the API-billed provider a Claude Code login never satisfies. Before 0.7.0 this route reported provider `anthropic`; that value is deprecated, kept as `deprecatedProvider` until 0.9.0), with login kept in Claude Code under the kit’s isolated `HOME=<stateDir>/openclaw/home` and `CLAUDE_CONFIG_DIR=<stateDir>/openclaw/home/.claude`; `kit.signIn(member, { authChoice: 'anthropic-cli' }, on)` starts `claude auth login --claudeai` with that `CLAUDE_CONFIG_DIR` (in its own login HOME) when it is signed out. The waiting view shows the native sign-in address and asks for the returned code; send it through the same handle's `paste`, or `openclawDevice(link).signIn.paste('claude-cli', code)` on the phone. Cancel and expiry stop only this login. The `setup.activate` call is the one commit point: a phone cancel or engine shutdown before it (even with a ready native row) declines with nothing activated; after it a cancel changes nothing, and the result is `done` once the engine restart is confirmed, or says the sign-in was saved but the engine did not restart in time. Engine shutdown also ends that restart wait. An already-ready native row can be detected and activated without a new login. If engine detection disagrees, the stale ready Claude roster row is cleared through Accounts, which deletes that folder's Claude login files (`.credentials.json`, `.claude.json`) but keeps the folder, and one fresh login is started. Pass the installed Claude binary's directory in `enginePath`. For remote ChatGPT use `openai-device-code` with `via: 'code'`; it needs no localhost callback on the phone. Native sign-out is through Claude Code. `apiKey` is explicit API billing (“API key (billed per use)”, `offer: false`): `signIn` asks for the key through `paste`, then engine activation verifies it. The `setup-token` choice is listed but never offered: the pinned Gateway has no app-guided sign-in for it. The kit never imports Claude credentials or implements direct Claude.ai OAuth; [Anthropic’s terms](https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use) apply.
package/SECURITY.md CHANGED
@@ -89,8 +89,12 @@ already copied credentials or erase snapshots/backups.
89
89
 
90
90
  `src/engine.ts` builds the gateway/doctor environment explicitly: HOME, XDG,
91
91
  provider-tool homes, tmp, config and state paths point into the app's tree. It does
92
- not forward shell API keys, `NODE_OPTIONS` or shell environment snapshots. The
93
- gateway binds `127.0.0.1` on its own port, uses token authentication and a signed
92
+ not forward shell API keys, `NODE_OPTIONS` or shell environment snapshots. The native
93
+ Claude sign-in child (`claude auth login`, started through `@byokit/accounts/cli`) gets
94
+ the same treatment: its env is only `PATH`, its own login HOME
95
+ (`openclaw/login-home`), `LANG`, `TMPDIR` and the `XDG_*` variables — never the gateway
96
+ token or the bridge socket. The gateway binds `127.0.0.1` on its own port, uses token
97
+ authentication and a signed
94
98
  device identity, and disables Control UI, discovery, channels, automatic updates
95
99
  and default remote memory search. Loopback is a reachability restriction, not a
96
100
  substitute for authentication against other local processes.
@@ -1,6 +1,24 @@
1
1
  import { createLocks } from './locks.ts';
2
+ import { type RouteView } from './routes.ts';
2
3
  import type { Account, AccountId, AccountPick, Defaults, GatewayTransport, Member, ModelInfo, MoveResult, Room, RunSelection, SignInOptions, SignInView } from './types.ts';
3
4
  export declare const modelsOf: (provider: string, answer: unknown) => ModelInfo[];
5
+ type IndexEntry = {
6
+ agent: string;
7
+ name: string;
8
+ route: string;
9
+ addedAt: number;
10
+ until?: number;
11
+ model?: string;
12
+ who?: string;
13
+ priorModel?: string;
14
+ };
15
+ type IndexFile = {
16
+ v: 1;
17
+ accounts: Record<string, IndexEntry>;
18
+ defaults: Defaults;
19
+ gone?: string[];
20
+ dropped?: string[];
21
+ };
4
22
  /** The agent a session lives on (5.15 Runs step 1); `id` absent: the member agent, which may hold several accounts. */
5
23
  export type Binding = {
6
24
  agentId: string;
@@ -26,7 +44,18 @@ type StartSignIn = (agentId: string, o: SignInOptions, on: (v: SignInView) => vo
26
44
  cancel(): void;
27
45
  done: Promise<SignInView>;
28
46
  };
47
+ /** Load one member's index. `damaged` means the file exists but is not JSON; the bytes are left alone for repair. */
48
+ export declare function loadIndex(root: string, member: Member): {
49
+ file: IndexFile;
50
+ damaged: boolean;
51
+ };
52
+ /** True when `agent` is the member agent or an `<member>--<hex6>` account agent of a damaged index. */
53
+ export declare function heldBackAgent(agent: string, damaged: ReadonlySet<string>): boolean;
29
54
  export declare function memberOfAccount(root: string, ref: string): Member | undefined;
55
+ /** Members whose `accounts/<member>.json` exists but is not readable JSON. */
56
+ export declare function damagedAccountMembers(root: string): Set<string>;
57
+ export declare function removedAccountAgents(root: string): Set<string>;
58
+ export declare function sweepOrphans(root: string, log?: (line: string) => void): void;
30
59
  export declare function createAccounts(ctx: {
31
60
  request: GatewayTransport['request'];
32
61
  root: string;
@@ -40,6 +69,7 @@ export declare function createAccounts(ctx: {
40
69
  session: string;
41
70
  }>;
42
71
  log?: (line: string) => void;
72
+ reseal?: () => Promise<void>;
43
73
  }): {
44
74
  list(member: Member): Promise<Account[]>;
45
75
  add(member: Member, o: AddOptions, on: (v: SignInView) => void): Promise<AccountSignIn>;
@@ -50,11 +80,15 @@ export declare function createAccounts(ctx: {
50
80
  room(member: Member, id: AccountId, demand?: string[]): Promise<Room>;
51
81
  bind(member: Member, sessionKey: string): Promise<Binding | undefined>;
52
82
  keyRefusal(member: Member, binding: Binding): Promise<string | undefined>;
83
+ key(member: Member, route: Pick<RouteView, 'choice' | 'provider' | 'label'>): Promise<void>;
84
+ unkey(member: Member, provider?: string): Promise<void>;
85
+ backfill(member: Member, found: Pick<RouteView, 'choice' | 'provider' | 'label'>[]): Promise<void>;
53
86
  pick(member: Member, sel: RunSelection, skip?: readonly AccountId[], binding?: Binding): Promise<{
54
87
  pick: AccountPick;
55
88
  agent?: string;
56
89
  }>;
57
90
  rest(member: Member, id: AccountId, until: number): Promise<void>;
58
91
  move(member: Member, sessionKey: string, to: AccountId): Promise<MoveResult>;
92
+ remove(member: Member, id: AccountId): Promise<void>;
59
93
  };
60
94
  export {};