@byokit/openclaw 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/README.md +101 -21
- package/SECURITY.md +6 -5
- package/dist/auth-status.d.ts +1 -0
- package/dist/auth-store.d.ts +30 -5
- package/dist/auth-store.js +245 -52
- package/dist/classify.js +2 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.js +17 -2
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/device.d.ts +1 -1
- package/dist/engine-patches.js +19 -13
- package/dist/engine.d.ts +5 -2
- package/dist/engine.js +111 -22
- package/dist/generated/methods.d.ts +7 -1
- package/dist/generated/params.d.ts +20 -0
- package/dist/generated/params.js +1 -1
- package/dist/generated/report.json +5 -5
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/kit.d.ts +6 -1
- package/dist/kit.js +45 -3
- package/dist/link.d.ts +1 -1
- package/dist/link.js +3 -3
- package/dist/members.d.ts +1 -0
- package/dist/members.js +12 -2
- package/dist/pick.d.ts +11 -0
- package/dist/pick.js +145 -0
- package/dist/signin.d.ts +1 -0
- package/dist/signin.js +13 -1
- package/dist/testing/fake-gateway.js +7 -1
- package/dist/types.d.ts +97 -1
- package/dist/words.js +3 -0
- package/dist/words.json +38 -1
- package/engine/package-lock.json +84 -84
- package/engine/package.json +1 -1
- package/engine/patches.json +17 -17
- package/package.json +8 -8
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,39 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.10.0 (2026-10-11)
|
|
6
|
+
|
|
7
|
+
- Dependency update: pins @byokit/discover 0.7.0.
|
|
8
|
+
|
|
9
|
+
- FIX (behavior change for installs with two or more members): a member no longer runs on another member's sign-in. The kit now forces `agents.defaults.authInheritance.agentId = 'byokit-base'` on every boot and config patch, an id it never creates, signs in to or runs, so the inherited base every agent reads through is empty. Before, the pinned engine made the first agent the inherited owner once a second agent was created; while an install's shared auth store had not yet moved into `state/openclaw.sqlite`, every later member's turns read that member's sign-ins through. From this release a member runs only on its own sign-ins: one that never signed in reads signed out, and an explicit pick of a provider it is not signed in to ends `{ ok: false, kind: 'signed-out' }` before any model request. `signOut(member, provider)` now passes `models.authLogout` the `profileIds` of that member agent's own profiles; without them the engine removed the provider from every owner store, the shared or inherited store other members read included. With none of its own it sends nothing. New members follow D9 (`/^[a-z](?!.*--)[a-z0-9-]{0,23}$/`, never starting `byokit-`); a member that already exists under the older rule, such as `a--b`, keeps working. `main`, `openclaw`, `crestodian` and `byokit-base` are never a member, even though `main` exists on every install: `ensureMember('main')` used to hand back the engine's own agent. The kit's fake gateway lists one `<provider>:default` profile per signed-in provider and checks `profileIds` on sign-out.
|
|
10
|
+
- FIX: restoring an engine home no longer pays one directory fsync per sealed object. `restore()` writes each plaintext file exactly as before (temp write, file fsync, atomic rename) but records the directories whose entries changed — the containing directory of every restored file and the parent of every directory it creates — and fsyncs each distinct directory once, after all files are written and before it removes the `restoring` marker. Every restored file is content-durable, and each rename and new directory entry is durable before the marker goes, so a crash mid-restore still keeps the sealed store intact. `seal()` and the stop path keep `put()` and its per-file directory fsync, byte-for-byte unchanged; nothing new is written to disk.
|
|
11
|
+
- FIX: A large engine home can no longer abort the process at seal time. AuthStore.collect() stays narrowed to credential state (credential paths under `state` plus non-cache `home` paths; tool caches are skipped; other `state` stores are sealed as objects outside the cap), and saved sign-in data is capped: over the fixed limit, `prepare()`, `start()`, `stop()` and an engine exit reject with exported `AuthStoreSealSizeError` (`code: 'auth-store-seal-size'`, fields `size`, a lower bound at the refusal, and `cap`). On that refusal the last good saved store is kept as it was and no live file is deleted; nothing else is promised. The kit reports `{ phase: 'failed', why: 'auth-store-seal-size', sealSize }` with words giving the size and limit in MB, and a refused `stop()` still releases the lock. `@byokit/secrets` encrypt() also emits its envelope through bounded chunk-wise escaping instead of a second full-size `JSON.stringify`, so `encrypt()` builds no snapshot-sized JSON string; the envelope is assembled as bytes, still transiently two byte copies. The README's include rule replaces the former "auth-store seal size demand" known limitation.
|
|
12
|
+
- FIX: an engine store that is already sealed unchanged in this process is no longer resealed on `stop()`, `prepare()` or an offline migration. `AuthStore` keeps an in-memory `object file name -> sha256 of the sealed plaintext payload` map, filled by `restore()` from each object it decrypts and refreshed after every successful per-object `seal()`; `sealObjects()` skips a re-seal when the object's plaintext hash is unchanged and its sealed file is still on disk, so the encrypt, verify decrypt and fsyncs are not repeated for transcripts and media the engine left alone. A changed object, a new object and a fresh process (empty map) seal exactly as before. The map is memory-only: no hash, size or mtime is written to disk or logs, so the at-rest contract (no plaintext, no new on-disk metadata) is unchanged.
|
|
13
|
+
- FIX: the credential seal no longer carries engine transcripts or other non-credential state. Credential files under `state` and `home` still go into the bounded `auth-store.sealed` blob, but every other file under `state` (the shared and per-agent SQLite databases with their WAL/SHM/journal sidecars, exported transcripts, legacy session stores, media, delivery-queue media, logs, caches and tmp) is sealed as its own encrypted file under `auth-store.objects/`, outside the 128 MiB total, and restored byte for byte on start; a single such file over 128 MiB refuses with `AuthStoreSealSizeError` naming it. After stop and remove no plaintext copy of those `state` files remains; regenerable caches under `home` stay on disk unsealed. A host whose transcripts and databases alone pass the cap now starts and stops normally instead of refusing with `AuthStoreSealSizeError`. Existing v1 snapshots that hold engine stores restore and reseal in the new layout. Rolling back to a kit older than this release keeps the blob credentials but not the engine databases (they are now objects), so the user signs in again. An over-cap engine store now reports its file name in the kit's refusal state, not as sign-in data.
|
|
14
|
+
- FIX: the engine is pinned to the OpenClaw 2026.8.35 extended-stable line, down from 2026.8.1 (and gateway-client back from the 2026.9.6 dependabot bump); `ENGINE_VERSION`, the shipped `engine/package.json` + integrity lock, `@openclaw/gateway-protocol`/`@openclaw/gateway-client` and the generated tables all carry 2026.8.35. The provider auth surface is byte-identical to 2026.8.1 (verified per-extension), so the route table is unchanged. Compatibility the new pin needs: the Workshop usage seam is matched by its stable filename prefix (the content hash changes per release), the pending package-lifecycle marker is completed with upstream's own postinstall while the set is writable (plugin installs disabled), and `openclaw.setup.activate.start` is registered in the runtime method table.
|
|
15
|
+
- FEAT: `@byokit/openclaw` names the multi-account shapes its later account surfaces build on — `Account`, `AccountId`, `AccountRef`, `SignInState`, `Room`, `RoomSpan`, `ModelInfo`, `RunSelection`, `Defaults`, `Considered`, `PickWhy`, `AccountPick` and `MoveResult` — restated structurally from `@byokit/accounts` (never imported), including the kit-only `bound`/`paid` failure states. The section 5.15 account words (Auto choice, room, pick explanations, reading age and the `account.*` refusals) join `words.json`, so they flow through the exact-sentence and plain-words tests.
|
|
16
|
+
- FEAT: `@byokit/openclaw` exports a pure start-of-run picker — `consider`, `chooseAccount` and `resolveSelection` — restated structurally from `@byokit/accounts`' `multi.ts` and never importing it. `resolveSelection` resolves `{ account: 'auto' | 'default' | id, model, needs }` with an optional kit-only `bound` list of the accounts a conversation may use, returning an `AccountPick` with its `considered` rows. Accounts outside `bound` are never candidates (a default outside `bound` is treated as not ready, its row `out: 'bound'`), and an explicit signed-out id in `bound` is returned as chosen and never replaced. The shared `fixtures/conformance/auto-pick-typescript.json` gains the kit-only bound rows, which `@byokit/accounts`' `multi.test.ts` skips.
|
|
17
|
+
- FIX: a failed engine install or post-build verification now retains a size-capped, environment-free diagnosis at
|
|
18
|
+
`<stateDir>/logs/engine-install-drift.json` before the failed temporary set is deleted: the first failed
|
|
19
|
+
root-manifest or package check (its path, expected vs actual version, or the read error), the npm path and
|
|
20
|
+
version, and the npm stderr tail. Previously the temp was deleted with the comparator's reason swallowed, so a
|
|
21
|
+
natural `drift-after-build` failure could not be diagnosed after the fact.
|
|
22
|
+
- FIX: explicitly offered sign-ins such as OpenRouter are no longer blocked by the engine plugin allow list. `KitOptions.offered` names the accounts an app offers; the kit adds the plugin of every route those name to `plugins.allow`, so an opted-in sign-in is not a dead end.
|
|
23
|
+
- FIX: the kit's `classify` now reads the ChatGPT/Codex "Next reset in N min/minutes/hour/hours" wording as well as "try again in ~N min/h", so `RunEnd.until` carries the stated reset for a subscription refusal instead of being empty; wording with no number stays without an `until` for the app's fallback.
|
|
24
|
+
- Dependency update: depend on `@byokit/pair` 0.9.0 instead of its deprecated `@byokit/link` shim.
|
|
25
|
+
|
|
26
|
+
## 0.9.0 (2026-10-09)
|
|
27
|
+
|
|
28
|
+
- Dependency update: pins @byokit/relay 0.6.0.
|
|
29
|
+
- Dependency update: pins @byokit/link 0.8.0.
|
|
30
|
+
- SECURITY: (from @byokit/pair 0.9.0) pinning and approval semantics. A compact QR's pending grant pins the key the code handshake authenticated, kept after Noise message 2 and before the device's identity goes out in message 3; if saving it fails, pairing stops before the computer learns the device. A device using `onPending` says so inside the handshake, and only then does the computer keep a yes given after its socket dropped, granting only the device key that handshake authenticated; any other device that leaves still gets no grant. A computer with another key at the same address is still refused, and a version 1 `pendingGrant` refuses a `host` that differs from its QR.
|
|
31
|
+
- FIX: (from @byokit/pair 0.9.0) a phone killed while the person at the computer decides now resumes against the pinned computer, for compact and version 1 QRs alike. `pairWithOffer` takes `onPending(grant)`, which hands over the pending grant to keep before the computer can approve this device, and `pendingGrant` accepts a compact QR given the authenticated `host` key, and `parseOffer` reads a compact QR for inspection (expiry, addresses, name, role) without one. Before, `pendingGrant` refused compact QRs, and even a version 1 pending grant never came online after a real kill. A socket that drops once the computer has taken the code now ends pairing with `unreachable` instead of trying the next address, where the spent code or ticket could only be refused as `wrong-code` or `expired`. A socket that closes while `onPending` is still saving ends pairing at once with that close; a version 1 `onPending` that never settles times out. The compact QR is unchanged: 109 characters for one relay address and the name Umer.
|
|
32
|
+
- FIX: (from @byokit/pair 0.9.0) the pairing picture's words match what the host now prints: scan the code, or type the shown code where the phone page asks for it, with no raw address to type.
|
|
33
|
+
- FIX: (from @byokit/pair 0.9.0) the browser pairing test closes only the Chromium processes it spawned — over that browser's own private CDP pipe — instead of signalling a whole process group, so a killed or timed-out test can no longer take out or leave behind the wrong process tree. `BYOKIT_CHROME` is the single documented key that selects the test browser.
|
|
34
|
+
|
|
35
|
+
- Cut first-install wall time ~3x (freeze ~20x) by dropping the 37,999 per-entry `fsync`s from `freeze()`: entries are chmodded read-only in bounded batches, one filesystem-wide `sync -f` flush runs where available (Linux, ~2 s measured vs ~110 s of per-file fsync transactions), and the manifest is read from the built tree once. Durability rule: `docs/runtime-kits.md` 5.16.
|
|
36
|
+
- FEAT: Name the engine's learning switch: `learning()`, `setLearning(mode | 'default')` and `restoreLearning(captured)` over the config key `skills.workshop.autonomous.mode` (`off | propose | auto`, engine default `auto` when absent), as a typed, file-backed veneer on the one-key pair — valid on a stopped home, before boot and after `stop()`. Absence is a value (`{ present: false }`); an unknown mode throws listing the accepted values, a stored value outside the enum makes `learning()` throw naming it, both writers throw when `KitOptions.config` also passes `skills`, and `restoreLearning` reads back and throws on mismatch. The README also documents the supported repair of a retained home killed without `stop()` (orphan gateway, stale lock, reseal, exit-78 doctor repair) and cross-references the separate auth-store seal size bound.
|
|
37
|
+
|
|
5
38
|
## 0.8.0 (2026-10-09)
|
|
6
39
|
|
|
7
40
|
- Forward actual thinking-token progress and report Gateway acceptance as `started`; reuse bounded, file-witnessed member admission status so ready runs avoid redundant sign-in checks without skipping authority checks. Device state uses the same live readiness boundary; `providerStatus` preserves unknown versus empty sign-in status.
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
</p>
|
|
8
8
|
|
|
9
9
|
<p align="center"><strong>The OpenClaw runtime kit: one object that runs the pinned engine for your app.</strong><br/>
|
|
10
|
-
It drives the pinned OpenClaw engine (<code>openclaw@2026.8.
|
|
10
|
+
It drives the pinned OpenClaw engine (<code>openclaw@2026.8.35</code>, protocol 4) through one kit object. The
|
|
11
11
|
aggregator's full operator surface stays available as typed pass-through calls (<code>call</code> for every operator
|
|
12
12
|
method, <code>callDynamic</code> for the rest), with plain-words helpers for members, sign-in, runs, approvals and
|
|
13
13
|
config. For apps built on OpenClaw, where the engine holds the subscriptions.</p>
|
|
@@ -20,7 +20,7 @@ npm install @byokit/openclaw
|
|
|
20
20
|
|
|
21
21
|
[](https://www.npmjs.com/package/@byokit/openclaw) · [Latest release](https://github.com/umeranjum17/byokit/releases?q=openclaw-v) · [All releases](https://github.com/umeranjum17/byokit/releases)
|
|
22
22
|
|
|
23
|
-
Node 22.18 or later. The kit brings the engine with it: the first `start()` installs the pinned `openclaw@2026.8.
|
|
23
|
+
Node 22.18 or later. The kit brings the engine with it: the first `start()` installs the pinned `openclaw@2026.8.35`
|
|
24
24
|
under your `stateDir` (a few minutes, needs the network once), and later starts run from there. For a whole app, a
|
|
25
25
|
phone page that pairs, signs in with ChatGPT, runs and answers approvals, see
|
|
26
26
|
[`examples/openclaw-kit`](../../examples/openclaw-kit).
|
|
@@ -30,7 +30,7 @@ phone page that pairs, signs in with ChatGPT, runs and answers approvals, see
|
|
|
30
30
|
`engine/patches.json` declares the exact upstream tarball integrity, commit, unique edits and before/after file
|
|
31
31
|
hashes. The Gateway Workshop review patch writes content-free, durable usage facts only when the kit sets its
|
|
32
32
|
accounting environment. The separately bundled worker and all other detached kinds remain uncovered.
|
|
33
|
-
The Claude CLI route also uses the bundled [Tooling-name correction](../../docs/runtime-kits.md#516-bundled-engine-patches-and-engine-started-usage);
|
|
33
|
+
The Claude CLI route also uses the bundled [Tooling-name correction](../../docs/runtime-kits.md#516-bundled-engine-patches-and-engine-started-usage-d19);
|
|
34
34
|
apps do not need consumer tool aliases.
|
|
35
35
|
The full upstream MIT notice ships in [engine/OPENCLAW-LICENSE](engine/OPENCLAW-LICENSE).
|
|
36
36
|
|
|
@@ -43,7 +43,10 @@ patch id (`null` after a provenance failure); a refusal is `why: 'engine-patch'`
|
|
|
43
43
|
|
|
44
44
|
Rollback selects a verified stock or older set; known sets and offline clones require no registry access.
|
|
45
45
|
Drift leaves the old bytes untouched and builds a replacement sibling; rebuilding stock may need the registry.
|
|
46
|
-
|
|
46
|
+
When an install or post-build verification fails, the kit retains a size-capped, environment-free diagnosis at
|
|
47
|
+
`<stateDir>/logs/engine-install-drift.json` — the first failed root-manifest or package check (path, expected vs
|
|
48
|
+
actual version, or the read error), the npm path and version, and the npm stderr tail — before the failed temporary
|
|
49
|
+
tree is deleted. Pre-change kits still run the untouched base install. No garbage collection: each retained set can occupy about
|
|
47
50
|
889 MB on this pin (reflinks may reduce physical storage). Whole-tree verification also adds preparation I/O;
|
|
48
51
|
read-only modes protect against accidental writes, not a malicious host app that owns the files.
|
|
49
52
|
|
|
@@ -93,7 +96,7 @@ async function dispatch(kit: OpenClawKit, taskId: string, attempt: number, messa
|
|
|
93
96
|
```
|
|
94
97
|
|
|
95
98
|
Omitting `idempotencyKey` preserves a fresh UUID per call. Supplied keys are non-empty strings passed unchanged.
|
|
96
|
-
**Not exactly-once:** engine 2026.8.
|
|
99
|
+
**Not exactly-once:** engine 2026.8.35 caches `agent` keys gateway-wide, across agents and sessions, without comparing
|
|
97
100
|
inputs. A collision silently returns the first run's result/error; a task id alone is not a safe key. Reconnects
|
|
98
101
|
retain the in-memory cache; engine process restarts lose it. Inactive entries expire after five minutes (60-second
|
|
99
102
|
cleanup tick), or earlier under the 1,000-entry cache limit; active and pending accepted runs are exempt.
|
|
@@ -185,7 +188,7 @@ RPC data is retained; existing typed `call` pass-through is unchanged. Windows a
|
|
|
185
188
|
refresh it. The engine response cache lasts 30 seconds; pending/stale/unknown data leaves `totals` absent.
|
|
186
189
|
An absent agent row is unavailable, not proof of zero usage. Reset/deletion/retention can reduce counters.
|
|
187
190
|
|
|
188
|
-
**Coverage is always `retained-transcripts-only`.** Stock 2026.8.
|
|
191
|
+
**Coverage is always `retained-transcripts-only`.** Stock 2026.8.35's detached Skill Workshop reviews are omitted
|
|
189
192
|
from its ledger; actual crash-recovery resumes and threshold memory flushes are counted through persisted
|
|
190
193
|
transcripts, never through a second side total. This reader cannot enforce a complete all-turn budget. The app owns budget,
|
|
191
194
|
share and unavailable/partial-data policy. Do not add run results to these totals: they already overlap. Engine
|
|
@@ -325,8 +328,7 @@ streams, [`examples/openclaw-kit`](../../examples/openclaw-kit) for a whole app,
|
|
|
325
328
|
|
|
326
329
|
Subscription sign-ins are offered by default. API-billed routes remain in `kit.routes()` with `offer: false`;
|
|
327
330
|
apps can offer them when the app or person opts in, labelled API key (billed per use). Each `Route` carries `billing` (`subscription`, `api` or
|
|
328
|
-
`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.
|
|
329
|
-
in the app configuration and pass its `choice` explicitly to `kit.signIn()`.
|
|
331
|
+
`local`) so the app can label what is billed. Proxy routes, compatibility aliases, local runtimes stay off. Native Claude Code sign-in is offered by default. The pinned Gateway guides only some routes; other choices need manual paste or key entry. Pass the accounts the app offers in `KitOptions.offered` (the provider, auth choice or alias each route carries, e.g. `'openrouter'`); the kit allow-lists the plugin of every route those name, so an opted-in sign-in is not blocked. An app can still name a plugin directly in `config.plugins.allow`, and pass the route's `choice` explicitly to `kit.signIn()`.
|
|
330
332
|
Offered provider plugins are allowed by default alongside the app's plugin ids.
|
|
331
333
|
|
|
332
334
|
## API at a glance
|
|
@@ -335,7 +337,7 @@ Entries:
|
|
|
335
337
|
|
|
336
338
|
- `.` is the host side: `OpenClawKit`, engine supervision, config invariants, approvals (Node).
|
|
337
339
|
- `./device` is the portable client for phones and browsers (no Node imports).
|
|
338
|
-
- `./link` is the host-side `@byokit/
|
|
340
|
+
- `./link` is the host-side `@byokit/pair` adapter: member-checked ops, sealed approval push (Node).
|
|
339
341
|
- `./testing` holds `fakeGateway`, the `openclawContract` suite and the scripted model stub.
|
|
340
342
|
|
|
341
343
|
| Export | What it does |
|
|
@@ -348,7 +350,7 @@ Entries:
|
|
|
348
350
|
| `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 |
|
|
349
351
|
| `LinkRefused`, `openNotice` (`./device`) | The host's own refusal as an error; opens a sealed approval notice |
|
|
350
352
|
| `words`, `stateWords`, `toAccountView` (`.`, `./device`) | The kit's sentences, so a phone shows the words the computer does; `toAccountView` feeds [`@byokit/ui`](../ui/README.md)'s `phaseOf` |
|
|
351
|
-
| `openclawLink(kit, o)` (`./link`) | `handle` / `stream` / `allow` for a `@byokit/
|
|
353
|
+
| `openclawLink(kit, o)` (`./link`) | `handle` / `stream` / `allow` for a `@byokit/pair` `Host`, checked per member, plus `onAction` for relay push actions. `OpenClawLinkOptions` / `OpenClawLinkHost` name both sides |
|
|
352
354
|
| `serve(o)` (`./link`) | Binds the link host per reach and returns its URLs (`OpenClawServeOptions` / `OpenClawServeHandle`) |
|
|
353
355
|
| `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` |
|
|
354
356
|
|
|
@@ -356,8 +358,8 @@ Entries:
|
|
|
356
358
|
|
|
357
359
|
Every method and event the pin publishes has a real type: `kit.call('sessions.list', params)` is checked in and
|
|
358
360
|
returns the engine's declared result, with no cast. For the slots the pin itself leaves undeclared the table stays
|
|
359
|
-
`unknown`, and `src/generated/report.json` names every one of them per release. On `openclaw@2026.8.
|
|
360
|
-
of
|
|
361
|
+
`unknown`, and `src/generated/report.json` names every one of them per release. On `openclaw@2026.8.35` that is 302
|
|
362
|
+
of 394 methods' params and 193 of their results, plus 21 of 55 event payloads; the other slots are not missing from
|
|
361
363
|
the kit, they are not published by the engine, and typing them here would be a guess that does not match the
|
|
362
364
|
gateway. `callDynamic` stays the same shape for a method the pin never declared.
|
|
363
365
|
|
|
@@ -391,6 +393,75 @@ back out), the write is atomic in `prepare()`'s exact shape, and the engine appl
|
|
|
391
393
|
your app does **not** also pass in `KitOptions.config`: that option is merged over the saved file on every boot and
|
|
392
394
|
wins. Don't interleave a narrow set with `patchConfig`, which rewrites the whole config through the Gateway.
|
|
393
395
|
|
|
396
|
+
## Engine learning state
|
|
397
|
+
|
|
398
|
+
The engine's learning switch is the config key `skills.workshop.autonomous.mode` (`off | propose | auto`; engine
|
|
399
|
+
default `auto` when absent). `off` keeps only the suggestion nudge, `propose` creates pending proposals, `auto`
|
|
400
|
+
applies captured proposals and runs the scanner-gated cleanup; the engine projects a system-owned cron job
|
|
401
|
+
`skill-collection-review-<agentId>` per workspace agent, enabled only on `auto`. The kit names it — file-backed like
|
|
402
|
+
the pair above, so it works on a stopped home, before boot and after `stop()`:
|
|
403
|
+
|
|
404
|
+
```ts
|
|
405
|
+
import { OpenClawKit } from '@byokit/openclaw';
|
|
406
|
+
|
|
407
|
+
const kit = new OpenClawKit({ stateDir: './openclaw-state' });
|
|
408
|
+
|
|
409
|
+
kit.learning(); // { present: false } | { present: true, mode: 'off' | 'propose' | 'auto' }
|
|
410
|
+
const before = kit.learning(); // capture before maintenance
|
|
411
|
+
kit.setLearning('off'); // returns the state it replaced
|
|
412
|
+
kit.setLearning('default'); // remove the key: the engine default (auto) applies again
|
|
413
|
+
kit.restoreLearning(before); // writes the capture, reads it back, throws on mismatch
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
Absence is a value (`{ present: false }`), never "probably default". An unknown mode throws listing the accepted
|
|
417
|
+
values (`off|propose|auto`, plus `default` for `setLearning`), a stored value outside the enum makes `learning()`
|
|
418
|
+
throw naming it, and both writers throw when the app also passes a `skills` key in `KitOptions.config` (that option
|
|
419
|
+
wins at every boot). A home that was never prepared surfaces the file read's ENOENT: `prepare()` once first.
|
|
420
|
+
|
|
421
|
+
## Retained home killed without stop()
|
|
422
|
+
|
|
423
|
+
A host crash or `kill -9` skips `stop()` cleanup. The next `prepare()` (the next `start()`) repairs the home
|
|
424
|
+
itself — no manual file surgery:
|
|
425
|
+
|
|
426
|
+
- It stops a leftover Gateway only after verifying it is a same-user orphan of this root (matching pid, identity,
|
|
427
|
+
start time, exe/cwd and env), and only when the auth-store lock's owner is dead; then it seals the leftover live
|
|
428
|
+
credential state (`state/` and the non-cache `home/` paths, see below) back into the store under the acquired
|
|
429
|
+
lock. Anything ambiguous (a live lock owner, an unverifiable pid, another writer alive, shutdown timeout) fails
|
|
430
|
+
closed with `EngineAlreadyRunningError` instead.
|
|
431
|
+
- A stale `auth-store.lock/` with a dead owner is recovered through a `recovery/` marker; interrupted
|
|
432
|
+
`cleanup`/`restoring` transitions are completed from the authenticated snapshot; empty live trees are treated as
|
|
433
|
+
debris.
|
|
434
|
+
- An engine exit code 78 means the engine asked for repair: the kit's own repair path runs
|
|
435
|
+
`doctor --fix --yes` with `doctorContext()`'s exact env.
|
|
436
|
+
|
|
437
|
+
What a killed home leaves behind: live plaintext `state/` and `home/` (never resealed), `auth-store.lock/` with a
|
|
438
|
+
dead owner pid, `gateway.pid` and `gateway.identity` (a still-live orphan Gateway is the dangerous case),
|
|
439
|
+
`openclaw.json` with its `.bak.*` journal, `engine-set`, `plugin/`, `workspaces/`, `usage/`, `tmp/`, `npm-cache/`
|
|
440
|
+
and `install-home/`. None of these is safe to edit by hand, and `auth-store.sealed` above all: never hand-edit it —
|
|
441
|
+
restore an authentic backup under the lock instead (see the credential sealing section). Keep backups before any
|
|
442
|
+
manual intervention.
|
|
443
|
+
|
|
444
|
+
Resealing after the kill seals every file under the engine `state` tree and the credential paths under `home`.
|
|
445
|
+
Credential files under `state` and `home` go into the bounded credential blob. Every other file under `state` is
|
|
446
|
+
sealed as its own object under `auth-store.objects/`, outside that bound: the shared and per-agent SQLite databases
|
|
447
|
+
(with their `-wal`/`-shm`/`-journal` sidecars, which mix credentials with session and transcript rows), exported
|
|
448
|
+
transcript artifacts (`state/transcripts`), legacy session stores (`state/sessions`, `state/agents/<agentId>/sessions`),
|
|
449
|
+
the media stores (`state/media`, `state/delivery-queue-media`), the control-UI and other caches (`state/cache`,
|
|
450
|
+
`state/completions`), logs (`state/logs`) and gateway temp/lock files (`state/tmp`). However large a long-used host's
|
|
451
|
+
transcripts and media grow in total, they never count toward the bound; each such file is capped individually at the same
|
|
452
|
+
limit (below). In `home` the XDG cache and npm cache homes
|
|
453
|
+
(`home/.cache`, `home/.npm`) and the transcript, log and cache subtrees of the CLIs the engine runs (`home/.codex`
|
|
454
|
+
sessions/log/cache/`.tmp`/`history.jsonl`, `home/.claude` projects/todos/shell-snapshots/statsig/file-history/`history.jsonl`)
|
|
455
|
+
stay on disk unsealed. Unknown paths are still sealed in the blob (a credential location we do not know about must
|
|
456
|
+
fail loudly, never drop silently). The blob is bounded: if the collected credential files together pass a fixed
|
|
457
|
+
limit, `prepare()`, `start()`, `stop()` and an engine exit reject with exported `AuthStoreSealSizeError`
|
|
458
|
+
(`code: 'auth-store-seal-size'`, fields `size` and `cap`). On that refusal the last good saved store is kept as it
|
|
459
|
+
was and no live file is deleted; nothing else is promised. A refused `stop()` still releases the lock. Its `size` is a
|
|
460
|
+
lower bound (the running total at the refusal). A single object file over the same limit refuses the same way, naming
|
|
461
|
+
the file in the error message, before any file is removed. Do not move or delete files under `state/` or `home/` to get under the
|
|
462
|
+
limit: the next successful seal would drop them. If your app hits this, report the error's size and cap to the byokit
|
|
463
|
+
maintainers; the limit was reached in real use.
|
|
464
|
+
|
|
394
465
|
## App-owned task recovery
|
|
395
466
|
|
|
396
467
|
If your app requeues interrupted tasks itself, declare their namespaces **before starting the kit**:
|
|
@@ -415,15 +486,16 @@ that decision. It needs the published kit's bundled engine patch, not a separate
|
|
|
415
486
|
|
|
416
487
|
## Phones and browsers
|
|
417
488
|
|
|
418
|
-
The host side serves member-checked ops over `@byokit/
|
|
489
|
+
The host side serves member-checked ops over `@byokit/pair`; the phone or browser uses the portable client.
|
|
490
|
+
(This kit depends on [`@byokit/pair`](../pair), and its examples import from there.)
|
|
419
491
|
|
|
420
492
|
```ts
|
|
421
|
-
import { Host, keyPair, type DeviceLink } from '@byokit/
|
|
493
|
+
import { Host, keyPair, type DeviceLink } from '@byokit/pair';
|
|
422
494
|
import { OpenClawKit } from '@byokit/openclaw';
|
|
423
495
|
import { openclawLink, serve } from '@byokit/openclaw/link';
|
|
424
496
|
import { openclawDevice } from '@byokit/openclaw/device';
|
|
425
497
|
|
|
426
|
-
// On the computer (Node): member-checked ops over @byokit/
|
|
498
|
+
// On the computer (Node): member-checked ops over @byokit/pair, served per reach.
|
|
427
499
|
declare const kit: OpenClawKit;
|
|
428
500
|
declare const ask: (question: string) => Promise<boolean>; // your own prompt
|
|
429
501
|
const api = openclawLink(kit, { memberOf: (grant) => (grant.meta as { member?: string } | undefined)?.member });
|
|
@@ -454,9 +526,14 @@ Route data follows the shared account-route vocabulary (D18 in [`docs/runtime-ki
|
|
|
454
526
|
|
|
455
527
|
The pinned engine has no supported hook for sealing OAuth profile writes. Its `auth-profiles` loader stores
|
|
456
528
|
credential JSON in agent SQLite databases and a shared state database, and doctor imports leave migration
|
|
457
|
-
archives. `authSeal` therefore protects credential state —
|
|
458
|
-
|
|
459
|
-
|
|
529
|
+
archives. `authSeal` therefore protects credential state — config/credential paths under `home` and every credential
|
|
530
|
+
path under the isolated `state` tree — in one bounded blob, `auth-store.sealed`. Every other file under `state`
|
|
531
|
+
(the shared state database `state/state/openclaw.sqlite` and the per-agent databases
|
|
532
|
+
`state/agents/<agentId>/agent/openclaw-agent.sqlite`, which mix credentials with transcript rows, plus transcripts,
|
|
533
|
+
media, logs, caches and session stores) is sealed as its own file under `auth-store.objects/`, outside the bound, so
|
|
534
|
+
a long-used host's total history never makes `start()` or `stop()` refuse; only a single file over the cap does. Regenerable tool caches in `home` stay on disk
|
|
535
|
+
unsealed. Only a credential blob, or a single engine store file, over the cap still refuses `start()` and `stop()` with
|
|
536
|
+
the typed size error instead of aborting the process. It uses the injected `SealingAdapter` from `@byokit/secrets`. `osKeyringSeal()` automatically
|
|
460
537
|
uses a persistent private file key for new stores when no non-interactive keyring is available.
|
|
461
538
|
Opening follows the saved envelope's mode. A locked or unresponsive keyring-only store leaves
|
|
462
539
|
`kit.state.phase === 'locked'`: `prepare()` and `start()` resolve, show `stateWords(kit.state)` for
|
|
@@ -497,13 +574,16 @@ every retained sealed migration archive before retiring an automatic key. Rotate
|
|
|
497
574
|
separately; dual-wrap rotation retains old wrapping keys for backups.
|
|
498
575
|
|
|
499
576
|
Without `authSeal`, engine credentials remain plaintext. With it, successful `prepare()` and `stop()` leave
|
|
500
|
-
only
|
|
501
|
-
disk unsealed. The adapter authenticates the snapshot
|
|
577
|
+
only sealed files: `auth-store.sealed` for the credential paths, and `auth-store.objects/` for the other engine stores
|
|
578
|
+
under `state`; regenerable caches under `home` stay on disk unsealed. The adapter authenticates the snapshot
|
|
502
579
|
before any restoration; a missing adapter rejects. A store the key cannot open (a wrong key,
|
|
503
580
|
damaged or tampered bytes) or whose payload is not a credential snapshot stays unchanged at
|
|
504
581
|
`auth-store.sealed`. Both `prepare()` and `start()` reject with exported `AuthStoreUnreadableError`
|
|
505
582
|
(`code: 'auth-store-unreadable'`, `reason: 'auth-failed' | 'invalid-snapshot'`). The kit reports
|
|
506
583
|
`{ phase: 'failed', why: 'auth-store-unreadable' }`; show `stateWords(kit.state)`, not a sign-in prompt.
|
|
584
|
+
Saved sign-in data over the cap rejects with `AuthStoreSealSizeError`, and the kit reports
|
|
585
|
+
`{ phase: 'failed', why: 'auth-store-seal-size', sealSize: { size, cap, file? } }` (`file` is set when an engine store is the one over the cap); the refusal rules are in
|
|
586
|
+
[Retained home killed without stop()](#retained-home-killed-without-stop).
|
|
507
587
|
Restore access to the original seal/key (and the original service/stateDir binding if the adapter uses
|
|
508
588
|
one), then retry `start()` with that adapter. Do not generate/rotate a key to repair an unreadable store.
|
|
509
589
|
If the bytes are damaged, stop all writers, acquire the host writer lock, and restore an authentic
|
|
@@ -553,7 +633,7 @@ retained-login cleanup and sealed approval limits.
|
|
|
553
633
|
|
|
554
634
|
## Status
|
|
555
635
|
|
|
556
|
-
Pinned to OpenClaw `2026.8.
|
|
636
|
+
Pinned to OpenClaw `2026.8.35` (protocol 4); `ENGINE_VERSION` and `PROTOCOL_VERSION` carry the pin. Signatures are
|
|
557
637
|
frozen (docs/runtime-kits.md §5).
|
|
558
638
|
|
|
559
639
|
## Links
|
package/SECURITY.md
CHANGED
|
@@ -41,7 +41,7 @@ for the public error and host repair procedure. Other sealing failures still rej
|
|
|
41
41
|
| Gateway token | `openclaw/token` | Random 32-byte bearer secret; newly created file is 0600. Treat it as operator access. |
|
|
42
42
|
| 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. |
|
|
43
43
|
| 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. |
|
|
44
|
-
| Sealed credential snapshot | `openclaw/auth-store.sealed` and retired `*.sealed` archives | With host-injected `authSeal`, authenticates and encrypts credential state (
|
|
44
|
+
| Sealed credential snapshot | `openclaw/auth-store.sealed` and retired `*.sealed` archives | With host-injected `authSeal`, authenticates and encrypts credential state (credential paths under `state` plus config/credential paths under `home`) while stopped; other engine `state` stores are sealed as objects under `auth-store.objects/`; regenerable caches stay on disk unsealed. Key security belongs to the adapter/host; an older authentic snapshot can be replayed. |
|
|
45
45
|
| 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. |
|
|
46
46
|
| 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. |
|
|
47
47
|
|
|
@@ -52,8 +52,8 @@ collects regular files and file symlinks whose fully resolved targets are regula
|
|
|
52
52
|
files within the isolated engine root; links restore as regular files at the link
|
|
53
53
|
paths. Outside-root, dangling and directory links, sockets, FIFOs and devices are
|
|
54
54
|
skipped without reading their contents. Collected POSIX modes are normalized;
|
|
55
|
-
other paths still need host protection.
|
|
56
|
-
|
|
55
|
+
other paths still need host protection. Regenerable caches under `home` stay on disk
|
|
56
|
+
unsealed after successful sealing; other skipped entries are not preserved in the snapshot or the objects. Never use a shared/writable tree or point
|
|
57
57
|
the kit at another product's state. Review existing permissions before adopting a
|
|
58
58
|
tree; use an OS-protected app directory, encrypted disk and restricted backups.
|
|
59
59
|
Do not log `doctorContext().env`, transport arguments, auth records or sign-in
|
|
@@ -61,8 +61,9 @@ callbacks. JavaScript strings and engine memory are not reliably zeroized.
|
|
|
61
61
|
|
|
62
62
|
The engine needs plaintext credentials and PEM during operation. With `authSeal`
|
|
63
63
|
(a host-injected `SealingAdapter` from `@byokit/secrets`), `src/auth-store.ts` seals
|
|
64
|
-
credential state —
|
|
65
|
-
|
|
64
|
+
credential state — credential paths under `state` plus config/credential paths under `home`, including SQLite journals — into one bounded blob,
|
|
65
|
+
and every other engine `state` store into its own object under `auth-store.objects/`, on successful `prepare()` and after
|
|
66
|
+
the engine exits on `stop()`; regenerable caches stay on disk unsealed. The scope is defined in [credential sealing](README.md#credential-sealing-and-threat-model). It verifies the adapter round-trip, writes via an
|
|
66
67
|
exclusive temporary file, fsyncs/renames the snapshot and verifies it again before
|
|
67
68
|
removing live plaintext. `start()` authenticates and validates snapshot paths before
|
|
68
69
|
restoring files. Missing/wrong keys or tampering reject without plaintext fallback.
|
package/dist/auth-status.d.ts
CHANGED
package/dist/auth-store.d.ts
CHANGED
|
@@ -1,20 +1,37 @@
|
|
|
1
1
|
import type { SealingAdapter } from '@byokit/secrets';
|
|
2
|
-
/** True for the
|
|
3
|
-
export declare function
|
|
2
|
+
/** True for the non-credential paths: under `home` they are never sealed; under `state` they are sealed as objects. */
|
|
3
|
+
export declare function nonCredential(name: string): boolean;
|
|
4
4
|
/** Opening failed; the original snapshot stays in place for repair and retry. */
|
|
5
5
|
export declare class AuthStoreUnreadableError extends Error {
|
|
6
6
|
readonly code = "auth-store-unreadable";
|
|
7
7
|
readonly reason: 'auth-failed' | 'invalid-snapshot';
|
|
8
8
|
constructor(reason: 'auth-failed' | 'invalid-snapshot');
|
|
9
9
|
}
|
|
10
|
+
/** Total raw credential bytes one snapshot may seal. The sealed payload is one runtime string, so collect()
|
|
11
|
+
* refuses a larger snapshot before reading it and the process never aborts inside the sealer. Credential
|
|
12
|
+
* state is normally a few MiB; the cap leaves several times that headroom. The same bound applies to each
|
|
13
|
+
* non-credential engine store under `state` sealed as its own object; their total is not bounded. */
|
|
14
|
+
export declare const SEAL_CAP_BYTES: number;
|
|
15
|
+
/** The saved sign-in data (or one object file, when `file` names it) is over `SEAL_CAP_BYTES`. On a refusal the
|
|
16
|
+
* last good saved store is kept as it was and no live file is deleted. `size` is a lower bound for the whole
|
|
17
|
+
* store (the running total when the cap was crossed) and the exact size when `file` is set. */
|
|
18
|
+
export declare class AuthStoreSealSizeError extends Error {
|
|
19
|
+
readonly code = "auth-store-seal-size";
|
|
20
|
+
readonly size: number;
|
|
21
|
+
readonly cap: number;
|
|
22
|
+
readonly file?: string;
|
|
23
|
+
constructor(size: number, cap: number, file?: string);
|
|
24
|
+
}
|
|
10
25
|
export declare class AuthStore {
|
|
11
26
|
private active;
|
|
12
27
|
private owned;
|
|
13
28
|
private queue;
|
|
14
29
|
private readonly file;
|
|
30
|
+
private readonly objects;
|
|
15
31
|
private readonly lock;
|
|
16
32
|
private readonly cleanup;
|
|
17
33
|
private readonly restoring;
|
|
34
|
+
private readonly objectHashes;
|
|
18
35
|
constructor(privateOptions: {
|
|
19
36
|
root: string;
|
|
20
37
|
stateDir: string;
|
|
@@ -26,13 +43,21 @@ export declare class AuthStore {
|
|
|
26
43
|
private serial;
|
|
27
44
|
private acquire;
|
|
28
45
|
private release;
|
|
46
|
+
private open;
|
|
47
|
+
private seal;
|
|
29
48
|
private read;
|
|
49
|
+
private readObject;
|
|
50
|
+
private inventory;
|
|
30
51
|
private collect;
|
|
31
|
-
/** Remove the live
|
|
32
|
-
*
|
|
33
|
-
* and the directories still holding them are left in place. */
|
|
52
|
+
/** Remove the live sealed trees: all of `state` (every file there is sealed, as a blob entry or an object),
|
|
53
|
+
* and in `home` exactly the sealed paths — regenerable caches and the directories still holding them are left in place. */
|
|
34
54
|
private remove;
|
|
55
|
+
private sealObjects;
|
|
35
56
|
private persist;
|
|
57
|
+
/** `mkdir -p` for a restore target that records the parent of every directory it creates: a new directory
|
|
58
|
+
* changes its parent's entries, so the parent needs a directory fsync too. Stops at the store root (it exists)
|
|
59
|
+
* and never fsyncs now; restore() fsyncs the collected directories once after all files are written. */
|
|
60
|
+
private ensureRestoreDirs;
|
|
36
61
|
private restore;
|
|
37
62
|
archives(): Promise<void>;
|
|
38
63
|
prepare(): Promise<void>;
|