@byokit/openclaw 0.6.3 → 0.8.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,18 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.8.0 (2026-10-09)
6
+
7
+ - Forward actual thinking-token progress and report Gateway acceptance as `started`; reuse bounded, file-witnessed member admission status so ready runs avoid redundant sign-in checks without skipping authority checks. Device state uses the same live readiness boundary; `providerStatus` preserves unknown versus empty sign-in status.
8
+ - FIX: On the Claude CLI route the printed `## Tooling` list now uses the Claude wire catalog names (`mcp__openclaw__<tool>`), so a listed tool can be called exactly as printed. Every other route keeps the stock prompt byte-identical, and already-prefixed names are never prefixed again.
9
+ - The deprecated `anthropic` provider alias on route `anthropic-cli` stays readable as `route.deprecatedProvider` (`'anthropic'`); its removal moves to 0.9.0.
10
+ - FIX: `OpenClawKit.abort` always returns a Promise, including after an engine drop; cancellation failures remain catchable without changing healthy abort results.
11
+ - FIX: `ensureMember` resolves only once `agents.list` shows the member (bounded ~5 s wait, then a plain error naming the member), so a turn sent right after it is no longer rejected as `unknown agent id` while the engine hot-reloads its roster.
12
+
13
+ ## 0.7.0 (2026-10-08)
14
+
15
+ - FIX: Unreadable sealed sign-ins now fail closed with `AuthStoreUnreadableError` and recovery words, leaving `auth-store.sealed` byte-identical for original-key or backup repair and retry instead of starting signed out with a replacement store.
16
+
5
17
  ## 0.6.3 (2026-10-07)
6
18
 
7
19
  - Dependency update: pins @byokit/relay 0.5.3.
package/README.md CHANGED
@@ -30,6 +30,8 @@ phone page that pairs, signs in with ChatGPT, runs and answers approvals, see
30
30
  `engine/patches.json` declares the exact upstream tarball integrity, commit, unique edits and before/after file
31
31
  hashes. The Gateway Workshop review patch writes content-free, durable usage facts only when the kit sets its
32
32
  accounting environment. The separately bundled worker and all other detached kinds remain uncovered.
33
+ The Claude CLI route also uses the bundled [Tooling-name correction](../../docs/runtime-kits.md#516-bundled-engine-patches-and-engine-started-usage);
34
+ apps do not need consumer tool aliases.
33
35
  The full upstream MIT notice ships in [engine/OPENCLAW-LICENSE](engine/OPENCLAW-LICENSE).
34
36
 
35
37
  Each `prepare()` verifies the **entire** selected engine tree, including unpatched files. The kit installs stock
@@ -130,7 +132,7 @@ For an owner who explicitly offers keys, `kit.addKey('ana', { authChoice: 'opena
130
132
  `ok`, `invalid` or `not_included`. Label the option **API key (billed per use)**. Select only `routes()` rows with
131
133
  `keyEntry: true`; API rows remain `offer: false`. Each route also carries its engine `revision`, `checked` date,
132
134
  label and key error word names. Submit the input directly, clear the field afterwards, and never log or save it in
133
- app state. The kit never returns engine key-check errors or the key. `ui-core/kits` supplies `keyStep`/`keyView`
135
+ app state. The kit never returns engine key-check errors or the key. [`@byokit/ui/kits`](../ui/README.md) supplies `keyStep`/`keyView`
134
136
  for entry, checking, success and failure states, using the kit's `key.*` words.
135
137
 
136
138
  To use the saved key for one run, pass `auth: 'apiKey'`. The key lives in the member's separate
@@ -142,9 +144,23 @@ because the engine's profile pins can rotate. The `byokit-key-` member prefix is
142
144
  replaces the previous key; a failed replacement leaves this option unavailable. If the normal agent already has
143
145
  an API key added through pass-through calls, `addKey` returns `invalid` and leaves that key untouched.
144
146
 
147
+ `OpenClawKit.abort` always returns a Promise. Await it or attach `.catch(...)`: an unavailable engine, transport
148
+ failure or invalid API-key session key rejects with the original error, never throws synchronously, and is not
149
+ reported as successful cancellation. Healthy cancellation ends the run as defined in
150
+ [the run contract](../../docs/runtime-kits.md#58-runs-members-and-streams).
151
+
145
152
  A run spec also takes `system`, `images` (`{ data, mimeType }[]`), `thinking` and `tools`, a subset of the app tools
146
153
  (`KitOptions.tools` names) this run may call; any other app tool is refused at the gate before `ToolHost.gate` sees
147
- it. Tool events carry the engine's call `id`, the `input` on `start` and the `output` and `error` on `end`. A run that
154
+ it. Run events report `started` when the Gateway accepts the request (not when it finishes), and forward
155
+ actual `thinking` progress as `{ type: 'thinking', tokens }`. Silent routes invent no progress; cached replays
156
+ without a new accepted frame emit no `started`. Handle these frames explicitly rather than treating every
157
+ non-text/tool frame as an end.
158
+ Live readiness reads (`providers`, `signedIn`, or device `state()`) prepare admission for the next run, avoiding
159
+ redundant checks while authority remains reusable. `providerStatus(member)` returns `undefined` for unknown
160
+ status rather than `[]`; `providers` maps unknown to `[]`, and `signedIn` to `false`.
161
+ See [the admission contract](../../docs/runtime-kits.md#58-runs-members-and-streams) for lifetime, expiry,
162
+ file witnesses, invalidation and remote-revocation limits.
163
+ Tool events carry the engine's call `id`, the `input` on `start` and the `output` and `error` on `end`. A run that
148
164
  ends ok carries `usage` (the engine's token total for the run, and `costUsd` when it priced the model) and
149
165
  `planWindow` (the subscription's quota windows as the engine last read them), each only when the engine reports it.
150
166
  `openclawDevice(link).run(message, o)` takes the same options over the link, and `state()` adds the kit and engine
@@ -331,7 +347,7 @@ Entries:
331
347
  | `RouteView`, `RouteFacts`, `JsonValue`, `UsageClient`, `DayUsageClient` (`.`) | The types `routes()`, `routes(facts)` and the usage readers return or take, nameable without a cast |
332
348
  | `openclawDevice(link)` (`./device`) | Portable client: state, routes, sign-in, runs, steer, abort, approvals, events, sealed notices, pass-through `call`. `OpenClawDevice` and `DeviceEndFrame` name what it returns |
333
349
  | `LinkRefused`, `openNotice` (`./device`) | The host's own refusal as an error; opens a sealed approval notice |
334
- | `words`, `stateWords`, `toAccountView` (`.`, `./device`) | The kit's sentences, so a phone shows the words the computer does; `toAccountView` feeds `@byokit/ui-core`'s `phaseOf` |
350
+ | `words`, `stateWords`, `toAccountView` (`.`, `./device`) | The kit's sentences, so a phone shows the words the computer does; `toAccountView` feeds [`@byokit/ui`](../ui/README.md)'s `phaseOf` |
335
351
  | `openclawLink(kit, o)` (`./link`) | `handle` / `stream` / `allow` for a `@byokit/link` `Host`, checked per member, plus `onAction` for relay push actions. `OpenClawLinkOptions` / `OpenClawLinkHost` name both sides |
336
352
  | `serve(o)` (`./link`) | Binds the link host per reach and returns its URLs (`OpenClawServeOptions` / `OpenClawServeHandle`) |
337
353
  | `fakeGateway`, `openclawContract`, `startModelStub`, `useModelStub` (`./testing`) | In-memory Gateway, the contract suite and the scripted model for tests. `fakeGateway`'s script is typed per method (`FakeScript`, `FakeHandler`), and a recorded `StubCall` carries a typed `StubRequest` |
@@ -484,8 +500,18 @@ Without `authSeal`, engine credentials remain plaintext. With it, successful `pr
484
500
  only a sealed snapshot, `auth-store.sealed`, for the sealed credential paths; regenerable caches stay on
485
501
  disk unsealed. The adapter authenticates the snapshot
486
502
  before any restoration; a missing adapter rejects. A store the key cannot open (a wrong key,
487
- damaged or tampered bytes) or whose payload is not a credential snapshot is kept aside as
488
- `auth-store.sealed.unreadable-<ms>` and the engine starts signed out (`why: 'sign-in-reset'`); sign in again. Files restored for the engine
503
+ damaged or tampered bytes) or whose payload is not a credential snapshot stays unchanged at
504
+ `auth-store.sealed`. Both `prepare()` and `start()` reject with exported `AuthStoreUnreadableError`
505
+ (`code: 'auth-store-unreadable'`, `reason: 'auth-failed' | 'invalid-snapshot'`). The kit reports
506
+ `{ phase: 'failed', why: 'auth-store-unreadable' }`; show `stateWords(kit.state)`, not a sign-in prompt.
507
+ Restore access to the original seal/key (and the original service/stateDir binding if the adapter uses
508
+ one), then retry `start()` with that adapter. Do not generate/rotate a key to repair an unreadable store.
509
+ If the bytes are damaged, stop all writers, acquire the host writer lock, and restore an authentic
510
+ backup with its matching key before retrying. There is no automatic reset or overwrite.
511
+ For stores kept aside by an older kit, restore the original `.unreadable-*` snapshot to
512
+ `auth-store.sealed` only while stopped under that lock, preserving both it and any replacement as
513
+ backups first; use the original seal. Without the matching key or an authentic backup the kit cannot
514
+ recover the sign-in. Files restored for the engine
489
515
  have mode 0600 and directories 0700. File symlinks are sealed only when their fully resolved targets are
490
516
  regular files inside the isolated engine root; they restore as regular files at the link paths. Outside-root,
491
517
  dangling and directory symlinks (including loops), sockets, FIFOs and devices are skipped.
@@ -540,4 +566,4 @@ frozen (docs/runtime-kits.md §5).
540
566
 
541
567
  Apache-2.0. See [LICENSE](LICENSE); the bundled engine's MIT text is [engine/OPENCLAW-LICENSE](engine/OPENCLAW-LICENSE).
542
568
 
543
- 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.8.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.
569
+ 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.
package/SECURITY.md CHANGED
@@ -29,9 +29,10 @@ retries after unlock. Opt-in dual wrapping opens through an owner-only host key
29
29
  its protection is only as strong as that file, which must stay outside sealed-store backups.
30
30
  Unlocked keyring-only snapshots upgrade atomically under the store's exclusive lock after verifying
31
31
  the replacement decrypts to identical contents. A store the key cannot authenticate, or whose authentic
32
- payload is not a snapshot, is never restored, overwritten or deleted: it is kept as
33
- `auth-store.sealed.unreadable-<ms>` and the engine starts signed out (`why: 'sign-in-reset'`). Other sealing
34
- failures still reject.
32
+ payload is not a snapshot, is never restored, renamed, overwritten or deleted: it stays byte-identical
33
+ at `auth-store.sealed`, blocking startup without plaintext fallback. No key rotation, reset or
34
+ new sign-in is attempted as recovery. See [credential sealing and recovery](README.md#credential-sealing-and-threat-model)
35
+ for the public error and host repair procedure. Other sealing failures still reject.
35
36
 
36
37
  ## Secrets at rest and in memory
37
38
 
@@ -5,6 +5,7 @@ export type AuthStatus = {
5
5
  status?: unknown;
6
6
  profiles?: {
7
7
  status?: unknown;
8
+ expiresAt?: unknown;
8
9
  }[];
9
10
  usage?: unknown;
10
11
  })[];
@@ -15,3 +16,11 @@ export type AuthStatus = {
15
16
  /** Native Claude login stays in Claude Code. The pin's profile status does not include this synthetic auth;
16
17
  * setup detection asks the CLI itself, without handing credentials to the kit. */
17
18
  export declare function authStatus(request: GatewayTransport['request'], agentId: string, refresh?: boolean, native?: boolean): Promise<AuthStatus>;
19
+ /** Admission snapshots, never credentials: per kit/agent/native route, at most 30s and never past reported expiry.
20
+ * Witness only app-owned file metadata (absence included); changed/unreadable state and auth/lifecycle mutations recheck.
21
+ * Remote revocation still comes from the real run's signed-out error (a local status read cannot prove it either).
22
+ */
23
+ export declare function createAuthStatus(root: string, request: GatewayTransport['request']): {
24
+ read: (agentId: string, refresh?: boolean, native?: boolean, reuse?: boolean) => Promise<AuthStatus>;
25
+ invalidate: () => void;
26
+ };
@@ -1,9 +1,13 @@
1
+ import { statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { signedInProviders, USABLE } from "./runs.js";
1
4
  /** Native Claude login stays in Claude Code. The pin's profile status does not include this synthetic auth;
2
5
  * setup detection asks the CLI itself, without handing credentials to the kit. */
3
6
  export async function authStatus(request, agentId, refresh = false, native = false) {
4
7
  const status = await request('models.authStatus', { agentId, ...(refresh ? { refresh: true } : {}) }, { timeoutMs: 20_000 });
5
- if (!native)
6
- return status;
8
+ return native ? nativeStatus(request, agentId, status) : status;
9
+ }
10
+ async function nativeStatus(request, agentId, status) {
7
11
  const detected = await request('openclaw.setup.detect', { agentId }, { timeoutMs: 20_000 }).catch(() => ({}));
8
12
  const rows = (status.providers ?? []).filter((row) => (typeof row === 'string' ? row : row.provider) !== 'claude-cli');
9
13
  if (detected.candidates?.some((candidate) => candidate.kind === 'claude-cli' && candidate.credentials === true)) {
@@ -11,3 +15,63 @@ export async function authStatus(request, agentId, refresh = false, native = fal
11
15
  }
12
16
  return { ...status, providers: rows };
13
17
  }
18
+ /** Admission snapshots, never credentials: per kit/agent/native route, at most 30s and never past reported expiry.
19
+ * Witness only app-owned file metadata (absence included); changed/unreadable state and auth/lifecycle mutations recheck.
20
+ * Remote revocation still comes from the real run's signed-out error (a local status read cannot prove it either).
21
+ */
22
+ export function createAuthStatus(root, request) {
23
+ const snapshots = new Map();
24
+ let generation = 0;
25
+ const invalidate = () => { generation++; snapshots.clear(); };
26
+ const witness = (agentId) => {
27
+ try {
28
+ return ['openclaw.json', `state/agents/${agentId}/agent/auth-profiles.json`,
29
+ 'state/auth-profiles.json', 'state/agents/main/agent/auth-profiles.json',
30
+ 'home/.claude/.credentials.json', 'home/.claude/.claude.json', 'home/.claude/settings.json',
31
+ 'home/.claude.json', 'home/.codex/auth.json'].map(path => {
32
+ try {
33
+ const s = statSync(join(root, path), { bigint: true });
34
+ return `${s.dev}:${s.ino}:${s.size}:${s.mtimeNs}:${s.ctimeNs}`;
35
+ }
36
+ catch (error) {
37
+ if (error.code === 'ENOENT')
38
+ return 'absent';
39
+ throw error;
40
+ }
41
+ }).join('|');
42
+ }
43
+ catch {
44
+ return undefined;
45
+ } // unreadable witnesses never authorize reuse
46
+ };
47
+ const read = async (agentId, refresh = false, native = false, reuse = true) => {
48
+ const now = Date.now(), key = `${agentId}:${native}`, before = witness(agentId), epoch = generation;
49
+ for (const [key, snapshot] of snapshots)
50
+ if (snapshot.until <= now)
51
+ snapshots.delete(key);
52
+ const cached = snapshots.get(key);
53
+ if (reuse && !refresh && before !== undefined && cached?.witness === before && cached.until > now)
54
+ return cached.status;
55
+ snapshots.delete(key);
56
+ snapshots.delete(`${agentId}:false`);
57
+ const profiles = await authStatus(request, agentId, refresh);
58
+ const status = native ? await nativeStatus(request, agentId, profiles) : profiles;
59
+ if (before !== undefined && epoch === generation && witness(agentId) === before) {
60
+ for (const [route, answer] of (native ? [[false, profiles], [true, status]] : [[false, profiles]])) {
61
+ const usable = signedInProviders(answer);
62
+ if (!usable?.length || (route && !usable.includes('claude-cli')))
63
+ continue;
64
+ let until = now + 30_000;
65
+ for (const row of answer.providers ?? [])
66
+ if (typeof row !== 'string')
67
+ for (const p of row.profiles ?? [])
68
+ if (USABLE.has(String(p?.status)) && typeof p.expiresAt === 'number' && Number.isFinite(p.expiresAt))
69
+ until = Math.min(until, p.expiresAt);
70
+ if (until > Date.now())
71
+ snapshots.set(`${agentId}:${route}`, { status: answer, until, witness: before });
72
+ }
73
+ }
74
+ return status;
75
+ };
76
+ return { read, invalidate };
77
+ }
@@ -1,9 +1,13 @@
1
1
  import type { SealingAdapter } from '@byokit/secrets';
2
2
  /** True for the regenerable cache paths the collector never seals; tests assert these are all that stay at rest. */
3
3
  export declare function cached(name: string): boolean;
4
+ /** Opening failed; the original snapshot stays in place for repair and retry. */
5
+ export declare class AuthStoreUnreadableError extends Error {
6
+ readonly code = "auth-store-unreadable";
7
+ readonly reason: 'auth-failed' | 'invalid-snapshot';
8
+ constructor(reason: 'auth-failed' | 'invalid-snapshot');
9
+ }
4
10
  export declare class AuthStore {
5
- /** Set when an unreadable store was kept aside and the engine starts signed out; the kit clears it on sign-in. */
6
- reset: boolean;
7
11
  private active;
8
12
  private owned;
9
13
  private queue;
@@ -23,8 +27,6 @@ export declare class AuthStore {
23
27
  private acquire;
24
28
  private release;
25
29
  private read;
26
- /** Never overwrite or delete a store this kit cannot open: keep its bytes aside and start signed out. */
27
- private setAside;
28
30
  private collect;
29
31
  /** Remove the live credential trees: `state` entirely (it is all credential state, including the
30
32
  * runtime entries the collector skips), and in `home` exactly the sealed paths — regenerable caches
@@ -3,7 +3,7 @@
3
3
  // `home` — never the regenerable tool caches, transcripts and logs a signed-in home accumulates.
4
4
  // Sealing those too once produced a snapshot past the runtime string limit and aborted boot.
5
5
  import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, writeFileSync, closeSync, fsyncSync, openSync } from 'node:fs';
6
- import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
6
+ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
7
7
  import { EngineAlreadyRunningError, pidAlive as live } from "./engine-status.js";
8
8
  const encoder = new TextEncoder();
9
9
  const archive = (name) => /^(auth-profiles|auth-state|auth|oauth)\.json\.(migrated-.+|sqlite-import\..+\.bak)$/.test(name)
@@ -88,9 +88,17 @@ function snapshot(text) {
88
88
  throw new Error('invalid sealed credential parent');
89
89
  return s;
90
90
  }
91
+ /** Opening failed; the original snapshot stays in place for repair and retry. */
92
+ export class AuthStoreUnreadableError extends Error {
93
+ code = 'auth-store-unreadable';
94
+ reason;
95
+ constructor(reason) {
96
+ super("Saved sign-in could not be opened; auth-store.sealed is unchanged. Restore the original seal/key access or a working backup with its matching key, then retry.");
97
+ this.name = 'AuthStoreUnreadableError';
98
+ this.reason = reason;
99
+ }
100
+ }
91
101
  export class AuthStore {
92
- /** Set when an unreadable store was kept aside and the engine starts signed out; the kit clears it on sign-in. */
93
- reset = false;
94
102
  active = false;
95
103
  owned = false;
96
104
  queue = Promise.resolve();
@@ -183,14 +191,14 @@ export class AuthStore {
183
191
  catch (error) {
184
192
  if (error?.code !== 'auth-failed')
185
193
  throw error;
186
- return this.setAside('could not be authenticated');
194
+ throw new AuthStoreUnreadableError('auth-failed');
187
195
  }
188
196
  let saved;
189
197
  try {
190
198
  saved = snapshot(text);
191
199
  }
192
200
  catch {
193
- return this.setAside('is not a credential snapshot');
201
+ throw new AuthStoreUnreadableError('invalid-snapshot');
194
202
  }
195
203
  const upgraded = seal.upgrade?.(bytes);
196
204
  if (upgraded) {
@@ -200,15 +208,6 @@ export class AuthStore {
200
208
  }
201
209
  return saved;
202
210
  }
203
- /** Never overwrite or delete a store this kit cannot open: keep its bytes aside and start signed out. */
204
- setAside(why) {
205
- const aside = `${this.file}.unreadable-${Date.now()}`;
206
- renameSync(this.file, aside);
207
- syncDir(this.o.root);
208
- this.reset = true;
209
- this.o.log?.(`sealed credential store ${why}; kept as ${basename(aside)}; sign in again`);
210
- return undefined;
211
- }
212
211
  collect() {
213
212
  const s = { v: 1, dirs: [], files: [] };
214
213
  const root = realpathSync(this.o.root);
package/dist/engine.js CHANGED
@@ -5,7 +5,7 @@ import { createServer } from 'node:net';
5
5
  import { join, dirname } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { setTimeout as delay } from 'node:timers/promises';
8
- import { AuthStore } from "./auth-store.js";
8
+ import { AuthStore, AuthStoreUnreadableError } from "./auth-store.js";
9
9
  import { EngineAlreadyRunningError, pidAlive, StartedProcesses } from "./engine-status.js";
10
10
  import { EnginePatchError, atomic, prepareEngineSet, processStartTime, readPatchSet, verifyEngineSet } from "./engine-patches.js";
11
11
  import { ENGINE_VERSION } from "./constants.js";
@@ -102,6 +102,8 @@ export class Engine {
102
102
  this.patchSet = null;
103
103
  this.state('failed', 'engine-patch');
104
104
  }
105
+ if (error instanceof AuthStoreUnreadableError)
106
+ this.state('failed', 'auth-store-unreadable');
105
107
  throw error;
106
108
  });
107
109
  this.prepared = pending;
@@ -293,6 +295,8 @@ export class Engine {
293
295
  catch (error) {
294
296
  if (this.locked(error))
295
297
  return undefined;
298
+ if (error instanceof AuthStoreUnreadableError)
299
+ this.state('failed', 'auth-store-unreadable');
296
300
  throw error;
297
301
  }
298
302
  try {
package/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@ export type { RouteFacts, RouteView } from './routes.ts';
6
6
  export { OpenClawKit, type KitOptions, type RetainedLogin } from './kit.ts';
7
7
  export type { AddKeyResult } from './keys.ts';
8
8
  export { stateWords, toAccountView, words, type AccountView, type WordKey } from './words.ts';
9
+ export { AuthStoreUnreadableError } from './auth-store.ts';
9
10
  export { EngineAlreadyRunningError } from './engine-status.ts';
10
11
  export { readAgentDayUsage } from './day-usage.ts';
11
12
  export type { AgentDayUsage, DayUsageClient, EngineStartedCharge, EngineStartedKind } from './day-usage.ts';
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@
3
3
  export { ENGINE_VERSION, OPERATOR_SCOPES, PROTOCOL_VERSION } from "./constants.js";
4
4
  export { OpenClawKit } from "./kit.js";
5
5
  export { stateWords, toAccountView, words } from "./words.js";
6
+ export { AuthStoreUnreadableError } from "./auth-store.js";
6
7
  export { EngineAlreadyRunningError } from "./engine-status.js";
7
8
  export { readAgentDayUsage } from "./day-usage.js";
8
9
  export { readAgentUsage, agentUsageOf } from "./usage.js";
package/dist/kit.d.ts CHANGED
@@ -55,6 +55,7 @@ export declare class OpenClawKit {
55
55
  private members?;
56
56
  private readonly bridge;
57
57
  private readonly keys;
58
+ private readonly admission;
58
59
  private readonly approvalsCtl;
59
60
  private listeners;
60
61
  private off;
@@ -99,6 +100,7 @@ export declare class OpenClawKit {
99
100
  }>;
100
101
  private signInCtx;
101
102
  routes(): RouteView[];
103
+ providerStatus(member: Member): Promise<string[] | undefined>;
102
104
  providers(member: Member): Promise<string[]>;
103
105
  signedIn(member: Member, provider: string): Promise<boolean>;
104
106
  signIn(member: Member, o: SignInOptions, on: (v: SignInView) => void): {
package/dist/kit.js CHANGED
@@ -11,11 +11,12 @@ import { gatewayTransport } from "./transport.js";
11
11
  import { createMembers } from "./members.js";
12
12
  import { keyAgentId, KEY_PREFIX, MEMBER_ID } from "./members.js";
13
13
  import { confirmRetainedLogin as confirmLogin, migrateRetainedLogin as migrateLogin } from "./migrate.js";
14
- import { createRuns } from "./runs.js";
14
+ import { createRuns, signedInProviders } from "./runs.js";
15
+ import { createAuthStatus } from "./auth-status.js";
15
16
  import { outputSchema } from "./output.js";
16
17
  import { createKeys } from "./keys.js";
17
18
  import { routes as routeTable } from "./routes.js";
18
- import { providers as engineProviders, signIn as startSignIn, signOut as engineSignOut } from "./signin.js";
19
+ import { signIn as startSignIn, signOut as engineSignOut } from "./signin.js";
19
20
  import { reconcileConfig, browserToolPolicySafe, browserProfileAcknowledged, browserSessionMayRun, memoryLimited as configMemoryLimited, configKeyPath, readConfigKey, writeConfigKey } from "./config.js";
20
21
  import { createBrowserHost } from "./browser/host.js";
21
22
  import { fileSignInStore } from "./browser/store.js";
@@ -428,6 +429,7 @@ export class OpenClawKit {
428
429
  // Only the socket itself is per-connection. Approvals always reaches the live transport through this.request().
429
430
  bridge;
430
431
  keys;
432
+ admission;
431
433
  approvalsCtl;
432
434
  listeners = new Set();
433
435
  off = [];
@@ -644,6 +646,7 @@ export class OpenClawKit {
644
646
  ...(o.browser ? { browserConfig: () => this.browserConfig() } : {}),
645
647
  gateBuiltins: o.gateBuiltins !== false, spawnEngine: o.spawnEngine !== false,
646
648
  onState: (s) => this.setState(s), onExit: () => this.closed('engine exited') });
649
+ this.admission = createAuthStatus(this.engine.root, (method, params, options) => this.request()(method, params, options));
647
650
  this.keys = createKeys({ root: this.engine.root,
648
651
  restarting: () => this.current.phase === 'restarting' || this.current.phase === 'starting',
649
652
  request: (method, params, options) => this.request()(method, params, options),
@@ -684,6 +687,8 @@ export class OpenClawKit {
684
687
  slot.bridge = this.bridge;
685
688
  }
686
689
  setState(s) {
690
+ if (s.phase !== 'ready')
691
+ this.admission?.invalidate();
687
692
  this.current = { ...s, ...(this.engine.patchSet !== undefined ? { patchSet: this.engine.patchSet } : {}) };
688
693
  this.o.onState?.(this.current);
689
694
  }
@@ -736,7 +741,7 @@ export class OpenClawKit {
736
741
  await this.bridge.start();
737
742
  this.off.push(transport.onEvent((e) => this.approvalsCtl.handleEvent(e)));
738
743
  this.failures = 0;
739
- this.setState(this.engine.authStore.reset ? { phase: 'ready', why: 'sign-in-reset' } : { phase: 'ready' });
744
+ this.setState({ phase: 'ready' });
740
745
  if (this.o.browser)
741
746
  await this.checkBrowserTools();
742
747
  // Replays the engine's native approval lists over the now-live transport (N9).
@@ -753,6 +758,10 @@ export class OpenClawKit {
753
758
  this.setState({ phase: 'failed', why: 'engine-already-running' });
754
759
  throw error;
755
760
  }
761
+ if (error instanceof Error && 'code' in error && error.code === 'auth-store-unreadable') {
762
+ this.setState({ phase: 'failed', why: 'auth-store-unreadable' });
763
+ throw error;
764
+ }
756
765
  if (patchFailure) {
757
766
  this.setState({ phase: 'failed', why: 'engine-patch' });
758
767
  throw error;
@@ -765,6 +774,7 @@ export class OpenClawKit {
765
774
  }
766
775
  }
767
776
  async disconnect() {
777
+ this.admission.invalidate();
768
778
  this.off.splice(0).forEach((fn) => fn());
769
779
  const transport = this.transport;
770
780
  this.transport = undefined;
@@ -812,7 +822,14 @@ export class OpenClawKit {
812
822
  request() {
813
823
  if (!this.transport || this.current.phase !== 'ready')
814
824
  throw new Error('gateway not ready');
815
- return this.transport.request.bind(this.transport);
825
+ const transport = this.transport;
826
+ return (method, params, options) => {
827
+ const changesAuth = /^(models\.authLogout|openclaw\.setup\.(auth\.start|activate)|wizard\.(next|cancel)|config\.(apply|patch|set)|agents\.(create|update|delete)|secrets\.(reload|store\.(set|delete))|byokit\.keys)$/.test(method);
828
+ if (!changesAuth || (method === 'byokit.keys' && params?.action === 'ready'))
829
+ return transport.request(method, params, options);
830
+ this.admission.invalidate();
831
+ return Promise.resolve().then(() => transport.request(method, params, options)).finally(() => this.admission.invalidate());
832
+ };
816
833
  }
817
834
  call(method, params, o) {
818
835
  if (this.o.browser && /^(config\.(apply|patch|set)|agents\.(create|update|delete)|plugins\.(install|uninstall|setEnabled|refresh))$/.test(method))
@@ -869,27 +886,26 @@ export class OpenClawKit {
869
886
  // sign-in (5.7): every body here is the module's, with this kit's transport, members and ports.
870
887
  signInCtx() {
871
888
  return { request: (method, params, o) => this.request()(method, params, o),
889
+ // Public readiness/sign-in reads stay live; their fresh result can prepare the next run's admission.
890
+ authStatus: (agentId, refresh, native) => this.admission.read(agentId, refresh, native, false),
872
891
  ensure: (member) => this.ensureMember(member), callbackPort: this.o.callbackPort ?? 1455,
873
892
  onDisconnect: (fn) => { this.signInDisconnects.add(fn); return () => { this.signInDisconnects.delete(fn); }; } };
874
893
  }
875
894
  routes() {
876
895
  return routeTable();
877
896
  }
897
+ async providerStatus(member) {
898
+ const { agentId } = await this.ensureMember(member);
899
+ return signedInProviders(await this.admission.read(agentId, false, true, false));
900
+ }
878
901
  providers(member) {
879
- return engineProviders(this.signInCtx(), member);
902
+ return this.providerStatus(member).then((names) => names ?? []);
880
903
  }
881
904
  signedIn(member, provider) {
882
- return engineProviders(this.signInCtx(), member).then((names) => names.includes(provider)).catch(() => false);
905
+ return this.providers(member).then((names) => names.includes(provider)).catch(() => false);
883
906
  }
884
907
  signIn(member, o, on) {
885
- return startSignIn(this.signInCtx(), member, o, (view) => {
886
- if (view.state === 'done' && this.engine.authStore.reset) {
887
- this.engine.authStore.reset = false;
888
- if (this.current.phase === 'ready')
889
- this.setState({ phase: 'ready' });
890
- }
891
- on(view);
892
- });
908
+ return startSignIn(this.signInCtx(), member, o, on);
893
909
  }
894
910
  signOut(member, provider) {
895
911
  return engineSignOut(this.signInCtx(), member, provider);
@@ -910,6 +926,8 @@ export class OpenClawKit {
910
926
  runs() {
911
927
  return createRuns({
912
928
  request: (method, params, o) => this.request()(method, params, o),
929
+ authStatus: this.admission.read,
930
+ invalidateAuth: this.admission.invalidate,
913
931
  onEvent: (fn) => {
914
932
  this.listeners.add(fn);
915
933
  return () => {
@@ -967,7 +985,7 @@ export class OpenClawKit {
967
985
  steer(sessionKey, text, o) {
968
986
  return this.runs().steer(this.runKey(sessionKey, o), text);
969
987
  }
970
- abort(sessionKey, o) {
988
+ async abort(sessionKey, o) {
971
989
  return this.runs().abort(this.runKey(sessionKey, o));
972
990
  }
973
991
  // approvals (5.9)
package/dist/link.js CHANGED
@@ -10,9 +10,7 @@ import { reach } from '@byokit/discover';
10
10
  import { ENGINE_VERSION } from "./constants.js";
11
11
  import { b64urlDecode, b64urlEncode, sealNotice } from "./notices.js";
12
12
  import { routeFor } from "./routes.js";
13
- import { authStatus } from "./auth-status.js";
14
13
  import { outputSchema } from "./output.js";
15
- import { signedInProviders } from "./runs.js";
16
14
  import { stateWords, words } from "./words.js";
17
15
  const VIEW_OPS = new Set(['oc.state', 'oc.routes', 'oc.signin.view', 'oc.sessions', 'oc.approvals', 'oc.events',
18
16
  'oc.browser.state', 'oc.browser.signins', 'oc.browser.thumb']);
@@ -228,20 +226,6 @@ export function openclawLink(kit, o) {
228
226
  return null;
229
227
  };
230
228
  const endReason = (error) => error instanceof PublicLinkError ? error.message.slice(0, 200) : 'failed';
231
- const signIns = async (member) => {
232
- try {
233
- const { agentId } = await kit.ensureMember(member);
234
- const status = await authStatus((method, params, options) => {
235
- if (method === 'models.authStatus')
236
- return kit.call(method, params, options);
237
- return kit.call('openclaw.setup.detect', params, options);
238
- }, agentId, false, true);
239
- return signedInProviders(status);
240
- }
241
- catch {
242
- return undefined;
243
- }
244
- };
245
229
  const handle = async (req, grant) => {
246
230
  const member = memberOf(grant);
247
231
  if (grant.role === 'view' && !VIEW_OPS.has(req.op))
@@ -314,7 +298,7 @@ export function openclawLink(kit, o) {
314
298
  case 'oc.state': {
315
299
  // signedIn: the providers the device member is usably signed in to (an expired or unfinished sign-in is not),
316
300
  // only while the engine can say: absent is unknown, never "none".
317
- const signedIn = kit.state.phase === 'ready' ? await signIns(member) : undefined;
301
+ const signedIn = kit.state.phase === 'ready' ? await kit.providerStatus(member).catch(() => undefined) : undefined;
318
302
  return { state: kit.state, words: stateWords(kit.state), version: KIT_VERSION, engine: ENGINE_VERSION,
319
303
  ...(signedIn ? { signedIn } : {}) };
320
304
  }
package/dist/members.js CHANGED
@@ -2,8 +2,13 @@ import { join } from 'node:path';
2
2
  export const MEMBER_ID = /^[a-z][a-z0-9-]{0,31}$/;
3
3
  export const KEY_PREFIX = 'byokit-key-';
4
4
  export const keyAgentId = (member) => `${KEY_PREFIX}${member}`;
5
+ // The engine answers agents.create before its roster hot-reload applies (up to ~113 ms observed), so a turn sent
6
+ // at once fails as 'unknown agent id'. ensure resolves only once agents.list shows the member.
7
+ const VISIBLE_TIMEOUT_MS = 5_000;
8
+ const VISIBLE_POLL_MS = 25;
5
9
  export function createMembers(ctx) {
6
10
  const cache = new Map();
11
+ const listed = (member) => ctx.request('agents.list').then((list) => list.agents.some((agent) => agent.id === member));
7
12
  return { ensure(member) {
8
13
  if (!MEMBER_ID.test(member) || member.startsWith(KEY_PREFIX))
9
14
  return Promise.reject(new Error(`invalid member id: ${member}`));
@@ -12,9 +17,15 @@ export function createMembers(ctx) {
12
17
  return existing;
13
18
  const workspace = join(ctx.root, 'workspaces', member);
14
19
  const result = (async () => {
15
- const list = await ctx.request('agents.list');
16
- if (!list.agents.some((agent) => agent.id === member))
20
+ if (!await listed(member)) {
17
21
  await ctx.request('agents.create', { name: member, workspace });
22
+ const deadline = Date.now() + VISIBLE_TIMEOUT_MS;
23
+ while (!await listed(member)) {
24
+ if (Date.now() >= deadline)
25
+ throw new Error(`agent ${member} is not listed by agents.list after agents.create`);
26
+ await new Promise((resolve) => setTimeout(resolve, VISIBLE_POLL_MS));
27
+ }
28
+ }
18
29
  return { agentId: member, workspace };
19
30
  })();
20
31
  cache.set(member, result);
package/dist/runs.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import type { Bridge } from './bridge.ts';
2
+ import { authStatus } from './auth-status.ts';
2
3
  import { outputSchema, type OutputSchema, type SchemaOutput } from './output.ts';
3
4
  import type { GatewayTransport, Member, RunEnd, RunEvent, RunSpec } from './types.ts';
5
+ export declare const USABLE: Set<string>;
4
6
  /**
5
7
  * The providers a `models.authStatus` answer shows usable (the rule a picked account is checked by), lowercased and
6
8
  * unique; undefined while the engine has no prepared status (unknown, not none).
@@ -8,6 +10,8 @@ import type { GatewayTransport, Member, RunEnd, RunEvent, RunSpec } from './type
8
10
  export declare function signedInProviders(status: unknown): string[] | undefined;
9
11
  export declare function createRuns(ctx: {
10
12
  request: GatewayTransport['request'];
13
+ authStatus?: (agentId: string, refresh: boolean, native: boolean) => ReturnType<typeof authStatus>;
14
+ invalidateAuth?: () => void;
11
15
  onEvent: GatewayTransport['onEvent'];
12
16
  ensure(member: Member): Promise<{
13
17
  agentId: string;
package/dist/runs.js CHANGED
@@ -11,7 +11,7 @@ const WAIT_CLIENT_MS = 3_610_000;
11
11
  // The `agent` request's final frame lands with the run's end; a run already over waits no longer than this for it.
12
12
  const FINAL_GRACE_MS = 5_000;
13
13
  const STATUS_MS = 5_000;
14
- const USABLE = new Set(['ok', 'expiring', 'static']);
14
+ export const USABLE = new Set(['ok', 'expiring', 'static']);
15
15
  const usable = (row) => typeof row === 'string'
16
16
  || (Array.isArray(row.profiles) && row.profiles.length > 0 ? row.profiles.some((p) => USABLE.has(String(p?.status)))
17
17
  : row.status === undefined || USABLE.has(String(row.status)));
@@ -80,6 +80,12 @@ export function createRuns(ctx) {
80
80
  return undefined;
81
81
  }
82
82
  };
83
+ const failure = (message) => {
84
+ const classified = classify(message);
85
+ if (classified.kind === 'signed-out')
86
+ ctx.invalidateAuth?.();
87
+ return { ok: false, ...classified, message };
88
+ };
83
89
  const run = async (spec, on, keyAgent, preparedOutput) => {
84
90
  // Member boundary first: a member never speaks in another member's session, refused before any request.
85
91
  if (!spec.sessionKey.startsWith(`agent:${spec.member}:`))
@@ -112,7 +118,10 @@ export function createRuns(ctx) {
112
118
  if (p.stream === 'lifecycle' && p.data?.phase === 'end'
113
119
  && p.data.aborted === true)
114
120
  abortedByEngine = true;
115
- if (p.stream === 'assistant' && typeof p.data?.text === 'string') {
121
+ if (p.stream === 'thinking' && count(p.data?.progressTokens)) {
122
+ on?.({ type: 'thinking', tokens: p.data.progressTokens });
123
+ }
124
+ else if (p.stream === 'assistant' && typeof p.data?.text === 'string') {
116
125
  last = p.data.text;
117
126
  on?.({ type: 'text', text: p.data.text });
118
127
  }
@@ -135,7 +144,8 @@ export function createRuns(ctx) {
135
144
  // The picked account must be the one called: check it is signed in for this member before the run, since
136
145
  // the engine would otherwise fail the run only after admitting it. An explicit provider/model is strict on
137
146
  // the pinned engine (never another provider or model), so this check plus the override is the guarantee.
138
- const status = (refresh) => authStatus(ctx.request, agentId, refresh, picked.provider === 'claude-cli');
147
+ const status = (refresh) => ctx.authStatus ? ctx.authStatus(agentId, refresh, picked.provider === 'claude-cli')
148
+ : authStatus(ctx.request, agentId, refresh, picked.provider === 'claude-cli');
139
149
  let auth = await status(false);
140
150
  if (auth.unavailable)
141
151
  auth = await status(true); // no prepared snapshot yet: build it once
@@ -151,8 +161,12 @@ export function createRuns(ctx) {
151
161
  const ack = new Promise((resolve) => { accepted = resolve; });
152
162
  // The run id is taken as the accepted frame lands, so no event read after it is missed.
153
163
  const onAccepted = (payload) => {
154
- if (isRecord(payload) && typeof payload.runId === 'string')
164
+ if (isRecord(payload) && typeof payload.runId === 'string') {
165
+ const first = !runId;
155
166
  runId = payload.runId;
167
+ if (first && payload.status === 'accepted')
168
+ on?.({ type: 'started' });
169
+ }
156
170
  accepted(payload);
157
171
  };
158
172
  const final = ctx.request('agent', {
@@ -199,11 +213,11 @@ export function createRuns(ctx) {
199
213
  return { ok: false, aborted: true };
200
214
  const message = typeof result.error === 'string' ? result.error
201
215
  : typeof result.message === 'string' ? result.message : '';
202
- return { ok: false, ...classify(message), message };
216
+ return failure(message);
203
217
  }
204
218
  catch (error) {
205
219
  const message = error instanceof Error ? error.message : String(error);
206
- return { ok: false, ...classify(message), message };
220
+ return failure(message);
207
221
  }
208
222
  finally {
209
223
  ended = true;
@@ -214,6 +228,6 @@ export function createRuns(ctx) {
214
228
  return {
215
229
  run,
216
230
  steer: (k, t) => ctx.request('sessions.steer', { sessionKey: k, message: t }),
217
- abort: (k) => ctx.request('chat.abort', { sessionKey: k }),
231
+ abort: async (k) => { await ctx.request('chat.abort', { sessionKey: k }); },
218
232
  };
219
233
  }
package/dist/signin.d.ts CHANGED
@@ -1,6 +1,8 @@
1
+ import { authStatus } from './auth-status.ts';
1
2
  import type { GatewayTransport, Member, SignInOptions, SignInView } from './types.ts';
2
3
  export type SignInCtx = {
3
4
  request: GatewayTransport['request'];
5
+ authStatus?: (agentId: string, refresh: boolean, native: boolean) => ReturnType<typeof authStatus>;
4
6
  ensure(member: Member): Promise<{
5
7
  agentId: string;
6
8
  }>;
package/dist/signin.js CHANGED
@@ -430,7 +430,8 @@ export function signIn(ctx, member, o, on) {
430
430
  /** Usable provider profiles plus native Claude Code readiness reported by the engine. */
431
431
  export async function providers(ctx, member, refresh) {
432
432
  const { agentId } = await ctx.ensure(member);
433
- return signedInProviders(await authStatus(ctx.request, agentId, refresh, true)) ?? [];
433
+ return signedInProviders(await (ctx.authStatus ? ctx.authStatus(agentId, refresh ?? false, true)
434
+ : authStatus(ctx.request, agentId, refresh, true))) ?? [];
434
435
  }
435
436
  export async function signOut(ctx, member, provider) {
436
437
  const { agentId } = await ctx.ensure(member);
@@ -65,6 +65,19 @@ export function openclawContract(make, o) {
65
65
  await kit.stop();
66
66
  }
67
67
  });
68
+ test('contract: a turn sent right after ensureMember is not an unknown agent', async () => {
69
+ const { kit } = await make();
70
+ try {
71
+ // The roster applies a beat after agents.create answers (engine hot-reload); ensureMember must wait
72
+ // until agents.list shows the member, so the first turn is not rejected as 'unknown agent id'.
73
+ await kit.ensureMember('m1');
74
+ const end = await kit.run({ member: 'm1', sessionKey: 'agent:m1:contract:6', message: 'hello fresh member' });
75
+ assert.ok(end.ok, JSON.stringify(end));
76
+ }
77
+ finally {
78
+ await kit.stop();
79
+ }
80
+ });
68
81
  test('contract: a member cannot run in another member\'s session', async () => {
69
82
  const { kit } = await make();
70
83
  try {
@@ -23,4 +23,6 @@ export type FakeGateway = {
23
23
  } & {
24
24
  transport: GatewayTransport;
25
25
  };
26
- export declare function fakeGateway(script?: FakeScript): FakeGateway;
26
+ export declare function fakeGateway(script?: FakeScript, o?: {
27
+ rosterApplyMs?: number;
28
+ }): FakeGateway;
@@ -94,7 +94,7 @@ function bridgeRequest(path, message) {
94
94
  });
95
95
  });
96
96
  }
97
- export function fakeGateway(script) {
97
+ export function fakeGateway(script, o) {
98
98
  const calls = [];
99
99
  const failures = new Map();
100
100
  const handlers = new Map();
@@ -212,8 +212,15 @@ export function fakeGateway(script) {
212
212
  agents: [...agents.values()].map((agent) => ({ id: agent.id })),
213
213
  }),
214
214
  'agents.create': (p) => {
215
- ensureAgent(String(p.name), p.workspace ? String(p.workspace) : undefined);
216
- return { id: String(p.name) };
215
+ const id = String(p.name);
216
+ const workspace = p.workspace ? String(p.workspace) : undefined;
217
+ // With rosterApplyMs the fake mirrors the pinned engine: create answers before the roster hot-reload
218
+ // makes the agent visible to agents.list (Crewhouse saw ~113 ms).
219
+ if (o?.rosterApplyMs)
220
+ setTimeout(() => ensureAgent(id, workspace), o.rosterApplyMs);
221
+ else
222
+ ensureAgent(id, workspace);
223
+ return { id };
217
224
  },
218
225
  'models.authStatus': (p) => ({
219
226
  providers: (agents.get(String(p?.agentId ?? 'main'))?.providers ?? []).map((provider) => ({ provider })),
package/dist/types.d.ts CHANGED
@@ -47,6 +47,11 @@ export interface RunSpec<S extends OutputSchema | undefined = OutputSchema | und
47
47
  register?: boolean;
48
48
  }
49
49
  export type RunEvent = {
50
+ type: 'started';
51
+ } | {
52
+ type: 'thinking';
53
+ tokens: number;
54
+ } | {
50
55
  type: 'text';
51
56
  text: string;
52
57
  } | {
@@ -127,7 +132,7 @@ export type Decision = {
127
132
  };
128
133
  export type KitState = {
129
134
  phase: 'stopped' | 'installing' | 'starting' | 'repairing' | 'ready' | 'restarting' | 'failed' | 'needs-update' | 'locked';
130
- why?: 'install' | 'handshake' | 'exited' | 'port' | 'version' | 'engine-already-running' | 'engine-patch' | 'sign-in-reset';
135
+ why?: 'install' | 'handshake' | 'exited' | 'port' | 'version' | 'engine-already-running' | 'engine-patch' | 'sign-in-reset' | 'auth-store-unreadable';
131
136
  retryAt?: number;
132
137
  patchSet?: string | null;
133
138
  };
@@ -160,7 +165,7 @@ export type Route = {
160
165
  via: 'browser' | 'code';
161
166
  };
162
167
  /** @deprecated This route's `provider` before 0.7.0 (`anthropic` on `anthropic-cli`); use `provider` (`claude-cli`).
163
- * Discovery data only, never a sign-in or `signedIn` id. Removed in 0.8.0, when that route's id becomes
168
+ * Discovery data only, never a sign-in or `signedIn` id. Removed in 0.9.0, when that route's id becomes
164
169
  * `claude-cli:cli:anthropic-cli`. */
165
170
  deprecatedProvider?: string;
166
171
  billingFrom?: 'source' | 'host';
package/dist/words.js CHANGED
@@ -21,6 +21,8 @@ export function stateWords(s) {
21
21
  // 'stopped' is never on screen (4.3 covers what a person can see), so 5.14 has no row for it.
22
22
  if (s.phase === 'failed' && s.why === 'engine-already-running')
23
23
  return words('engine.alreadyRunning');
24
+ if (s.why === 'auth-store-unreadable')
25
+ return words('engine.authStoreUnreadable');
24
26
  if (s.why === 'sign-in-reset')
25
27
  return words('engine.signInAgain');
26
28
  return s.phase === 'stopped' ? '' : words(PHASE_WORDS[s.phase]);
package/dist/words.json CHANGED
@@ -3,6 +3,7 @@
3
3
  "engine.starting": "Starting up…",
4
4
  "engine.repairing": "Fixing a small problem with the setup. This takes a moment.",
5
5
  "engine.locked": "Your saved sign-in is locked. Unlock your password storage, then try again.",
6
+ "engine.authStoreUnreadable": "Your saved sign-in couldn't be opened. It is unchanged. Restore its original key or a working backup, then retry.",
6
7
  "engine.signInAgain": "Your saved sign-in couldn't be opened, so it was kept aside. Sign in again.",
7
8
  "engine.ready": "Ready.",
8
9
  "engine.restarting": "Something stopped. Starting it again by itself.",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "v": 1,
3
- "id": "171ce088cd516dcf",
3
+ "id": "ee8913bf172b63b2",
4
4
  "upstream": {
5
5
  "name": "openclaw",
6
6
  "version": "2026.8.1",
@@ -54,6 +54,17 @@
54
54
  "replace": "// BYOKit: caller-owned task prefixes never acquire automatic restart recovery ownership.\nconst byokitAppOwnedSessionPrefixes = (() => {\n\ttry {\n\t\tconst prefixes = JSON.parse(process.env.BYOKIT_APP_OWNED_SESSION_PREFIXES ?? \"[]\");\n\t\treturn Array.isArray(prefixes) && prefixes.every((prefix) => {\n\t\t\tconst agent = typeof prefix === \"string\" && /^agent:([a-z0-9-]+):/.exec(prefix);\n\t\t\treturn agent && !`agent:${agent[1]}:main`.startsWith(prefix);\n\t\t}) ? prefixes : [];\n\t} catch { return []; }\n})();\nfunction isMainRestartRecoveryCandidate(entry, sessionKey) {\n\tif (byokitAppOwnedSessionPrefixes.some((prefix) => sessionKey.startsWith(prefix))) return false;"
55
55
  }
56
56
  ]
57
+ },
58
+ {
59
+ "path": "dist/prepare.runtime-y2eXKhY3.js",
60
+ "before": "001ebad506ae9550984e3256eb1a632daa2e3ad2b6e76e2ff903979eed3d04fc",
61
+ "after": "f9955de9dbdc30a6675802e940015ad77db3016e84c22a7e282cc5ed4ee350f8",
62
+ "edits": [
63
+ {
64
+ "find": "let systemPrompt = !skipsTurnPreparation ? backendResolved.transformSystemPrompt?.({\n\t\t\tconfig: params.config,\n\t\t\tworkspaceDir,\n\t\t\tprovider: params.provider,\n\t\t\tmodelId,\n\t\t\tmodelDisplay,\n\t\t\tagentId: sessionAgentId,\n\t\t\tsystemPrompt: builtSystemPrompt\n\t\t}) ?? builtSystemPrompt : builtSystemPrompt;\n",
65
+ "replace": "let systemPrompt = !skipsTurnPreparation ? backendResolved.transformSystemPrompt?.({\n\t\t\tconfig: params.config,\n\t\t\tworkspaceDir,\n\t\t\tprovider: params.provider,\n\t\t\tmodelId,\n\t\t\tmodelDisplay,\n\t\t\tagentId: sessionAgentId,\n\t\t\tsystemPrompt: builtSystemPrompt\n\t\t}) ?? builtSystemPrompt : builtSystemPrompt;\n\t\t// BYOKit: Claude wire names for the Tooling list (ch-cli-tool-prefix). The Claude wire catalog\n\t\t// exposes OpenClaw tools as mcp__openclaw__<name>, so the printed Tooling list on that route must\n\t\t// use those exact names. Every other route keeps the stock prompt byte-identical; already-prefixed\n\t\t// names are never prefixed again. Order, summaries and error semantics are unchanged.\n\t\tif (!skipsTurnPreparation && isClaudeCli && promptTools.length > 0) {\n\t\t\tconst byokitBareNames = new Set(promptTools.map((tool) => tool?.name).filter((name) => typeof name === \"string\" && name.length > 0 && !name.startsWith(\"mcp__openclaw__\")));\n\t\t\tif (byokitBareNames.size > 0) {\n\t\t\t\tconst byokitLines = systemPrompt.split(\"\\n\");\n\t\t\t\tlet byokitInTooling = false;\n\t\t\t\tlet byokitToolingArmed = false;\n\t\t\t\tfor (let byokitI = 0; byokitI < byokitLines.length; byokitI++) {\n\t\t\t\t\tconst byokitLine = byokitLines[byokitI];\n\t\t\t\t\tif (byokitLine.startsWith(\"## \")) {\n\t\t\t\t\t\tbyokitInTooling = byokitLine === \"## Tooling\";\n\t\t\t\t\t\tbyokitToolingArmed = false;\n\t\t\t\t\t} else if (byokitInTooling && !byokitToolingArmed) {\n\t\t\t\t\t\tif (byokitLine === \"Tools policy-filtered. Names case-sensitive; call exact.\") byokitToolingArmed = true;\n\t\t\t\t\t} else if (byokitInTooling && byokitToolingArmed && byokitLine.startsWith(\"- \")) {\n\t\t\t\t\t\tconst byokitMatch = /^- (\\S+?)(:.*)?$/.exec(byokitLine);\n\t\t\t\t\t\tif (byokitMatch && byokitBareNames.has(byokitMatch[1])) byokitLines[byokitI] = `- mcp__openclaw__${byokitMatch[1]}${byokitMatch[2] ?? \"\"}`;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tsystemPrompt = byokitLines.join(\"\\n\");\n\t\t\t}\n\t\t}\n"
66
+ }
67
+ ]
57
68
  }
58
69
  ]
59
70
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byokit/openclaw",
3
- "version": "0.6.3",
3
+ "version": "0.8.0",
4
4
  "description": "The OpenClaw runtime kit: the pinned engine's full operator surface with plain-words helpers for members, sign-in, runs and approvals.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",