@byokit/openclaw 0.9.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 +21 -0
- package/README.md +55 -26
- 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.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 +1 -0
- package/dist/kit.js +10 -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 +88 -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 +6 -6
package/dist/auth-store.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
// The pin persists OAuth JSON in both shared and agent SQLite. There is no public persistence hook.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// Credential state under `home` and `state` is sealed as one bounded blob (`auth-store.sealed`, capped at
|
|
3
|
+
// SEAL_CAP_BYTES, refusing with a typed error). Every other file under `state` — the SQLite databases, which mix
|
|
4
|
+
// credentials with transcript rows, and the regenerable caches, transcripts, media, logs and session stores beside
|
|
5
|
+
// them — is sealed as its own file under `auth-store.objects/`. Each such file is capped individually at
|
|
6
|
+
// SEAL_CAP_BYTES; the total across objects is not, so growth in aggregate never refuses.
|
|
5
7
|
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, writeFileSync, closeSync, fsyncSync, openSync } from 'node:fs';
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
6
9
|
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
7
10
|
import { EngineAlreadyRunningError, pidAlive as live } from "./engine-status.js";
|
|
8
11
|
const encoder = new TextEncoder();
|
|
9
|
-
const archive = (name) => /^(auth-profiles|auth-state|auth|oauth)\.json\.(migrated-.+|sqlite-import\..+\.bak)$/.test(name)
|
|
10
|
-
|| name.endsWith('.moved-to-engine');
|
|
12
|
+
const archive = (name) => !name.endsWith('.sealed') && (/^(auth-profiles|auth-state|auth|oauth)\.json\.(migrated-.+|sqlite-import\..+\.bak)$/.test(name)
|
|
13
|
+
|| name.endsWith('.moved-to-engine'));
|
|
11
14
|
function regular(path) {
|
|
12
15
|
if (!lstatSync(path).isFile())
|
|
13
16
|
throw new Error(`credential store requires regular files: ${path}`);
|
|
@@ -47,20 +50,72 @@ function put(path, bytes) {
|
|
|
47
50
|
rmSync(tmp, { force: true });
|
|
48
51
|
}
|
|
49
52
|
}
|
|
53
|
+
// Restore-only variant of put(): the same temp write, file fsync and atomic rename, but it records the
|
|
54
|
+
// containing directory in `changed` instead of fsyncing it now. restore() fsyncs each distinct directory
|
|
55
|
+
// once after every file is written, so a restore still makes each rename durable before it removes the
|
|
56
|
+
// restoring marker, without one directory fsync per object. seal() and the stop path keep put().
|
|
57
|
+
function putRestore(path, bytes, changed) {
|
|
58
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
59
|
+
const tmp = `${path}.sealing-${process.pid}`;
|
|
60
|
+
try {
|
|
61
|
+
writeFileSync(tmp, bytes, { mode: 0o600, flag: 'wx' });
|
|
62
|
+
const fd = openSync(tmp, 'r');
|
|
63
|
+
try {
|
|
64
|
+
fsyncSync(fd);
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
closeSync(fd);
|
|
68
|
+
}
|
|
69
|
+
renameSync(tmp, path);
|
|
70
|
+
changed.add(dirname(path));
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
rmSync(tmp, { force: true });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
50
76
|
// Credential state is what restores a working signed-in session. Regenerable tool caches, transcripts
|
|
51
|
-
// and logs never do
|
|
52
|
-
// transcript/log/cache subtrees of the Codex and Claude Code CLIs it runs
|
|
77
|
+
// and logs never do. In `home`: the XDG cache and npm cache homes the kit's engine environment pins, plus
|
|
78
|
+
// the transcript/log/cache subtrees of the Codex and Claude Code CLIs it runs; those stay on disk unsealed.
|
|
79
|
+
// In the isolated engine `state` tree the pinned engine (2026.8.1, `resolveStateDatabasePath`/`resolveOpenClawAgentSqlitePath`/
|
|
80
|
+
// `resolveOAuthDir`) keeps credentials in the shared and per-agent SQLite databases and `credentials/`, and
|
|
81
|
+
// writes non-credential data beside them: the caches (`cache/` control-UI assets, shell snapshots and
|
|
82
|
+
// worker bundles, `completions/`), exported transcript artifacts (`transcripts/`), legacy session stores
|
|
83
|
+
// (`sessions/`, `agents/<agentId>/sessions/`), the media stores (`media/`, `delivery-queue-media/`),
|
|
84
|
+
// logs (`logs/`) and gateway temp/lock files (`tmp/`). Unknown paths stay
|
|
53
85
|
// sealed — a credential location we do not know about must fail loudly, never drop silently.
|
|
54
86
|
const HOME_CACHES = {
|
|
55
87
|
home: ['.cache', '.npm'],
|
|
56
88
|
'home/.codex': ['sessions', 'log', 'cache', '.tmp', 'history.jsonl'],
|
|
57
89
|
'home/.claude': ['projects', 'todos', 'shell-snapshots', 'statsig', 'file-history', 'history.jsonl'],
|
|
58
90
|
};
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
91
|
+
// Positively non-credential subtrees directly under the isolated engine `state` tree. They are sealed as
|
|
92
|
+
// separate objects, never in the credential blob.
|
|
93
|
+
const STATE_NON_CREDENTIAL = new Set(['cache', 'completions', 'transcripts', 'sessions', 'media', 'delivery-queue-media', 'logs', 'tmp']);
|
|
94
|
+
// Legacy per-agent session migration sources and archives.
|
|
95
|
+
const AGENT_SESSIONS = /^state\/agents\/[^/]+\/sessions(\/|$)/;
|
|
96
|
+
// The engine's SQLite databases and their journal/WAL sidecars.
|
|
97
|
+
const DATABASE = /\.sqlite(-wal|-shm|-journal)?$/;
|
|
98
|
+
/** True for the non-credential paths: under `home` they are never sealed; under `state` they are sealed as objects. */
|
|
99
|
+
export function nonCredential(name) {
|
|
100
|
+
if (AGENT_SESSIONS.test(name))
|
|
101
|
+
return true;
|
|
102
|
+
const slash = name.lastIndexOf('/');
|
|
103
|
+
const parent = slash === -1 ? '' : name.slice(0, slash);
|
|
104
|
+
if (parent === 'state')
|
|
105
|
+
return STATE_NON_CREDENTIAL.has(name.slice(slash + 1));
|
|
106
|
+
return (HOME_CACHES[parent] ?? []).includes(name.slice(slash + 1));
|
|
63
107
|
}
|
|
108
|
+
// A file under `state` that is sealed as its own object: the SQLite databases and anything in a non-credential subtree.
|
|
109
|
+
function apart(name) {
|
|
110
|
+
if (!name.startsWith('state/'))
|
|
111
|
+
return false;
|
|
112
|
+
if (DATABASE.test(name))
|
|
113
|
+
return true;
|
|
114
|
+
const parts = name.split('/');
|
|
115
|
+
return parts.some((_, i) => nonCredential(parts.slice(0, i + 1).join('/')));
|
|
116
|
+
}
|
|
117
|
+
const objectName = (name) => `${createHash('sha256').update(name).digest('hex')}.sealed`;
|
|
118
|
+
const objectHash = (payload) => createHash('sha256').update(payload).digest('hex');
|
|
64
119
|
function safePath(path) {
|
|
65
120
|
return typeof path === 'string' && /^(state|home)(\/[^/]+)*$/.test(path)
|
|
66
121
|
&& !path.split('/').some((part) => part === '.' || part === '..' || part.includes('\\') || part.includes('\0'));
|
|
@@ -98,17 +153,46 @@ export class AuthStoreUnreadableError extends Error {
|
|
|
98
153
|
this.reason = reason;
|
|
99
154
|
}
|
|
100
155
|
}
|
|
156
|
+
/** Total raw credential bytes one snapshot may seal. The sealed payload is one runtime string, so collect()
|
|
157
|
+
* refuses a larger snapshot before reading it and the process never aborts inside the sealer. Credential
|
|
158
|
+
* state is normally a few MiB; the cap leaves several times that headroom. The same bound applies to each
|
|
159
|
+
* non-credential engine store under `state` sealed as its own object; their total is not bounded. */
|
|
160
|
+
export const SEAL_CAP_BYTES = 128 * 1024 * 1024;
|
|
161
|
+
/** The saved sign-in data (or one object file, when `file` names it) is over `SEAL_CAP_BYTES`. On a refusal the
|
|
162
|
+
* last good saved store is kept as it was and no live file is deleted. `size` is a lower bound for the whole
|
|
163
|
+
* store (the running total when the cap was crossed) and the exact size when `file` is set. */
|
|
164
|
+
export class AuthStoreSealSizeError extends Error {
|
|
165
|
+
code = 'auth-store-seal-size';
|
|
166
|
+
size;
|
|
167
|
+
cap;
|
|
168
|
+
file;
|
|
169
|
+
constructor(size, cap, file) {
|
|
170
|
+
const MiB = 1024 * 1024;
|
|
171
|
+
super(`${file ? `Engine data ${file}` : 'Your saved sign-in data'} is too large to keep safely (more than ${Math.floor(size / MiB)} MB; the limit is ${cap / MiB} MB). ` +
|
|
172
|
+
'Your sign-ins were kept.');
|
|
173
|
+
this.name = 'AuthStoreSealSizeError';
|
|
174
|
+
this.size = size;
|
|
175
|
+
this.cap = cap;
|
|
176
|
+
this.file = file;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
101
179
|
export class AuthStore {
|
|
102
180
|
active = false;
|
|
103
181
|
owned = false;
|
|
104
182
|
queue = Promise.resolve();
|
|
105
183
|
file;
|
|
184
|
+
objects;
|
|
106
185
|
lock;
|
|
107
186
|
cleanup;
|
|
108
187
|
restoring;
|
|
188
|
+
// Object file name -> sha256 of the plaintext payload currently sealed at that file. Kept only in memory: it
|
|
189
|
+
// never becomes on-disk metadata. Filled by restore() from what is decrypted, refreshed after each successful
|
|
190
|
+
// seal. A process that never restored starts with an empty map and reseals every object, as before.
|
|
191
|
+
objectHashes = new Map();
|
|
109
192
|
constructor(privateOptions) {
|
|
110
193
|
this.o = privateOptions;
|
|
111
194
|
this.file = join(this.o.root, 'auth-store.sealed');
|
|
195
|
+
this.objects = join(this.o.root, 'auth-store.objects');
|
|
112
196
|
this.lock = join(this.o.root, 'auth-store.lock');
|
|
113
197
|
this.cleanup = join(this.o.root, 'auth-store.cleanup');
|
|
114
198
|
this.restoring = join(this.o.root, 'auth-store.restoring');
|
|
@@ -178,21 +262,32 @@ export class AuthStore {
|
|
|
178
262
|
rmSync(this.lock, { recursive: true });
|
|
179
263
|
this.owned = false;
|
|
180
264
|
}
|
|
181
|
-
|
|
182
|
-
if (!existsSync(this.file))
|
|
183
|
-
return undefined;
|
|
184
|
-
regular(this.file);
|
|
185
|
-
const seal = this.o.seal;
|
|
186
|
-
const bytes = readFileSync(this.file);
|
|
187
|
-
let text;
|
|
265
|
+
open(bytes) {
|
|
188
266
|
try {
|
|
189
|
-
|
|
267
|
+
return this.o.seal.decryptString(bytes);
|
|
190
268
|
}
|
|
191
269
|
catch (error) {
|
|
192
270
|
if (error?.code !== 'auth-failed')
|
|
193
271
|
throw error;
|
|
194
272
|
throw new AuthStoreUnreadableError('auth-failed');
|
|
195
273
|
}
|
|
274
|
+
}
|
|
275
|
+
seal(path, payload) {
|
|
276
|
+
const seal = this.o.seal;
|
|
277
|
+
const sealed = seal.encryptString(payload);
|
|
278
|
+
if (seal.decryptString(Buffer.from(sealed)) !== payload)
|
|
279
|
+
throw new Error('credential seal verification failed');
|
|
280
|
+
put(path, sealed);
|
|
281
|
+
if (seal.decryptString(readFileSync(path)) !== payload)
|
|
282
|
+
throw new Error('credential seal verification failed');
|
|
283
|
+
}
|
|
284
|
+
async read() {
|
|
285
|
+
if (!existsSync(this.file))
|
|
286
|
+
return undefined;
|
|
287
|
+
regular(this.file);
|
|
288
|
+
const seal = this.o.seal;
|
|
289
|
+
const bytes = readFileSync(this.file);
|
|
290
|
+
const text = this.open(bytes);
|
|
196
291
|
let saved;
|
|
197
292
|
try {
|
|
198
293
|
saved = snapshot(text);
|
|
@@ -208,22 +303,45 @@ export class AuthStore {
|
|
|
208
303
|
}
|
|
209
304
|
return saved;
|
|
210
305
|
}
|
|
211
|
-
|
|
212
|
-
|
|
306
|
+
readObject(file) {
|
|
307
|
+
let text = '';
|
|
308
|
+
let entry = null;
|
|
309
|
+
try {
|
|
310
|
+
text = this.open(readFileSync(file));
|
|
311
|
+
entry = JSON.parse(text);
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
if (error instanceof AuthStoreUnreadableError)
|
|
315
|
+
throw error;
|
|
316
|
+
throw new AuthStoreUnreadableError('invalid-snapshot');
|
|
317
|
+
}
|
|
318
|
+
const path = entry?.path;
|
|
319
|
+
const data = entry?.data;
|
|
320
|
+
if (!safePath(path) || !apart(path) || typeof data !== 'string' || Buffer.from(data, 'base64').toString('base64') !== data) {
|
|
321
|
+
throw new AuthStoreUnreadableError('invalid-snapshot');
|
|
322
|
+
}
|
|
323
|
+
return [path, data, text];
|
|
324
|
+
}
|
|
325
|
+
inventory() {
|
|
326
|
+
const dirs = [];
|
|
327
|
+
const files = [];
|
|
328
|
+
const objects = [];
|
|
329
|
+
let total = 0;
|
|
213
330
|
const root = realpathSync(this.o.root);
|
|
214
331
|
const walk = (path) => {
|
|
215
332
|
const name = relative(this.o.root, path).split('\\').join('/');
|
|
216
|
-
if (
|
|
217
|
-
return; //
|
|
333
|
+
if (name.startsWith('home/') && nonCredential(name))
|
|
334
|
+
return; // regenerable home caches stay on disk; skipping a directory skips its subtree
|
|
218
335
|
const stat = lstatSync(path);
|
|
219
336
|
if (stat.isDirectory()) {
|
|
220
|
-
|
|
221
|
-
|
|
337
|
+
if (!apart(name))
|
|
338
|
+
dirs.push(name);
|
|
222
339
|
for (const child of readdirSync(path).sort())
|
|
223
340
|
walk(join(path, child));
|
|
224
341
|
return;
|
|
225
342
|
}
|
|
226
343
|
let file = path;
|
|
344
|
+
let target = stat;
|
|
227
345
|
if (stat.isSymbolicLink()) {
|
|
228
346
|
try {
|
|
229
347
|
file = realpathSync(path);
|
|
@@ -233,26 +351,44 @@ export class AuthStore {
|
|
|
233
351
|
return;
|
|
234
352
|
throw error;
|
|
235
353
|
}
|
|
236
|
-
const
|
|
237
|
-
if (isAbsolute(
|
|
238
|
-
return;
|
|
239
|
-
if (!lstatSync(file).isFile())
|
|
354
|
+
const rel = relative(root, file);
|
|
355
|
+
if (isAbsolute(rel) || rel === '..' || rel.startsWith(`..${sep}`))
|
|
240
356
|
return;
|
|
357
|
+
target = lstatSync(file);
|
|
241
358
|
}
|
|
242
|
-
|
|
359
|
+
if (!target.isFile())
|
|
243
360
|
return;
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
361
|
+
if (apart(name)) {
|
|
362
|
+
if (target.size > SEAL_CAP_BYTES)
|
|
363
|
+
throw new AuthStoreSealSizeError(target.size, SEAL_CAP_BYTES, name);
|
|
364
|
+
objects.push({ name, file, size: target.size });
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
// Refuse by size before any file is touched or read: an over-cap file must never reach memory or the sealer.
|
|
368
|
+
total += target.size;
|
|
369
|
+
if (total > SEAL_CAP_BYTES)
|
|
370
|
+
throw new AuthStoreSealSizeError(total, SEAL_CAP_BYTES);
|
|
371
|
+
files.push({ name, file, size: target.size });
|
|
247
372
|
};
|
|
248
373
|
for (const dir of ['state', 'home'])
|
|
249
374
|
if (existsSync(join(this.o.root, dir)))
|
|
250
375
|
walk(join(this.o.root, dir));
|
|
251
|
-
return
|
|
376
|
+
return { dirs, files, objects };
|
|
377
|
+
}
|
|
378
|
+
collect() {
|
|
379
|
+
const { dirs, files, objects } = this.inventory();
|
|
380
|
+
for (const dir of dirs)
|
|
381
|
+
chmodSync(join(this.o.root, dir), 0o700);
|
|
382
|
+
// Read the checked target, but save the link's path: restore materializes a regular file there.
|
|
383
|
+
for (const { file } of [...files, ...objects])
|
|
384
|
+
chmodSync(file, 0o600);
|
|
385
|
+
return {
|
|
386
|
+
saved: { v: 1, dirs, files: files.map(({ name, file }) => [name, readFileSync(file).toString('base64')]) },
|
|
387
|
+
objects,
|
|
388
|
+
};
|
|
252
389
|
}
|
|
253
|
-
/** Remove the live
|
|
254
|
-
*
|
|
255
|
-
* and the directories still holding them are left in place. */
|
|
390
|
+
/** Remove the live sealed trees: all of `state` (every file there is sealed, as a blob entry or an object),
|
|
391
|
+
* and in `home` exactly the sealed paths — regenerable caches and the directories still holding them are left in place. */
|
|
256
392
|
remove(s) {
|
|
257
393
|
rmSync(join(this.o.root, 'state'), { recursive: true, force: true });
|
|
258
394
|
for (const [path] of s.files)
|
|
@@ -271,6 +407,28 @@ export class AuthStore {
|
|
|
271
407
|
}
|
|
272
408
|
}
|
|
273
409
|
}
|
|
410
|
+
sealObjects(objects) {
|
|
411
|
+
mkdirSync(this.objects, { recursive: true, mode: 0o700 });
|
|
412
|
+
const keep = new Set();
|
|
413
|
+
for (const { name, file } of objects) {
|
|
414
|
+
const object = objectName(name);
|
|
415
|
+
keep.add(object);
|
|
416
|
+
const target = join(this.objects, object);
|
|
417
|
+
const payload = JSON.stringify({ path: name, data: readFileSync(file).toString('base64') });
|
|
418
|
+
// An object this process already sealed (or restored) unchanged is not resealed: its plaintext hash is
|
|
419
|
+
// the same and its sealed file is still on disk, so the encrypt + verify + fsyncs are skipped. A changed
|
|
420
|
+
// file, a new one, or one whose sealed file vanished falls through to the normal seal.
|
|
421
|
+
const hash = objectHash(payload);
|
|
422
|
+
if (this.objectHashes.get(object) === hash && existsSync(target))
|
|
423
|
+
continue;
|
|
424
|
+
this.objectHashes.delete(object);
|
|
425
|
+
this.seal(target, payload);
|
|
426
|
+
this.objectHashes.set(object, hash);
|
|
427
|
+
}
|
|
428
|
+
for (const entry of readdirSync(this.objects))
|
|
429
|
+
if (!keep.has(entry))
|
|
430
|
+
rmSync(join(this.objects, entry), { force: true });
|
|
431
|
+
}
|
|
274
432
|
async persist() {
|
|
275
433
|
if (!this.o.seal)
|
|
276
434
|
return;
|
|
@@ -282,40 +440,66 @@ export class AuthStore {
|
|
|
282
440
|
removeMarker(this.restoring);
|
|
283
441
|
this.o.log?.('interrupted credential transition recovered');
|
|
284
442
|
}
|
|
285
|
-
const saved = this.collect();
|
|
443
|
+
const { saved, objects } = this.collect();
|
|
286
444
|
// Live trees with nothing sealable are debris (crash leftovers, or only the caches a previous stop
|
|
287
445
|
// left in place) — keep the sealed credentials instead of replacing them with an empty snapshot.
|
|
288
446
|
// A running engine always leaves files behind (its SQLite stores), so a genuine sign-out still re-seals.
|
|
289
|
-
if (!saved.files.length && previous)
|
|
447
|
+
if (!saved.files.length && !objects.length && previous)
|
|
290
448
|
return;
|
|
291
449
|
// The payload is built exactly once; verification decrypts the sealed bytes and compares to it.
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
if (this.o.seal.decryptString(Buffer.from(sealed)) !== payload)
|
|
295
|
-
throw new Error('credential seal verification failed');
|
|
296
|
-
put(this.file, sealed);
|
|
297
|
-
if (this.o.seal.decryptString(readFileSync(this.file)) !== payload)
|
|
298
|
-
throw new Error('credential seal verification failed');
|
|
299
|
-
if (previous?.dirs.some(cached))
|
|
450
|
+
this.seal(this.file, JSON.stringify(saved));
|
|
451
|
+
if (previous?.dirs.some(nonCredential))
|
|
300
452
|
this.o.log?.('sealed credential store re-sealed: tool caches no longer sealed');
|
|
453
|
+
this.sealObjects(objects);
|
|
301
454
|
put(this.cleanup, encoder.encode('1'));
|
|
302
455
|
this.remove(saved);
|
|
303
456
|
removeMarker(this.cleanup);
|
|
304
457
|
}
|
|
458
|
+
/** `mkdir -p` for a restore target that records the parent of every directory it creates: a new directory
|
|
459
|
+
* changes its parent's entries, so the parent needs a directory fsync too. Stops at the store root (it exists)
|
|
460
|
+
* and never fsyncs now; restore() fsyncs the collected directories once after all files are written. */
|
|
461
|
+
ensureRestoreDirs(dir, changed) {
|
|
462
|
+
const created = [];
|
|
463
|
+
for (let d = dir; d !== this.o.root && d !== dirname(d) && !existsSync(d); d = dirname(d))
|
|
464
|
+
created.push(d);
|
|
465
|
+
for (const d of created.reverse()) {
|
|
466
|
+
mkdirSync(d, { mode: 0o700 });
|
|
467
|
+
changed.add(dirname(d));
|
|
468
|
+
}
|
|
469
|
+
}
|
|
305
470
|
async restore() {
|
|
306
471
|
if (!this.o.seal)
|
|
307
472
|
return;
|
|
308
473
|
const saved = await this.read();
|
|
309
474
|
// A crashed host may leave a newer live state; prepare seals that before restore.
|
|
310
475
|
put(this.restoring, encoder.encode('1'));
|
|
476
|
+
const changed = new Set();
|
|
311
477
|
if (saved) {
|
|
312
478
|
for (const dir of saved.dirs.sort((a, b) => a.split('/').length - b.split('/').length))
|
|
313
|
-
|
|
479
|
+
this.ensureRestoreDirs(join(this.o.root, dir), changed);
|
|
314
480
|
for (const [path, data] of saved.files)
|
|
315
|
-
|
|
481
|
+
putRestore(join(this.o.root, path), Buffer.from(data, 'base64'), changed);
|
|
482
|
+
}
|
|
483
|
+
if (existsSync(this.objects)) {
|
|
484
|
+
for (const entry of readdirSync(this.objects)) {
|
|
485
|
+
const [path, data, payload] = this.readObject(join(this.objects, entry));
|
|
486
|
+
this.objectHashes.set(entry, objectHash(payload));
|
|
487
|
+
// An object path can be several directories below an existing one, and those directories are not in
|
|
488
|
+
// `saved.dirs` (they sit in a non-credential subtree). Create them through ensureRestoreDirs so the
|
|
489
|
+
// parent of every directory this restore creates — not just each object's own directory — is fsynced
|
|
490
|
+
// before the marker goes; otherwise a crash after marker removal could lose a directory entry and the
|
|
491
|
+
// next prepare() would seal the now-incomplete live tree over the good sealed objects.
|
|
492
|
+
const target = join(this.o.root, path);
|
|
493
|
+
this.ensureRestoreDirs(dirname(target), changed);
|
|
494
|
+
putRestore(target, Buffer.from(data, 'base64'), changed);
|
|
495
|
+
}
|
|
316
496
|
}
|
|
317
497
|
for (const dir of ['state', 'home'])
|
|
318
|
-
|
|
498
|
+
this.ensureRestoreDirs(join(this.o.root, dir), changed);
|
|
499
|
+
// Each restored file's content is durable already (putRestore fsyncs it); every rename and every new
|
|
500
|
+
// directory entry is made durable here, once per distinct directory, before the marker is removed.
|
|
501
|
+
for (const dir of changed)
|
|
502
|
+
syncDir(dir);
|
|
319
503
|
removeMarker(this.restoring);
|
|
320
504
|
}
|
|
321
505
|
async archives() {
|
|
@@ -373,8 +557,17 @@ export class AuthStore {
|
|
|
373
557
|
return this.serial(async () => {
|
|
374
558
|
if (!this.owned)
|
|
375
559
|
return;
|
|
376
|
-
|
|
377
|
-
|
|
560
|
+
try {
|
|
561
|
+
await this.archives();
|
|
562
|
+
await this.persist();
|
|
563
|
+
}
|
|
564
|
+
catch (error) {
|
|
565
|
+
if (error instanceof AuthStoreSealSizeError) {
|
|
566
|
+
this.active = false;
|
|
567
|
+
this.release();
|
|
568
|
+
}
|
|
569
|
+
throw error;
|
|
570
|
+
}
|
|
378
571
|
this.active = false;
|
|
379
572
|
this.release();
|
|
380
573
|
});
|
package/dist/classify.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// The regexes are Crewhouse `classifyText`; the kinds are the kit's: rate_limit|overloaded → resting,
|
|
3
3
|
// signed_out → signed-out, not_included → plan, network → network, anything unrecognised → other.
|
|
4
4
|
export function classify(message) {
|
|
5
|
-
|
|
5
|
+
// Kept in step with @byokit/accounts' classifyFailure: both read the retry delay from either wording.
|
|
6
|
+
const retry = /(?:try again in|next reset in)\s*~?(\d+)\s*(min|h)/i.exec(message);
|
|
6
7
|
const until = retry ? Date.now() + Number(retry[1]) * (retry[2].toLowerCase() === 'h' ? 3_600_000 : 60_000) : undefined;
|
|
7
8
|
const kind = /your plan doesn't include/i.test(message) ? 'plan'
|
|
8
9
|
: /usage limit|rate.?limit|quota|too many requests|\b429\b/i.test(message) ? 'resting'
|
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { dirname, join } from 'node:path';
|
|
2
2
|
import { routes } from "./routes.js";
|
|
3
|
-
import { MEMBER_ID, KEY_PREFIX, keyAgentId } from "./members.js";
|
|
3
|
+
import { MEMBER_ID, KEY_PREFIX, BASE_AGENT, keyAgentId } from "./members.js";
|
|
4
4
|
const object = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
5
5
|
const merge = (a, b) => {
|
|
6
6
|
for (const [k, v] of Object.entries(b))
|
|
@@ -26,6 +26,16 @@ const safeMemory = (memory) => {
|
|
|
26
26
|
s.provider = 'none';
|
|
27
27
|
s.fallback = 'none';
|
|
28
28
|
};
|
|
29
|
+
/** The plugin of every route an app offers by name (5.6). A consumer names its offered accounts; each is the engine
|
|
30
|
+
* provider, the auth choice or an alias a route carries. Routes nobody names stay out. */
|
|
31
|
+
function offeredPlugins(offered) {
|
|
32
|
+
if (!Array.isArray(offered) || !offered.length)
|
|
33
|
+
return [];
|
|
34
|
+
const named = new Set(offered);
|
|
35
|
+
return routes().filter(route => route.plugin && !route.needs?.plugin
|
|
36
|
+
&& (named.has(route.provider) || named.has(route.choice) || (route.aliases ?? []).some(alias => named.has(alias))))
|
|
37
|
+
.map(route => route.plugin);
|
|
38
|
+
}
|
|
29
39
|
export function reconcileConfig(saved, o) {
|
|
30
40
|
const c = merge(merge({}, object(saved) ? saved : {}), object(o.app) ? o.app : {});
|
|
31
41
|
merge(c, {
|
|
@@ -37,6 +47,9 @@ export function reconcileConfig(saved, o) {
|
|
|
37
47
|
c.channels ??= {};
|
|
38
48
|
c.agents ??= {};
|
|
39
49
|
c.agents.defaults ??= {};
|
|
50
|
+
// Auth isolation (5.15): left unset, the pin makes the first agent the inherited owner once the roster grows, and
|
|
51
|
+
// every later agent (other members too) reads its sign-ins. The base is an id the kit never creates.
|
|
52
|
+
c.agents.defaults.authInheritance = { agentId: BASE_AGENT };
|
|
40
53
|
c.agents.defaults.models ??= {};
|
|
41
54
|
c.agents.defaults.models['openai/*'] = { ...c.agents.defaults.models['openai/*'], agentRuntime: { id: 'openclaw' } };
|
|
42
55
|
const allow = c.agents.defaults.modelPolicy?.allow;
|
|
@@ -59,7 +72,9 @@ export function reconcileConfig(saved, o) {
|
|
|
59
72
|
// Every default-eligible bundled route, not only the ready ones: anthropic-cli waits on its binary and
|
|
60
73
|
// shares its plugin with the explicit apiKey route, so neither may depend on another route being offered.
|
|
61
74
|
...routes().filter(route => route.offerPolicy === 'default' && !route.needs?.plugin && route.plugin)
|
|
62
|
-
.map(route => route.plugin)
|
|
75
|
+
.map(route => route.plugin),
|
|
76
|
+
// Plus every explicitly offered route the app names, so an opted-in sign-in (OpenRouter) is not blocked.
|
|
77
|
+
...offeredPlugins(o.offered)])];
|
|
63
78
|
c.plugins.entries ??= {};
|
|
64
79
|
c.plugins.entries[o.pluginId] = merge(c.plugins.entries[o.pluginId] ?? {}, { hooks: { timeouts: { before_tool_call: 200_000 } } });
|
|
65
80
|
if (o.browser) {
|
package/dist/constants.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export declare const ENGINE_VERSION = "2026.8.
|
|
1
|
+
export declare const ENGINE_VERSION = "2026.8.35";
|
|
2
2
|
export declare const PROTOCOL_VERSION = 4;
|
|
3
3
|
export declare const OPERATOR_SCOPES: readonly ["operator.read", "operator.write", "operator.admin", "operator.approvals", "operator.questions", "operator.pairing", "operator.talk"];
|
package/dist/constants.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The pins this kit is built against (D4, D6). Data, not behavior: an installed engine on another version is
|
|
2
2
|
// `needs-update`, never silently run.
|
|
3
|
-
export const ENGINE_VERSION = '2026.8.
|
|
3
|
+
export const ENGINE_VERSION = '2026.8.35';
|
|
4
4
|
export const PROTOCOL_VERSION = 4;
|
|
5
5
|
export const OPERATOR_SCOPES = [
|
|
6
6
|
'operator.read',
|
package/dist/device.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BrowserDevice, SignInRefusedWhy } from './browser.ts';
|
|
2
|
-
import type { DeviceLink } from '@byokit/
|
|
2
|
+
import type { DeviceLink } from '@byokit/pair';
|
|
3
3
|
import type { RouteView } from './routes.ts';
|
|
4
4
|
import { type AccountView } from './words.ts';
|
|
5
5
|
import type { Approval, Decision, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, KitState, RunEnd, OutputSchema, SchemaOutput, RunEvent, SignInView } from './types.ts';
|
package/dist/engine.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { AuthStore } from './auth-store.ts';
|
|
1
|
+
import { AuthStore, AuthStoreSealSizeError } from './auth-store.ts';
|
|
2
2
|
import type { KitOptions } from './kit.ts';
|
|
3
3
|
import type { KitState, ToolSpec } from './types.ts';
|
|
4
|
-
export type EngineOptions = Pick<KitOptions, 'stateDir' | 'engineDir' | 'npmPath' | 'enginePath' | 'config' | 'appOwnedSessions' | 'installPolicy' | 'log' | 'bridge' | 'authSeal'> & {
|
|
4
|
+
export type EngineOptions = Pick<KitOptions, 'stateDir' | 'engineDir' | 'npmPath' | 'enginePath' | 'config' | 'offered' | 'appOwnedSessions' | 'installPolicy' | 'log' | 'bridge' | 'authSeal'> & {
|
|
5
5
|
pluginId: string;
|
|
6
6
|
tools: ToolSpec[];
|
|
7
7
|
gateBuiltins?: boolean;
|
|
@@ -16,6 +16,7 @@ export type EngineOptions = Pick<KitOptions, 'stateDir' | 'engineDir' | 'npmPath
|
|
|
16
16
|
onState(s: KitState): void;
|
|
17
17
|
onExit(code: number | null): void;
|
|
18
18
|
};
|
|
19
|
+
export declare const refusedOnly: (error: unknown) => AuthStoreSealSizeError;
|
|
19
20
|
export declare class Engine {
|
|
20
21
|
readonly root: string;
|
|
21
22
|
readonly bridgeSock: string;
|
|
@@ -38,6 +39,8 @@ export declare class Engine {
|
|
|
38
39
|
private readonly appOwnedSessionPrefixes;
|
|
39
40
|
constructor(o: EngineOptions);
|
|
40
41
|
private state;
|
|
42
|
+
private retainInstallDiagnostics;
|
|
43
|
+
private exitedState;
|
|
41
44
|
private get entry();
|
|
42
45
|
private freePort;
|
|
43
46
|
prepare(): Promise<void>;
|