@byokit/openclaw 0.6.1 → 0.6.3

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,41 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.6.3 (2026-10-07)
6
+
7
+ - Dependency update: pins @byokit/relay 0.5.3.
8
+ - Dependency update: pins @byokit/link 0.7.1.
9
+
10
+ - FIX: Route `anthropic-cli` now reports provider `claude-cli`, the id `signedIn`, `providers()` and runs already use for a Claude Code login, so `kit.signedIn(member, route.provider)` answers true once Claude Code is signed in. **Deprecated:** the old provider `anthropic` on this route; it stays readable as `route.deprecatedProvider` (`'anthropic'`) through 0.7.x and is removed in 0.8.0. Match this route by `provider === 'claude-cli'` (or by `choice === 'anthropic-cli'`). The route id `anthropic:cli:anthropic-cli` is kept through 0.7.x and becomes `claude-cli:cli:anthropic-cli` in 0.8.0; choice `anthropic-cli` and the `{ provider: 'claude-cli', via: 'browser' }` link selector are unchanged. `anthropic` remains the API-billed provider only: a Claude Code login never makes `signedIn(member, 'anthropic')` true.
11
+ FIX: Engine start on macOS no longer sticks on a dead-but-unreaped guard process. `pidAlive` detects zombies portably — `ps` state where there is no `/proc` — instead of Linux-only `/proc`, so a stale guard reads as stopped and start can recover. A live pid still reads as owned, an invalid guard still reads as ambiguous, and anything unexpected from the probe keeps the guard. Linux answers are unchanged.
12
+ - FIX: Code sign-ins whose pinned engine prints the code only into a note now hand the caller the code, link, expiry (`expiresAt`) and instructions (`message`) in the same `SignInView` shape as structured codes, and wait out device approval instead of failing with `gateway request timeout for wizard.next` after two minutes.
13
+ - The O16 day-usage engine tests no longer fail when a run straddles UTC midnight: a case that could still be running at midnight starts just after it, so the day (and month) it reads is the day its engine stamps charges in.
14
+ - The `relay` option takes anything with `RelayClient`'s `notify`, so a host can hand the kit a relay it opens after `Host.open` (the client needs the open host). A `RelayClient` still fits; behaviour is unchanged.
15
+ - FIX: The sealed credential store is written as `v: 1` again. 0.6.2's `v: 2` tag made every earlier kit (through 0.6.1) refuse an intact store with `invalid sealed credential store`, so a host rolled back to an earlier kit could not start and lost its saved sign-in. `v: 2` stores still restore and re-seal as `v: 1`.
16
+ - A saved store the key cannot open (a different key, damaged or tampered bytes) or whose payload is not a credential snapshot no longer blocks engine start: it is kept as `auth-store.sealed.unreadable-<ms>` (never overwritten or deleted), the engine starts signed out, and the kit reports `{ phase: 'ready', why: 'sign-in-reset' }` with the words "Your saved sign-in couldn't be opened, so it was kept aside. Sign in again." until a sign-in through the kit completes.
17
+ - FIX: Every wizard `text` step now reaches the caller as `SignInView.prompt`, not only sensitive ones. A non-sensitive step shows the engine's question as written (`github-copilot-enterprise` asks for the Enterprise domain first), so the sign-in no longer waits 15 minutes on a screen with nothing to answer. Once a step is sensitive (or the choice is `setup-token`), the prompt stays the fixed `Sign-in token` label and no gateway prose is shown, as before.
18
+ - FIX: `setup-token` is no longer offered. The pinned Gateway has no app-guided sign-in for it and refuses every attempt with "That provider setup is not available on this Gateway.", so it is listed with `readiness: 'no_upstream_flow'`, the link no longer maps `{ provider: 'anthropic', via: 'browser' }` to it. `plugins.allow` now holds the plugin of every default-eligible bundled route rather than only the ready ones, so `anthropic-cli` and `apiKey` keep the `anthropic` plugin allowed without app config.
19
+ - The R1 restart-recovery probe accepts both restart-time gate denials for the unowned recovering session (HEAD's `unknown run` and the published kit's `can't check this action right now`); any other transcript text still fails the denial assertion.
20
+
21
+ ## 0.6.2 (2026-10-05)
22
+
23
+ FIX: The browser broker now holds its CDP endpoint back until Chromium's initial tab exists, so `/json/list` can never serve an empty browser to an attaching engine on a cold or slow start; a browser that never lists a tab fails the launch honestly instead.
24
+ - Remove the fake Gateway's own happy-path self-tests for default member/auth and the device-code wizard script from `test/fake.test.ts`. They asserted the synthetic fixture's own response shapes and exact number of progress pulls, not product behaviour. Member creation, caching and validation, device-code sign-in through `kit.signIn`/`kit.signedIn`, `health` pass-through and sign-out remain covered by the shared public consumer contract (`src/testing/contract.ts`) and the packed Chromium journey in `examples/openclaw-kit/e2e.test.ts`. Every negative, cancellation, denial, gate, drop and refusal assertion is kept unchanged, and no test was added.
25
+ - The quickstart's example output is what the example actually prints today: the offered-route list was stale
26
+ (it still showed `anthropic-cli` and a browser `setup-token` and missed `opencode-go`). The block is now the
27
+ real output of the runnable fake-Gateway example, followed by a "read next" pointer to the run contract, the
28
+ kit example and SECURITY.md.
29
+ FIX: The sealed credential store now seals credential state only — the complete `state` tree plus config/credential paths under `home` — instead of the whole engine home, so a real signed-in home with tool caches no longer produces a sealed payload past the runtime string limit and aborts boot. Regenerable caches, transcripts and logs (`home/.cache`, `home/.npm`, and the `sessions`/`log`/`cache`/`.tmp`/`history.jsonl` subtrees of `.codex`, the `projects`/`todos`/`shell-snapshots`/`statsig`/`file-history`/`history.jsonl` subtrees of `.claude`) stay on disk unsealed; unknown `home` paths stay sealed. The sealed payload is built exactly once and verified by decrypting the sealed bytes. Existing `v: 1` snapshots still restore completely and re-seal once as `v: 2` with a log line; no credential is ever dropped.
30
+ - Every type an entry hands a caller is now nameable from that entry, and none of them carries `any`.
31
+ `.` exports `GatewayMethods`, `RouteView`, `RouteFacts`, `JsonValue`, `UsageClient` and `DayUsageClient`;
32
+ `./device` exports `OpenClawDevice` and `DeviceEndFrame`; `./link` exports `OpenClawLinkOptions`,
33
+ `OpenClawLinkHost`, `OpenClawServeOptions` and `OpenClawServeHandle`; `./testing` exports `FakeGateway`,
34
+ `FakeHandler`, `FakeParams` and `StubRequest`, types `fakeGateway`'s script per method, and records each
35
+ stubbed request as a typed `StubRequest` instead of `any`. An app can write its own generic wrapper over
36
+ `call` and annotate what `routes()`, `openclawDevice()` and `openclawLink()` return without a cast. The
37
+ Gateway slots the pinned engine does not declare stay `unknown` and are listed per release in
38
+ `src/generated/report.json`; the README now says so beside the API table.
39
+
5
40
  ## 0.6.1 (2026-10-04)
6
41
 
7
42
  FIX: Let apps declare `appOwnedSessions.keyPrefixes` before Gateway startup so caller-requeued task sessions do not also receive an engine-started recovery turn. Preserve session history, policy gates, API-key session mapping and stock recovery for other namespaces.
package/README.md CHANGED
@@ -102,6 +102,9 @@ just to read a final result: on cache eviction that would start another run. App
102
102
  recovery. Typed `kit.call('agent', ...)` remains exact upstream pass-through. Details and real-engine proof are
103
103
  in [the run contract](../../docs/runtime-kits.md#58-runs-members-and-streams).
104
104
 
105
+ Every code route shows its code the same way: a waiting view carries `code`, `url`, `expiresAt` (epoch ms, when
106
+ the engine gives a lifetime) and the engine's own instructions in `message`, including routes whose pinned engine
107
+ prints the code only into a note's text.
105
108
  Device approval can take longer than two minutes. The kit waits through the engine's advertised code lifetime
106
109
  (`expiresInMinutes` on the pin, or `expires_in` seconds when supplied), including progress pulls. If the engine
107
110
  supplies no lifetime, it owns the deadline. Ordinary wizard requests keep their 120-second timeout.
@@ -109,7 +112,7 @@ supplies no lifetime, it owns the deadline. Ordinary wizard requests keep their
109
112
  `done` returns a typed `SignInView`: `why: 'expired'` for an expired code, `why: 'declined'` for cancellation,
110
113
  with a plain sentence in `error`. `SignInOptions` is exported from the host entry.
111
114
 
112
- For a subscription setup-token or any sensitive wizard text step, pass the token once through the same
115
+ For any sensitive wizard text step, pass the secret once through the same
113
116
  `signIn.paste(token)` channel and clear the input afterwards. The kit displays only a fixed entry label;
114
117
  subsequent wizard errors, links and codes are withheld, and failures use a fixed message rather than
115
118
  engine text that could echo the token. It is sent only as this member's wizard answer, never in views,
@@ -288,17 +291,22 @@ state: ready | protocol: 4
288
291
  event: {"type":"text","text":"fake: Say hello."}
289
292
  event: {"type":"text","text":"fake: Say hello."}
290
293
  end: {"ok":true,"text":"fake: Say hello.","usage":{"input":10,"output":16,"total":26}}
291
- route: openai subscription browser
292
- route: openai-device-code subscription code
293
- route: xai-oauth subscription code
294
294
  route: github-copilot subscription code
295
295
  route: github-copilot-enterprise subscription code
296
296
  route: minimax-global-oauth subscription code
297
297
  route: minimax-cn-oauth subscription code
298
- route: anthropic-cli subscription browser
299
- route: setup-token subscription browser
298
+ route: openai subscription browser
299
+ route: openai-device-code subscription code
300
+ route: opencode-go subscription plan_key
301
+ route: xai-oauth subscription code
300
302
  ```
301
303
 
304
+ The block above is the whole quickstart: copy it into a `.mts` file next to your app's
305
+ `node_modules` and `node` it. No engine, no account, no network. Then read
306
+ [the run contract](../../docs/runtime-kits.md#58-runs-members-and-streams) for retries and
307
+ streams, [`examples/openclaw-kit`](../../examples/openclaw-kit) for a whole app, and
308
+ [SECURITY.md](SECURITY.md) before you store anything the engine returns.
309
+
302
310
  Subscription sign-ins are offered by default. API-billed routes remain in `kit.routes()` with `offer: false`;
303
311
  apps can offer them when the app or person opts in, labelled API key (billed per use). Each `Route` carries `billing` (`subscription`, `api` or
304
312
  `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. For an opted-in route, allow its `plugin` id
@@ -319,12 +327,23 @@ Entries:
319
327
  | `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` |
320
328
  | `ENGINE_VERSION`, `PROTOCOL_VERSION`, `OPERATOR_SCOPES` (`.`) | The pinned engine version, its protocol and the operator scopes the kit connects with |
321
329
  | `KitOptions`, `RunSpec`, `RunEvent`, `RunEnd`, `RunUsage`, `PlanWindow`, `Route`, `Approval`, `Decision`, `ToolSpec`, `ToolHost`, `KitState`, ... (`.`) | Public types (docs/runtime-kits.md §5.2) |
322
- | `openclawDevice(link)` (`./device`) | Portable client: state, routes, sign-in, runs, steer, abort, approvals, events, sealed notices, pass-through `call` |
330
+ | `GatewayMethods`, `GatewayMethod`, `GatewayParams`, `GatewayResult`, `GatewayEventName`, `GatewayEventPayload` (`.`) | The generated pass-through tables, so an app can write its own generic wrapper over `call` / `onEvent` once |
331
+ | `RouteView`, `RouteFacts`, `JsonValue`, `UsageClient`, `DayUsageClient` (`.`) | The types `routes()`, `routes(facts)` and the usage readers return or take, nameable without a cast |
332
+ | `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 |
323
333
  | `LinkRefused`, `openNotice` (`./device`) | The host's own refusal as an error; opens a sealed approval notice |
324
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` |
325
- | `openclawLink(kit, o)` (`./link`) | `handle` / `stream` / `allow` for a `@byokit/link` `Host`, checked per member, plus `onAction` for relay push actions |
326
- | `serve(o)` (`./link`) | Binds the link host per reach and returns its URLs |
327
- | `fakeGateway`, `openclawContract`, `startModelStub`, `useModelStub` (`./testing`) | In-memory Gateway, the contract suite and the scripted model for tests |
335
+ | `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
+ | `serve(o)` (`./link`) | Binds the link host per reach and returns its URLs (`OpenClawServeOptions` / `OpenClawServeHandle`) |
337
+ | `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` |
338
+
339
+ ### What the pass-through cannot type
340
+
341
+ Every method and event the pin publishes has a real type: `kit.call('sessions.list', params)` is checked in and
342
+ returns the engine's declared result, with no cast. For the slots the pin itself leaves undeclared the table stays
343
+ `unknown`, and `src/generated/report.json` names every one of them per release. On `openclaw@2026.8.1` that is 301
344
+ of 393 methods' params and 192 of their results, plus 21 of 55 event payloads; the other slots are not missing from
345
+ the kit, they are not published by the engine, and typing them here would be a guess that does not match the
346
+ gateway. `callDynamic` stays the same shape for a method the pin never declared.
328
347
 
329
348
  ## Owned browser (unqualified handoff)
330
349
 
@@ -419,8 +438,9 @@ Route data follows the shared account-route vocabulary (D18 in [`docs/runtime-ki
419
438
 
420
439
  The pinned engine has no supported hook for sealing OAuth profile writes. Its `auth-profiles` loader stores
421
440
  credential JSON in agent SQLite databases and a shared state database, and doctor imports leave migration
422
- archives. `authSeal` therefore protects the complete isolated `state` and `home` directories, including SQLite
423
- journals. It uses the injected `SealingAdapter` from `@byokit/secrets`. `osKeyringSeal()` automatically
441
+ archives. `authSeal` therefore protects credential state — the complete isolated `state` tree plus
442
+ config/credential paths under `home`, including SQLite journals — while regenerable tool caches,
443
+ transcripts and logs stay on disk unsealed. It uses the injected `SealingAdapter` from `@byokit/secrets`. `osKeyringSeal()` automatically
424
444
  uses a persistent private file key for new stores when no non-interactive keyring is available.
425
445
  Opening follows the saved envelope's mode. A locked or unresponsive keyring-only store leaves
426
446
  `kit.state.phase === 'locked'`: `prepare()` and `start()` resolve, show `stateWords(kit.state)` for
@@ -461,8 +481,11 @@ every retained sealed migration archive before retiring an automatic key. Rotate
461
481
  separately; dual-wrap rotation retains old wrapping keys for backups.
462
482
 
463
483
  Without `authSeal`, engine credentials remain plaintext. With it, successful `prepare()` and `stop()` leave
464
- only a sealed snapshot, `auth-store.sealed`, for those directories. The adapter authenticates the snapshot
465
- before any restoration; a wrong key, tampering, or a missing adapter rejects. Files restored for the engine
484
+ only a sealed snapshot, `auth-store.sealed`, for the sealed credential paths; regenerable caches stay on
485
+ disk unsealed. The adapter authenticates the snapshot
486
+ 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
466
489
  have mode 0600 and directories 0700. File symlinks are sealed only when their fully resolved targets are
467
490
  regular files inside the isolated engine root; they restore as regular files at the link paths. Outside-root,
468
491
  dangling and directory symlinks (including loops), sockets, FIFOs and devices are skipped.
@@ -474,7 +497,8 @@ inspection or a rewritten command line), another host is alive, or shutdown time
474
497
  `{ phase: 'failed', why: 'engine-already-running' }`; show `stateWords(kit.state)` and retry after the other
475
498
  session stops. Failed-start cleanup and `stop()` on an instance with no ownership preserve the other
476
499
  writer's pid, lock, sealed snapshot and live state.
477
- Sealing copies the whole store through memory, so startup and stop cost grows with session history.
500
+ The sealed payload is built once and verified by decrypting the sealed bytes, so startup and stop cost
501
+ grows with credential state, not session history.
478
502
 
479
503
  The migration doctor temporarily opens the same store and reseals it even when import fails. Once the gateway
480
504
  verifies every provider, `confirmRetainedLogin()` removes the explicitly passed legacy source and writes only
@@ -516,4 +540,4 @@ frozen (docs/runtime-kits.md §5).
516
540
 
517
541
  Apache-2.0. See [LICENSE](LICENSE); the bundled engine's MIT text is [engine/OPENCLAW-LICENSE](engine/OPENCLAW-LICENSE).
518
542
 
519
- Claude: `anthropic-cli` uses your own unmodified Claude Code login on this machine (provider `claude-cli`, plugin `anthropic`; subscription billing), 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 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.
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.
package/SECURITY.md CHANGED
@@ -28,7 +28,10 @@ resolve with `phase: 'locked'`, do not restore plaintext or launch the engine, a
28
28
  retries after unlock. Opt-in dual wrapping opens through an owner-only host key without prompting;
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
- the replacement decrypts to identical contents. Other sealing failures still reject.
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
35
 
33
36
  ## Secrets at rest and in memory
34
37
 
@@ -37,7 +40,7 @@ the replacement decrypts to identical contents. Other sealing failures still rej
37
40
  | Gateway token | `openclaw/token` | Random 32-byte bearer secret; newly created file is 0600. Treat it as operator access. |
38
41
  | Device identity | `openclaw/device.json` | Ed25519 private key stored as **unencrypted PKCS#8 PEM** in JSON, newly created 0600. The transport accepts both kit and legacy paired shapes without rewriting. Theft can impersonate the operator identity. |
39
42
  | Provider profiles | `openclaw/state/` and `openclaw/home/`, including agent/shared SQLite databases, journals and migration JSON | Access/refresh tokens or API keys are **plaintext while running**; without `authSeal` they also remain plaintext while stopped. Migration stages `auth-profiles.json` at 0600 in a newly created 0700 directory. Theft may authorize provider calls and refresh. |
40
- | Sealed credential snapshot | `openclaw/auth-store.sealed` and retired `*.sealed` archives | With host-injected `authSeal`, authenticates and encrypts the complete state/home trees while stopped. Key security belongs to the adapter/host; an older authentic snapshot can be replayed. |
43
+ | Sealed credential snapshot | `openclaw/auth-store.sealed` and retired `*.sealed` archives | With host-injected `authSeal`, authenticates and encrypts credential state (the complete `state` tree plus config/credential paths under `home`) while stopped; regenerable caches stay on disk unsealed. Key security belongs to the adapter/host; an older authentic snapshot can be replayed. |
41
44
  | Saved configuration, logs and sessions | `openclaw/openclaw.json`, `logs/`, engine state | May contain app-supplied keys, prompts, tool inputs, sign-in URLs/codes or provider diagnostics. Treat the whole tree as secret. Engine stdout/stderr is appended to a newly created 0600 log; it is not a guaranteed credential-redaction layer. |
42
45
  | Notice seed and decrypted approvals | Device app storage and memory | The app stores the 32-byte seed; the kit receives it only for key derivation/decryption. Seed theft exposes notices for its box key. |
43
46
 
@@ -48,8 +51,8 @@ collects regular files and file symlinks whose fully resolved targets are regula
48
51
  files within the isolated engine root; links restore as regular files at the link
49
52
  paths. Outside-root, dangling and directory links, sockets, FIFOs and devices are
50
53
  skipped without reading their contents. Collected POSIX modes are normalized;
51
- other paths still need host protection. Skipped entries are not preserved in the
52
- snapshot and are removed with the live trees after successful sealing. Never use a shared/writable tree or point
54
+ other paths still need host protection. Excluded caches are not preserved in the
55
+ snapshot and stay on disk unsealed after successful sealing; other skipped entries are not preserved in the snapshot. Never use a shared/writable tree or point
53
56
  the kit at another product's state. Review existing permissions before adopting a
54
57
  tree; use an OS-protected app directory, encrypted disk and restricted backups.
55
58
  Do not log `doctorContext().env`, transport arguments, auth records or sign-in
@@ -57,8 +60,8 @@ callbacks. JavaScript strings and engine memory are not reliably zeroized.
57
60
 
58
61
  The engine needs plaintext credentials and PEM during operation. With `authSeal`
59
62
  (a host-injected `SealingAdapter` from `@byokit/secrets`), `src/auth-store.ts` seals
60
- all of state/home, including SQLite journals, on successful `prepare()` and after
61
- the engine exits on `stop()`. It verifies the adapter round-trip, writes via an
63
+ credential state — the complete `state` tree plus config/credential paths under `home`, including SQLite journals — on successful `prepare()` and after
64
+ the engine exits on `stop()`, while regenerable caches stay on disk unsealed. It verifies the adapter round-trip, writes via an
62
65
  exclusive temporary file, fsyncs/renames the snapshot and verifies it again before
63
66
  removing live plaintext. `start()` authenticates and validates snapshot paths before
64
67
  restoring files. Missing/wrong keys or tampering reject without plaintext fallback.
@@ -69,7 +72,7 @@ orderly shutdown. An abrupt host exit can leave live plaintext; the next prepare
69
72
  seals leftovers only after the orphan writer has exited. Interrupted restore or
70
73
  removal recovers from the authenticated snapshot. A sealing failure retains
71
74
  recoverable files and rejects; the host must resolve it and retry. Snapshot size
72
- and memory cost grow with the complete state/home trees. Keys, gateway token,
75
+ and memory cost grow with sealed credential state; the payload is built once and excluded caches never enter it. Keys, gateway token,
73
76
  device PEM, inline config secrets, logs, install/cache files and app workspaces
74
77
  are outside this seal. OS-keyring or host-key adapters must keep the key separate
75
78
  from state/backups. There is no rollback protection. File removal cannot erase old
@@ -1,5 +1,9 @@
1
1
  import type { SealingAdapter } from '@byokit/secrets';
2
+ /** True for the regenerable cache paths the collector never seals; tests assert these are all that stay at rest. */
3
+ export declare function cached(name: string): boolean;
2
4
  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;
3
7
  private active;
4
8
  private owned;
5
9
  private queue;
@@ -19,7 +23,13 @@ export declare class AuthStore {
19
23
  private acquire;
20
24
  private release;
21
25
  private read;
26
+ /** Never overwrite or delete a store this kit cannot open: keep its bytes aside and start signed out. */
27
+ private setAside;
22
28
  private collect;
29
+ /** Remove the live credential trees: `state` entirely (it is all credential state, including the
30
+ * runtime entries the collector skips), and in `home` exactly the sealed paths — regenerable caches
31
+ * and the directories still holding them are left in place. */
32
+ private remove;
23
33
  private persist;
24
34
  private restore;
25
35
  archives(): Promise<void>;
@@ -1,7 +1,9 @@
1
1
  // The pin persists OAuth JSON in both shared and agent SQLite. There is no public persistence hook.
2
- // Seal the complete isolated stores, including journals, only after their writer has exited.
3
- import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync, closeSync, fsyncSync, openSync } from 'node:fs';
4
- import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
2
+ // Seal only credential state — the engine's complete `state` tree plus every config/credential path in
3
+ // `home` — never the regenerable tool caches, transcripts and logs a signed-in home accumulates.
4
+ // Sealing those too once produced a snapshot past the runtime string limit and aborted boot.
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';
5
7
  import { EngineAlreadyRunningError, pidAlive as live } from "./engine-status.js";
6
8
  const encoder = new TextEncoder();
7
9
  const archive = (name) => /^(auth-profiles|auth-state|auth|oauth)\.json\.(migrated-.+|sqlite-import\..+\.bak)$/.test(name)
@@ -45,13 +47,27 @@ function put(path, bytes) {
45
47
  rmSync(tmp, { force: true });
46
48
  }
47
49
  }
50
+ // Credential state is what restores a working signed-in session. Regenerable tool caches, transcripts
51
+ // and logs never do: the XDG cache and npm cache homes the kit's engine environment pins, plus the
52
+ // transcript/log/cache subtrees of the Codex and Claude Code CLIs it runs in `home`. Unknown paths stay
53
+ // sealed — a credential location we do not know about must fail loudly, never drop silently.
54
+ const HOME_CACHES = {
55
+ home: ['.cache', '.npm'],
56
+ 'home/.codex': ['sessions', 'log', 'cache', '.tmp', 'history.jsonl'],
57
+ 'home/.claude': ['projects', 'todos', 'shell-snapshots', 'statsig', 'file-history', 'history.jsonl'],
58
+ };
59
+ /** True for the regenerable cache paths the collector never seals; tests assert these are all that stay at rest. */
60
+ export function cached(name) {
61
+ const parent = name.includes('/') ? name.slice(0, name.lastIndexOf('/')) : '';
62
+ return (HOME_CACHES[parent] ?? []).includes(name.slice(name.lastIndexOf('/') + 1));
63
+ }
48
64
  function safePath(path) {
49
65
  return typeof path === 'string' && /^(state|home)(\/[^/]+)*$/.test(path)
50
66
  && !path.split('/').some((part) => part === '.' || part === '..' || part.includes('\\') || part.includes('\0'));
51
67
  }
52
68
  function snapshot(text) {
53
69
  const s = JSON.parse(text);
54
- if (s?.v !== 1 || !Array.isArray(s.dirs) || !Array.isArray(s.files))
70
+ if ((s?.v !== 1 && s?.v !== 2) || !Array.isArray(s.dirs) || !Array.isArray(s.files))
55
71
  throw new Error('invalid sealed credential store');
56
72
  const paths = new Set();
57
73
  for (const path of s.dirs) {
@@ -73,6 +89,8 @@ function snapshot(text) {
73
89
  return s;
74
90
  }
75
91
  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;
76
94
  active = false;
77
95
  owned = false;
78
96
  queue = Promise.resolve();
@@ -158,8 +176,22 @@ export class AuthStore {
158
176
  regular(this.file);
159
177
  const seal = this.o.seal;
160
178
  const bytes = readFileSync(this.file);
161
- const text = seal.decryptString(bytes);
162
- const saved = snapshot(text);
179
+ let text;
180
+ try {
181
+ text = seal.decryptString(bytes);
182
+ }
183
+ catch (error) {
184
+ if (error?.code !== 'auth-failed')
185
+ throw error;
186
+ return this.setAside('could not be authenticated');
187
+ }
188
+ let saved;
189
+ try {
190
+ saved = snapshot(text);
191
+ }
192
+ catch {
193
+ return this.setAside('is not a credential snapshot');
194
+ }
163
195
  const upgraded = seal.upgrade?.(bytes);
164
196
  if (upgraded) {
165
197
  if (seal.decryptString(Buffer.from(upgraded)) !== text)
@@ -168,11 +200,22 @@ export class AuthStore {
168
200
  }
169
201
  return saved;
170
202
  }
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
+ }
171
212
  collect() {
172
213
  const s = { v: 1, dirs: [], files: [] };
173
214
  const root = realpathSync(this.o.root);
174
215
  const walk = (path) => {
175
216
  const name = relative(this.o.root, path).split('\\').join('/');
217
+ if (cached(name))
218
+ return; // tool caches are never credential state; skipping a directory skips its subtree
176
219
  const stat = lstatSync(path);
177
220
  if (stat.isDirectory()) {
178
221
  chmodSync(path, 0o700);
@@ -208,31 +251,56 @@ export class AuthStore {
208
251
  walk(join(this.o.root, dir));
209
252
  return s;
210
253
  }
254
+ /** Remove the live credential trees: `state` entirely (it is all credential state, including the
255
+ * runtime entries the collector skips), and in `home` exactly the sealed paths — regenerable caches
256
+ * and the directories still holding them are left in place. */
257
+ remove(s) {
258
+ rmSync(join(this.o.root, 'state'), { recursive: true, force: true });
259
+ for (const [path] of s.files)
260
+ if (path.startsWith('home/'))
261
+ rmSync(join(this.o.root, path), { force: true });
262
+ for (const path of [...s.dirs].sort((a, b) => b.split('/').length - a.split('/').length)) {
263
+ if (!path.startsWith('home/'))
264
+ continue;
265
+ try {
266
+ rmdirSync(join(this.o.root, path));
267
+ }
268
+ catch (error) {
269
+ const code = error.code;
270
+ if (code !== 'ENOTEMPTY' && code !== 'ENOENT')
271
+ throw error; // a directory still holding caches stays
272
+ }
273
+ }
274
+ }
211
275
  async persist() {
212
276
  if (!this.o.seal)
213
277
  return;
214
278
  // Always authenticate an earlier snapshot before considering a leftover live store after a crash.
215
279
  const previous = await this.read();
216
280
  if (previous && (existsSync(this.cleanup) || existsSync(this.restoring))) {
217
- for (const dir of ['state', 'home'])
218
- rmSync(join(this.o.root, dir), { recursive: true, force: true });
281
+ this.remove(previous);
219
282
  removeMarker(this.cleanup);
220
283
  removeMarker(this.restoring);
221
284
  this.o.log?.('interrupted credential transition recovered');
222
285
  }
223
- const hasLive = ['state', 'home'].some((dir) => existsSync(join(this.o.root, dir)));
224
- if (!hasLive && previous)
286
+ const saved = this.collect();
287
+ // Live trees with nothing sealable are debris (crash leftovers, or only the caches a previous stop
288
+ // left in place) — keep the sealed credentials instead of replacing them with an empty snapshot.
289
+ // A running engine always leaves files behind (its SQLite stores), so a genuine sign-out still re-seals.
290
+ if (!saved.files.length && previous)
225
291
  return;
226
- const text = JSON.stringify(this.collect());
227
- const sealed = this.o.seal.encryptString(text);
228
- if (this.o.seal.decryptString(Buffer.from(sealed)) !== text)
292
+ // The payload is built exactly once; verification decrypts the sealed bytes and compares to it.
293
+ const payload = JSON.stringify(saved);
294
+ const sealed = this.o.seal.encryptString(payload);
295
+ if (this.o.seal.decryptString(Buffer.from(sealed)) !== payload)
229
296
  throw new Error('credential seal verification failed');
230
297
  put(this.file, sealed);
231
- if (this.o.seal.decryptString(readFileSync(this.file)) !== text)
298
+ if (this.o.seal.decryptString(readFileSync(this.file)) !== payload)
232
299
  throw new Error('credential seal verification failed');
300
+ if (previous?.dirs.some(cached))
301
+ this.o.log?.('sealed credential store re-sealed: tool caches no longer sealed');
233
302
  put(this.cleanup, encoder.encode('1'));
234
- for (const dir of ['state', 'home'])
235
- rmSync(join(this.o.root, dir), { recursive: true, force: true });
303
+ this.remove(saved);
236
304
  removeMarker(this.cleanup);
237
305
  }
238
306
  async restore() {
@@ -668,10 +668,23 @@ export async function launchBroker(o) {
668
668
  await send('Browser.getVersion');
669
669
  await send('Browser.setDownloadBehavior', { behavior: 'deny', eventsEnabled: false });
670
670
  await send('Target.setDiscoverTargets', { discover: true });
671
- const result = await send('Target.getTargets');
672
- for (const t of result.targetInfos)
673
- targets.set(t.targetId, t);
674
- lastAgent = result.targetInfos.find(t => t.type === 'page')?.targetId;
671
+ // Chromium creates the initial tab after the pipe already answers, so a cold start can list zero
672
+ // page targets here. Never expose the endpoint before the agent tab exists: a discovery /json/list
673
+ // would honestly report an empty browser and break every engine that lists targets at attach.
674
+ const tabUntil = Date.now() + 30_000;
675
+ let pages = [];
676
+ for (;;) {
677
+ const result = await send('Target.getTargets');
678
+ for (const t of result.targetInfos)
679
+ targets.set(t.targetId, t);
680
+ pages = result.targetInfos.filter(t => t.type === 'page');
681
+ if (pages.length > 0 || Date.now() >= tabUntil)
682
+ break;
683
+ await new Promise(resolve => setTimeout(resolve, 25));
684
+ }
685
+ lastAgent = pages[0]?.targetId;
686
+ if (!lastAgent)
687
+ throw failure();
675
688
  await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', () => resolve()); });
676
689
  port = server.address().port;
677
690
  }
package/dist/config.js CHANGED
@@ -56,7 +56,10 @@ export function reconcileConfig(saved, o) {
56
56
  c.plugins.load ??= {};
57
57
  c.plugins.load.paths = [...new Set([...(c.plugins.load.paths ?? []).filter((p) => !p.includes('byokit-openclaw-bridge')), o.pluginDir])];
58
58
  c.plugins.allow = [...new Set([...(c.plugins.allow ?? []), o.pluginId,
59
- ...routes().filter(route => route.offer && route.plugin).map(route => route.plugin)])];
59
+ // Every default-eligible bundled route, not only the ready ones: anthropic-cli waits on its binary and
60
+ // shares its plugin with the explicit apiKey route, so neither may depend on another route being offered.
61
+ ...routes().filter(route => route.offerPolicy === 'default' && !route.needs?.plugin && route.plugin)
62
+ .map(route => route.plugin)])];
60
63
  c.plugins.entries ??= {};
61
64
  c.plugins.entries[o.pluginId] = merge(c.plugins.entries[o.pluginId] ?? {}, { hooks: { timeouts: { before_tool_call: 200_000 } } });
62
65
  if (o.browser) {
@@ -69,8 +69,10 @@ export type AgentDayUsage = {
69
69
  export declare function engineStartedOf(raw: unknown, member: Member, window: AgentDayUsage['window']): AgentDayUsage['engineStarted'] & {
70
70
  complete: boolean;
71
71
  };
72
- /** Read the transcript and Workshop terms independently. A failed term never becomes zero. */
73
- export declare function readAgentDayUsage(client: {
72
+ /** What `readAgentDayUsage` needs from a reader: the kit's `call` plus its own string-typed RPC. */
73
+ export type DayUsageClient = {
74
74
  callDynamic(method: string, params?: unknown): Promise<unknown>;
75
75
  call(method: 'sessions.usage', params: SessionsUsageParams): Promise<unknown>;
76
- }, member: Member, window: AgentDayUsage['window']): Promise<AgentDayUsage>;
76
+ };
77
+ /** Read the transcript and Workshop terms independently. A failed term never becomes zero. */
78
+ export declare function readAgentDayUsage(client: DayUsageClient, member: Member, window: AgentDayUsage['window']): Promise<AgentDayUsage>;
package/dist/device.d.ts CHANGED
@@ -27,7 +27,8 @@ export type SessionRow = {
27
27
  sessionKey: string;
28
28
  [k: string]: unknown;
29
29
  };
30
- type EndFrame<T = unknown> = {
30
+ /** The frame `oc.run`'s stream ends with: the run's `RunEnd`, beside its events. */
31
+ export type DeviceEndFrame<T = unknown> = {
31
32
  type: 'end';
32
33
  end: RunEnd<T>;
33
34
  };
@@ -78,7 +79,8 @@ export type SignInNotice = {
78
79
  export declare function openSignInNotice(data: Record<string, unknown>, seed: Uint8Array): SignInNotice | null;
79
80
  /** Transient streams bypass link's durable request/answer cache: no lease nonce or frame is persisted. */
80
81
  export declare function browserDevice(link: DeviceLink): BrowserDevice;
81
- export declare function openclawDevice(link: DeviceLink): {
82
+ /** `openclawDevice`'s device surface: every `oc.*` op the computer answers, typed. */
83
+ export type OpenClawDevice = {
82
84
  browser: BrowserDevice;
83
85
  state(): Promise<DeviceState>;
84
86
  routes(): Promise<RouteView[]>;
@@ -90,7 +92,7 @@ export declare function openclawDevice(link: DeviceLink): {
90
92
  };
91
93
  signOut(p: string): Promise<void>;
92
94
  sessions(): Promise<SessionRow[]>;
93
- run<const S extends OutputSchema | undefined = undefined>(message: string, o?: DeviceRunOptions<S>): AsyncIterable<RunEvent | EndFrame<SchemaOutput<S>>>;
95
+ run<const S extends OutputSchema | undefined = undefined>(message: string, o?: DeviceRunOptions<S>): AsyncIterable<RunEvent | DeviceEndFrame<SchemaOutput<S>>>;
94
96
  steer(k: string, t: string, o?: {
95
97
  auth?: 'apiKey';
96
98
  }): Promise<void>;
@@ -105,6 +107,7 @@ export declare function openclawDevice(link: DeviceLink): {
105
107
  call<M extends GatewayMethod>(method: M, params: GatewayParams<M>): Promise<GatewayResult<M>>;
106
108
  callDynamic(method: string, params?: unknown): Promise<unknown>;
107
109
  };
110
+ export declare function openclawDevice(link: DeviceLink): OpenClawDevice;
108
111
  export { openNotice } from './notices.ts';
109
112
  export { readAgentUsage, agentUsageOf } from './usage.ts';
110
113
  export { readAgentDayUsage } from './day-usage.ts';
@@ -1,4 +1,4 @@
1
- import type { ChildProcess } from 'node:child_process';
1
+ import { type ChildProcess } from 'node:child_process';
2
2
  /** Every process the kit itself spawned, held for shutdown by pid. */
3
3
  export declare class StartedProcesses {
4
4
  private readonly started;
@@ -16,4 +16,4 @@ export declare class EngineAlreadyRunningError extends Error {
16
16
  readonly code = "engine-already-running";
17
17
  constructor();
18
18
  }
19
- export declare function pidAlive(pid: number): boolean;
19
+ export declare function pidAlive(pid: number, platform?: NodeJS.Platform): boolean;
@@ -1,4 +1,5 @@
1
1
  import { readFileSync } from 'node:fs';
2
+ import { spawnSync } from 'node:child_process';
2
3
  /** Every process the kit itself spawned, held for shutdown by pid. */
3
4
  export class StartedProcesses {
4
5
  started = new Set();
@@ -38,20 +39,40 @@ export class EngineAlreadyRunningError extends Error {
38
39
  this.name = 'EngineAlreadyRunningError';
39
40
  }
40
41
  }
41
- export function pidAlive(pid) {
42
+ export function pidAlive(pid, platform = process.platform) {
42
43
  if (!Number.isSafeInteger(pid) || pid < 1)
43
44
  return true; // An invalid guard is ambiguous, never stale.
44
45
  try {
45
46
  process.kill(pid, 0);
46
47
  // An orphan can remain a zombie until its new parent reaps it; it has no writer left.
47
- if (process.platform === 'linux') {
48
+ if (platform === 'linux') {
48
49
  const stat = readFileSync(`/proc/${pid}/stat`, 'utf8');
49
50
  if (stat.slice(stat.lastIndexOf(')') + 2).startsWith('Z '))
50
51
  return false;
51
52
  }
53
+ else if (platform !== 'win32' && psReportsZombie(pid)) {
54
+ // No /proc here (notably macOS): only a definite zombie reads as stale.
55
+ return false;
56
+ }
52
57
  return true;
53
58
  }
54
59
  catch (error) {
55
60
  return error.code !== 'ESRCH';
56
61
  }
57
62
  }
63
+ /**
64
+ * Why ps: `ps -o stat=` exists on macOS and Linux with the same leading-letter state
65
+ * convention, so a `Z` first letter names the same kernel fact the /proc read names on
66
+ * Linux. Linux keeps its /proc read (no subprocess, unchanged answers); every other Unix
67
+ * asks ps. Only positive `Z` evidence flips the answer — a missing ps, an empty reply,
68
+ * or gibberish keeps the guard, so a reused live pid is never stolen.
69
+ */
70
+ function psReportsZombie(pid) {
71
+ try {
72
+ const probed = spawnSync('ps', ['-o', 'stat=', '-p', String(pid)], { encoding: 'utf8', timeout: 5000 });
73
+ return (probed.stdout ?? '').trimStart().startsWith('Z');
74
+ }
75
+ catch {
76
+ return false;
77
+ }
78
+ }
package/dist/engine.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { AuthStore } from './auth-store.ts';
1
2
  import type { KitOptions } from './kit.ts';
2
3
  import type { KitState, ToolSpec } from './types.ts';
3
4
  export type EngineOptions = Pick<KitOptions, 'stateDir' | 'engineDir' | 'npmPath' | 'enginePath' | 'config' | 'appOwnedSessions' | 'installPolicy' | 'log' | 'bridge' | 'authSeal'> & {
@@ -24,7 +25,7 @@ export declare class Engine {
24
25
  private child?;
25
26
  private readonly started;
26
27
  private starting?;
27
- private readonly authStore;
28
+ readonly authStore: AuthStore;
28
29
  private stopping;
29
30
  private credentialsLocked;
30
31
  private repaired;
package/dist/index.d.ts CHANGED
@@ -1,11 +1,14 @@
1
1
  export { ENGINE_VERSION, OPERATOR_SCOPES, PROTOCOL_VERSION } from './constants.ts';
2
2
  export type { Approval, CallOptions, Decision, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, GateResult, Hello, KitState, KitEventName, KitEventPayload, BrowserPing, Member, PlanWindow, Route, RunEnd, RunEvent, RunRef, RunSpec, OutputSchema, SchemaOutput, RunUsage, SignInOptions, SignInView, ToolHost, ToolSpec, } from './types.ts';
3
+ export type { GatewayMethods } from './generated/methods.ts';
4
+ export type { JsonValue } from './output.ts';
5
+ export type { RouteFacts, RouteView } from './routes.ts';
3
6
  export { OpenClawKit, type KitOptions, type RetainedLogin } from './kit.ts';
4
7
  export type { AddKeyResult } from './keys.ts';
5
8
  export { stateWords, toAccountView, words, type AccountView, type WordKey } from './words.ts';
6
9
  export { EngineAlreadyRunningError } from './engine-status.ts';
7
10
  export { readAgentDayUsage } from './day-usage.ts';
8
- export type { AgentDayUsage, EngineStartedCharge, EngineStartedKind } from './day-usage.ts';
11
+ export type { AgentDayUsage, DayUsageClient, EngineStartedCharge, EngineStartedKind } from './day-usage.ts';
9
12
  export { readAgentUsage, agentUsageOf } from './usage.ts';
10
- export type { AgentUsageReading, LedgerUsageTotals, UsageCache, UsageWindow } from './usage.ts';
13
+ export type { AgentUsageReading, LedgerUsageTotals, UsageCache, UsageClient, UsageWindow } from './usage.ts';
11
14
  export type { LiveSource, BrowserState, SignInMethodHint, SignInReason, SignInChoice, SettledState, SettledReason, ResumeState, NeedSignIn, TakeoverLease, LiveViewState, LiveFrame, LiveInput, ThumbnailResult, SignInRefusedWhy, BrowserOptions, SiteVerifier, BrowserHost, BrowserDevice } from './browser.ts';
package/dist/kit.js CHANGED
@@ -736,7 +736,7 @@ export class OpenClawKit {
736
736
  await this.bridge.start();
737
737
  this.off.push(transport.onEvent((e) => this.approvalsCtl.handleEvent(e)));
738
738
  this.failures = 0;
739
- this.setState({ phase: 'ready' });
739
+ this.setState(this.engine.authStore.reset ? { phase: 'ready', why: 'sign-in-reset' } : { phase: 'ready' });
740
740
  if (this.o.browser)
741
741
  await this.checkBrowserTools();
742
742
  // Replays the engine's native approval lists over the now-live transport (N9).
@@ -882,7 +882,14 @@ export class OpenClawKit {
882
882
  return engineProviders(this.signInCtx(), member).then((names) => names.includes(provider)).catch(() => false);
883
883
  }
884
884
  signIn(member, o, on) {
885
- return startSignIn(this.signInCtx(), 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
+ });
886
893
  }
887
894
  signOut(member, provider) {
888
895
  return engineSignOut(this.signInCtx(), member, provider);
package/dist/link.d.ts CHANGED
@@ -1,27 +1,35 @@
1
1
  import type { BrowserHost } from './browser.ts';
2
2
  import { type IncomingMessage, type ServerResponse } from 'node:http';
3
3
  import { type Grant, type Host, type HostOptions } from '@byokit/link';
4
- import { type ServeIngress, type Via } from '@byokit/reach';
4
+ import { type ServeIngress, type Via } from '@byokit/discover';
5
5
  import type { PushAction, RelayClient } from '@byokit/relay';
6
6
  import type { OpenClawKit } from './kit.ts';
7
7
  import type { Member } from './types.ts';
8
- export declare function openclawLink(kit: OpenClawKit & {
9
- browser?: BrowserHost;
10
- }, o: {
8
+ /** `openclawLink`'s options: which member a grant acts for, the D8 pass-through predicate, sealed relay push. */
9
+ export type OpenClawLinkOptions = {
11
10
  memberOf: (grant: Grant) => Member | undefined;
12
11
  passThrough?: (method: string, grant: Grant) => boolean;
13
- relay?: RelayClient;
14
- }): Pick<HostOptions, 'handle' | 'stream' | 'allow' | 'onGrantRemoved'> & {
12
+ relay?: Pick<RelayClient, 'notify'>;
13
+ };
14
+ /** What `openclawLink` hands the link host: its request/stream handlers plus the kit's push entry point. */
15
+ export type OpenClawLinkHost = Pick<HostOptions, 'handle' | 'stream' | 'allow' | 'onGrantRemoved'> & {
15
16
  onAction(a: PushAction): Promise<unknown>;
16
17
  };
17
- export declare function serve(o: {
18
+ export declare function openclawLink(kit: OpenClawKit & {
19
+ browser?: BrowserHost;
20
+ }, o: OpenClawLinkOptions): OpenClawLinkHost;
21
+ /** `serve`'s options: the link host to accept on, and the ingress to publish or replace. */
22
+ export type OpenClawServeOptions = {
18
23
  host: Host;
19
24
  port: number;
20
25
  via?: Via;
21
26
  previous?: ServeIngress;
22
27
  http?: (req: IncomingMessage, res: ServerResponse) => void;
23
- }): Promise<{
28
+ };
29
+ /** The running ingress: its urls, the entry that reaches the engine, and an orderly close. */
30
+ export type OpenClawServeHandle = {
24
31
  urls: string[];
25
32
  ingress?: ServeIngress;
26
33
  close(): Promise<void>;
27
- }>;
34
+ };
35
+ export declare function serve(o: OpenClawServeOptions): Promise<OpenClawServeHandle>;
package/dist/link.js CHANGED
@@ -6,7 +6,7 @@ import { readFileSync } from 'node:fs';
6
6
  import { createServer } from 'node:http';
7
7
  import { WebSocketServer } from 'ws';
8
8
  import { PublicLinkError, } from '@byokit/link';
9
- import { reach } from '@byokit/reach';
9
+ 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";
@@ -618,8 +618,6 @@ export function openclawLink(kit, o) {
618
618
  };
619
619
  return { handle, stream, allow: () => true, onAction, onGrantRemoved: grant => revoke(grant.id) };
620
620
  }
621
- // ponytail: this ~40-line serve() wiring is deliberately duplicated per runtime kit (D3);
622
- // extract it into a shared package when a third runtime kit appears.
623
621
  export async function serve(o) {
624
622
  const r = await reach({ port: o.port, via: o.via, previous: o.previous });
625
623
  const server = createServer(o.http ??
package/dist/routes.json CHANGED
@@ -122,7 +122,7 @@
122
122
  },
123
123
  {
124
124
  "choice": "anthropic-cli",
125
- "provider": "anthropic",
125
+ "provider": "claude-cli",
126
126
  "plugin": "anthropic",
127
127
  "billing": "subscription",
128
128
  "via": "cli",
@@ -134,6 +134,7 @@
134
134
  "provider": "claude-cli",
135
135
  "via": "browser"
136
136
  },
137
+ "deprecatedProvider": "anthropic",
137
138
  "reason": "Anthropic Claude CLI: subscription sign-in.",
138
139
  "source": "extensions/anthropic/openclaw.plugin.json (2026.8.1)",
139
140
  "revision": "2026.8.1",
@@ -173,13 +174,9 @@
173
174
  "via": "setup_token",
174
175
  "auth": "token",
175
176
  "prerequisite": null,
176
- "offer": true,
177
- "offerPolicy": "default",
178
- "legacy": {
179
- "provider": "anthropic",
180
- "via": "browser"
181
- },
182
- "reason": "Anthropic setup-token: subscription sign-in.",
177
+ "offer": false,
178
+ "offerPolicy": "explicit",
179
+ "reason": "The pinned Gateway has no app-guided sign-in for this choice.",
183
180
  "source": "extensions/anthropic/openclaw.plugin.json (2026.8.1)",
184
181
  "revision": "2026.8.1",
185
182
  "checked": "2026-10-02",
@@ -201,7 +198,7 @@
201
198
  "id": "anthropic",
202
199
  "method": "setup-token",
203
200
  "revision": "ea806575e6450e4d1efdfc72c19f04be982a1b9b",
204
- "flow": "present"
201
+ "flow": "absent"
205
202
  }
206
203
  },
207
204
  {
package/dist/signin.js CHANGED
@@ -15,6 +15,19 @@ const CANCEL_MS = 10_000;
15
15
  const RECONFIRM_MS = 60_000;
16
16
  const TURNS = 200;
17
17
  const PASTE_MS = 15 * 60_000;
18
+ // Two of the pin's code routes print their code only into a note's text ("Code: X" with
19
+ // "Code expires in N minutes", or "enter the code X." with "Expires at: <ISO>"), never as `deviceCode`.
20
+ // Read it back so every code route reaches the caller in the one structured shape.
21
+ const noteCode = (step) => {
22
+ const text = step.type === 'note' ? step.message ?? '' : '';
23
+ const code = /^Code: (\S+)$/m.exec(text)?.[1] ?? /enter the code (\S+?)\.?$/m.exec(text)?.[1];
24
+ if (!code)
25
+ return undefined;
26
+ const minutes = /Code expires in (\d+) minutes/.exec(text)?.[1];
27
+ const at = Date.parse(/Expires at: (\S+)/.exec(text)?.[1] ?? '');
28
+ return { code, message: text, ...(minutes ? { expiresInMinutes: Number(minutes) }
29
+ : Number.isFinite(at) ? { expires_in: Math.max(0, Math.floor((at - Date.now()) / 1_000)) } : {}) };
30
+ };
18
31
  const cut = (value) => (value instanceof Error ? value.message : String(value)).slice(0, 200);
19
32
  const pullOptions = (signal) => ({ timeoutMs: PULL_MS, signal });
20
33
  /**
@@ -268,19 +281,22 @@ export function signIn(ctx, member, o, on) {
268
281
  step = undefined;
269
282
  if (current.type === 'text' && current.sensitive)
270
283
  sensitive = true;
271
- if (current.deviceCode && current.type !== 'text') {
284
+ const deviceCode = current.type === 'text' ? undefined : current.deviceCode ?? noteCode(current);
285
+ if (deviceCode) {
272
286
  stopApproval();
273
287
  approval = true;
274
288
  sawDeviceCode = true;
275
289
  // The pin converts its provider duration to minutes. Prefer exact seconds if the engine supplies them.
276
- const duration = current.deviceCode.expires_in !== undefined
277
- ? Number(current.deviceCode.expires_in) * 1_000
278
- : Number(current.deviceCode.expiresInMinutes) * 60_000;
279
- if (Number.isFinite(duration) && duration >= 0) {
290
+ const duration = deviceCode.expires_in !== undefined
291
+ ? Number(deviceCode.expires_in) * 1_000
292
+ : Number(deviceCode.expiresInMinutes) * 60_000;
293
+ const timed = Number.isFinite(duration) && duration >= 0;
294
+ if (timed)
280
295
  codeTimer = setTimeout(() => { codeExpired = true; owner.abort(); }, duration);
281
- }
296
+ const message = deviceCode.message ?? current.message;
282
297
  // The code is on the card; acknowledging it lets the engine poll the provider itself.
283
- say({ ...(current.deviceCode.code ? { code: current.deviceCode.code } : {}), ...(current.externalUrl ? { url: current.externalUrl } : {}) });
298
+ say({ ...(deviceCode.code ? { code: deviceCode.code } : {}), ...(current.externalUrl ? { url: current.externalUrl } : {}),
299
+ ...(timed ? { expiresAt: Date.now() + duration } : {}), ...(message ? { message } : {}) });
284
300
  const next = await answer(current);
285
301
  if (next.done) {
286
302
  terminal = next;
@@ -292,8 +308,9 @@ export function signIn(ctx, member, o, on) {
292
308
  if (current.type !== 'progress')
293
309
  stopApproval();
294
310
  if (current.type === 'text') {
295
- if (current.sensitive)
296
- say({ prompt: 'Sign-in token' });
311
+ // Every text step is a question the person must answer, so it is always asked. A non-sensitive one is not a
312
+ // secret by the engine's own flag: its question is shown as written. Once anything is sensitive, only the fixed label is.
313
+ say({ prompt: sensitive ? 'Sign-in token' : current.message || 'Paste what the sign-in page gave you' });
297
314
  const value = await waitForPaste();
298
315
  if (value === undefined)
299
316
  return signal.aborted ? cancelled() : expired();
@@ -10,8 +10,7 @@ import assert from 'node:assert/strict';
10
10
  import { PROTOCOL_VERSION } from "../constants.js";
11
11
  import { GATEWAY_CAPS } from "../transport.js";
12
12
  import { releaseStub, stubHolding } from "./model-stub.js";
13
- // The generated method table arrives with O2; until then kit.call's typed surface accepts no method names, so the
14
- // suite drives pass-through through this string-typed view of the same runtime path.
13
+ /** The contract drives pass-through by method name, so results arrive untyped and are narrowed per case. */
15
14
  const call = (kit, method, params) => kit.call(method, params);
16
15
  const REQUIRED_METHODS = [
17
16
  'agent', 'agent.wait', 'agents.create', 'agents.list', 'chat.abort', 'config.get', 'config.patch',
@@ -1,9 +1,16 @@
1
- import type { GatewayTransport } from '../types.ts';
1
+ import type { GatewayMethod, GatewayParams, GatewayTransport } from '../types.ts';
2
2
  import type { KitOptions } from '../kit.ts';
3
- type Handler = (params: any, bridgeSock: string) => unknown;
4
- /** Pre-registered handlers for `fakeGateway(script)`, on top of the defaults (a same-name entry replaces one). */
5
- export type FakeScript = Record<string, Handler>;
6
- export declare function fakeGateway(script?: FakeScript): {
3
+ /** A scripted handler's params: the generated type when the pin publishes one, else the raw params object. */
4
+ export type FakeParams<M extends GatewayMethod> = unknown extends GatewayParams<M> ? Record<string, unknown> : GatewayParams<M>;
5
+ /** A scripted handler for one method, with that method's params typed. */
6
+ export type FakeHandler<M extends GatewayMethod = GatewayMethod> = (params: FakeParams<M>, bridgeSock: string) => unknown;
7
+ /** Pre-registered handlers for `fakeGateway(script)`, on top of the defaults (a same-name entry replaces one).
8
+ * Only methods the pinned engine publishes are scriptable, exactly as `kit.call` and `kit.callDynamic` divide them. */
9
+ export type FakeScript = {
10
+ [M in GatewayMethod]?: FakeHandler<M>;
11
+ };
12
+ /** The fake's control surface: the transport to hand a kit, the calls it saw, and its triggers. */
13
+ export type FakeGateway = {
7
14
  factory: NonNullable<KitOptions['transport']>;
8
15
  calls: {
9
16
  method: string;
@@ -12,8 +19,8 @@ export declare function fakeGateway(script?: FakeScript): {
12
19
  emit(event: string, payload?: unknown): void;
13
20
  failNext(method: string, message: string): void;
14
21
  drop(why: string): void;
15
- handle(method: string, fn: Handler): void;
22
+ handle(method: string, fn: FakeHandler): void;
16
23
  } & {
17
24
  transport: GatewayTransport;
18
25
  };
19
- export {};
26
+ export declare function fakeGateway(script?: FakeScript): FakeGateway;
@@ -245,12 +245,12 @@ export function fakeGateway(script) {
245
245
  'openclaw.setup.activate': (p) => {
246
246
  if (p.kind !== 'api-key')
247
247
  throw new Error('only the API-key setup path is simulated');
248
- const route = routes().find((r) => r.choice === p.authChoice && r.keyEntry);
248
+ const route = routes().find((r) => r.choice === String(p.authChoice) && r.keyEntry);
249
249
  if (!route)
250
250
  return { ok: false, status: 'unavailable' };
251
251
  const model = `${route.provider}/fake-key-model`;
252
- keys.set(p.agentId, { provider: route.provider, key: p.apiKey, model, sealed: false });
253
- signInAgent(p.agentId, route.provider);
252
+ keys.set(String(p.agentId), { provider: route.provider, key: String(p.apiKey), model, sealed: false });
253
+ signInAgent(String(p.agentId), route.provider);
254
254
  return { ok: true, status: 'ok', modelRef: model };
255
255
  },
256
256
  'openclaw.setup.auth.start': (p) => {
@@ -325,14 +325,15 @@ export function fakeGateway(script) {
325
325
  'exec.approval.request': (p) => {
326
326
  const id = String(p.id ?? `exec-${randomUUID()}`);
327
327
  const now = Date.now();
328
+ const plan = isPlainObject(p.systemRunPlan) ? p.systemRunPlan : {};
328
329
  emit('exec.approval.requested', {
329
330
  approvalKind: 'exec', id, createdAtMs: now, expiresAtMs: now + 180_000,
330
331
  request: {
331
332
  command: p.command,
332
333
  // Like the real engine, which normalizes the exec policy mode into request.ask (B8).
333
334
  ask: p.ask ?? 'on-miss',
334
- agentId: p.agentId ?? p.systemRunPlan?.agentId,
335
- sessionKey: p.sessionKey ?? p.systemRunPlan?.sessionKey,
335
+ agentId: p.agentId ?? plan.agentId,
336
+ sessionKey: p.sessionKey ?? plan.sessionKey,
336
337
  },
337
338
  });
338
339
  return { id };
@@ -366,6 +367,7 @@ export function fakeGateway(script) {
366
367
  };
367
368
  for (const [method, handler] of Object.entries(defaults))
368
369
  handlers.set(method, handler);
370
+ // A script handler is already per-method typed; the loose internal map only dispatches it.
369
371
  for (const [method, handler] of Object.entries(script ?? {}))
370
372
  handlers.set(method, handler);
371
373
  const dispatch = async (method, params, bridgeSock) => {
@@ -377,7 +379,7 @@ export function fakeGateway(script) {
377
379
  const handler = handlers.get(method);
378
380
  if (!handler)
379
381
  throw new Error(`unknown method: ${method}`);
380
- return await handler(params ?? {}, bridgeSock);
382
+ return await handler(isPlainObject(params) ? params : {}, bridgeSock);
381
383
  };
382
384
  const hello = () => ({
383
385
  protocol: PROTOCOL_VERSION,
@@ -1,4 +1,4 @@
1
- export { fakeGateway, type FakeScript } from './fake-gateway.ts';
1
+ export { fakeGateway, type FakeGateway, type FakeHandler, type FakeParams, type FakeScript } from './fake-gateway.ts';
2
2
  export { fakeBrowserHost, createBrowserHostFake, browserHostContract, memorySignInStore, type FakeBrowserHost, type FakeBrowserHostOptions, type BrowserFixture } from './browser.ts';
3
3
  export { openclawContract, type ContractFixture } from './contract.ts';
4
- export { STUB_USAGE, startModelStub, useModelStub, releaseStub, stubHolding, toolCalls, type ModelStub, type ModelStubOptions, type StubCall } from './model-stub.ts';
4
+ export { STUB_USAGE, startModelStub, useModelStub, releaseStub, stubHolding, toolCalls, type ModelStub, type ModelStubOptions, type StubCall, type StubRequest } from './model-stub.ts';
@@ -1,8 +1,21 @@
1
1
  import type { OpenClawKit } from '../kit.ts';
2
+ export type StubRequest = {
3
+ model?: unknown;
4
+ input?: unknown;
5
+ messages?: {
6
+ role?: string;
7
+ content?: unknown;
8
+ name?: unknown;
9
+ tool_call_id?: unknown;
10
+ }[];
11
+ stream_options?: {
12
+ include_usage?: boolean;
13
+ };
14
+ };
2
15
  export type StubCall = {
3
16
  authorization: string;
4
17
  path: string;
5
- body: any;
18
+ body: StubRequest;
6
19
  };
7
20
  /** The token usage every stub reply reports when asked (`stream_options.include_usage`), per model call. */
8
21
  export declare const STUB_USAGE: {
@@ -58,7 +58,8 @@ export const releaseStub = (key, reply = '') => {
58
58
  }
59
59
  };
60
60
  export const stubHolding = (key) => (key ? holds.has(key) : holds.size > 0);
61
- const words = (m) => typeof m?.content === 'string' ? m.content : Array.isArray(m?.content) ? m.content.map((c) => c.text ?? '').join('') : '';
61
+ const words = (m) => typeof m?.content === 'string' ? m.content : Array.isArray(m?.content)
62
+ ? m.content.map((c) => (typeof c?.text === 'string' ? c.text : '')).join('') : '';
62
63
  export function startModelStub(script = [], o = {}) {
63
64
  const calls = [];
64
65
  const queue = [...script];
@@ -97,7 +98,7 @@ export function startModelStub(script = [], o = {}) {
97
98
  const phrased = (text) => /hit the limit|no helpers in plan|sign me out|ask permission/i.test(text);
98
99
  const lastUser = [...users].reverse().find((m) => structural(words(m)))
99
100
  ?? [...users].reverse().find((m) => phrased(words(m)))
100
- ?? users.at(-1);
101
+ ?? users.at(-1) ?? {};
101
102
  const said = words(lastUser);
102
103
  const anchor = messages.lastIndexOf(lastUser);
103
104
  const results = anchor < 0 ? [] : messages.slice(anchor + 1).filter((m) => m.role === 'tool');
@@ -113,7 +114,7 @@ export function startModelStub(script = [], o = {}) {
113
114
  if (said.startsWith(routingMarker)) {
114
115
  const options = [...said.matchAll(/^- ([a-z0-9-]+):/gm)].map((m) => m[1]);
115
116
  const pick = /\[route ([a-z0-9-]+|\?)\]/.exec(said)?.[1] ?? 'chief';
116
- return plain(res, body, JSON.stringify(Object.fromEntries(options.map((o) => [o, pick === '?' ? 1 / options.length : o === pick ? 0.9 : 0.1 / (options.length - 1)]))));
117
+ return plain(res, body.model, JSON.stringify(Object.fromEntries(options.map((o) => [o, pick === '?' ? 1 / options.length : o === pick ? 0.9 : 0.1 / (options.length - 1)]))));
117
118
  }
118
119
  if (/sign me out/i.test(said)) {
119
120
  res.writeHead(401, { 'content-type': 'application/json' });
@@ -167,21 +168,18 @@ export function startModelStub(script = [], o = {}) {
167
168
  });
168
169
  });
169
170
  }
170
- const plain = (res, body, text) => {
171
+ const plain = (res, model, text) => {
171
172
  const id = randomUUID();
172
173
  res.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache' });
173
- const chunk = (delta, finish = null) => res.write(`data: ${JSON.stringify({ id, object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: body.model, choices: [{ index: 0, delta, finish_reason: finish }] })}\n\n`);
174
+ const chunk = (delta, finish = null) => res.write(`data: ${JSON.stringify({ id, object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model, choices: [{ index: 0, delta, finish_reason: finish }] })}\n\n`);
174
175
  chunk({ role: 'assistant', content: text });
175
176
  chunk({}, 'stop');
176
177
  res.end('data: [DONE]\n\n');
177
178
  };
178
179
  // provider id 'byokit-stub', model 'test': the stub becomes every agent's primary model.
179
180
  export async function useModelStub(kit, stub) {
180
- // The generated method table arrives with O2; until then kit.call's typed surface accepts no method names, so
181
- // configure through this string-typed view of the same runtime path.
182
- const call = kit.call.bind(kit);
183
- const current = await call('config.get');
184
- await call('config.patch', { baseHash: current.hash, raw: JSON.stringify({
181
+ const current = await kit.call('config.get', {});
182
+ await kit.call('config.patch', { baseHash: current.hash, raw: JSON.stringify({
185
183
  models: { providers: { 'byokit-stub': {
186
184
  baseUrl: stub.url, apiKey: 'byokit-stub', api: 'openai-completions',
187
185
  models: [{ id: 'test', name: 'Test', reasoning: true, input: ['text'],
package/dist/types.d.ts CHANGED
@@ -103,6 +103,8 @@ export type SignInView = {
103
103
  via: 'browser' | 'code';
104
104
  url?: string;
105
105
  code?: string;
106
+ expiresAt?: number;
107
+ message?: string;
106
108
  prompt?: string;
107
109
  error?: string;
108
110
  why?: 'busy' | 'declined' | 'expired' | 'failed';
@@ -125,7 +127,7 @@ export type Decision = {
125
127
  };
126
128
  export type KitState = {
127
129
  phase: 'stopped' | 'installing' | 'starting' | 'repairing' | 'ready' | 'restarting' | 'failed' | 'needs-update' | 'locked';
128
- why?: 'install' | 'handshake' | 'exited' | 'port' | 'version' | 'engine-already-running' | 'engine-patch';
130
+ why?: 'install' | 'handshake' | 'exited' | 'port' | 'version' | 'engine-already-running' | 'engine-patch' | 'sign-in-reset';
129
131
  retryAt?: number;
130
132
  patchSet?: string | null;
131
133
  };
@@ -157,6 +159,10 @@ export type Route = {
157
159
  provider: string;
158
160
  via: 'browser' | 'code';
159
161
  };
162
+ /** @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
164
+ * `claude-cli:cli:anthropic-cli`. */
165
+ deprecatedProvider?: string;
160
166
  billingFrom?: 'source' | 'host';
161
167
  group?: 'models' | 'services';
162
168
  platforms?: {
package/dist/usage.d.ts CHANGED
@@ -46,6 +46,8 @@ export type AgentUsageReading = {
46
46
  /** Normalize an explicitly agent-scoped UTC response. No missing datum is replaced with zero. */
47
47
  export declare function agentUsageOf(raw: unknown, member: Member, window: UsageWindow, receivedAt?: number): AgentUsageReading;
48
48
  /** Works with kit.call or the portable device.call; retains the complete existing pass-through. RPC errors reject. */
49
- export declare function readAgentUsage(client: {
49
+ /** What `readAgentUsage` needs from a reader: the kit (`call`) or a device (`callDynamic` + `call`) both fit. */
50
+ export type UsageClient = {
50
51
  call(method: 'sessions.usage', params: SessionsUsageParams, options?: CallOptions): Promise<unknown>;
51
- }, member: Member, window: UsageWindow, options?: CallOptions): Promise<AgentUsageReading>;
52
+ };
53
+ export declare function readAgentUsage(client: UsageClient, member: Member, window: UsageWindow, options?: CallOptions): Promise<AgentUsageReading>;
package/dist/usage.js CHANGED
@@ -53,7 +53,6 @@ export function agentUsageOf(raw, member, window, receivedAt = Date.now()) {
53
53
  result.totals = totals;
54
54
  return result;
55
55
  }
56
- /** Works with kit.call or the portable device.call; retains the complete existing pass-through. RPC errors reject. */
57
56
  export async function readAgentUsage(client, member, window, options) {
58
57
  validate(member, window);
59
58
  const raw = await client.call('sessions.usage', { agentId: member, startDate: window.startDate, endDate: window.endDate, mode: 'utc', groupBy: 'instance', limit: 1 }, options);
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 === 'sign-in-reset')
25
+ return words('engine.signInAgain');
24
26
  return s.phase === 'stopped' ? '' : words(PHASE_WORDS[s.phase]);
25
27
  }
26
28
  export function toAccountView(view, ready) {
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.signInAgain": "Your saved sign-in couldn't be opened, so it was kept aside. Sign in again.",
6
7
  "engine.ready": "Ready.",
7
8
  "engine.restarting": "Something stopped. Starting it again by itself.",
8
9
  "engine.alreadyRunning": "Your saved sign-in is in use. Try again after the other session stops.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byokit/openclaw",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
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",
@@ -60,9 +60,9 @@
60
60
  "prepack": "tsc -b && node ../../scripts/fix-words-dts.cjs"
61
61
  },
62
62
  "dependencies": {
63
- "@byokit/link": "0.7.0",
64
- "@byokit/relay": "0.5.2",
65
- "@byokit/reach": "0.6.0",
63
+ "@byokit/link": "0.7.1",
64
+ "@byokit/relay": "0.5.3",
65
+ "@byokit/discover": "0.6.0",
66
66
  "@byokit/seal": "0.3.0",
67
67
  "@byokit/secrets": "0.5.0",
68
68
  "@openclaw/gateway-client": "2026.9.6",