@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 +12 -0
- package/README.md +51 -0
- package/dist/engine-patches.js +19 -13
- package/dist/index.d.ts +1 -1
- package/dist/kit.d.ts +5 -1
- package/dist/kit.js +35 -0
- package/dist/types.d.ts +9 -0
- package/package.json +4 -4
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**:
|
package/dist/engine-patches.js
CHANGED
|
@@ -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
|
-
|
|
185
|
-
//
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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.
|
|
64
|
-
"@byokit/relay": "0.
|
|
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.
|
|
70
|
+
"ws": "8.22.0"
|
|
71
71
|
},
|
|
72
72
|
"devDependencies": {
|
|
73
73
|
"@byokit/ui-core": "0.6.0"
|