@byokit/openclaw 0.5.1 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +56 -3
  2. package/README.md +205 -15
  3. package/SECURITY.md +6 -6
  4. package/dist/auth-store.d.ts +6 -0
  5. package/dist/auth-store.js +58 -15
  6. package/dist/bridge.d.ts +10 -2
  7. package/dist/bridge.js +61 -14
  8. package/dist/browser/broker.d.ts +63 -0
  9. package/dist/browser/broker.js +1003 -0
  10. package/dist/browser/host.d.ts +98 -0
  11. package/dist/browser/host.js +945 -0
  12. package/dist/browser/resume.d.ts +10 -0
  13. package/dist/browser/resume.js +23 -0
  14. package/dist/browser/store.d.ts +25 -0
  15. package/dist/browser/store.js +151 -0
  16. package/dist/browser/verify.d.ts +17 -0
  17. package/dist/browser/verify.js +38 -0
  18. package/dist/browser.d.ts +212 -0
  19. package/dist/browser.js +4 -0
  20. package/dist/config.d.ts +28 -0
  21. package/dist/config.js +130 -0
  22. package/dist/day-usage.d.ts +78 -0
  23. package/dist/day-usage.js +104 -0
  24. package/dist/device.d.ts +38 -5
  25. package/dist/device.js +224 -4
  26. package/dist/engine-patches.d.ts +36 -0
  27. package/dist/engine-patches.js +320 -0
  28. package/dist/engine-status.d.ts +13 -0
  29. package/dist/engine-status.js +31 -0
  30. package/dist/engine.d.ts +13 -1
  31. package/dist/engine.js +99 -52
  32. package/dist/index.d.ts +9 -1
  33. package/dist/index.js +2 -0
  34. package/dist/kit.d.ts +28 -3
  35. package/dist/kit.js +319 -12
  36. package/dist/link.d.ts +16 -5
  37. package/dist/link.js +333 -6
  38. package/dist/routes.d.ts +19 -8
  39. package/dist/routes.js +44 -11
  40. package/dist/routes.json +3775 -356
  41. package/dist/runs.js +8 -2
  42. package/dist/signin.d.ts +1 -1
  43. package/dist/signin.js +19 -4
  44. package/dist/testing/browser.d.ts +50 -0
  45. package/dist/testing/browser.js +166 -0
  46. package/dist/testing/contract.js +1 -2
  47. package/dist/testing/fake-gateway.d.ts +14 -7
  48. package/dist/testing/fake-gateway.js +8 -6
  49. package/dist/testing/index.d.ts +3 -2
  50. package/dist/testing/index.js +1 -0
  51. package/dist/testing/model-stub.d.ts +15 -1
  52. package/dist/testing/model-stub.js +11 -11
  53. package/dist/types.d.ts +50 -3
  54. package/dist/usage-boots.d.ts +8 -0
  55. package/dist/usage-boots.js +26 -0
  56. package/dist/usage.d.ts +53 -0
  57. package/dist/usage.js +60 -0
  58. package/dist/words.json +42 -0
  59. package/engine/OPENCLAW-LICENSE +24 -0
  60. package/engine/patches.json +59 -0
  61. package/engine/patches.schema.json +41 -0
  62. package/package.json +12 -4
  63. package/plugin/index.js +78 -5
  64. package/plugin/usage.js +125 -0
package/CHANGELOG.md CHANGED
@@ -2,9 +2,62 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
- ## 0.5.1 (2026-10-02)
6
-
7
- - The README points to the shared account-route vocabulary (D18); `routes()` still returns today's table.
5
+ ## 0.6.2 (2026-10-05)
6
+
7
+ 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.
8
+ - 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.
9
+ - The quickstart's example output is what the example actually prints today: the offered-route list was stale
10
+ (it still showed `anthropic-cli` and a browser `setup-token` and missed `opencode-go`). The block is now the
11
+ real output of the runnable fake-Gateway example, followed by a "read next" pointer to the run contract, the
12
+ kit example and SECURITY.md.
13
+ 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.
14
+ - Every type an entry hands a caller is now nameable from that entry, and none of them carries `any`.
15
+ `.` exports `GatewayMethods`, `RouteView`, `RouteFacts`, `JsonValue`, `UsageClient` and `DayUsageClient`;
16
+ `./device` exports `OpenClawDevice` and `DeviceEndFrame`; `./link` exports `OpenClawLinkOptions`,
17
+ `OpenClawLinkHost`, `OpenClawServeOptions` and `OpenClawServeHandle`; `./testing` exports `FakeGateway`,
18
+ `FakeHandler`, `FakeParams` and `StubRequest`, types `fakeGateway`'s script per method, and records each
19
+ stubbed request as a typed `StubRequest` instead of `any`. An app can write its own generic wrapper over
20
+ `call` and annotate what `routes()`, `openclawDevice()` and `openclawLink()` return without a cast. The
21
+ Gateway slots the pinned engine does not declare stay `unknown` and are listed per release in
22
+ `src/generated/report.json`; the README now says so beside the API table.
23
+
24
+ ## 0.6.1 (2026-10-04)
25
+
26
+ 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.
27
+ FEAT: Add the internal pipe-only browser broker with fenced private targets, lease-bound live sessions and exact-origin site-data clearing. Sign-in handoff remains unprotected until parked-session protection is qualified.
28
+ FIX: Launch each broker's Chromium with its own short mode-0700 temp directory under the caller's temp directory, removed on close, instead of using the profile path as TMPDIR; deep app profile paths overflowed Chromium's singleton socket path and aborted startup.
29
+ FIX: A controlling viewer ignores frames from a previous target and drops input until the current private target has delivered its first image, so popup switches cannot map input with stale coordinates.
30
+ FIX: The broker's browser endpoint and `/json/version` now use Chromium's canonical `/devtools/browser/<id>?token=` shape, so the pinned engine's CDP discovery finds `/json/list` instead of failing with HTTP 404; token, host and origin guards are unchanged and the old bare path is refused.
31
+ FIX: Chromium's sandbox stays on; a launch refused for no usable sandbox (for example where unprivileged user namespaces are disabled) now fails with typed `BrowserSandboxUnavailable` instead of a generic failure, and is never retried unsandboxed. Startup stderr is used only in memory for that check.
32
+ FIX: Wire opt-in member browsers to the shipped bridge and stock engine, with a dead default profile, member-pinned browser actions, kit browser invalidation events and scoped browser-engine checks. Sign-in handoff remains `handoff-unprotected`; no recovery-protection or automatic resume redispatch claim is made.
33
+ SECURITY: Register the published awaited OpenClaw/Codex tool-result middleware, scrub owned broker capabilities before live model feedback, recheck session admission per result, and terminate on refusal or unavailable protection. Transcript hooks are supplementary; actual full privacy/recovery qualification remains pending.
34
+ FIX: Honor the stock allow/alsoAllow schema, create model-free audit sessions for effective-tool checks, and acknowledge redacted CDP profiles only against an unchanged exact host-owned endpoint and matching applied-source revisions.
35
+ FIX: Explicitly clear hostile node selection across the stock hook's shallow parameter merge; browser proof fixtures distinguish normal model completion from successful tool execution.
36
+ SECURITY: A definite failed sign-in resume no longer permits arbitrary recovery turns: replacement requires a fresh live kit registration matching the exact engine run id and session, revoked on release or bridge shutdown. Source controls cover parked and every unproved resume state; actual engine qualification remains pending. Reconnect keeps tool subsets but revokes submission authority, and waits for old Unix socket teardown before rebinding; shutdown cancels outstanding calls.
37
+ SECURITY: Browser use requires a closed explicit safe tool policy across every engine agent, account agent and delegate; unsafe and unknown effective tools refuse every browser, model-facing profile management/evaluation is blocked before app approval, and raw agent/plugin/config policy mutations require the guarded config path.
38
+ - FEAT: Portable types for the browser sign-in handoff and live view (`NeedSignIn`, `BrowserState`, `LiveViewState`, `BrowserHost`, `BrowserDevice` and friends) from `.` and `./device`, per spec 5.17. Types only: no runtime ships yet, and handoff stays refused until parked sessions are protected.
39
+ - Allow the internal browser host to attach newly created owned-member browsers and replace exact owned endpoints with stale-identity guards, private-target cleanup barriers and read-only versioned bindings for guarded configuration publication. Request bounded thumbnails through the owned viewer. Production sign-in handoff remains blocked pending qualification.
40
+ - Add the internal browser handoff host, durable settlement and conservative resume bookkeeping, with offline synthetic fixtures. Production sign-in remains blocked pending recovery-protection qualification.
41
+ - Add member-scoped browser sign-in actions, private browser live view and portable device helpers. Browser frames and lease capabilities travel on transient encrypted streams, outside durable link answer caches; control requires the host's immediate grant-revocation seam. Watching or reconnecting never dispatches a model run.
42
+ - FIX: Preserve browser reply and event order when messages arrive together, so page evaluation can receive its initial context instead of waiting indefinitely.
43
+ SECURITY: Ship a content-addressed bundled-engine patch manifest and the pinned OpenClaw MIT notice. Fully verified read-only sibling engine sets replace in-place installation; base and existing set bytes are never changed, even when shared Gateways are live. `KitState.patchSet` and `why: 'engine-patch'` expose provenance; rollback selects verified stock offline, while stock drift may need registry access. The initial patch set is empty.
44
+ - `getConfigKey` / `setConfigKey` read and write exactly one dotted config key in the file `prepare()` owns, so an app can narrow-read and restore a value (for example `skills.workshop.autonomous.mode`) around a boot without a whole `config.get`, whose result redacts token-bearing values. Only that key changes, the write is atomic in `prepare()`'s exact shape and happens only when the bytes change, and `undefined` removes the key.
45
+ FEAT: Add portable `readAgentDayUsage` / `AgentDayUsage` readings that combine fresh retained transcripts with durable Gateway Skill Workshop review charges. UTC/IANA windows, pending/interrupted/missing usage and conservative completeness let callers keep unknown budgets unknown. The original partial usage API is unchanged; other detached kinds and the worker bundle remain uncovered.
46
+ FIX: Persist detached Workshop review usage, including reported usage on failed outcomes, without double-counting memory flushes, recovery resumes or run results. Per-boot/month attempt counters, fsynced launch and clean-stop records, phase deduplication and bounded month reads preserve failure holes and crash incompleteness.
47
+ SECURITY: The bundled checksum-pinned Gateway Workshop patch and operator-read usage RPC write/read accounting identities, times and reported counters only, never prompts, outputs, tool arguments or credentials. Device reads stay member-scoped and never access host files; unknown costs and unverified billing identity are not guessed.
48
+ FIX: `stop()` now signals only the pids the kit itself spawned and never a process group, so shutdown no longer reaches processes the kit never started. Every spawn is recorded when it starts and dropped when it exits; the gateway is left to shut its own sessions down on its own SIGTERM.
49
+ - Unreleased source dependency metadata: pin @byokit/seal 0.3.0 and @byokit/relay 0.5.2; this package's existing version is not republished. Previously published consumer metadata remains unchanged.
50
+ - SECURITY: Sensitive subscription sign-in tokens stay out of sign-in views and results, including engine errors, links and codes after token entry.
51
+ - FIX: Subscription setup-token sign-in now answers sensitive wizard text steps from the existing paste channel, once per step, with cancellation and timeout preserved. API key (billed per use) entry still requires explicit selection.
52
+
53
+ ## 0.6.0 (2026-10-02)
54
+
55
+ - Portable `readAgentUsage` / `agentUsageOf` expose explicit-agent UTC retained-transcript totals with raw responses, cache freshness and missing-price counters preserved. Coverage stays partial: stock-engine detached Workshop reviews are absent, and unavailable data never becomes zero; complete internal-turn accounting is not claimed.
56
+ - FIX: Host runs can reuse a caller-stable per-dispatch `idempotencyKey` after a lost connection instead of starting a second engine run. Omission keeps fresh UUIDs. Document the pinned engine's gateway-wide, bounded, in-memory cache and honest in-flight replay limits; this is not exactly-once across engine restarts.
57
+
58
+ FIX: Generate complete pinned route discovery: 91 auth choices and five cloud/CLI providers, including external plugin dependencies, plan-key entry, service grouping and corrected provider/billing labels. `routes()` computes readiness without reading credentials or installing plugins; legacy boolean `offer` is ready default subscriptions only, with `offerPolicy` retaining eligibility. Missing plugins, binaries, client registrations and choiceless wizard flows remain visible, not falsely usable. Explicit API/local/endpoint routes never become default billing; endpoint billing requires host input. Full discovery also reaches the existing phone/web link without hiding unavailable rows. Existing choice IDs, legacy explicit browser/code selectors, native Claude activation guards and ChatGPT pairing are retained; corrected manifest provider IDs and semantic `via` values replace inaccurate discovery labels. Listing an API or unavailable route never authorizes a sign-in or makes it a default.
59
+
60
+ - The README points to the shared account-route vocabulary (D18); pinned route discovery is not proof of additional implemented authentication flows.
8
61
  - FIX: ChatGPT sign-in now interrupts abandoned wizard/paste waits on gateway disconnect and reconfirms a new, usable OAuth profile for the selected member through the reconnected engine. Recovery is bounded and fails closed for unknown, missing, pre-existing or unrelated credentials, cancellation and expiry. A synchronous cleanup error during reconnect no longer suppresses the terminal sign-in view.
9
62
 
10
63
  ## 0.5.0 (2026-10-01)
package/README.md CHANGED
@@ -25,6 +25,26 @@ under your `stateDir` (a few minutes, needs the network once), and later starts
25
25
  phone page that pairs, signs in with ChatGPT, runs and answers approvals, see
26
26
  [`examples/openclaw-kit`](../../examples/openclaw-kit).
27
27
 
28
+ ## Bundled engine provenance
29
+
30
+ `engine/patches.json` declares the exact upstream tarball integrity, commit, unique edits and before/after file
31
+ hashes. The Gateway Workshop review patch writes content-free, durable usage facts only when the kit sets its
32
+ accounting environment. The separately bundled worker and all other detached kinds remain uncovered.
33
+ The full upstream MIT notice ships in [engine/OPENCLAW-LICENSE](engine/OPENCLAW-LICENSE).
34
+
35
+ Each `prepare()` verifies the **entire** selected engine tree, including unpatched files. The kit installs stock
36
+ into a private temporary tree, clones verified stock for a changed set, then publishes read-only sibling trees
37
+ under `<engineDir>.sets/`. It never patches, reinstalls or deletes the base `<engineDir>/node_modules` or any
38
+ published set, even when a shared Gateway is running. `doctorContext().entry` points at the selected set after
39
+ prepare; `<stateDir>/openclaw/engine-set` records that selection. `kit.state.patchSet` is its content-addressed
40
+ patch id (`null` after a provenance failure); a refusal is `why: 'engine-patch'` with a typed error code.
41
+
42
+ Rollback selects a verified stock or older set; known sets and offline clones require no registry access.
43
+ Drift leaves the old bytes untouched and builds a replacement sibling; rebuilding stock may need the registry.
44
+ Pre-change kits still run the untouched base install. No garbage collection: each retained set can occupy about
45
+ 889 MB on this pin (reflinks may reduce physical storage). Whole-tree verification also adds preparation I/O;
46
+ read-only modes protect against accidental writes, not a malicious host app that owns the files.
47
+
28
48
  ## Quickstart
29
49
 
30
50
  Start the engine, add a member, sign them in with a subscription, run a message (typechecked; running it installs
@@ -56,6 +76,32 @@ console.log(end);
56
76
  await kit.stop();
57
77
  ```
58
78
 
79
+ For a host run that may be retried after a lost connection, mint and persist an action key **once**, then reuse
80
+ that key and the same run inputs:
81
+
82
+ ```ts
83
+ import type { OpenClawKit } from '@byokit/openclaw';
84
+
85
+ async function dispatch(kit: OpenClawKit, taskId: string, attempt: number, message: string) {
86
+ const actionKey = `task:${taskId}:attempt:${attempt}:${crypto.randomUUID()}`;
87
+ // Persist actionKey with this dispatch before calling; a new action/attempt gets a new nonce.
88
+ const spec = { member: 'umer', sessionKey: `agent:umer:task:${taskId}`, message, idempotencyKey: actionKey };
89
+ return kit.run(spec); // reconnect retry: kit.run(spec), not another dispatch()/new actionKey
90
+ }
91
+ ```
92
+
93
+ Omitting `idempotencyKey` preserves a fresh UUID per call. Supplied keys are non-empty strings passed unchanged.
94
+ **Not exactly-once:** engine 2026.8.1 caches `agent` keys gateway-wide, across agents and sessions, without comparing
95
+ inputs. A collision silently returns the first run's result/error; a task id alone is not a safe key. Reconnects
96
+ retain the in-memory cache; engine process restarts lose it. Inactive entries expire after five minutes (60-second
97
+ cleanup tick), or earlier under the 1,000-entry cache limit; active and pending accepted runs are exempt.
98
+ An in-flight replay waits on the original run but cannot replay old events or subscribe to its final response:
99
+ text may be a capped 4096-character terminal snapshot, usage stays absent, and capped JSON may fail schema
100
+ validation. A completed cached replay can return full text/usage. The helper never sends another `agent` request
101
+ just to read a final result: on cache eviction that would start another run. Apps still own durable task/effect
102
+ recovery. Typed `kit.call('agent', ...)` remains exact upstream pass-through. Details and real-engine proof are
103
+ in [the run contract](../../docs/runtime-kits.md#58-runs-members-and-streams).
104
+
59
105
  Device approval can take longer than two minutes. The kit waits through the engine's advertised code lifetime
60
106
  (`expiresInMinutes` on the pin, or `expires_in` seconds when supplied), including progress pulls. If the engine
61
107
  supplies no lifetime, it owns the deadline. Ordinary wizard requests keep their 120-second timeout.
@@ -63,6 +109,13 @@ supplies no lifetime, it owns the deadline. Ordinary wizard requests keep their
63
109
  `done` returns a typed `SignInView`: `why: 'expired'` for an expired code, `why: 'declined'` for cancellation,
64
110
  with a plain sentence in `error`. `SignInOptions` is exported from the host entry.
65
111
 
112
+ For a subscription setup-token or any sensitive wizard text step, pass the token once through the same
113
+ `signIn.paste(token)` channel and clear the input afterwards. The kit displays only a fixed entry label;
114
+ subsequent wizard errors, links and codes are withheld, and failures use a fixed message rather than
115
+ engine text that could echo the token. It is sent only as this member's wizard answer, never in views,
116
+ results or kit logs. Ordinary browser paste and typed `kit.call('wizard.next', …)` pass-through are unchanged.
117
+ API key (billed per use) entry still requires explicit selection; there is no subscription-to-key fallback.
118
+
66
119
  To choose which of a member's accounts a run uses, pass `model: 'provider/model'` in the run spec (for example
67
120
  `'openai/gpt-5.1'`, where `provider` is an id `kit.providers(member)` lists). That provider is the one called and billed for this run only, with no fallback to another
68
121
  provider or model. If the member isn't signed in to it, the run ends `{ ok: false, kind: 'signed-out' }` and the
@@ -94,6 +147,71 @@ ends ok carries `usage` (the engine's token total for the run, and `costUsd` whe
94
147
  `openclawDevice(link).run(message, o)` takes the same options over the link, and `state()` adds the kit and engine
95
148
  versions and the providers the device's member is signed in to.
96
149
 
150
+ ### Observed per-agent ledger usage (not complete engine spend)
151
+
152
+ ```ts
153
+ import { readAgentUsage } from '@byokit/openclaw/usage'; // portable; kit or device client
154
+ import type { OpenClawKit } from '@byokit/openclaw';
155
+
156
+ async function showUsage(kit: OpenClawKit) {
157
+ const reading = await readAgentUsage(kit, 'umer', { startDate: '2026-10-02', endDate: '2026-10-02' });
158
+ // Caller decides whether partial coverage is acceptable; never turn unavailable totals into zero.
159
+ if (reading.state === 'available') console.log(reading.totals);
160
+ }
161
+ ```
162
+
163
+ `agentUsageOf(raw, member, window)` also normalizes an existing explicitly agent-scoped UTC response. Full raw
164
+ RPC data is retained; existing typed `call` pass-through is unchanged. Windows are inclusive UTC calendar days.
165
+ `receivedAt`, result-assembly `updatedAt` and `cache.refreshedAt` are separate: receiving cached data does not
166
+ refresh it. The engine response cache lasts 30 seconds; pending/stale/unknown data leaves `totals` absent.
167
+ An absent agent row is unavailable, not proof of zero usage. Reset/deletion/retention can reduce counters.
168
+
169
+ **Coverage is always `retained-transcripts-only`.** Stock 2026.8.1's detached Skill Workshop reviews are omitted
170
+ from its ledger; actual crash-recovery resumes and threshold memory flushes are counted through persisted
171
+ transcripts, never through a second side total. This reader cannot enforce a complete all-turn budget. The app owns budget,
172
+ share and unavailable/partial-data policy. Do not add run results to these totals: they already overlap. Engine
173
+ cost fields are price counters, not a bill or subscription quota; zero cost with `missingCostEntries > 0` is
174
+ unknown cost, and tokens never establish subscription plan weights. No billing conversion is performed.
175
+
176
+ ### Day readings including Gateway Workshop reviews
177
+
178
+ ```ts
179
+ import { readAgentDayUsage } from '@byokit/openclaw/day-usage'; // also exported by . and ./device
180
+
181
+ async function dayBudget(kitOrDevice: Parameters<typeof readAgentDayUsage>[0]) {
182
+ const startMs = Date.parse('2026-10-02');
183
+ const day = await readAgentDayUsage(kitOrDevice, 'umer', {
184
+ startMs, endMs: startMs + 86_400_000 - 1, mode: 'utc',
185
+ });
186
+ return day.knownTotalTokens !== undefined && day.knownTotalTokens > 190 ? 'over'
187
+ : day.complete ? 'under' : 'unknown'; // policy is the caller's
188
+ }
189
+ ```
190
+
191
+ This additive reader leaves `readAgentUsage` unchanged. Coverage is **retained transcripts + Gateway Workshop
192
+ reviews**, not all engine spend. Reviews crossing midnight land on their ended day. UTC and IANA
193
+ `{ mode: 'time-zone', timeZone: 'America/New_York' }` apply to both terms; Gateway-local zones are refused.
194
+ The engine transcript RPC reports calendar days: use complete inclusive days in that zone. A partial-day window
195
+ leaves the transcript term unavailable rather than inventing a millisecond total.
196
+
197
+ Review facts carry only run/session/agent/model/profile ids, times, outcomes and reported tokens. Their cost is
198
+ always `missing`; billing stays `unknown` without verified selected-route identity, never inferred from a provider.
199
+ Missing usage is `reported-missing`, a live start is `pending`, and an old boot's start is `interrupted`; none means
200
+ zero. Facts deduplicate by charge and phase. `knownTotalTokens` is the sum of reported counters only and is absent
201
+ when either source is unavailable. `complete: false` is a budget-policy unknown, not permission to proceed.
202
+
203
+ The append-only `openclaw/usage/` ledger survives session reset, engine replacement and restart. Start records are
204
+ fsynced before spawning, using an accounting UUID separate from unchanged pid/start-time ownership. Month attempt
205
+ counters and failures survive clean stop; holes, missing counters, corrupt lines and overlapping crash boots keep
206
+ windows incomplete permanently. Reads open only requested UTC months plus boot records. No rotation yet.
207
+ Device reads use the operator-read plugin RPC, never host files; the link adapter restricts it to the granted member.
208
+ As with the existing transcript reader, the app must explicitly allow member-scoped `sessions.usage` pass-through.
209
+
210
+ Uncovered: worker-bundle work, skill collection review, history scans, slug generation, active-memory recall,
211
+ out-of-turn compaction and one-shot helper completions. Do not add `RunEnd.usage`, memory flush or recovery totals
212
+ again: they already overlap retained transcripts. Offline scripted-provider tests verify seven Gateway accounting
213
+ cases; they do not establish live subscription quota, provider bills or universal coverage.
214
+
97
215
  For a validated JSON answer, pass a literal `schema`. The result's `data` is inferred from that schema:
98
216
 
99
217
  ```ts
@@ -170,17 +288,23 @@ state: ready | protocol: 4
170
288
  event: {"type":"text","text":"fake: Say hello."}
171
289
  event: {"type":"text","text":"fake: Say hello."}
172
290
  end: {"ok":true,"text":"fake: Say hello.","usage":{"input":10,"output":16,"total":26}}
173
- route: openai subscription browser
174
- route: openai-device-code subscription code
175
- route: xai-oauth subscription code
291
+ route: setup-token subscription setup_token
176
292
  route: github-copilot subscription code
177
293
  route: github-copilot-enterprise subscription code
178
294
  route: minimax-global-oauth subscription code
179
295
  route: minimax-cn-oauth subscription code
180
- route: anthropic-cli subscription browser
181
- route: setup-token subscription browser
296
+ route: openai subscription browser
297
+ route: openai-device-code subscription code
298
+ route: opencode-go subscription plan_key
299
+ route: xai-oauth subscription code
182
300
  ```
183
301
 
302
+ The block above is the whole quickstart: copy it into a `.mts` file next to your app's
303
+ `node_modules` and `node` it. No engine, no account, no network. Then read
304
+ [the run contract](../../docs/runtime-kits.md#58-runs-members-and-streams) for retries and
305
+ streams, [`examples/openclaw-kit`](../../examples/openclaw-kit) for a whole app, and
306
+ [SECURITY.md](SECURITY.md) before you store anything the engine returns.
307
+
184
308
  Subscription sign-ins are offered by default. API-billed routes remain in `kit.routes()` with `offer: false`;
185
309
  apps can offer them when the app or person opts in, labelled API key (billed per use). Each `Route` carries `billing` (`subscription`, `api` or
186
310
  `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
@@ -198,15 +322,78 @@ Entries:
198
322
 
199
323
  | Export | What it does |
200
324
  | --- | --- |
201
- | `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`, `memoryLimited`, `doctorContext` |
325
+ | `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` |
202
326
  | `ENGINE_VERSION`, `PROTOCOL_VERSION`, `OPERATOR_SCOPES` (`.`) | The pinned engine version, its protocol and the operator scopes the kit connects with |
203
327
  | `KitOptions`, `RunSpec`, `RunEvent`, `RunEnd`, `RunUsage`, `PlanWindow`, `Route`, `Approval`, `Decision`, `ToolSpec`, `ToolHost`, `KitState`, ... (`.`) | Public types (docs/runtime-kits.md §5.2) |
204
- | `openclawDevice(link)` (`./device`) | Portable client: state, routes, sign-in, runs, steer, abort, approvals, events, sealed notices, pass-through `call` |
328
+ | `GatewayMethods`, `GatewayMethod`, `GatewayParams`, `GatewayResult`, `GatewayEventName`, `GatewayEventPayload` (`.`) | The generated pass-through tables, so an app can write its own generic wrapper over `call` / `onEvent` once |
329
+ | `RouteView`, `RouteFacts`, `JsonValue`, `UsageClient`, `DayUsageClient` (`.`) | The types `routes()`, `routes(facts)` and the usage readers return or take, nameable without a cast |
330
+ | `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 |
205
331
  | `LinkRefused`, `openNotice` (`./device`) | The host's own refusal as an error; opens a sealed approval notice |
206
332
  | `words`, `stateWords`, `toAccountView` (`.`, `./device`) | The kit's sentences, so a phone shows the words the computer does; `toAccountView` feeds `@byokit/ui-core`'s `phaseOf` |
207
- | `openclawLink(kit, o)` (`./link`) | `handle` / `stream` / `allow` for a `@byokit/link` `Host`, checked per member, plus `onAction` for relay push actions |
208
- | `serve(o)` (`./link`) | Binds the link host per reach and returns its URLs |
209
- | `fakeGateway`, `openclawContract`, `startModelStub`, `useModelStub` (`./testing`) | In-memory Gateway, the contract suite and the scripted model for tests |
333
+ | `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 |
334
+ | `serve(o)` (`./link`) | Binds the link host per reach and returns its URLs (`OpenClawServeOptions` / `OpenClawServeHandle`) |
335
+ | `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` |
336
+
337
+ ### What the pass-through cannot type
338
+
339
+ Every method and event the pin publishes has a real type: `kit.call('sessions.list', params)` is checked in and
340
+ returns the engine's declared result, with no cast. For the slots the pin itself leaves undeclared the table stays
341
+ `unknown`, and `src/generated/report.json` names every one of them per release. On `openclaw@2026.8.1` that is 301
342
+ of 393 methods' params and 192 of their results, plus 21 of 55 event payloads; the other slots are not missing from
343
+ the kit, they are not published by the engine, and typing them here would be a guess that does not match the
344
+ gateway. `callDynamic` stays the same shape for a method the pin never declared.
345
+
346
+ ## Owned browser (unqualified handoff)
347
+
348
+ `browser: { executablePath, members }` opts into isolated member Chromium profiles. It requires a closed
349
+ explicit tool allowlist and keeps `request_sign_in` refused: protected production handoff is not qualified.
350
+ Public live view/thumbnail APIs are model-free; source tests alone do not prove private/restart protection.
351
+ The opt-in physical fixture retains raw JPEG/control inputs before decoding, pipe chunk order, complete
352
+ synthetic provider bodies and authoritative SQLite transcripts, including failures.
353
+
354
+ ## One config key, narrowed
355
+
356
+ `patchConfig` is the only whole-config writer, and it can only start from `config.get`, whose result redacts
357
+ token-bearing values: an app cannot read one key through it and write it back unchanged. For a single key, use the
358
+ narrow pair over the config file the engine loads at boot:
359
+
360
+ ```ts
361
+ import { OpenClawKit } from '@byokit/openclaw';
362
+
363
+ const kit = new OpenClawKit({ stateDir: './openclaw-state' });
364
+
365
+ kit.getConfigKey('skills.workshop.autonomous.mode'); // value, or undefined
366
+ kit.setConfigKey('skills.workshop.autonomous.mode', 'off'); // returns the value it replaced
367
+ kit.setConfigKey('skills.workshop.autonomous.mode', undefined); // remove it again
368
+ ```
369
+
370
+ One dotted path of `[A-Za-z0-9_-]` segments; prototype-reaching segments are refused. Only that key changes (every
371
+ other key keeps its order, an unchanged value writes no bytes, and removing a key takes the empty objects it created
372
+ back out), the write is atomic in `prepare()`'s exact shape, and the engine applies it at its next boot. Narrow a key
373
+ your app does **not** also pass in `KitOptions.config`: that option is merged over the saved file on every boot and
374
+ wins. Don't interleave a narrow set with `patchConfig`, which rewrites the whole config through the Gateway.
375
+
376
+ ## App-owned task recovery
377
+
378
+ If your app requeues interrupted tasks itself, declare their namespaces **before starting the kit**:
379
+
380
+ ```ts
381
+ import { OpenClawKit } from '@byokit/openclaw';
382
+
383
+ const kit = new OpenClawKit({
384
+ stateDir: './openclaw-state',
385
+ appOwnedSessions: { keyPrefixes: ['agent:m1:crewhouse:'] },
386
+ // tools and host as usual
387
+ });
388
+ ```
389
+
390
+ Matching task sessions are excluded from the bundled engine's automatic restart continuation; your app
391
+ continues the same session key with `kit.run`. Prefixes must start with `agent:<member>:` and must not cover
392
+ that member's `main` key. The kit also covers a public member's isolated API-key session rewrite. Omitted/empty
393
+ keeps stock recovery, including its existing retry accounting; no histories or recovery markers are deleted,
394
+ no tool gate is bypassed, and cron behavior is unchanged. Changing ownership requires an engine restart.
395
+ This option does not decide whether an interrupted external action is safe to retry; your app still owns
396
+ that decision. It needs the published kit's bundled engine patch, not a separately installed stock CLI.
210
397
 
211
398
  ## Phones and browsers
212
399
 
@@ -249,8 +436,9 @@ Route data follows the shared account-route vocabulary (D18 in [`docs/runtime-ki
249
436
 
250
437
  The pinned engine has no supported hook for sealing OAuth profile writes. Its `auth-profiles` loader stores
251
438
  credential JSON in agent SQLite databases and a shared state database, and doctor imports leave migration
252
- archives. `authSeal` therefore protects the complete isolated `state` and `home` directories, including SQLite
253
- journals. It uses the injected `SealingAdapter` from `@byokit/secrets`. `osKeyringSeal()` automatically
439
+ archives. `authSeal` therefore protects credential state — the complete isolated `state` tree plus
440
+ config/credential paths under `home`, including SQLite journals — while regenerable tool caches,
441
+ transcripts and logs stay on disk unsealed. It uses the injected `SealingAdapter` from `@byokit/secrets`. `osKeyringSeal()` automatically
254
442
  uses a persistent private file key for new stores when no non-interactive keyring is available.
255
443
  Opening follows the saved envelope's mode. A locked or unresponsive keyring-only store leaves
256
444
  `kit.state.phase === 'locked'`: `prepare()` and `start()` resolve, show `stateWords(kit.state)` for
@@ -291,7 +479,8 @@ every retained sealed migration archive before retiring an automatic key. Rotate
291
479
  separately; dual-wrap rotation retains old wrapping keys for backups.
292
480
 
293
481
  Without `authSeal`, engine credentials remain plaintext. With it, successful `prepare()` and `stop()` leave
294
- only a sealed snapshot, `auth-store.sealed`, for those directories. The adapter authenticates the snapshot
482
+ only a sealed snapshot, `auth-store.sealed`, for the sealed credential paths; regenerable caches stay on
483
+ disk unsealed. The adapter authenticates the snapshot
295
484
  before any restoration; a wrong key, tampering, or a missing adapter rejects. Files restored for the engine
296
485
  have mode 0600 and directories 0700. File symlinks are sealed only when their fully resolved targets are
297
486
  regular files inside the isolated engine root; they restore as regular files at the link paths. Outside-root,
@@ -304,7 +493,8 @@ inspection or a rewritten command line), another host is alive, or shutdown time
304
493
  `{ phase: 'failed', why: 'engine-already-running' }`; show `stateWords(kit.state)` and retry after the other
305
494
  session stops. Failed-start cleanup and `stop()` on an instance with no ownership preserve the other
306
495
  writer's pid, lock, sealed snapshot and live state.
307
- Sealing copies the whole store through memory, so startup and stop cost grows with session history.
496
+ The sealed payload is built once and verified by decrypting the sealed bytes, so startup and stop cost
497
+ grows with credential state, not session history.
308
498
 
309
499
  The migration doctor temporarily opens the same store and reseals it even when import fails. Once the gateway
310
500
  verifies every provider, `confirmRetainedLogin()` removes the explicitly passed legacy source and writes only
@@ -344,6 +534,6 @@ frozen (docs/runtime-kits.md §5).
344
534
 
345
535
  ## License
346
536
 
347
- Apache-2.0. See [LICENSE](LICENSE) and [NOTICE](../../NOTICE).
537
+ Apache-2.0. See [LICENSE](LICENSE); the bundled engine's MIT text is [engine/OPENCLAW-LICENSE](engine/OPENCLAW-LICENSE).
348
538
 
349
539
  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.
package/SECURITY.md CHANGED
@@ -37,7 +37,7 @@ the replacement decrypts to identical contents. Other sealing failures still rej
37
37
  | Gateway token | `openclaw/token` | Random 32-byte bearer secret; newly created file is 0600. Treat it as operator access. |
38
38
  | 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
39
  | 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. |
40
+ | 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
41
  | 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
42
  | 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
43
 
@@ -48,8 +48,8 @@ collects regular files and file symlinks whose fully resolved targets are regula
48
48
  files within the isolated engine root; links restore as regular files at the link
49
49
  paths. Outside-root, dangling and directory links, sockets, FIFOs and devices are
50
50
  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
51
+ other paths still need host protection. Excluded caches are not preserved in the
52
+ 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
53
  the kit at another product's state. Review existing permissions before adopting a
54
54
  tree; use an OS-protected app directory, encrypted disk and restricted backups.
55
55
  Do not log `doctorContext().env`, transport arguments, auth records or sign-in
@@ -57,8 +57,8 @@ callbacks. JavaScript strings and engine memory are not reliably zeroized.
57
57
 
58
58
  The engine needs plaintext credentials and PEM during operation. With `authSeal`
59
59
  (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
60
+ credential state — the complete `state` tree plus config/credential paths under `home`, including SQLite journals — on successful `prepare()` and after
61
+ the engine exits on `stop()`, while regenerable caches stay on disk unsealed. It verifies the adapter round-trip, writes via an
62
62
  exclusive temporary file, fsyncs/renames the snapshot and verifies it again before
63
63
  removing live plaintext. `start()` authenticates and validates snapshot paths before
64
64
  restoring files. Missing/wrong keys or tampering reject without plaintext fallback.
@@ -69,7 +69,7 @@ orderly shutdown. An abrupt host exit can leave live plaintext; the next prepare
69
69
  seals leftovers only after the orphan writer has exited. Interrupted restore or
70
70
  removal recovers from the authenticated snapshot. A sealing failure retains
71
71
  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,
72
+ and memory cost grow with sealed credential state; the payload is built once and excluded caches never enter it. Keys, gateway token,
73
73
  device PEM, inline config secrets, logs, install/cache files and app workspaces
74
74
  are outside this seal. OS-keyring or host-key adapters must keep the key separate
75
75
  from state/backups. There is no rollback protection. File removal cannot erase old
@@ -1,4 +1,6 @@
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 {
3
5
  private active;
4
6
  private owned;
@@ -20,6 +22,10 @@ export declare class AuthStore {
20
22
  private release;
21
23
  private read;
22
24
  private collect;
25
+ /** Remove the live credential trees: `state` entirely (it is all credential state, including the
26
+ * runtime entries the collector skips), and in `home` exactly the sealed paths — regenerable caches
27
+ * and the directories still holding them are left in place. */
28
+ private remove;
23
29
  private persist;
24
30
  private restore;
25
31
  archives(): Promise<void>;
@@ -1,6 +1,8 @@
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';
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';
4
6
  import { 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();
@@ -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) {
@@ -169,10 +185,12 @@ export class AuthStore {
169
185
  return saved;
170
186
  }
171
187
  collect() {
172
- const s = { v: 1, dirs: [], files: [] };
188
+ const s = { v: 2, dirs: [], files: [] };
173
189
  const root = realpathSync(this.o.root);
174
190
  const walk = (path) => {
175
191
  const name = relative(this.o.root, path).split('\\').join('/');
192
+ if (cached(name))
193
+ return; // tool caches are never credential state; skipping a directory skips its subtree
176
194
  const stat = lstatSync(path);
177
195
  if (stat.isDirectory()) {
178
196
  chmodSync(path, 0o700);
@@ -208,31 +226,56 @@ export class AuthStore {
208
226
  walk(join(this.o.root, dir));
209
227
  return s;
210
228
  }
229
+ /** Remove the live credential trees: `state` entirely (it is all credential state, including the
230
+ * runtime entries the collector skips), and in `home` exactly the sealed paths — regenerable caches
231
+ * and the directories still holding them are left in place. */
232
+ remove(s) {
233
+ rmSync(join(this.o.root, 'state'), { recursive: true, force: true });
234
+ for (const [path] of s.files)
235
+ if (path.startsWith('home/'))
236
+ rmSync(join(this.o.root, path), { force: true });
237
+ for (const path of [...s.dirs].sort((a, b) => b.split('/').length - a.split('/').length)) {
238
+ if (!path.startsWith('home/'))
239
+ continue;
240
+ try {
241
+ rmdirSync(join(this.o.root, path));
242
+ }
243
+ catch (error) {
244
+ const code = error.code;
245
+ if (code !== 'ENOTEMPTY' && code !== 'ENOENT')
246
+ throw error; // a directory still holding caches stays
247
+ }
248
+ }
249
+ }
211
250
  async persist() {
212
251
  if (!this.o.seal)
213
252
  return;
214
253
  // Always authenticate an earlier snapshot before considering a leftover live store after a crash.
215
254
  const previous = await this.read();
216
255
  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 });
256
+ this.remove(previous);
219
257
  removeMarker(this.cleanup);
220
258
  removeMarker(this.restoring);
221
259
  this.o.log?.('interrupted credential transition recovered');
222
260
  }
223
- const hasLive = ['state', 'home'].some((dir) => existsSync(join(this.o.root, dir)));
224
- if (!hasLive && previous)
261
+ const saved = this.collect();
262
+ // Live trees with nothing sealable are debris (crash leftovers, or only the caches a previous stop
263
+ // left in place) — keep the sealed credentials instead of replacing them with an empty snapshot.
264
+ // A running engine always leaves files behind (its SQLite stores), so a genuine sign-out still re-seals.
265
+ if (!saved.files.length && previous)
225
266
  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)
267
+ // The payload is built exactly once; verification decrypts the sealed bytes and compares to it.
268
+ const payload = JSON.stringify(saved);
269
+ const sealed = this.o.seal.encryptString(payload);
270
+ if (this.o.seal.decryptString(Buffer.from(sealed)) !== payload)
229
271
  throw new Error('credential seal verification failed');
230
272
  put(this.file, sealed);
231
- if (this.o.seal.decryptString(readFileSync(this.file)) !== text)
273
+ if (this.o.seal.decryptString(readFileSync(this.file)) !== payload)
232
274
  throw new Error('credential seal verification failed');
275
+ if (previous?.v === 1)
276
+ this.o.log?.('sealed credential store re-sealed: tool caches no longer sealed');
233
277
  put(this.cleanup, encoder.encode('1'));
234
- for (const dir of ['state', 'home'])
235
- rmSync(join(this.o.root, dir), { recursive: true, force: true });
278
+ this.remove(saved);
236
279
  removeMarker(this.cleanup);
237
280
  }
238
281
  async restore() {
@@ -252,7 +295,7 @@ export class AuthStore {
252
295
  removeMarker(this.restoring);
253
296
  }
254
297
  async archives() {
255
- const ignored = new Set([resolve(this.o.engineDir), ...['npm-cache', 'install-home', 'tmp', 'plugin', 'workspaces'].map((d) => resolve(this.o.root, d))]);
298
+ const ignored = new Set([resolve(this.o.engineDir), resolve(`${this.o.engineDir}.sets`), ...['npm-cache', 'install-home', 'tmp', 'plugin', 'workspaces'].map((d) => resolve(this.o.root, d))]);
256
299
  const walk = async (dir) => {
257
300
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
258
301
  const path = join(dir, entry.name);
package/dist/bridge.d.ts CHANGED
@@ -24,6 +24,7 @@ export declare function writePlugin(dir: string, o: {
24
24
  tools: ToolSpec[];
25
25
  paramPrefix: string;
26
26
  gateBuiltins: boolean;
27
+ browser?: boolean;
27
28
  }): void;
28
29
  export declare class Bridge {
29
30
  private readonly path;
@@ -33,7 +34,11 @@ export declare class Bridge {
33
34
  private readonly approvalTimeoutMs;
34
35
  private readonly onAsk;
35
36
  private readonly onAskGone;
37
+ private readonly beforeAgentRun?;
38
+ private readonly browserCapabilities?;
36
39
  private server?;
40
+ private closing;
41
+ private readonly sockets;
37
42
  private readonly runs;
38
43
  private readonly live;
39
44
  private readonly permits;
@@ -49,11 +54,14 @@ export declare class Bridge {
49
54
  approvalTimeoutMs: number;
50
55
  onAsk(a: Approval): void;
51
56
  onAskGone(id: string): void;
57
+ beforeAgentRun?: (key: string, runId?: string) => Promise<boolean>;
58
+ browserCapabilities?: () => readonly string[];
52
59
  });
53
60
  start(): Promise<void>;
54
61
  stop(): void;
55
- /** Register one run; the returned release ends only this registration (the key stays while another run holds it). */
56
- register(run: RunRef, tools?: readonly string[]): () => void;
62
+ isRegisteredRun(key: string, runId?: string): boolean;
63
+ /** Register one run; release revokes only this exact registration, even when sessions overlap. */
64
+ register(run: RunRef, tools?: readonly string[], runId?: string): () => void;
57
65
  unregister(sessionKey: string): void;
58
66
  allowOnce(rule: {
59
67
  keyPrefix: string;