@byokit/openclaw 0.11.1 → 0.13.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 +22 -0
- package/README.md +38 -6
- package/SECURITY.md +3 -2
- package/dist/accounts.d.ts +19 -2
- package/dist/accounts.js +618 -165
- 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/auth-store.d.ts +6 -0
- package/dist/auth-store.js +135 -23
- 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 +4 -4
- package/dist/device.js +11 -1
- package/dist/engine.d.ts +1 -0
- package/dist/engine.js +16 -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 +24 -2
- package/dist/kit.js +139 -29
- package/dist/link.js +21 -11
- package/dist/routes.d.ts +1 -0
- package/dist/routes.js +5 -3
- package/dist/routes.json +31 -31
- package/dist/runs.d.ts +1 -1
- package/dist/runs.js +32 -10
- package/dist/signin.d.ts +2 -0
- package/dist/signin.js +39 -15
- package/dist/testing/fake-gateway.d.ts +19 -0
- package/dist/testing/fake-gateway.js +137 -12
- package/dist/testing/model-stub.d.ts +1 -0
- package/dist/testing/model-stub.js +2 -1
- package/dist/words.json +8 -1
- package/package.json +1 -1
- package/plugin/accounts.js +12 -3
- package/plugin/catalog.js +48 -0
- package/plugin/index.js +2 -1
- package/plugin/keys.js +22 -4
|
@@ -117,6 +117,7 @@ export function fakeGateway(script, o) {
|
|
|
117
117
|
const orders = new Map();
|
|
118
118
|
const models = new Map();
|
|
119
119
|
const wizards = new Map();
|
|
120
|
+
let signInFields;
|
|
120
121
|
// Each workspace's files by name, as `agents.files.get/set` and the create-time IDENTITY.md rewrite see them.
|
|
121
122
|
const workspaces = new Map();
|
|
122
123
|
const filesOf = (agentId) => {
|
|
@@ -138,6 +139,20 @@ export function fakeGateway(script, o) {
|
|
|
138
139
|
...(content === undefined ? {} : { size: Buffer.byteLength(content), content }) } };
|
|
139
140
|
};
|
|
140
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);
|
|
141
156
|
// The engine's own cached read of each provider's usage endpoint (O14): cold until a `models.authStatus`
|
|
142
157
|
// answer has carried the provider once (no `usage` on that first answer), then whatever `setUsage` stored.
|
|
143
158
|
// A pre-sign-in read names no provider, so it warms nothing: the journey's first room read stays cold.
|
|
@@ -270,10 +285,14 @@ export function fakeGateway(script, o) {
|
|
|
270
285
|
if (params.agentId != null && !agents.has(String(params.agentId)))
|
|
271
286
|
throw new Error(`unknown agent: ${params.agentId}`);
|
|
272
287
|
// Like the engine: the provider/model the run is called on, its own override or the member's (here openai).
|
|
273
|
-
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,
|
|
274
289
|
provider: String(params.provider ?? 'openai'), model: String(params.model ?? 'gpt-5.1'), waiters: [] };
|
|
275
290
|
runs.set(run.runId, run);
|
|
291
|
+
const session = sessionOf(run.agentId, run.sessionKey);
|
|
292
|
+
session.provider = run.provider;
|
|
293
|
+
session.model = run.model;
|
|
276
294
|
const message = String(params.message ?? '');
|
|
295
|
+
append(session, { role: 'user', content: [{ type: 'text', text: message }] });
|
|
277
296
|
const text = `fake: ${message}`;
|
|
278
297
|
setTimeout(() => {
|
|
279
298
|
void (async () => {
|
|
@@ -304,8 +323,11 @@ export function fakeGateway(script, o) {
|
|
|
304
323
|
// Completion rides `phase: 'result'` like the real engine (O11), not `'end'`, with its result.
|
|
305
324
|
emitRun(run, 'tool', { phase: 'result', name: call.name, toolCallId, isError,
|
|
306
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 }] });
|
|
307
328
|
}
|
|
308
329
|
emitRun(run, 'assistant', { text });
|
|
330
|
+
append(session, { role: 'assistant', content: [{ type: 'text', text }] });
|
|
309
331
|
// The engine sums the run's usage (zero buckets omitted); the fake counts characters.
|
|
310
332
|
run.usage = { input: message.length, output: text.length, total: message.length + text.length };
|
|
311
333
|
setTimeout(() => finish(run, { status: 'ok', terminalReply: { text } }), ABORT_WINDOW_MS);
|
|
@@ -424,20 +446,27 @@ export function fakeGateway(script, o) {
|
|
|
424
446
|
},
|
|
425
447
|
'byokit.keys': (p) => {
|
|
426
448
|
const id = `byokit-key-${p.member}`;
|
|
427
|
-
if (p.action === 'prepare') {
|
|
449
|
+
if (p.action === 'prepare' || p.action === 'clear') {
|
|
428
450
|
keys.delete(id);
|
|
429
451
|
ensureAgent(id).providers = [];
|
|
430
452
|
return { ok: true };
|
|
431
453
|
}
|
|
432
454
|
const key = keys.get(id);
|
|
433
455
|
if (!key)
|
|
434
|
-
return { ok: false };
|
|
456
|
+
return { ok: false, absent: true };
|
|
435
457
|
if (p.action === 'seal') {
|
|
436
458
|
key.sealed = true;
|
|
459
|
+
if (typeof p.model === 'string')
|
|
460
|
+
key.model = p.model;
|
|
437
461
|
return { ok: true };
|
|
438
462
|
}
|
|
439
|
-
return
|
|
463
|
+
return key.sealed ? { ok: true, model: key.model } : { ok: false, absent: true };
|
|
440
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' }),
|
|
441
470
|
// Account sealing (5.15): the same five actions, branches and refusals the shipped plugin method serves.
|
|
442
471
|
'byokit.accounts': (p) => {
|
|
443
472
|
const action = String(p.action ?? '');
|
|
@@ -485,11 +514,21 @@ export function fakeGateway(script, o) {
|
|
|
485
514
|
return { ok: true, profiles: Object.entries(store).map(([profileId, credential]) => ({ provider: credential.provider,
|
|
486
515
|
profileId, type: credential.type, status: statusOf(credential, Date.now()), ...(whoOf(credential) ? { who: whoOf(credential) } : {}) })) };
|
|
487
516
|
if (action === 'clear') {
|
|
488
|
-
profiles
|
|
489
|
-
|
|
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
|
+
}
|
|
490
529
|
const cleared = agents.get(id);
|
|
491
530
|
if (cleared)
|
|
492
|
-
cleared.providers = [];
|
|
531
|
+
cleared.providers = scope ? cleared.providers.filter((x) => x !== scope) : [];
|
|
493
532
|
return { ok: true };
|
|
494
533
|
}
|
|
495
534
|
if (action === 'ready') {
|
|
@@ -512,7 +551,8 @@ export function fakeGateway(script, o) {
|
|
|
512
551
|
const profileId = primary.slice(at + 1);
|
|
513
552
|
const credential = store[profileId];
|
|
514
553
|
const account = ACCOUNT_AGENT.test(id);
|
|
515
|
-
if (!credential || credential.provider !== p.provider
|
|
554
|
+
if (!credential || credential.provider !== p.provider
|
|
555
|
+
|| (!account && credential.type === 'api_key' && p.planKey !== true))
|
|
516
556
|
return { ok: false };
|
|
517
557
|
for (const [otherId, other] of Object.entries(store))
|
|
518
558
|
if (account ? otherId !== profileId : other.provider === p.provider && otherId !== profileId)
|
|
@@ -537,15 +577,21 @@ export function fakeGateway(script, o) {
|
|
|
537
577
|
'openclaw.setup.activate': (p) => {
|
|
538
578
|
if (p.kind !== 'api-key')
|
|
539
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`).
|
|
540
581
|
const route = routes().find((r) => r.choice === String(p.authChoice) && r.keyEntry);
|
|
541
|
-
if (!route)
|
|
542
|
-
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.' };
|
|
543
584
|
const model = `${route.provider}/fake-key-model`;
|
|
544
585
|
keys.set(String(p.agentId), { provider: route.provider, key: String(p.apiKey), model, sealed: false });
|
|
545
|
-
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`);
|
|
546
588
|
return { ok: true, status: 'ok', modelRef: model };
|
|
547
589
|
},
|
|
548
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.');
|
|
549
595
|
// The pin's single setup admission: a second start while a session runs is refused.
|
|
550
596
|
if (wizards.size)
|
|
551
597
|
throw new Error('setup is already in progress');
|
|
@@ -566,7 +612,7 @@ export function fakeGateway(script, o) {
|
|
|
566
612
|
if (session.pulls++ === 0)
|
|
567
613
|
return { done: false, step: PROGRESS_STEP };
|
|
568
614
|
wizards.delete(String(p.sessionId));
|
|
569
|
-
signInAgent(session.agentId, 'openai');
|
|
615
|
+
signInAgent(session.agentId, 'openai', signInFields);
|
|
570
616
|
return { done: true, status: 'done', modelActivation: { modelRef: 'openai/gpt-5.1' } };
|
|
571
617
|
},
|
|
572
618
|
'wizard.cancel': (p) => {
|
|
@@ -584,6 +630,66 @@ export function fakeGateway(script, o) {
|
|
|
584
630
|
run.waiters.push(resolve);
|
|
585
631
|
}),
|
|
586
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
|
+
},
|
|
587
693
|
'chat.abort': (p) => {
|
|
588
694
|
for (const run of runs.values()) {
|
|
589
695
|
if (run.result || run.sessionKey !== String(p.sessionKey ?? ''))
|
|
@@ -746,6 +852,25 @@ export function fakeGateway(script, o) {
|
|
|
746
852
|
}
|
|
747
853
|
perAgent.set(provider, windows);
|
|
748
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
|
+
},
|
|
861
|
+
refresh: (agentId, provider, o) => {
|
|
862
|
+
const agent = ensureAgent(agentId);
|
|
863
|
+
if (!agent.providers.includes(provider))
|
|
864
|
+
agent.providers.push(provider);
|
|
865
|
+
const profileId = `${provider}:default`;
|
|
866
|
+
storeOf(agentId)[profileId] = { provider, type: 'oauth', status: 'ok', access: 'fake-access',
|
|
867
|
+
refresh: 'fake-refresh', expires: o?.expires ?? Date.now() + 30 * 86_400_000,
|
|
868
|
+
accountId: `fake-refresh-${agentId}` };
|
|
869
|
+
if (o?.order !== false)
|
|
870
|
+
orderOf(agentId)[provider] = [profileId];
|
|
871
|
+
else if (orderOf(agentId)[provider])
|
|
872
|
+
delete orderOf(agentId)[provider];
|
|
873
|
+
},
|
|
749
874
|
transport: factory({ port: 0, token: 'test', identityPath: '', bridgeSock: '' }),
|
|
750
875
|
};
|
|
751
876
|
}
|
|
@@ -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/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.13.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",
|
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 },
|