@byokit/openclaw 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.9.0 (2026-10-09)
6
+
7
+ - Dependency update: pins @byokit/relay 0.6.0.
8
+ - Dependency update: pins @byokit/link 0.8.0.
9
+ - 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.
10
+ - 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.
11
+ - 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.
12
+ - 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.
13
+
14
+ - 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.
15
+ - 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.
16
+
5
17
  ## 0.8.0 (2026-10-09)
6
18
 
7
19
  - 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
@@ -391,6 +391,57 @@ back out), the write is atomic in `prepare()`'s exact shape, and the engine appl
391
391
  your app does **not** also pass in `KitOptions.config`: that option is merged over the saved file on every boot and
392
392
  wins. Don't interleave a narrow set with `patchConfig`, which rewrites the whole config through the Gateway.
393
393
 
394
+ ## Engine learning state
395
+
396
+ The engine's learning switch is the config key `skills.workshop.autonomous.mode` (`off | propose | auto`; engine
397
+ default `auto` when absent). `off` keeps only the suggestion nudge, `propose` creates pending proposals, `auto`
398
+ applies captured proposals and runs the scanner-gated cleanup; the engine projects a system-owned cron job
399
+ `skill-collection-review-<agentId>` per workspace agent, enabled only on `auto`. The kit names it — file-backed like
400
+ the pair above, so it works on a stopped home, before boot and after `stop()`:
401
+
402
+ ```ts
403
+ import { OpenClawKit } from '@byokit/openclaw';
404
+
405
+ const kit = new OpenClawKit({ stateDir: './openclaw-state' });
406
+
407
+ kit.learning(); // { present: false } | { present: true, mode: 'off' | 'propose' | 'auto' }
408
+ const before = kit.learning(); // capture before maintenance
409
+ kit.setLearning('off'); // returns the state it replaced
410
+ kit.setLearning('default'); // remove the key: the engine default (auto) applies again
411
+ kit.restoreLearning(before); // writes the capture, reads it back, throws on mismatch
412
+ ```
413
+
414
+ Absence is a value (`{ present: false }`), never "probably default". An unknown mode throws listing the accepted
415
+ values (`off|propose|auto`, plus `default` for `setLearning`), a stored value outside the enum makes `learning()`
416
+ throw naming it, and both writers throw when the app also passes a `skills` key in `KitOptions.config` (that option
417
+ wins at every boot). A home that was never prepared surfaces the file read's ENOENT: `prepare()` once first.
418
+
419
+ ## Retained home killed without stop()
420
+
421
+ A host crash or `kill -9` skips `stop()` cleanup. The next `prepare()` (the next `start()`) repairs the home
422
+ itself — no manual file surgery:
423
+
424
+ - It stops a leftover Gateway only after verifying it is a same-user orphan of this root (matching pid, identity,
425
+ start time, exe/cwd and env), and only when the auth-store lock's owner is dead; then it seals the leftover live
426
+ `state/` and `home/` trees back into the store under the acquired lock. Anything ambiguous (a live lock owner, an
427
+ unverifiable pid, another writer alive, shutdown timeout) fails closed with `EngineAlreadyRunningError` instead.
428
+ - A stale `auth-store.lock/` with a dead owner is recovered through a `recovery/` marker; interrupted
429
+ `cleanup`/`restoring` transitions are completed from the authenticated snapshot; empty live trees are treated as
430
+ debris.
431
+ - An engine exit code 78 means the engine asked for repair: the kit's own repair path runs
432
+ `doctor --fix --yes` with `doctorContext()`'s exact env.
433
+
434
+ What a killed home leaves behind: live plaintext `state/` and `home/` (never resealed), `auth-store.lock/` with a
435
+ dead owner pid, `gateway.pid` and `gateway.identity` (a still-live orphan Gateway is the dangerous case),
436
+ `openclaw.json` with its `.bak.*` journal, `engine-set`, `plugin/`, `workspaces/`, `usage/`, `tmp/`, `npm-cache/`
437
+ and `install-home/`. None of these is safe to edit by hand, and `auth-store.sealed` above all: never hand-edit it —
438
+ restore an authentic backup under the lock instead (see the credential sealing section). Keep backups before any
439
+ manual intervention.
440
+
441
+ If a leftover `home/` carries large regenerable tool caches, resealing can exceed the runtime string bound and
442
+ abort; that seal size bound is a separate, known limitation (tracked as the auth-store seal size demand), not
443
+ fixed by the recovery path — move only regenerable cache trees aside (keeping backups) and retry `start()`.
444
+
394
445
  ## App-owned task recovery
395
446
 
396
447
  If your app requeues interrupted tasks itself, declare their namespaces **before starting the kit**:
@@ -1,4 +1,5 @@
1
1
  // Internal artifact seam. Published patch semantics live only in engine/patches.json.
2
+ import { spawn } from 'node:child_process';
2
3
  import { createHash, randomUUID } from 'node:crypto';
3
4
  import { closeSync, constants, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs';
4
5
  import { chmod, cp, open, readdir, rm } from 'node:fs/promises';
@@ -180,22 +181,27 @@ async function removeTemp(dir) {
180
181
  await walk(dir);
181
182
  await rm(dir, { recursive: true, force: true });
182
183
  }
184
+ const flushFs = (dir) => new Promise(resolve => {
185
+ // One filesystem-wide syncfs through `sync -f` (~2 s vs ~110 s of per-file fsyncs on the reference btrfs host);
186
+ // durability rule: docs/runtime-kits.md 5.16.
187
+ if (process.platform !== 'linux')
188
+ return resolve();
189
+ const child = spawn('sync', ['-f', dir], { stdio: 'ignore' });
190
+ child.once('error', () => resolve());
191
+ child.once('exit', () => resolve());
192
+ });
193
+ // Freeze makes the tree read-only and returns its entries as the manifest; durability rule: docs/runtime-kits.md 5.16.
183
194
  async function freeze(dir) {
184
- const entries = tree(dir);
185
- // Bound descriptors/I/O while yielding the host event loop. Every regular file and directory is fsynced.
195
+ // Bound I/O while yielding the host event loop: chmod only, in batches. Links are left untouched (and keep
196
+ // their recorded mode); every file becomes 0444 and every directory 0555, exactly what the manifest records.
197
+ const entries = tree(dir).map(entry => entry.kind === 'link' ? entry : { ...entry, mode: entry.kind === 'dir' ? 0o555 : 0o444 });
186
198
  const paths = [...entries.filter(e => e.kind === 'file'), ...entries.filter(e => e.kind === 'dir').reverse()];
187
199
  for (let i = 0; i < paths.length; i += 16)
188
200
  await Promise.all(paths.slice(i, i + 16).map(async (entry) => {
189
- const path = join(dir, entry.path);
190
- await chmod(path, entry.kind === 'dir' ? 0o555 : 0o444);
191
- const fd = await open(path, 'r');
192
- try {
193
- await fd.sync();
194
- }
195
- finally {
196
- await fd.close();
197
- }
201
+ await chmod(join(dir, entry.path), entry.mode);
198
202
  }));
203
+ await flushFs(dir);
204
+ return entries;
199
205
  }
200
206
  export function engineSetName(set) { return `${sha256(set.upstream.integrity).slice(0, 16)}-${set.id}`; }
201
207
  export async function prepareEngineSet(engineDir, set, install, installMatches) {
@@ -257,8 +263,8 @@ export async function prepareEngineSet(engineDir, set, install, installMatches)
257
263
  atomic(path, after);
258
264
  }
259
265
  metadata(tmp, set);
260
- await freeze(tmp);
261
- const bytes = JSON.stringify(tree(tmp));
266
+ const entries = await freeze(tmp);
267
+ const bytes = JSON.stringify(entries);
262
268
  // Root alone is writable while publishing metadata, then made read-only too.
263
269
  await chmod(tmp, 0o700);
264
270
  atomic(join(tmp, '.byokit-tree'), bytes);
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { ENGINE_VERSION, OPERATOR_SCOPES, PROTOCOL_VERSION } from './constants.ts';
2
- export type { Approval, CallOptions, Decision, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, GateResult, Hello, KitState, KitEventName, KitEventPayload, BrowserPing, Member, PlanWindow, Route, RunEnd, RunEvent, RunRef, RunSpec, OutputSchema, SchemaOutput, RunUsage, SignInOptions, SignInView, ToolHost, ToolSpec, } from './types.ts';
2
+ export type { Approval, CallOptions, Decision, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, GateResult, Hello, KitState, KitEventName, KitEventPayload, BrowserPing, LearningCapture, LearningMode, Member, PlanWindow, Route, RunEnd, RunEvent, RunRef, RunSpec, OutputSchema, SchemaOutput, RunUsage, SignInOptions, SignInView, ToolHost, ToolSpec, } from './types.ts';
3
3
  export type { GatewayMethods } from './generated/methods.ts';
4
4
  export type { JsonValue } from './output.ts';
5
5
  export type { RouteFacts, RouteView } from './routes.ts';
package/dist/kit.d.ts CHANGED
@@ -2,7 +2,7 @@ import type { SealingAdapter } from '@byokit/secrets';
2
2
  import { type AddKeyResult } from './keys.ts';
3
3
  import { type RouteView } from './routes.ts';
4
4
  import type { BrowserHost, BrowserOptions } from './browser.ts';
5
- import type { Approval, CallOptions, Decision, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, Hello, KitState, KitEventName, KitEventPayload, Member, RunEnd, RunEvent, RunSpec, OutputSchema, SchemaOutput, SignInOptions, SignInView, ToolHost, ToolSpec } from './types.ts';
5
+ import type { Approval, CallOptions, Decision, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, Hello, KitState, KitEventName, KitEventPayload, LearningCapture, LearningMode, Member, RunEnd, RunEvent, RunSpec, OutputSchema, SchemaOutput, SignInOptions, SignInView, ToolHost, ToolSpec } from './types.ts';
6
6
  export type KitOptions = {
7
7
  stateDir: string;
8
8
  authSeal?: SealingAdapter;
@@ -142,6 +142,10 @@ export declare class OpenClawKit {
142
142
  memoryLimited(member: Member): boolean;
143
143
  getConfigKey(key: string): unknown;
144
144
  setConfigKey(key: string, value: unknown): unknown;
145
+ learning(): LearningCapture;
146
+ setLearning(mode: LearningMode | 'default'): LearningCapture;
147
+ restoreLearning(captured: LearningCapture): LearningCapture;
148
+ private assertLearningWritable;
145
149
  doctorContext(): {
146
150
  entry: string;
147
151
  env: Record<string, string>;
package/dist/kit.js CHANGED
@@ -21,6 +21,8 @@ import { reconcileConfig, browserToolPolicySafe, browserProfileAcknowledged, bro
21
21
  import { createBrowserHost } from "./browser/host.js";
22
22
  import { fileSignInStore } from "./browser/store.js";
23
23
  import { launchBroker } from "./browser/broker.js";
24
+ /** The engine's learning-mode config key (5.6 "Engine learning state"). */
25
+ const LEARNING_KEY = 'skills.workshop.autonomous.mode';
24
26
  const operatorMethods = new Set([
25
27
  'agent',
26
28
  'agent.identity.get',
@@ -1042,5 +1044,38 @@ export class OpenClawKit {
1042
1044
  atomic(path, next);
1043
1045
  return previous;
1044
1046
  }
1047
+ // The engine's learning switch, named: a typed veneer over the narrow pair above, file-backed and boot-free
1048
+ // (5.6 "Engine learning state"). The engine itself falls back silently on a stored value outside its enum;
1049
+ // these refuse, so a typo or a corrupted file surfaces instead of quietly flipping learning.
1050
+ learning() {
1051
+ const value = this.getConfigKey(LEARNING_KEY);
1052
+ if (value === undefined)
1053
+ return { present: false };
1054
+ if (value !== 'off' && value !== 'propose' && value !== 'auto')
1055
+ throw new Error(`learning: stored ${LEARNING_KEY} is ${JSON.stringify(value)}, not one of off|propose|auto`);
1056
+ return { present: true, mode: value };
1057
+ }
1058
+ setLearning(mode) {
1059
+ if (mode !== 'off' && mode !== 'propose' && mode !== 'auto' && mode !== 'default')
1060
+ throw new Error(`setLearning: unknown learning mode ${JSON.stringify(mode)}, expected off|propose|auto|default`);
1061
+ this.assertLearningWritable('setLearning');
1062
+ const previous = this.learning();
1063
+ this.setConfigKey(LEARNING_KEY, mode === 'default' ? undefined : mode);
1064
+ return previous;
1065
+ }
1066
+ restoreLearning(captured) {
1067
+ if (captured.present && captured.mode !== 'off' && captured.mode !== 'propose' && captured.mode !== 'auto')
1068
+ throw new Error(`restoreLearning: unknown learning mode ${JSON.stringify(captured.mode)}, expected off|propose|auto`);
1069
+ this.assertLearningWritable('restoreLearning');
1070
+ this.setConfigKey(LEARNING_KEY, captured.present ? captured.mode : undefined);
1071
+ const read = this.learning();
1072
+ if (read.present !== captured.present || (read.present && captured.present && read.mode !== captured.mode))
1073
+ throw new Error(`learning restore readback mismatch: captured ${JSON.stringify(captured)}, read ${JSON.stringify(read)}`);
1074
+ return read;
1075
+ }
1076
+ assertLearningWritable(op) {
1077
+ if (this.o.config?.skills !== undefined)
1078
+ throw new Error(`${op}: KitOptions.config also passes skills; that option wins at every boot (5.6)`);
1079
+ }
1045
1080
  doctorContext() { return this.engine.doctorContext(); }
1046
1081
  }
package/dist/types.d.ts CHANGED
@@ -2,6 +2,15 @@ import type { OutputSchema } from './output.ts';
2
2
  import type { GatewayEventName, GatewayEventPayload } from './generated/events.ts';
3
3
  export type { OutputSchema, SchemaOutput } from './output.ts';
4
4
  export type Member = string;
5
+ /** The engine's learning mode: config key `skills.workshop.autonomous.mode` (5.6). */
6
+ export type LearningMode = 'off' | 'propose' | 'auto';
7
+ /** Exactly what `learning()` saw: absence is a value, not "probably default" (5.6). */
8
+ export type LearningCapture = {
9
+ present: false;
10
+ } | {
11
+ present: true;
12
+ mode: LearningMode;
13
+ };
5
14
  export interface ToolSpec {
6
15
  name: string;
7
16
  description: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byokit/openclaw",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "The OpenClaw runtime kit: the pinned engine's full operator surface with plain-words helpers for members, sign-in, runs and approvals.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -60,14 +60,14 @@
60
60
  "prepack": "tsc -b && node ../../scripts/fix-words-dts.cjs"
61
61
  },
62
62
  "dependencies": {
63
- "@byokit/link": "0.7.1",
64
- "@byokit/relay": "0.5.3",
63
+ "@byokit/link": "0.8.0",
64
+ "@byokit/relay": "0.6.0",
65
65
  "@byokit/discover": "0.6.0",
66
66
  "@byokit/seal": "0.3.0",
67
67
  "@byokit/secrets": "0.5.0",
68
68
  "@openclaw/gateway-client": "2026.9.6",
69
69
  "@openclaw/gateway-protocol": "2026.8.1",
70
- "ws": "8.21.3"
70
+ "ws": "8.22.0"
71
71
  },
72
72
  "devDependencies": {
73
73
  "@byokit/ui-core": "0.6.0"