@byokit/openclaw 0.13.0 → 0.14.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 +18 -0
- package/README.md +25 -11
- package/SECURITY.md +6 -2
- package/dist/accounts.d.ts +31 -1
- package/dist/accounts.js +429 -97
- package/dist/auth-store.d.ts +2 -0
- package/dist/auth-store.js +72 -7
- package/dist/engine.d.ts +9 -0
- package/dist/engine.js +55 -4
- package/dist/index.d.ts +2 -2
- package/dist/keys.d.ts +13 -2
- package/dist/keys.js +110 -59
- package/dist/kit.d.ts +11 -2
- package/dist/kit.js +60 -10
- package/dist/link.js +12 -6
- package/dist/pick.js +4 -0
- package/dist/runs.js +23 -6
- package/dist/signin.d.ts +4 -0
- package/dist/signin.js +80 -22
- package/dist/testing/fake-gateway.js +22 -1
- package/dist/thinking.d.ts +16 -0
- package/dist/thinking.js +62 -0
- package/dist/types.d.ts +8 -0
- package/dist/words.json +5 -1
- package/package.json +2 -1
- package/plugin/keys.js +3 -2
package/dist/auth-store.d.ts
CHANGED
|
@@ -53,6 +53,8 @@ export declare class AuthStore {
|
|
|
53
53
|
* and in `home` exactly the sealed paths — regenerable caches and the directories still holding them are left in place. */
|
|
54
54
|
private remove;
|
|
55
55
|
private sealObjects;
|
|
56
|
+
/** Re-attach held-back sealed paths a damaged index kept out of the live inventory. */
|
|
57
|
+
private mergeHeldBack;
|
|
56
58
|
private persist;
|
|
57
59
|
/** `mkdir -p` for a restore target that records the parent of every directory it creates: a new directory
|
|
58
60
|
* changes its parent's entries, so the parent needs a directory fsync too. Stops at the store root (it exists)
|
package/dist/auth-store.js
CHANGED
|
@@ -8,7 +8,8 @@ import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync,
|
|
|
8
8
|
import { createHash } from 'node:crypto';
|
|
9
9
|
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
10
10
|
import { EngineAlreadyRunningError, pidAlive as live } from "./engine-status.js";
|
|
11
|
-
import { removedAccountAgents, sweepOrphans } from "./accounts.js";
|
|
11
|
+
import { damagedAccountMembers, heldBackAgent, removedAccountAgents, sweepOrphans } from "./accounts.js";
|
|
12
|
+
const agentDirOf = (path) => path.startsWith('state/agents/') ? path.split('/')[2] : undefined;
|
|
12
13
|
const encoder = new TextEncoder();
|
|
13
14
|
const archive = (name) => !name.endsWith('.sealed') && (/^(auth-profiles|auth-state|auth|oauth)\.json\.(migrated-.+|sqlite-import\..+\.bak)$/.test(name)
|
|
14
15
|
|| name.endsWith('.moved-to-engine'));
|
|
@@ -322,8 +323,12 @@ export class AuthStore {
|
|
|
322
323
|
catch {
|
|
323
324
|
throw new AuthStoreUnreadableError('invalid-snapshot');
|
|
324
325
|
}
|
|
326
|
+
// Removed agents leave the sealed store; damaged-index agents stay sealed (held back) until their index is repaired.
|
|
325
327
|
const gone = removedAccountAgents(this.o.root);
|
|
326
|
-
const keep = (path) =>
|
|
328
|
+
const keep = (path) => {
|
|
329
|
+
const agent = agentDirOf(path);
|
|
330
|
+
return agent === undefined || !gone.has(agent);
|
|
331
|
+
};
|
|
327
332
|
const kept = { v: 1, dirs: saved.dirs.filter(keep), files: saved.files.filter(([path]) => keep(path)) };
|
|
328
333
|
if (kept.dirs.length !== saved.dirs.length || kept.files.length !== saved.files.length) {
|
|
329
334
|
this.seal(this.file, JSON.stringify(kept));
|
|
@@ -375,10 +380,16 @@ export class AuthStore {
|
|
|
375
380
|
let total = 0;
|
|
376
381
|
const root = realpathSync(this.o.root);
|
|
377
382
|
const gone = removedAccountAgents(this.o.root);
|
|
383
|
+
const damaged = damagedAccountMembers(this.o.root);
|
|
378
384
|
const walk = (path) => {
|
|
379
385
|
const name = relative(this.o.root, path).split('\\').join('/');
|
|
380
|
-
if (name.startsWith('state/agents/')
|
|
381
|
-
|
|
386
|
+
if (name.startsWith('state/agents/')) {
|
|
387
|
+
const agent = name.split('/')[2];
|
|
388
|
+
if (gone.has(agent))
|
|
389
|
+
return; // a late refresh never enters a seal
|
|
390
|
+
if (heldBackAgent(agent, damaged))
|
|
391
|
+
return; // damaged index: leave sealed copy; never reseal a late live write
|
|
392
|
+
}
|
|
382
393
|
if (name.startsWith('home/') && nonCredential(name))
|
|
383
394
|
return; // regenerable home caches stay on disk; skipping a directory skips its subtree
|
|
384
395
|
let stat;
|
|
@@ -449,7 +460,7 @@ export class AuthStore {
|
|
|
449
460
|
return { dirs, files, objects };
|
|
450
461
|
}
|
|
451
462
|
collect() {
|
|
452
|
-
sweepOrphans(this.o.root);
|
|
463
|
+
sweepOrphans(this.o.root, this.o.log);
|
|
453
464
|
const { dirs, files, objects } = this.inventory();
|
|
454
465
|
for (const dir of dirs) {
|
|
455
466
|
try {
|
|
@@ -536,10 +547,48 @@ export class AuthStore {
|
|
|
536
547
|
this.seal(target, payload);
|
|
537
548
|
this.objectHashes.set(object, hash);
|
|
538
549
|
}
|
|
550
|
+
// Held-back agent objects stay sealed while their index is damaged (inventory skipped their live trees).
|
|
551
|
+
const damaged = damagedAccountMembers(this.o.root);
|
|
552
|
+
if (damaged.size) {
|
|
553
|
+
for (const entry of readdirSync(this.objects)) {
|
|
554
|
+
if (keep.has(entry))
|
|
555
|
+
continue;
|
|
556
|
+
try {
|
|
557
|
+
const agent = agentDirOf(this.readObject(join(this.objects, entry))[0]);
|
|
558
|
+
if (agent !== undefined && heldBackAgent(agent, damaged))
|
|
559
|
+
keep.add(entry);
|
|
560
|
+
}
|
|
561
|
+
catch { /* unreadable object: leave for the next start's auth-store check */ }
|
|
562
|
+
}
|
|
563
|
+
}
|
|
539
564
|
for (const entry of readdirSync(this.objects))
|
|
540
565
|
if (!keep.has(entry))
|
|
541
566
|
rmSync(join(this.objects, entry), { force: true });
|
|
542
567
|
}
|
|
568
|
+
/** Re-attach held-back sealed paths a damaged index kept out of the live inventory. */
|
|
569
|
+
mergeHeldBack(previous, saved) {
|
|
570
|
+
if (!previous)
|
|
571
|
+
return;
|
|
572
|
+
const damaged = damagedAccountMembers(this.o.root);
|
|
573
|
+
if (!damaged.size)
|
|
574
|
+
return;
|
|
575
|
+
const held = (path) => {
|
|
576
|
+
const agent = agentDirOf(path);
|
|
577
|
+
return agent !== undefined && heldBackAgent(agent, damaged);
|
|
578
|
+
};
|
|
579
|
+
const dirs = new Set(saved.dirs);
|
|
580
|
+
for (const dir of previous.dirs)
|
|
581
|
+
if (held(dir) && !dirs.has(dir)) {
|
|
582
|
+
saved.dirs.push(dir);
|
|
583
|
+
dirs.add(dir);
|
|
584
|
+
}
|
|
585
|
+
const files = new Set(saved.files.map(([path]) => path));
|
|
586
|
+
for (const entry of previous.files)
|
|
587
|
+
if (held(entry[0]) && !files.has(entry[0])) {
|
|
588
|
+
saved.files.push(entry);
|
|
589
|
+
files.add(entry[0]);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
543
592
|
async persist() {
|
|
544
593
|
if (!this.o.seal)
|
|
545
594
|
return;
|
|
@@ -557,6 +606,7 @@ export class AuthStore {
|
|
|
557
606
|
// A running engine always leaves files behind (its SQLite stores), so a genuine sign-out still re-seals.
|
|
558
607
|
if (!saved.files.length && !objects.length && previous)
|
|
559
608
|
return;
|
|
609
|
+
this.mergeHeldBack(previous, saved);
|
|
560
610
|
// The payload is built exactly once; verification decrypts the sealed bytes and compares to it.
|
|
561
611
|
this.seal(this.file, JSON.stringify(saved));
|
|
562
612
|
if (previous?.dirs.some(nonCredential))
|
|
@@ -582,14 +632,25 @@ export class AuthStore {
|
|
|
582
632
|
if (!this.o.seal)
|
|
583
633
|
return;
|
|
584
634
|
const saved = await this.read();
|
|
635
|
+
const damaged = damagedAccountMembers(this.o.root);
|
|
636
|
+
const skipHeld = (path) => {
|
|
637
|
+
const agent = agentDirOf(path);
|
|
638
|
+
return agent !== undefined && heldBackAgent(agent, damaged);
|
|
639
|
+
};
|
|
585
640
|
// A crashed host may leave a newer live state; prepare seals that before restore.
|
|
586
641
|
put(this.restoring, encoder.encode('1'));
|
|
587
642
|
const changed = new Set();
|
|
588
643
|
if (saved) {
|
|
589
|
-
for (const dir of saved.dirs.sort((a, b) => a.split('/').length - b.split('/').length))
|
|
644
|
+
for (const dir of saved.dirs.sort((a, b) => a.split('/').length - b.split('/').length)) {
|
|
645
|
+
if (skipHeld(dir))
|
|
646
|
+
continue;
|
|
590
647
|
this.ensureRestoreDirs(join(this.o.root, dir), changed);
|
|
591
|
-
|
|
648
|
+
}
|
|
649
|
+
for (const [path, data] of saved.files) {
|
|
650
|
+
if (skipHeld(path))
|
|
651
|
+
continue;
|
|
592
652
|
putRestore(join(this.o.root, path), Buffer.from(data, 'base64'), changed);
|
|
653
|
+
}
|
|
593
654
|
}
|
|
594
655
|
if (existsSync(this.objects)) {
|
|
595
656
|
for (const entry of readdirSync(this.objects)) {
|
|
@@ -598,6 +659,8 @@ export class AuthStore {
|
|
|
598
659
|
// no hash, so the next seal reseals it in the current mode even when unchanged.
|
|
599
660
|
if (!this.o.seal.upgrade?.(bytes))
|
|
600
661
|
this.objectHashes.set(entry, objectHash(payload));
|
|
662
|
+
if (skipHeld(path))
|
|
663
|
+
continue;
|
|
601
664
|
// An object path can be several directories below an existing one, and those directories are not in
|
|
602
665
|
// `saved.dirs` (they sit in a non-credential subtree). Create them through ensureRestoreDirs so the
|
|
603
666
|
// parent of every directory this restore creates — not just each object's own directory — is fsynced
|
|
@@ -667,7 +730,9 @@ export class AuthStore {
|
|
|
667
730
|
// keeps the sealed credentials instead of replacing them with an empty snapshot.
|
|
668
731
|
if (!saved.files.length && !objects.length && previous)
|
|
669
732
|
return;
|
|
733
|
+
this.mergeHeldBack(previous, saved);
|
|
670
734
|
this.seal(this.file, JSON.stringify(saved));
|
|
735
|
+
// Database objects stay at their last stop-time seal; only the credential blob is refreshed here.
|
|
671
736
|
});
|
|
672
737
|
}
|
|
673
738
|
start() {
|
package/dist/engine.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { remoteCliHost } from '@byokit/accounts/cli';
|
|
1
2
|
import { AuthStore, AuthStoreSealSizeError } from './auth-store.ts';
|
|
2
3
|
import type { KitOptions } from './kit.ts';
|
|
3
4
|
import type { KitState, ToolSpec } from './types.ts';
|
|
@@ -28,6 +29,10 @@ export declare class Engine {
|
|
|
28
29
|
private starting?;
|
|
29
30
|
readonly authStore: AuthStore;
|
|
30
31
|
private stopping;
|
|
32
|
+
private stopOwner;
|
|
33
|
+
private claudeLogin?;
|
|
34
|
+
private claudeAccounts?;
|
|
35
|
+
private claudeReset?;
|
|
31
36
|
private credentialsLocked;
|
|
32
37
|
private repaired;
|
|
33
38
|
private prepared?;
|
|
@@ -49,6 +54,10 @@ export declare class Engine {
|
|
|
49
54
|
entry: string;
|
|
50
55
|
env: Record<string, string>;
|
|
51
56
|
};
|
|
57
|
+
/** The engine's own shutdown signal; aborted by stop() and replaced by the next start(). */
|
|
58
|
+
get stopSignal(): AbortSignal;
|
|
59
|
+
nativeClaudeLogin(): ReturnType<typeof remoteCliHost>;
|
|
60
|
+
resetNativeClaudeLogin(): Promise<void>;
|
|
52
61
|
doctor(timeoutMs: number): {
|
|
53
62
|
status: number | null;
|
|
54
63
|
};
|
package/dist/engine.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { spawn, spawnSync } from 'node:child_process';
|
|
2
2
|
import { randomBytes, generateKeyPairSync, randomUUID } from 'node:crypto';
|
|
3
|
-
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, writeFileSync, copyFileSync, rmSync, realpathSync, statSync } from 'node:fs';
|
|
3
|
+
import { accessSync, constants, closeSync, existsSync, mkdirSync, openSync, readFileSync, writeFileSync, copyFileSync, rmSync, realpathSync, statSync } from 'node:fs';
|
|
4
4
|
import { createServer } from 'node:net';
|
|
5
5
|
import { basename, join, dirname } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
8
|
+
import { cliAccounts, remoteCliHost } from '@byokit/accounts/cli';
|
|
8
9
|
import { AuthStore, AuthStoreUnreadableError, AuthStoreSealSizeError } from "./auth-store.js";
|
|
9
10
|
import { EngineAlreadyRunningError, pidAlive, StartedProcesses } from "./engine-status.js";
|
|
10
11
|
import { EnginePatchError, atomic, prepareEngineSet, processStartTime, readPatchSet, sha256, verifyEngineSet } from "./engine-patches.js";
|
|
@@ -76,6 +77,13 @@ export class Engine {
|
|
|
76
77
|
starting;
|
|
77
78
|
authStore;
|
|
78
79
|
stopping = false;
|
|
80
|
+
// Aborted by stop(): a sign-in drive that has not reached its commit point then declines at once, the key paste
|
|
81
|
+
// included (it has no native host signal of its own). start() replaces it, so an earlier shutdown cannot decline
|
|
82
|
+
// a later drive.
|
|
83
|
+
stopOwner = new AbortController();
|
|
84
|
+
claudeLogin;
|
|
85
|
+
claudeAccounts;
|
|
86
|
+
claudeReset;
|
|
79
87
|
credentialsLocked = false;
|
|
80
88
|
repaired = false;
|
|
81
89
|
prepared;
|
|
@@ -285,6 +293,42 @@ export class Engine {
|
|
|
285
293
|
BYOKIT_APP_OWNED_SESSION_PREFIXES: JSON.stringify(this.appOwnedSessionPrefixes),
|
|
286
294
|
} };
|
|
287
295
|
}
|
|
296
|
+
/** The engine's own shutdown signal; aborted by stop() and replaced by the next start(). */
|
|
297
|
+
get stopSignal() { return this.stopOwner.signal; }
|
|
298
|
+
nativeClaudeLogin() {
|
|
299
|
+
if (this.claudeLogin)
|
|
300
|
+
return this.claudeLogin;
|
|
301
|
+
const { env } = this.doctorContext();
|
|
302
|
+
const bin = env.PATH.split(':').map(path => join(path, 'claude')).find(path => { try {
|
|
303
|
+
accessSync(path, constants.X_OK);
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
return false;
|
|
308
|
+
} });
|
|
309
|
+
const bins = bin ? { claude: bin } : {};
|
|
310
|
+
// The login runs in its own HOME: CLAUDE_CONFIG_DIR points it at the engine's folder, which accounts refuses as a HOME's default login.
|
|
311
|
+
for (const dir of ['home', 'login-home'])
|
|
312
|
+
mkdirSync(join(this.root, dir), { recursive: true, mode: 0o700 });
|
|
313
|
+
const cli = cliAccounts({ stateDir: this.root, bins, env: { PATH: env.PATH, HOME: join(this.root, 'login-home'), LANG: env.LANG, TMPDIR: env.TMPDIR, XDG_CONFIG_HOME: env.XDG_CONFIG_HOME, XDG_CACHE_HOME: env.XDG_CACHE_HOME, XDG_DATA_HOME: env.XDG_DATA_HOME, XDG_STATE_HOME: env.XDG_STATE_HOME, }, managedFolders: { claude: env.CLAUDE_CONFIG_DIR } });
|
|
314
|
+
this.claudeAccounts = cli;
|
|
315
|
+
this.claudeLogin = remoteCliHost({ cli, bins });
|
|
316
|
+
return this.claudeLogin;
|
|
317
|
+
}
|
|
318
|
+
resetNativeClaudeLogin() {
|
|
319
|
+
if (this.claudeReset)
|
|
320
|
+
return this.claudeReset;
|
|
321
|
+
const cli = this.claudeAccounts, host = this.claudeLogin;
|
|
322
|
+
if (!cli || !host || host.signal.aborted)
|
|
323
|
+
return Promise.resolve();
|
|
324
|
+
this.claudeReset = (async () => { for (const row of await cli.list()) {
|
|
325
|
+
if (host.signal.aborted)
|
|
326
|
+
return;
|
|
327
|
+
if (row.provider === 'claude' && row.state === 'ready' && cli.folderOf(row.id) === join(this.root, 'home', '.claude'))
|
|
328
|
+
await cli.remove(row.id);
|
|
329
|
+
} })().finally(() => { this.claudeReset = undefined; });
|
|
330
|
+
return this.claudeReset;
|
|
331
|
+
}
|
|
288
332
|
doctor(timeoutMs) {
|
|
289
333
|
const { entry, env } = this.doctorContext();
|
|
290
334
|
if (this.o.spawnEngine)
|
|
@@ -355,11 +399,12 @@ export class Engine {
|
|
|
355
399
|
}
|
|
356
400
|
// The account orphan sweep (5.15): a late engine write after a removal cannot be ruled out by a bounded check, so
|
|
357
401
|
// prepare and the sealed restore remove each `<root>/state/agents/<member>--<hex6>` directory a removal recorded as
|
|
358
|
-
// gone and no index names
|
|
359
|
-
// is written before its agent is created, so an
|
|
402
|
+
// gone and no index names, and drop member-store auth profiles a removal recorded as `dropped`. No roster here: the
|
|
403
|
+
// gateway is down, and no add is in flight while preparing (an entry is written before its agent is created, so an
|
|
404
|
+
// account being added is named).
|
|
360
405
|
sweepAccountOrphans() {
|
|
361
406
|
try {
|
|
362
|
-
sweepOrphans(this.root);
|
|
407
|
+
sweepOrphans(this.root, this.o.log);
|
|
363
408
|
}
|
|
364
409
|
catch (error) {
|
|
365
410
|
this.o.log?.(`prepare: orphans are not swept: ${String(error.message).slice(0, 200)}`);
|
|
@@ -369,6 +414,7 @@ export class Engine {
|
|
|
369
414
|
if (this.starting)
|
|
370
415
|
return this.starting;
|
|
371
416
|
this.stopping = false;
|
|
417
|
+
this.stopOwner = new AbortController();
|
|
372
418
|
const pending = this.startOnce();
|
|
373
419
|
this.starting = pending;
|
|
374
420
|
void pending.finally(() => { if (this.starting === pending)
|
|
@@ -514,6 +560,11 @@ export class Engine {
|
|
|
514
560
|
withAuthStore(task) { return this.authStore.offline(task); }
|
|
515
561
|
async stop() {
|
|
516
562
|
this.stopping = true;
|
|
563
|
+
this.stopOwner.abort();
|
|
564
|
+
await this.claudeLogin?.close();
|
|
565
|
+
await this.claudeReset?.catch(() => { });
|
|
566
|
+
this.claudeLogin = undefined;
|
|
567
|
+
this.claudeAccounts = undefined;
|
|
517
568
|
await this.starting?.catch(() => { });
|
|
518
569
|
const child = this.child;
|
|
519
570
|
await this.started.terminate(delay);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export { ENGINE_VERSION, OPERATOR_SCOPES, PROTOCOL_VERSION } from './constants.ts';
|
|
2
|
-
export type { Account, AccountId, AccountPick, AccountRef, Approval, CallOptions, Considered, Decision, Defaults, ModelInfo, MoveResult, PickWhy, Room, RoomSpan, RunSelection, SignInState, 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';
|
|
2
|
+
export type { Account, AccountId, AccountPick, AccountRef, Approval, CallOptions, Considered, Decision, Defaults, ModelInfo, MoveResult, PickWhy, Room, RoomSpan, RunSelection, SignInState, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, GateResult, Hello, KitState, KitEventName, KitEventPayload, BrowserPing, LearningCapture, LearningMode, Member, PlanWindow, Route, RunEnd, RunEvent, RunRef, RunSpec, OutputSchema, SchemaOutput, RunUsage, ThinkingAsked, ThinkingReport, ThinkingUsed, 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';
|
|
6
6
|
export { OpenClawKit, type KitOptions, type RetainedLogin } from './kit.ts';
|
|
7
|
-
export type { AddKeyResult, KeyFailureReason } from './keys.ts';
|
|
7
|
+
export type { AddKeyHandle, AddKeyResult, KeyFailureReason } from './keys.ts';
|
|
8
8
|
export { stateWords, toAccountView, words, type AccountView, type WordKey } from './words.ts';
|
|
9
9
|
export { chooseAccount, consider, resolveSelection, type RoomOf } from './pick.ts';
|
|
10
10
|
export { AuthStoreUnreadableError, AuthStoreSealSizeError } from './auth-store.ts';
|
package/dist/keys.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ActivationCtx } from './activation.ts';
|
|
2
|
+
import { type RouteView } from './routes.ts';
|
|
2
3
|
import type { Member, ModelInfo } from './types.ts';
|
|
3
4
|
export type KeyFailureReason = 'auth' | 'billing' | 'empty' | 'model' | 'network' | 'engine' | 'inference' | 'busy';
|
|
4
5
|
export type AddKeyResult = {
|
|
@@ -7,6 +8,10 @@ export type AddKeyResult = {
|
|
|
7
8
|
status: 'invalid' | 'not_included' | 'retry';
|
|
8
9
|
reason: KeyFailureReason;
|
|
9
10
|
};
|
|
11
|
+
/** The add resolves like a Promise; `cancel()` stops a pending add and clears any partial key (retry/busy). */
|
|
12
|
+
export type AddKeyHandle = Promise<AddKeyResult> & {
|
|
13
|
+
cancel(): void;
|
|
14
|
+
};
|
|
10
15
|
export declare function createKeys(ctx: ActivationCtx & {
|
|
11
16
|
root: string;
|
|
12
17
|
ensure(member: Member): Promise<{
|
|
@@ -14,6 +19,9 @@ export declare function createKeys(ctx: ActivationCtx & {
|
|
|
14
19
|
}>;
|
|
15
20
|
restarting?(): boolean;
|
|
16
21
|
prepareOwner(agentId: string): Promise<void>;
|
|
22
|
+
indexLane(member: Member, route: RouteView): Promise<void>;
|
|
23
|
+
dropLane(member: Member, provider?: string): Promise<void>;
|
|
24
|
+
backfillLane(member: Member, model: string | undefined): Promise<void>;
|
|
17
25
|
}): {
|
|
18
26
|
models: (member: Member, o: {
|
|
19
27
|
authChoice: string;
|
|
@@ -23,12 +31,15 @@ export declare function createKeys(ctx: ActivationCtx & {
|
|
|
23
31
|
authChoice: string;
|
|
24
32
|
apiKey: string;
|
|
25
33
|
model?: string;
|
|
26
|
-
}):
|
|
34
|
+
}): AddKeyHandle;
|
|
27
35
|
ready(member: Member, o?: {
|
|
28
36
|
once?: boolean;
|
|
29
37
|
}): Promise<{
|
|
30
38
|
agentId: string;
|
|
31
39
|
model: string;
|
|
32
40
|
} | undefined>;
|
|
33
|
-
|
|
41
|
+
remove(member: Member, o?: {
|
|
42
|
+
provider?: string;
|
|
43
|
+
}): Promise<void>;
|
|
44
|
+
sweep(clear: boolean): Promise<boolean>;
|
|
34
45
|
};
|
package/dist/keys.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { activate } from "./activation.js";
|
|
3
3
|
import { keyAgentId, MEMBER_ID, KEY_PREFIX, untilListed } from "./members.js";
|
|
4
|
+
import { providerOf } from "./pick.js";
|
|
4
5
|
import { routes } from "./routes.js";
|
|
5
6
|
import { modelsOf } from "./accounts.js";
|
|
6
7
|
import { LIVE_CATALOGS } from "./constants.js";
|
|
@@ -73,62 +74,82 @@ export function createKeys(ctx) {
|
|
|
73
74
|
return {
|
|
74
75
|
models,
|
|
75
76
|
exclusive,
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
await untilListed(ctx.request, agentId);
|
|
101
|
-
if (!(await control(member, 'prepare', route.provider)).ok)
|
|
102
|
-
return failure('retry', 'engine');
|
|
103
|
-
// The public pin's typed API-key path, never a CLI-login import. Discard the engine's lines/errors.
|
|
104
|
-
sent = true;
|
|
105
|
-
const result = await activate(ctx, { kind: 'api-key', agentId, authChoice: o.authChoice, apiKey: o.apiKey }, { timeoutMs: 180_000 });
|
|
106
|
-
if (!result.ok && !result.late)
|
|
107
|
-
return abandon(probeResult(result.status));
|
|
108
|
-
if (result.late)
|
|
109
|
-
return abandon(failure('retry', 'busy'));
|
|
110
|
-
if (o.model !== undefined && !(await models(member, o)).some((m) => m.id === o.model && m.available))
|
|
111
|
-
return abandon(failure('not_included', 'model'));
|
|
112
|
-
// Prepare before publishing the ready marker: failed preparation remains unusable even if clear fails.
|
|
77
|
+
add(member, o) {
|
|
78
|
+
const ac = new AbortController();
|
|
79
|
+
const cancel = () => { if (!ac.signal.aborted)
|
|
80
|
+
ac.abort(); };
|
|
81
|
+
const done = (async () => {
|
|
82
|
+
if (!check(member))
|
|
83
|
+
throw new Error('Choose your own member.');
|
|
84
|
+
if (!o.apiKey.trim())
|
|
85
|
+
return failure('invalid', 'empty');
|
|
86
|
+
const route = routes().find((r) => r.choice === o.authChoice && r.keyEntry && r.readiness === 'ready');
|
|
87
|
+
if (!route || (o.model !== undefined && !LIVE_CATALOGS.includes(route.provider)))
|
|
88
|
+
return failure('not_included', 'model');
|
|
89
|
+
if (ac.signal.aborted)
|
|
90
|
+
return failure('retry', 'busy');
|
|
91
|
+
return exclusive(member, async () => {
|
|
92
|
+
let sent = false;
|
|
93
|
+
// A key the engine may have taken but not sealed is cleared (prepare already emptied the agent); if the clear
|
|
94
|
+
// does not reach the engine, the key stays unsealed and unusable until the next key add or kit start, so runs
|
|
95
|
+
// fail closed.
|
|
96
|
+
const abandon = async (result) => {
|
|
97
|
+
await control(member, 'clear').catch(() => undefined);
|
|
98
|
+
return result;
|
|
99
|
+
};
|
|
100
|
+
const cancelled = () => failure('retry', 'busy');
|
|
113
101
|
try {
|
|
114
|
-
|
|
102
|
+
if (ac.signal.aborted)
|
|
103
|
+
return cancelled();
|
|
104
|
+
await ctx.ensure(member);
|
|
105
|
+
const agentId = keyAgentId(member);
|
|
106
|
+
const list = await ctx.request('agents.list');
|
|
107
|
+
if (!list.agents.some((a) => a.id === agentId))
|
|
108
|
+
await ctx.request('agents.create', { name: agentId, workspace: join(ctx.root, 'key-workspaces', member) });
|
|
109
|
+
// Creation answers before the runtime roster/owners publish, just as for ordinary members.
|
|
110
|
+
await untilListed(ctx.request, agentId);
|
|
111
|
+
if (ac.signal.aborted)
|
|
112
|
+
return cancelled();
|
|
113
|
+
if (!(await control(member, 'prepare', route.provider)).ok)
|
|
114
|
+
return failure('retry', 'engine');
|
|
115
|
+
// The public pin's typed API-key path, never a CLI-login import. Discard the engine's lines/errors.
|
|
116
|
+
sent = true;
|
|
117
|
+
if (ac.signal.aborted)
|
|
118
|
+
return abandon(cancelled());
|
|
119
|
+
const result = await activate(ctx, { kind: 'api-key', agentId, authChoice: o.authChoice, apiKey: o.apiKey }, { timeoutMs: 180_000, signal: ac.signal });
|
|
120
|
+
if (ac.signal.aborted)
|
|
121
|
+
return abandon(cancelled());
|
|
122
|
+
if (!result.ok && !result.late)
|
|
123
|
+
return abandon(probeResult(result.status));
|
|
124
|
+
if (result.late)
|
|
125
|
+
return abandon(failure('retry', 'busy'));
|
|
126
|
+
if (o.model !== undefined && !(await models(member, o)).some((m) => m.id === o.model && m.available))
|
|
127
|
+
return abandon(failure('not_included', 'model'));
|
|
128
|
+
// Prepare before publishing the ready marker: failed preparation remains unusable even if clear fails.
|
|
129
|
+
try {
|
|
130
|
+
await ctx.prepareOwner(agentId);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return abandon(failure('retry', 'engine'));
|
|
134
|
+
}
|
|
135
|
+
if (ac.signal.aborted)
|
|
136
|
+
return abandon(cancelled());
|
|
137
|
+
if (o.model !== undefined && LIVE_CATALOGS.includes(route.provider) && !(await registered(route.provider, o.model)))
|
|
138
|
+
return abandon(failure('retry', 'model'));
|
|
139
|
+
if (!(await control(member, 'seal', undefined, o.model)).ok)
|
|
140
|
+
return abandon(failure('retry', 'engine'));
|
|
141
|
+
await ctx.indexLane(member, route);
|
|
142
|
+
return { status: 'ok' };
|
|
115
143
|
}
|
|
116
|
-
catch {
|
|
117
|
-
|
|
144
|
+
catch (error) {
|
|
145
|
+
const reason = errorReason(error, sent);
|
|
146
|
+
if (sent)
|
|
147
|
+
return abandon(failure('retry', reason));
|
|
148
|
+
return failure('retry', reason);
|
|
118
149
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
return { status: 'ok' };
|
|
123
|
-
return abandon(failure('retry', 'engine'));
|
|
124
|
-
}
|
|
125
|
-
catch (error) {
|
|
126
|
-
const reason = errorReason(error, sent);
|
|
127
|
-
if (sent)
|
|
128
|
-
return abandon(failure('retry', reason));
|
|
129
|
-
return failure('retry', reason);
|
|
130
|
-
}
|
|
131
|
-
});
|
|
150
|
+
});
|
|
151
|
+
})();
|
|
152
|
+
return Object.assign(done, { cancel });
|
|
132
153
|
},
|
|
133
154
|
async ready(member, o = {}) {
|
|
134
155
|
if (!check(member))
|
|
@@ -147,11 +168,37 @@ export function createKeys(ctx) {
|
|
|
147
168
|
}
|
|
148
169
|
}
|
|
149
170
|
},
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
|
|
171
|
+
// Remove this member's saved key: the key agent's store and its ready marker, so the lane reads signed out and
|
|
172
|
+
// stays signed out, plus its `<provider>.key` index entry, so the accounts list stops naming the removed key.
|
|
173
|
+
// `provider` scopes it to the key actually saved for that provider (the member agent's own
|
|
174
|
+
// sign-out never reaches this lane), so signing out of one provider never drops another's key. A lane that reads
|
|
175
|
+
// not ready has nothing saved to remove; a refused read or clear is a typed refusal, never raw engine text.
|
|
176
|
+
// Serialized with adds and key runs like every other key change.
|
|
177
|
+
async remove(member, o = {}) {
|
|
178
|
+
if (!check(member))
|
|
179
|
+
throw new Error('Choose your own member.');
|
|
180
|
+
await exclusive(member, async () => {
|
|
181
|
+
const refused = () => { throw new Error('The key could not be removed.'); };
|
|
182
|
+
if (o.provider !== undefined) {
|
|
183
|
+
// An unreadable lane cannot name its provider, so removal never guesses and risks another provider's key.
|
|
184
|
+
const ready = await control(member, 'ready').catch(() => undefined);
|
|
185
|
+
if (!ready)
|
|
186
|
+
refused();
|
|
187
|
+
else if (ready.ok !== true || providerOf(ready.model ?? '') !== o.provider)
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const cleared = await control(member, 'clear').catch(() => undefined);
|
|
191
|
+
if (cleared?.ok !== true && cleared?.absent !== true)
|
|
192
|
+
refused();
|
|
193
|
+
await ctx.dropLane(member, o.provider);
|
|
194
|
+
});
|
|
195
|
+
},
|
|
196
|
+
// Kit connect calls this once the gateway is ready: a failed add whose abandon clear never reached the engine
|
|
197
|
+
// leaves an unsealed key with no ready marker, so drop it (`clear`, until a sweep completes). A single ready read
|
|
198
|
+
// per key agent, no retries; an add in flight for that member is waited for. Only an absent marker is cleared; a
|
|
199
|
+
// refused read or clear (the gateway left ready) makes the sweep incomplete. A sealed lane is backfilled on every
|
|
200
|
+
// call. Resolves true once every key agent was read.
|
|
201
|
+
async sweep(clear) {
|
|
155
202
|
let agents;
|
|
156
203
|
try {
|
|
157
204
|
agents = (await ctx.request('agents.list')).agents;
|
|
@@ -168,7 +215,11 @@ export function createKeys(ctx) {
|
|
|
168
215
|
continue;
|
|
169
216
|
await exclusive(member, async () => {
|
|
170
217
|
const ready = await control(member, 'ready').catch(() => undefined);
|
|
171
|
-
if (ready?.ok === true)
|
|
218
|
+
if (ready?.ok === true) {
|
|
219
|
+
await ctx.backfillLane(member, ready.model);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (!clear)
|
|
172
223
|
return;
|
|
173
224
|
if (ready?.absent !== true) {
|
|
174
225
|
complete = false;
|
package/dist/kit.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SealingAdapter } from '@byokit/secrets';
|
|
2
|
-
import { type
|
|
2
|
+
import { type AddKeyHandle } from './keys.ts';
|
|
3
3
|
import { type RouteView } from './routes.ts';
|
|
4
4
|
import type { BrowserHost, BrowserOptions } from './browser.ts';
|
|
5
5
|
import type { Account, AccountId, AccountPick, Approval, CallOptions, Decision, Defaults, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, Hello, KitState, KitEventName, KitEventPayload, LearningCapture, LearningMode, Member, ModelInfo, MoveResult, Room, RunEnd, RunEvent, RunRef, RunSelection, RunSpec, OutputSchema, SchemaOutput, SignInOptions, SignInView, ToolHost, ToolSpec } from './types.ts';
|
|
@@ -158,7 +158,16 @@ export declare class OpenClawKit {
|
|
|
158
158
|
authChoice: string;
|
|
159
159
|
apiKey: string;
|
|
160
160
|
model?: string;
|
|
161
|
-
}):
|
|
161
|
+
}): AddKeyHandle;
|
|
162
|
+
private indexKeyLane;
|
|
163
|
+
/** Remove this member's saved key (the `byokit-key-<member>` lane: its credential store and ready marker), so
|
|
164
|
+
* `signedIn` is false for it and a restart cannot revive it. `provider` removes only that provider's saved key;
|
|
165
|
+
* with no saved key the call resolves without doing anything. */
|
|
166
|
+
removeKey(member: Member, o?: {
|
|
167
|
+
provider?: string;
|
|
168
|
+
}): Promise<void>;
|
|
169
|
+
private dropKeyLane;
|
|
170
|
+
private backfillKeyLane;
|
|
162
171
|
private prepareKeyOwner;
|
|
163
172
|
private syncCatalogs;
|
|
164
173
|
migrateRetainedLogin(member: Member, source: RetainedLogin): Promise<'staged' | 'nothing' | 'failed'>;
|