@byokit/openclaw 0.11.0 → 0.12.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 +27 -0
- package/README.md +45 -7
- package/SECURITY.md +3 -2
- package/dist/accounts.d.ts +45 -1
- package/dist/accounts.js +641 -46
- package/dist/activation.d.ts +17 -0
- package/dist/activation.js +9 -0
- package/dist/approvals.d.ts +2 -0
- package/dist/approvals.js +9 -9
- package/dist/attribution.d.ts +3 -0
- package/dist/attribution.js +25 -0
- package/dist/bridge.d.ts +3 -1
- package/dist/bridge.js +4 -6
- package/dist/config.js +10 -0
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +2 -0
- package/dist/device.d.ts +5 -2
- package/dist/device.js +11 -1
- package/dist/engine.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/keys.d.ts +19 -5
- package/dist/keys.js +135 -23
- package/dist/kit.d.ts +38 -1
- package/dist/kit.js +153 -28
- package/dist/link.js +21 -11
- package/dist/members.d.ts +3 -0
- package/dist/members.js +13 -9
- package/dist/pick.d.ts +7 -3
- package/dist/pick.js +34 -5
- package/dist/routes.d.ts +1 -0
- package/dist/routes.js +5 -3
- package/dist/routes.json +31 -31
- package/dist/runs.d.ts +7 -0
- package/dist/runs.js +111 -11
- package/dist/signin.d.ts +3 -0
- package/dist/signin.js +51 -17
- package/dist/testing/fake-gateway.d.ts +18 -0
- package/dist/testing/fake-gateway.js +210 -16
- package/dist/testing/model-stub.d.ts +1 -0
- package/dist/testing/model-stub.js +2 -1
- package/dist/types.d.ts +19 -0
- package/dist/words.json +8 -1
- package/package.json +2 -2
- package/plugin/accounts.js +12 -3
- package/plugin/catalog.js +48 -0
- package/plugin/index.js +2 -1
- package/plugin/keys.js +22 -4
|
@@ -20,6 +20,24 @@ export type FakeGateway = {
|
|
|
20
20
|
failNext(method: string, message: string): void;
|
|
21
21
|
drop(why: string): void;
|
|
22
22
|
handle(method: string, fn: FakeHandler): void;
|
|
23
|
+
/** Usage windows a later `models.authStatus` read reports for the agent's provider (O14: the Auto input). */
|
|
24
|
+
setUsage(agentId: string, provider: string, windows: {
|
|
25
|
+
label: string;
|
|
26
|
+
usedPercent?: number;
|
|
27
|
+
resetAt?: number;
|
|
28
|
+
}[]): void;
|
|
29
|
+
/** Profile fields every later wizard sign-in stores (O14 identity): `accountId: undefined` signs in with no account
|
|
30
|
+
* id, so no `who`. With no call, or `signInAs()`, the account id is `fake-<agentId>`. */
|
|
31
|
+
signInAs(fields?: {
|
|
32
|
+
accountId?: string;
|
|
33
|
+
email?: string;
|
|
34
|
+
expires?: number;
|
|
35
|
+
}): void;
|
|
36
|
+
/** A sealed profile already in an agent's store, as a sign-in of another provider leaves it; the agent's model points at it. */
|
|
37
|
+
seedProfile(agentId: string, profileId: string, profile: {
|
|
38
|
+
provider: string;
|
|
39
|
+
type: string;
|
|
40
|
+
} & Record<string, unknown>): void;
|
|
23
41
|
} & {
|
|
24
42
|
transport: GatewayTransport;
|
|
25
43
|
};
|
|
@@ -14,6 +14,13 @@ const DEVICE_STEP = {
|
|
|
14
14
|
deviceCode: { code: 'CREW-2026', expiresInMinutes: 15 }, externalUrl: 'https://auth.openai.com/codex/device',
|
|
15
15
|
};
|
|
16
16
|
const PROGRESS_STEP = { id: 'step-wait', type: 'progress', message: 'Waiting for authorization', executor: 'gateway' };
|
|
17
|
+
// ChatGPT's browser route: the pin hands its unsigned authorize URL to the next step as `externalUrl`.
|
|
18
|
+
const BROWSER_STEP = {
|
|
19
|
+
id: 'step-browser', type: 'note', executor: 'client', message: 'Open this URL in your LOCAL browser.',
|
|
20
|
+
externalUrl: 'https://auth.openai.com/oauth/authorize?response_type=code&client_id=fake-client&redirect_uri=http%3A%2F%2Flocalhost%3A1455%2Fauth%2Fcallback&state=fake-state',
|
|
21
|
+
};
|
|
22
|
+
// The pin's agents.create into an existing workspace rewrites IDENTITY.md's Name line (mergeIdentityMarkdownContent).
|
|
23
|
+
const IDENTITY_NAME = /^- \*\*Name:\*\*.*$/m;
|
|
17
24
|
const EVENTS = [
|
|
18
25
|
'agent',
|
|
19
26
|
'exec.approval.requested', 'exec.approval.resolved',
|
|
@@ -110,7 +117,47 @@ export function fakeGateway(script, o) {
|
|
|
110
117
|
const orders = new Map();
|
|
111
118
|
const models = new Map();
|
|
112
119
|
const wizards = new Map();
|
|
120
|
+
let signInFields;
|
|
121
|
+
// Each workspace's files by name, as `agents.files.get/set` and the create-time IDENTITY.md rewrite see them.
|
|
122
|
+
const workspaces = new Map();
|
|
123
|
+
const filesOf = (agentId) => {
|
|
124
|
+
const agent = agents.get(agentId);
|
|
125
|
+
if (!agent)
|
|
126
|
+
throw new Error(`unknown agent id "${agentId}"`);
|
|
127
|
+
const workspace = agent.workspace ?? `workspace-${agentId}`;
|
|
128
|
+
let files = workspaces.get(workspace);
|
|
129
|
+
if (!files) {
|
|
130
|
+
files = new Map();
|
|
131
|
+
workspaces.set(workspace, files);
|
|
132
|
+
}
|
|
133
|
+
return { workspace, files };
|
|
134
|
+
};
|
|
135
|
+
const fileAnswer = (agentId, name) => {
|
|
136
|
+
const { workspace, files } = filesOf(agentId);
|
|
137
|
+
const content = files.get(name);
|
|
138
|
+
return { agentId, workspace, file: { name, path: join(workspace, name), missing: content === undefined,
|
|
139
|
+
...(content === undefined ? {} : { size: Buffer.byteLength(content), content }) } };
|
|
140
|
+
};
|
|
113
141
|
const runs = new Map();
|
|
142
|
+
const sessions = new Map();
|
|
143
|
+
const sessionOf = (agentId, key) => {
|
|
144
|
+
let session = sessions.get(`${agentId}\n${key}`);
|
|
145
|
+
if (!session) {
|
|
146
|
+
session = { sessionId: randomUUID(), updatedAt: 0, rows: [] };
|
|
147
|
+
sessions.set(`${agentId}\n${key}`, session);
|
|
148
|
+
}
|
|
149
|
+
return session;
|
|
150
|
+
};
|
|
151
|
+
const append = (session, row) => {
|
|
152
|
+
session.rows.push(row);
|
|
153
|
+
session.updatedAt = Math.max(Date.now(), session.updatedAt + 1);
|
|
154
|
+
};
|
|
155
|
+
const activeOn = (agentId, key) => [...runs.values()].some((run) => !run.result && run.agentId === agentId && run.sessionKey === key);
|
|
156
|
+
// The engine's own cached read of each provider's usage endpoint (O14): cold until a `models.authStatus`
|
|
157
|
+
// answer has carried the provider once (no `usage` on that first answer), then whatever `setUsage` stored.
|
|
158
|
+
// A pre-sign-in read names no provider, so it warms nothing: the journey's first room read stays cold.
|
|
159
|
+
const usage = new Map();
|
|
160
|
+
const warmed = new Set();
|
|
114
161
|
const configBox = { hash: 'hash-1', config: {} };
|
|
115
162
|
let hashSeq = 1;
|
|
116
163
|
const ensureAgent = (id, workspace) => {
|
|
@@ -238,10 +285,14 @@ export function fakeGateway(script, o) {
|
|
|
238
285
|
if (params.agentId != null && !agents.has(String(params.agentId)))
|
|
239
286
|
throw new Error(`unknown agent: ${params.agentId}`);
|
|
240
287
|
// Like the engine: the provider/model the run is called on, its own override or the member's (here openai).
|
|
241
|
-
const run = { runId: randomUUID(), sessionKey: String(params.sessionKey ?? ''), seq: 0,
|
|
288
|
+
const run = { runId: randomUUID(), agentId: String(params.agentId ?? 'main'), sessionKey: String(params.sessionKey ?? ''), seq: 0,
|
|
242
289
|
provider: String(params.provider ?? 'openai'), model: String(params.model ?? 'gpt-5.1'), waiters: [] };
|
|
243
290
|
runs.set(run.runId, run);
|
|
291
|
+
const session = sessionOf(run.agentId, run.sessionKey);
|
|
292
|
+
session.provider = run.provider;
|
|
293
|
+
session.model = run.model;
|
|
244
294
|
const message = String(params.message ?? '');
|
|
295
|
+
append(session, { role: 'user', content: [{ type: 'text', text: message }] });
|
|
245
296
|
const text = `fake: ${message}`;
|
|
246
297
|
setTimeout(() => {
|
|
247
298
|
void (async () => {
|
|
@@ -272,8 +323,11 @@ export function fakeGateway(script, o) {
|
|
|
272
323
|
// Completion rides `phase: 'result'` like the real engine (O11), not `'end'`, with its result.
|
|
273
324
|
emitRun(run, 'tool', { phase: 'result', name: call.name, toolCallId, isError,
|
|
274
325
|
result: { content: [{ type: 'text', text: output }] } });
|
|
326
|
+
append(session, { role: 'assistant', content: [{ type: 'toolCall', id: toolCallId, name: call.name, arguments: call.input }] });
|
|
327
|
+
append(session, { role: 'toolResult', toolCallId, toolName: call.name, isError, content: [{ type: 'text', text: output }] });
|
|
275
328
|
}
|
|
276
329
|
emitRun(run, 'assistant', { text });
|
|
330
|
+
append(session, { role: 'assistant', content: [{ type: 'text', text }] });
|
|
277
331
|
// The engine sums the run's usage (zero buckets omitted); the fake counts characters.
|
|
278
332
|
run.usage = { input: message.length, output: text.length, total: message.length + text.length };
|
|
279
333
|
setTimeout(() => finish(run, { status: 'ok', terminalReply: { text } }), ABORT_WINDOW_MS);
|
|
@@ -304,16 +358,41 @@ export function fakeGateway(script, o) {
|
|
|
304
358
|
}),
|
|
305
359
|
'agents.create': (p) => {
|
|
306
360
|
const id = String(p.name);
|
|
361
|
+
if (agents.has(id))
|
|
362
|
+
throw new Error(`agent "${id}" already exists`);
|
|
307
363
|
const workspace = p.workspace ? String(p.workspace) : undefined;
|
|
364
|
+
if (workspace) {
|
|
365
|
+
const files = workspaces.get(workspace) ?? new Map();
|
|
366
|
+
workspaces.set(workspace, files);
|
|
367
|
+
const identity = files.get('IDENTITY.md') ?? '# IDENTITY.md - Agent Identity\n\n';
|
|
368
|
+
files.set('IDENTITY.md', IDENTITY_NAME.test(identity) ? identity.replace(IDENTITY_NAME, `- **Name:** ${id}`) : `${identity}- **Name:** ${id}\n`);
|
|
369
|
+
}
|
|
308
370
|
// With rosterApplyMs the fake mirrors the pinned engine: create answers before the roster hot-reload
|
|
309
371
|
// makes the agent visible to agents.list (Crewhouse saw ~113 ms).
|
|
310
372
|
if (o?.rosterApplyMs)
|
|
311
373
|
setTimeout(() => ensureAgent(id, workspace), o.rosterApplyMs);
|
|
312
374
|
else
|
|
313
375
|
ensureAgent(id, workspace);
|
|
314
|
-
return { id };
|
|
376
|
+
return { ok: true, agentId: id, name: id, workspace: workspace ?? `workspace-${id}` };
|
|
377
|
+
},
|
|
378
|
+
// deleteFiles: false keeps the workspace, as the kit always asks; the agent's own store and model go.
|
|
379
|
+
'agents.delete': (p) => {
|
|
380
|
+
const id = String(p.agentId);
|
|
381
|
+
if (!agents.delete(id))
|
|
382
|
+
throw new Error(`agent "${id}" not found`);
|
|
383
|
+
profiles.delete(id);
|
|
384
|
+
orders.delete(id);
|
|
385
|
+
models.delete(id);
|
|
386
|
+
return { ok: true, agentId: id, removedBindings: 0 };
|
|
387
|
+
},
|
|
388
|
+
'agents.files.get': (p) => fileAnswer(String(p.agentId), String(p.name)),
|
|
389
|
+
'agents.files.set': (p) => {
|
|
390
|
+
filesOf(String(p.agentId)).files.set(String(p.name), String(p.content));
|
|
391
|
+
return { ok: true, ...fileAnswer(String(p.agentId), String(p.name)) };
|
|
315
392
|
},
|
|
316
393
|
// One profile per signed-in provider, `<provider>:default`, as the pin lists an agent's own profiles.
|
|
394
|
+
// Plus its cached usage once warm: cold on the first answer carrying the provider (no `usage`), then the
|
|
395
|
+
// stored windows.
|
|
317
396
|
'models.authStatus': (p) => {
|
|
318
397
|
const id = String(p?.agentId ?? 'main');
|
|
319
398
|
const store = storeOf(id);
|
|
@@ -322,12 +401,26 @@ export function fakeGateway(script, o) {
|
|
|
322
401
|
providers: (agents.get(id)?.providers ?? []).map((provider) => {
|
|
323
402
|
const profileId = `${provider}:default`;
|
|
324
403
|
const credential = store[profileId];
|
|
404
|
+
const key = `${id}\n${provider}`;
|
|
405
|
+
const cold = !warmed.has(key);
|
|
406
|
+
warmed.add(key);
|
|
407
|
+
const windows = cold ? undefined : usage.get(id)?.get(provider);
|
|
325
408
|
return { provider,
|
|
326
409
|
profiles: [{ profileId, type: credential?.type ?? 'oauth',
|
|
327
|
-
status: credential ? statusOf(credential, now) : 'ok' }]
|
|
410
|
+
status: credential ? statusOf(credential, now) : 'ok' }],
|
|
411
|
+
...(windows !== undefined ? { usage: { windows } } : {}) };
|
|
328
412
|
}),
|
|
329
413
|
};
|
|
330
414
|
},
|
|
415
|
+
// The runtime-allowed catalog for the agent's signed-in providers (O14): one available row each. A test
|
|
416
|
+
// scripting exact availability scripts this method with `handle`.
|
|
417
|
+
'models.list': (p) => {
|
|
418
|
+
const id = String(p?.agentId ?? 'main');
|
|
419
|
+
return {
|
|
420
|
+
models: (agents.get(id)?.providers ?? []).map((provider) => ({ id: `${provider}/default`,
|
|
421
|
+
name: 'Default', provider, available: true })),
|
|
422
|
+
};
|
|
423
|
+
},
|
|
331
424
|
'models.authLogout': (p) => {
|
|
332
425
|
const agent = agents.get(String(p?.agentId ?? 'main'));
|
|
333
426
|
// The pin refuses a profileIds list naming a profile the agent does not hold.
|
|
@@ -353,20 +446,27 @@ export function fakeGateway(script, o) {
|
|
|
353
446
|
},
|
|
354
447
|
'byokit.keys': (p) => {
|
|
355
448
|
const id = `byokit-key-${p.member}`;
|
|
356
|
-
if (p.action === 'prepare') {
|
|
449
|
+
if (p.action === 'prepare' || p.action === 'clear') {
|
|
357
450
|
keys.delete(id);
|
|
358
451
|
ensureAgent(id).providers = [];
|
|
359
452
|
return { ok: true };
|
|
360
453
|
}
|
|
361
454
|
const key = keys.get(id);
|
|
362
455
|
if (!key)
|
|
363
|
-
return { ok: false };
|
|
456
|
+
return { ok: false, absent: true };
|
|
364
457
|
if (p.action === 'seal') {
|
|
365
458
|
key.sealed = true;
|
|
459
|
+
if (typeof p.model === 'string')
|
|
460
|
+
key.model = p.model;
|
|
366
461
|
return { ok: true };
|
|
367
462
|
}
|
|
368
|
-
return
|
|
463
|
+
return key.sealed ? { ok: true, model: key.model } : { ok: false, absent: true };
|
|
369
464
|
},
|
|
465
|
+
// The provider's live catalog hook (5.6): every live model with its own api, once an agent holds the provider's key.
|
|
466
|
+
'byokit.catalog': (p) => ([...keys.values()].some((key) => key.provider === p.provider) ? { ok: true, models: [
|
|
467
|
+
{ id: 'fake-key-model', name: 'Fake Key Model', api: 'openai-completions' },
|
|
468
|
+
{ id: 'fake-live-model', name: 'Fake Live Model', api: 'openai-responses' }
|
|
469
|
+
] } : { ok: false, reason: 'no-key' }),
|
|
370
470
|
// Account sealing (5.15): the same five actions, branches and refusals the shipped plugin method serves.
|
|
371
471
|
'byokit.accounts': (p) => {
|
|
372
472
|
const action = String(p.action ?? '');
|
|
@@ -414,11 +514,21 @@ export function fakeGateway(script, o) {
|
|
|
414
514
|
return { ok: true, profiles: Object.entries(store).map(([profileId, credential]) => ({ provider: credential.provider,
|
|
415
515
|
profileId, type: credential.type, status: statusOf(credential, Date.now()), ...(whoOf(credential) ? { who: whoOf(credential) } : {}) })) };
|
|
416
516
|
if (action === 'clear') {
|
|
417
|
-
profiles
|
|
418
|
-
|
|
517
|
+
// Mirrors the plugin: a provider scope drops only that provider's profiles, order and lastGood.
|
|
518
|
+
const scope = typeof p.provider === 'string' && p.provider ? String(p.provider) : undefined;
|
|
519
|
+
if (!scope) {
|
|
520
|
+
profiles.set(id, {});
|
|
521
|
+
orders.set(id, {});
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
for (const [profileId, credential] of Object.entries(store))
|
|
525
|
+
if (credential.provider === scope)
|
|
526
|
+
delete store[profileId];
|
|
527
|
+
delete order[scope];
|
|
528
|
+
}
|
|
419
529
|
const cleared = agents.get(id);
|
|
420
530
|
if (cleared)
|
|
421
|
-
cleared.providers = [];
|
|
531
|
+
cleared.providers = scope ? cleared.providers.filter((x) => x !== scope) : [];
|
|
422
532
|
return { ok: true };
|
|
423
533
|
}
|
|
424
534
|
if (action === 'ready') {
|
|
@@ -441,7 +551,8 @@ export function fakeGateway(script, o) {
|
|
|
441
551
|
const profileId = primary.slice(at + 1);
|
|
442
552
|
const credential = store[profileId];
|
|
443
553
|
const account = ACCOUNT_AGENT.test(id);
|
|
444
|
-
if (!credential || credential.provider !== p.provider
|
|
554
|
+
if (!credential || credential.provider !== p.provider
|
|
555
|
+
|| (!account && credential.type === 'api_key' && p.planKey !== true))
|
|
445
556
|
return { ok: false };
|
|
446
557
|
for (const [otherId, other] of Object.entries(store))
|
|
447
558
|
if (account ? otherId !== profileId : other.provider === p.provider && otherId !== profileId)
|
|
@@ -466,17 +577,26 @@ export function fakeGateway(script, o) {
|
|
|
466
577
|
'openclaw.setup.activate': (p) => {
|
|
467
578
|
if (p.kind !== 'api-key')
|
|
468
579
|
throw new Error('only the API-key setup path is simulated');
|
|
580
|
+
// The pin takes a pasted key only for a choice whose manifest allows one (`appGuidedSecret`).
|
|
469
581
|
const route = routes().find((r) => r.choice === String(p.authChoice) && r.keyEntry);
|
|
470
|
-
if (!route)
|
|
471
|
-
return { ok: false, status: 'unavailable' };
|
|
582
|
+
if (!route || route.upstream?.flow === 'absent')
|
|
583
|
+
return { ok: false, status: 'unavailable', error: 'That key-based provider is not available on this Gateway.' };
|
|
472
584
|
const model = `${route.provider}/fake-key-model`;
|
|
473
585
|
keys.set(String(p.agentId), { provider: route.provider, key: String(p.apiKey), model, sealed: false });
|
|
474
|
-
signInAgent(String(p.agentId), route.provider);
|
|
586
|
+
signInAgent(String(p.agentId), route.provider, { type: 'api_key', key: String(p.apiKey) });
|
|
587
|
+
setPrimaryOfId(String(p.agentId), `${model}@${route.provider}:default`);
|
|
475
588
|
return { ok: true, status: 'ok', modelRef: model };
|
|
476
589
|
},
|
|
477
590
|
'openclaw.setup.auth.start': (p) => {
|
|
591
|
+
// The pin starts only an app-guided login; a key-entry choice is set up from its pasted key instead.
|
|
592
|
+
const route = routes().find((r) => r.choice === String(p.authChoice ?? ''));
|
|
593
|
+
if (!route || route.keyEntry || route.via === 'cli' || route.upstream?.flow === 'absent')
|
|
594
|
+
throw new Error('That provider setup is not available on this Gateway.');
|
|
595
|
+
// The pin's single setup admission: a second start while a session runs is refused.
|
|
596
|
+
if (wizards.size)
|
|
597
|
+
throw new Error('setup is already in progress');
|
|
478
598
|
const sessionId = String(p.sessionId ?? `fake-${randomUUID()}`);
|
|
479
|
-
wizards.set(sessionId, { agentId: String(p.agentId ?? 'main'), answered: false, pulls: 0 });
|
|
599
|
+
wizards.set(sessionId, { agentId: String(p.agentId ?? 'main'), authChoice: String(p.authChoice ?? ''), answered: false, pulls: 0 });
|
|
480
600
|
return { sessionId, done: false, status: 'running' };
|
|
481
601
|
},
|
|
482
602
|
'wizard.next': (p) => {
|
|
@@ -488,11 +608,11 @@ export function fakeGateway(script, o) {
|
|
|
488
608
|
return { done: false, step: PROGRESS_STEP };
|
|
489
609
|
}
|
|
490
610
|
if (!session.answered)
|
|
491
|
-
return { done: false, step: DEVICE_STEP };
|
|
611
|
+
return { done: false, step: session.authChoice === 'openai' ? BROWSER_STEP : DEVICE_STEP };
|
|
492
612
|
if (session.pulls++ === 0)
|
|
493
613
|
return { done: false, step: PROGRESS_STEP };
|
|
494
614
|
wizards.delete(String(p.sessionId));
|
|
495
|
-
signInAgent(session.agentId, 'openai');
|
|
615
|
+
signInAgent(session.agentId, 'openai', signInFields);
|
|
496
616
|
return { done: true, status: 'done', modelActivation: { modelRef: 'openai/gpt-5.1' } };
|
|
497
617
|
},
|
|
498
618
|
'wizard.cancel': (p) => {
|
|
@@ -510,6 +630,66 @@ export function fakeGateway(script, o) {
|
|
|
510
630
|
run.waiters.push(resolve);
|
|
511
631
|
}),
|
|
512
632
|
'sessions.steer': () => ({}),
|
|
633
|
+
// The pin: a key the agent holds answers its canonical key; a missing one errors unless `allowMissing`, which answers `{ ok: false }`.
|
|
634
|
+
'sessions.resolve': (p) => {
|
|
635
|
+
const key = String(p.key ?? ''), agentId = String(p.agentId ?? 'main');
|
|
636
|
+
if (sessions.has(`${agentId}\n${key}`))
|
|
637
|
+
return { ok: true, key, agentId };
|
|
638
|
+
if (p.allowMissing === true)
|
|
639
|
+
return { ok: false };
|
|
640
|
+
throw new Error(`No session found: ${key}`);
|
|
641
|
+
},
|
|
642
|
+
// The pin's rows for one agent (`search` matches a key substring), `hasActiveRun` while a run on it is live.
|
|
643
|
+
'sessions.list': (p) => {
|
|
644
|
+
const agentId = String(p.agentId ?? 'main'), search = String(p.search ?? '').toLowerCase();
|
|
645
|
+
const rows = [...sessions].filter(([id]) => id.startsWith(`${agentId}\n`)).map(([id, s]) => ({ key: id.slice(agentId.length + 1), s }))
|
|
646
|
+
.filter(({ key }) => key.toLowerCase().includes(search))
|
|
647
|
+
.map(({ key, s }) => ({ key, sessionId: s.sessionId, updatedAt: s.updatedAt, hasActiveRun: activeOn(agentId, key),
|
|
648
|
+
...(s.provider ? { modelProvider: s.provider } : {}), ...(s.model ? { model: s.model } : {}) }));
|
|
649
|
+
return { count: rows.length, sessions: rows };
|
|
650
|
+
},
|
|
651
|
+
// `fork` copies the parent's whole transcript, from any agent, into a new session id; refused while the parent runs.
|
|
652
|
+
'sessions.create': (p) => {
|
|
653
|
+
const agentId = String(p.agentId ?? 'main'), key = String(p.key ?? '');
|
|
654
|
+
if (!agents.has(agentId))
|
|
655
|
+
throw new Error(`unknown agent id "${agentId}"`);
|
|
656
|
+
const rows = [];
|
|
657
|
+
let model = {};
|
|
658
|
+
if (p.fork === true) {
|
|
659
|
+
const parentKey = String(p.parentSessionKey ?? '');
|
|
660
|
+
const parentAgent = /^agent:([^:]+):/.exec(parentKey)?.[1] ?? 'main';
|
|
661
|
+
const parent = sessions.get(`${parentAgent}\n${parentKey}`);
|
|
662
|
+
if (!parent)
|
|
663
|
+
throw new Error(`No session found: ${parentKey}`);
|
|
664
|
+
if (activeOn(parentAgent, parentKey))
|
|
665
|
+
throw new Error(`Parent session ${parentKey} is still active; try again in a moment.`);
|
|
666
|
+
rows.push(...structuredClone(parent.rows));
|
|
667
|
+
model = { provider: parent.provider, model: parent.model };
|
|
668
|
+
}
|
|
669
|
+
const ref = typeof p.model === 'string' ? p.model : undefined;
|
|
670
|
+
if (ref)
|
|
671
|
+
model = ref.includes('/') ? { provider: ref.slice(0, ref.indexOf('/')), model: ref.slice(ref.indexOf('/') + 1) } : { ...model, model: ref };
|
|
672
|
+
const session = { sessionId: randomUUID(), updatedAt: Date.now(), rows, ...model };
|
|
673
|
+
sessions.set(`${agentId}\n${key}`, session);
|
|
674
|
+
return { ok: true, key, sessionId: session.sessionId, runStarted: false };
|
|
675
|
+
},
|
|
676
|
+
// A changed session (id or `updatedAt` other than expected) is refused before anything goes, as the pin does.
|
|
677
|
+
'sessions.delete': (p) => {
|
|
678
|
+
const agentId = String(p.agentId ?? 'main'), key = String(p.key ?? '');
|
|
679
|
+
const session = sessions.get(`${agentId}\n${key}`);
|
|
680
|
+
if ((p.expectedSessionId !== undefined && session?.sessionId !== p.expectedSessionId)
|
|
681
|
+
|| (p.expectedSessionUpdatedAt !== undefined && session?.updatedAt !== p.expectedSessionUpdatedAt))
|
|
682
|
+
throw new Error(`Session ${key} changed before deletion. Retry.`);
|
|
683
|
+
if (session && activeOn(agentId, key))
|
|
684
|
+
throw new Error(`Session ${key} is still active; try again.`);
|
|
685
|
+
return { ok: true, key, deleted: sessions.delete(`${agentId}\n${key}`), archived: [] };
|
|
686
|
+
},
|
|
687
|
+
'chat.history': (p) => {
|
|
688
|
+
const key = String(p.sessionKey ?? '');
|
|
689
|
+
const agentId = String(p.agentId ?? /^agent:([^:]+):/.exec(key)?.[1] ?? 'main');
|
|
690
|
+
const session = sessions.get(`${agentId}\n${key}`);
|
|
691
|
+
return { sessionKey: key, ...(session ? { sessionId: session.sessionId } : {}), messages: structuredClone(session?.rows ?? []) };
|
|
692
|
+
},
|
|
513
693
|
'chat.abort': (p) => {
|
|
514
694
|
for (const run of runs.values()) {
|
|
515
695
|
if (run.result || run.sessionKey !== String(p.sessionKey ?? ''))
|
|
@@ -664,6 +844,20 @@ export function fakeGateway(script, o) {
|
|
|
664
844
|
failNext,
|
|
665
845
|
drop,
|
|
666
846
|
handle: (method, fn) => { handlers.set(method, fn); },
|
|
847
|
+
setUsage: (agentId, provider, windows) => {
|
|
848
|
+
let perAgent = usage.get(agentId);
|
|
849
|
+
if (!perAgent) {
|
|
850
|
+
perAgent = new Map();
|
|
851
|
+
usage.set(agentId, perAgent);
|
|
852
|
+
}
|
|
853
|
+
perAgent.set(provider, windows);
|
|
854
|
+
},
|
|
855
|
+
signInAs: (fields) => { signInFields = fields && { ...fields }; },
|
|
856
|
+
seedProfile: (agentId, profileId, profile) => {
|
|
857
|
+
ensureAgent(agentId);
|
|
858
|
+
storeOf(agentId)[profileId] = profile;
|
|
859
|
+
setPrimaryOfId(agentId, `${profile.provider}/default@${profileId}`);
|
|
860
|
+
},
|
|
667
861
|
transport: factory({ port: 0, token: 'test', identityPath: '', bridgeSock: '' }),
|
|
668
862
|
};
|
|
669
863
|
}
|
|
@@ -16,6 +16,7 @@ export type StubCall = {
|
|
|
16
16
|
authorization: string;
|
|
17
17
|
path: string;
|
|
18
18
|
body: StubRequest;
|
|
19
|
+
session?: string;
|
|
19
20
|
};
|
|
20
21
|
/** The token usage every stub reply reports when asked (`stream_options.include_usage`), per model call. */
|
|
21
22
|
export declare const STUB_USAGE: {
|
|
@@ -77,7 +77,8 @@ export function startModelStub(script = [], o = {}) {
|
|
|
77
77
|
res.writeHead(400).end();
|
|
78
78
|
return;
|
|
79
79
|
}
|
|
80
|
-
const call = { authorization: String(req.headers.authorization ?? ''), path: req.url ?? '', body
|
|
80
|
+
const call = { authorization: String(req.headers.authorization ?? ''), path: req.url ?? '', body,
|
|
81
|
+
...(req.headers['x-opencode-session'] ? { session: String(req.headers['x-opencode-session']) } : {}) };
|
|
81
82
|
calls.push(call);
|
|
82
83
|
o.onCall?.(call); // Fixture evidence is persisted before any response/hold awaits.
|
|
83
84
|
if (req.url === '/api/embed' || req.url === '/api/embeddings') {
|
package/dist/types.d.ts
CHANGED
|
@@ -50,6 +50,8 @@ export interface RunSpec<S extends OutputSchema | undefined = OutputSchema | und
|
|
|
50
50
|
}[];
|
|
51
51
|
thinking?: 'off' | 'low' | 'medium' | 'high';
|
|
52
52
|
model?: string;
|
|
53
|
+
account?: AccountRef | 'default' | 'auto';
|
|
54
|
+
needs?: string[];
|
|
53
55
|
/** Explicit paid key choice. Uses a separate member-owned agent and separate session history. */
|
|
54
56
|
auth?: 'apiKey';
|
|
55
57
|
tools?: string[];
|
|
@@ -60,6 +62,14 @@ export type RunEvent = {
|
|
|
60
62
|
} | {
|
|
61
63
|
type: 'thinking';
|
|
62
64
|
tokens: number;
|
|
65
|
+
} | {
|
|
66
|
+
type: 'account';
|
|
67
|
+
account: AccountId;
|
|
68
|
+
model: string;
|
|
69
|
+
sessionKey: string;
|
|
70
|
+
how: 'chosen' | 'default' | 'auto';
|
|
71
|
+
why: PickWhy;
|
|
72
|
+
considered: Considered[];
|
|
63
73
|
} | {
|
|
64
74
|
type: 'text';
|
|
65
75
|
text: string;
|
|
@@ -98,6 +108,8 @@ export type RunEnd<T = unknown> = {
|
|
|
98
108
|
data?: T;
|
|
99
109
|
usage?: RunUsage;
|
|
100
110
|
planWindow?: PlanWindow;
|
|
111
|
+
account?: AccountId;
|
|
112
|
+
model?: string;
|
|
101
113
|
} | {
|
|
102
114
|
ok: false;
|
|
103
115
|
aborted: true;
|
|
@@ -319,6 +331,13 @@ export type AccountPick = {
|
|
|
319
331
|
code: 'none' | 'not_included' | 'unknown_account' | 'bound' | 'paid';
|
|
320
332
|
reason: string;
|
|
321
333
|
considered: Considered[];
|
|
334
|
+
} | {
|
|
335
|
+
ok: false;
|
|
336
|
+
code: 'resting';
|
|
337
|
+
account: AccountId;
|
|
338
|
+
until?: number;
|
|
339
|
+
reason: string;
|
|
340
|
+
considered: Considered[];
|
|
322
341
|
};
|
|
323
342
|
export type MoveResult = {
|
|
324
343
|
ok: true;
|
package/dist/words.json
CHANGED
|
@@ -107,6 +107,13 @@
|
|
|
107
107
|
"ago.hours": "{n} h",
|
|
108
108
|
"account.bound": "This conversation uses {name}. Move it to switch accounts.",
|
|
109
109
|
"account.paid": "This conversation uses {name}, which is billed per use. Choose {name} to keep going.",
|
|
110
|
+
"account.another": "another account",
|
|
110
111
|
"account.signOutFirst": "To add a different {provider} account, sign out of {provider} in your browser first.",
|
|
111
|
-
"account.legacyMember": "This person needs a new profile before adding more accounts."
|
|
112
|
+
"account.legacyMember": "This person needs a new profile before adding more accounts.",
|
|
113
|
+
"move.unsupported": "This conversation has not started yet, so there is nothing to move.",
|
|
114
|
+
"move.busy": "Wait for this conversation to finish before moving it.",
|
|
115
|
+
"move.notReady": "Sign in to {name} first.",
|
|
116
|
+
"move.closeFailed": "This conversation now continues on {name}, but its earlier copy could not be removed.",
|
|
117
|
+
"move.taken": "{name} already holds a conversation under this name, so nothing was moved.",
|
|
118
|
+
"run.moved": "This conversation just moved to another account. Send it again to continue there."
|
|
112
119
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@byokit/openclaw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"dependencies": {
|
|
63
63
|
"@byokit/pair": "0.10.0",
|
|
64
64
|
"@byokit/relay": "0.6.1",
|
|
65
|
-
"@byokit/discover": "0.8.
|
|
65
|
+
"@byokit/discover": "0.8.1",
|
|
66
66
|
"@byokit/seal": "0.3.0",
|
|
67
67
|
"@byokit/secrets": "0.5.1",
|
|
68
68
|
"@openclaw/gateway-client": "2026.8.35",
|
package/plugin/accounts.js
CHANGED
|
@@ -124,8 +124,10 @@ async function handle(params) {
|
|
|
124
124
|
let sealed = false;
|
|
125
125
|
const store = await updateAuthProfileStoreWithLock({ agentDir, saveOptions: LOCAL, updater(s) {
|
|
126
126
|
const credential = s.profiles?.[profileId];
|
|
127
|
-
// No such profile, or the named profile belongs to another provider; on a member agent an api_key is refused
|
|
128
|
-
|
|
127
|
+
// No such profile, or the named profile belongs to another provider; on a member agent an api_key is refused
|
|
128
|
+
// unless it is a plan key (subscription billing, never billed per use).
|
|
129
|
+
if (!credential || credential.provider !== provider
|
|
130
|
+
|| (!account && credential.type === 'api_key' && params.planKey !== true)) return false;
|
|
129
131
|
who = whoOf(credential);
|
|
130
132
|
s.profiles = sealProfiles(s.profiles, provider, profileId, account);
|
|
131
133
|
s.order = account ? { [provider]: [profileId] } : { ...(s.order ?? {}), [provider]: [profileId] };
|
|
@@ -137,8 +139,15 @@ async function handle(params) {
|
|
|
137
139
|
}
|
|
138
140
|
|
|
139
141
|
if (params.action === 'clear') {
|
|
142
|
+
// With a provider only that provider's profiles go: a member agent keeps its other providers' sign-ins.
|
|
143
|
+
const provider = params.provider;
|
|
144
|
+
const scoped = typeof provider === 'string' && provider.length > 0;
|
|
140
145
|
const cleared = await updateAuthProfileStoreWithLock({ agentDir, saveOptions: LOCAL, updater(s) {
|
|
141
|
-
s.profiles = {}; s.order = {}; delete s.lastGood; return true;
|
|
146
|
+
if (!scoped) { s.profiles = {}; s.order = {}; delete s.lastGood; return true; }
|
|
147
|
+
s.profiles = Object.fromEntries(Object.entries(s.profiles ?? {}).filter(([, p]) => p.provider !== provider));
|
|
148
|
+
if (s.order) delete s.order[provider];
|
|
149
|
+
if (object(s.lastGood)) delete s.lastGood[provider];
|
|
150
|
+
return true;
|
|
142
151
|
} });
|
|
143
152
|
return cleared ? { ok: true } : { ok: false };
|
|
144
153
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// byokit.catalog (5.6 live provider catalogs): the provider plugin's own live model list, with each model's api and
|
|
2
|
+
// baseUrl from its catalog data, so the kit can register models embedded runs cannot resolve otherwise. The engine's
|
|
3
|
+
// catalog loader stops at a registered list; the provider's catalog hook does not.
|
|
4
|
+
export function registerCatalog(api) {
|
|
5
|
+
api.registerGatewayMethod('byokit.catalog', async ({ params, respond }) => {
|
|
6
|
+
try {
|
|
7
|
+
const provider = String(params?.provider ?? '');
|
|
8
|
+
const { resolveAgentDir } = await import('openclaw/plugin-sdk/agent-runtime');
|
|
9
|
+
const { getRuntimeConfig } = await import('openclaw/plugin-sdk/config-runtime');
|
|
10
|
+
const { resolvePluginProviders } = await import('openclaw/plugin-sdk/provider-catalog-runtime');
|
|
11
|
+
const { resolveApiKeyForProvider } = await import('openclaw/plugin-sdk/provider-auth-runtime');
|
|
12
|
+
const config = getRuntimeConfig();
|
|
13
|
+
const hook = resolvePluginProviders({ config, env: process.env }).find((p) => p.id === provider)?.catalog;
|
|
14
|
+
if (!provider || typeof hook?.run !== 'function') return respond(true, { ok: false, reason: 'failed' });
|
|
15
|
+
// The first agent holding the provider's key; every account of one provider sees the same catalog.
|
|
16
|
+
let unreadable = false;
|
|
17
|
+
for (const agentId of new Set([...Object.keys(config.agents?.entries ?? {}), 'main'])) {
|
|
18
|
+
const agentDir = resolveAgentDir(config, agentId);
|
|
19
|
+
const apiKey = await resolveApiKeyForProvider({ provider, cfg: config, agentDir })
|
|
20
|
+
.then((a) => a?.apiKey, () => { unreadable = true; return undefined; });
|
|
21
|
+
if (!apiKey) continue;
|
|
22
|
+
const run = async (discoveryApiKey) => (await hook.run({ config, agentDir, env: process.env, providerIds: [provider],
|
|
23
|
+
resolveProviderApiKey: () => ({ apiKey, discoveryApiKey }),
|
|
24
|
+
resolveProviderAuth: () => ({ apiKey, discoveryApiKey, mode: 'api_key', source: 'profile' }) }))?.provider;
|
|
25
|
+
const live = await run(apiKey);
|
|
26
|
+
// Each api's endpoint comes from the pinned catalog (no discovery key): upstream now names endpoints the
|
|
27
|
+
// pinned engine does not recognize, and it sends Go's routing header only to ones it does.
|
|
28
|
+
// ponytail: drop when the engine pin recognizes the live endpoints.
|
|
29
|
+
const pinned = await run(undefined);
|
|
30
|
+
const endpoint = new Map();
|
|
31
|
+
for (const m of pinned?.models ?? []) {
|
|
32
|
+
const api = m.api ?? pinned.api;
|
|
33
|
+
if (!endpoint.has(api) && (m.baseUrl ?? pinned.baseUrl)) endpoint.set(api, m.baseUrl ?? pinned.baseUrl);
|
|
34
|
+
}
|
|
35
|
+
if (!live?.models?.length) return respond(true, { ok: false, reason: 'failed' });
|
|
36
|
+
// A pinned model the live list omits stays in the catalog; live wins by id.
|
|
37
|
+
const liveIds = new Set(live.models.map((m) => m.id));
|
|
38
|
+
const models = [...live.models, ...(pinned?.models ?? []).filter((m) => !liveIds.has(m.id))]
|
|
39
|
+
.map(({ provider: _p, apiKey: _k, headers: _h, ...model }) =>
|
|
40
|
+
(endpoint.has(model.api) ? { ...model, baseUrl: endpoint.get(model.api) } : model));
|
|
41
|
+
return respond(true, { ok: true, models });
|
|
42
|
+
}
|
|
43
|
+
respond(true, { ok: false, reason: unreadable ? 'failed' : 'no-key' });
|
|
44
|
+
} catch {
|
|
45
|
+
respond(true, { ok: false, reason: 'failed' }); // never engine text: a hook error could carry the key
|
|
46
|
+
}
|
|
47
|
+
}, { scope: 'operator.admin' });
|
|
48
|
+
}
|
package/plugin/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { readFileSync } from 'node:fs';
|
|
|
6
6
|
import { registerKeys } from './keys.js';
|
|
7
7
|
import { registerUsage } from './usage.js';
|
|
8
8
|
import { registerAccounts } from './accounts.js';
|
|
9
|
+
import { registerCatalog } from './catalog.js';
|
|
9
10
|
|
|
10
11
|
/** TypeBox's Kind is a global symbol, so these schemas are valid TypeBox without importing typebox. */
|
|
11
12
|
const Kind = Symbol.for('TypeBox.Kind');
|
|
@@ -147,7 +148,7 @@ export default {
|
|
|
147
148
|
jsonSchema: { type: 'object', additionalProperties: false, properties: {} },
|
|
148
149
|
},
|
|
149
150
|
register(api) {
|
|
150
|
-
if (api.registerGatewayMethod) { registerKeys(api); registerUsage(api); registerAccounts(api); }
|
|
151
|
+
if (api.registerGatewayMethod) { registerKeys(api); registerUsage(api); registerAccounts(api); registerCatalog(api); }
|
|
151
152
|
if (BROWSER) {
|
|
152
153
|
if (typeof api.registerAgentToolResultMiddleware !== 'function') throw new Error('browser result protection unavailable');
|
|
153
154
|
api.registerAgentToolResultMiddleware(async (event, ctx) => {
|
package/plugin/keys.js
CHANGED
|
@@ -4,7 +4,7 @@ export function registerKeys(api) {
|
|
|
4
4
|
api.registerGatewayMethod('byokit.keys', async ({ params, respond }) => {
|
|
5
5
|
const member = params?.member;
|
|
6
6
|
if (typeof member !== 'string' || !/^[a-z][a-z0-9-]{0,31}$/.test(member) || member.startsWith('byokit-key-')
|
|
7
|
-
|| !['prepare', 'seal', 'ready'].includes(params.action)) {
|
|
7
|
+
|| !['prepare', 'seal', 'ready', 'clear'].includes(params.action)) {
|
|
8
8
|
respond(true, { ok: false });
|
|
9
9
|
return;
|
|
10
10
|
}
|
|
@@ -27,7 +27,10 @@ export function registerKeys(api) {
|
|
|
27
27
|
const marker = join(keyDir, 'byokit-key-ready.json');
|
|
28
28
|
if (params.action === 'ready') {
|
|
29
29
|
if (diagnostics) diagnostics.stage = 'marker';
|
|
30
|
-
|
|
30
|
+
let text;
|
|
31
|
+
try { text = await readFile(marker, 'utf8'); }
|
|
32
|
+
catch (error) { if (error?.code !== 'ENOENT') throw error; reply({ ok: false, absent: true }); return; }
|
|
33
|
+
const selected = JSON.parse(text);
|
|
31
34
|
let ready = false;
|
|
32
35
|
if (diagnostics) diagnostics.stage = 'store';
|
|
33
36
|
await updateAuthProfileStoreWithLock({ agentDir: keyDir, updater(store) {
|
|
@@ -45,7 +48,6 @@ export function registerKeys(api) {
|
|
|
45
48
|
reply({ ok: ready, ...(ready ? { model: selected.model } : {}) });
|
|
46
49
|
return;
|
|
47
50
|
}
|
|
48
|
-
await rm(marker, { force: true });
|
|
49
51
|
if (params.action === 'prepare') {
|
|
50
52
|
// Only local profiles are loaded by this transaction: no shared-store read-through or CLI sync.
|
|
51
53
|
const normal = await updateAuthProfileStoreWithLock({
|
|
@@ -66,6 +68,18 @@ export function registerKeys(api) {
|
|
|
66
68
|
const cleared = await updateAuthProfileStoreWithLock({ agentDir: keyDir,
|
|
67
69
|
saveOptions: { filterExternalAuthProfiles: false, syncExternalCli: false },
|
|
68
70
|
updater(store) { store.profiles = {}; store.order = {}; return true; } });
|
|
71
|
+
// The marker goes only once the replacement passes its own checks: a refused prepare keeps it.
|
|
72
|
+
if (cleared) await rm(marker, { force: true });
|
|
73
|
+
respond(true, { ok: Boolean(cleared) });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (params.action === 'clear') {
|
|
77
|
+
// A failed add drops the key it activated; if this clear never reaches the engine, the unsealed key stays but is
|
|
78
|
+
// unusable until the next add's prepare or the next kit start's sweep.
|
|
79
|
+
const cleared = await updateAuthProfileStoreWithLock({ agentDir: keyDir,
|
|
80
|
+
saveOptions: { filterExternalAuthProfiles: false, syncExternalCli: false },
|
|
81
|
+
updater(store) { store.profiles = {}; store.order = {}; delete store.lastGood; return true; } });
|
|
82
|
+
if (cleared) await rm(marker, { force: true });
|
|
69
83
|
respond(true, { ok: Boolean(cleared) });
|
|
70
84
|
return;
|
|
71
85
|
}
|
|
@@ -74,7 +88,11 @@ export function registerKeys(api) {
|
|
|
74
88
|
const at = typeof primary === 'string' ? primary.lastIndexOf('@') : -1;
|
|
75
89
|
if (at < 1) { respond(true, { ok: false }); return; }
|
|
76
90
|
const profileId = primary.slice(at + 1);
|
|
77
|
-
const model = primary.slice(0, at);
|
|
91
|
+
const model = params.model ?? primary.slice(0, at);
|
|
92
|
+
// The kit checks the route catalog before seal; a direct call still cannot cross the activated provider.
|
|
93
|
+
if (typeof model !== 'string' || !model || model.split('/')[0] !== primary.split('/')[0]) {
|
|
94
|
+
respond(true, { ok: false }); return;
|
|
95
|
+
}
|
|
78
96
|
let found = false;
|
|
79
97
|
const sealed = await updateAuthProfileStoreWithLock({ agentDir: keyDir,
|
|
80
98
|
saveOptions: { filterExternalAuthProfiles: false, syncExternalCli: false },
|