@byokit/openclaw 0.3.6 → 0.5.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 +64 -2
- package/dist/device.d.ts +16 -9
- package/dist/device.js +6 -5
- package/dist/engine.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/keys.d.ts +20 -0
- package/dist/keys.js +73 -0
- package/dist/kit.d.ts +16 -4
- package/dist/kit.js +47 -5
- package/dist/link.js +12 -6
- package/dist/members.d.ts +2 -0
- package/dist/members.js +3 -1
- package/dist/output.d.ts +68 -0
- package/dist/output.js +166 -0
- package/dist/routes.json +271 -58
- package/dist/runs.d.ts +2 -1
- package/dist/runs.js +28 -10
- package/dist/testing/fake-gateway.js +33 -1
- package/dist/types.d.ts +17 -3
- package/dist/types.js +0 -1
- package/dist/words.json +9 -1
- package/package.json +4 -4
- package/plugin/index.js +2 -0
- package/plugin/keys.js +101 -0
package/dist/runs.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
4
4
|
import { authStatus } from "./auth-status.js";
|
|
5
|
+
import { outputSchema } from "./output.js";
|
|
6
|
+
import { words } from "./words.js";
|
|
5
7
|
import { classify } from "./classify.js";
|
|
6
8
|
// The gateway gives up on a silent run after an hour; we wait ten minutes longer for the reply frame itself.
|
|
7
9
|
const WAIT_MS = 3_600_000;
|
|
@@ -78,17 +80,20 @@ export function createRuns(ctx) {
|
|
|
78
80
|
return undefined;
|
|
79
81
|
}
|
|
80
82
|
};
|
|
81
|
-
const run = async (spec, on) => {
|
|
83
|
+
const run = async (spec, on, keyAgent, preparedOutput) => {
|
|
82
84
|
// Member boundary first: a member never speaks in another member's session, refused before any request.
|
|
83
85
|
if (!spec.sessionKey.startsWith(`agent:${spec.member}:`))
|
|
84
86
|
throw new Error(`refused: "${spec.sessionKey}" is not a session of member "${spec.member}"`);
|
|
87
|
+
const output = preparedOutput ?? (spec.schema === undefined ? undefined : outputSchema(spec.schema));
|
|
88
|
+
const system = [spec.system, output?.prompt].filter(Boolean).join('\n\n');
|
|
85
89
|
const picked = spec.model === undefined ? undefined : account(spec.model);
|
|
86
90
|
for (const tool of spec.tools ?? [])
|
|
87
91
|
if (!ctx.tools.has(tool))
|
|
88
92
|
throw new Error(`refused: "${tool}" is not one of this kit's tools`);
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
|
|
93
|
+
const agentId = keyAgent ?? (await ctx.ensure(spec.member)).agentId;
|
|
94
|
+
const sessionKey = keyAgent ? `agent:${keyAgent}:${spec.sessionKey.slice(`agent:${spec.member}:`.length)}` : spec.sessionKey;
|
|
95
|
+
const release = spec.register !== false ? ctx.bridge.register({ sessionKey, member: spec.member }, spec.tools) : undefined;
|
|
96
|
+
let last;
|
|
92
97
|
let runId = ''; // gateway events for other runs carry a real runId and never match the empty one
|
|
93
98
|
let ended = false;
|
|
94
99
|
// The engine flags an aborted run on its lifecycle end event; the wait receipt itself only says
|
|
@@ -148,11 +153,11 @@ export function createRuns(ctx) {
|
|
|
148
153
|
};
|
|
149
154
|
const final = ctx.request('agent', {
|
|
150
155
|
agentId,
|
|
151
|
-
sessionKey
|
|
156
|
+
sessionKey,
|
|
152
157
|
message: spec.message,
|
|
153
158
|
idempotencyKey: randomUUID(),
|
|
154
159
|
...(picked ?? {}),
|
|
155
|
-
...(
|
|
160
|
+
...(system ? { extraSystemPrompt: system } : {}),
|
|
156
161
|
...(spec.images ? { attachments: spec.images.map((image) => ({ mimeType: image.mimeType, content: image.data })) } : {}),
|
|
157
162
|
...(spec.thinking ? { thinking: spec.thinking } : {}),
|
|
158
163
|
}, { expectFinal: true, timeoutMs: WAIT_CLIENT_MS, onAccepted });
|
|
@@ -161,15 +166,28 @@ export function createRuns(ctx) {
|
|
|
161
166
|
runId = started.runId;
|
|
162
167
|
const result = await ctx.request('agent.wait', { runId, timeoutMs: WAIT_MS }, { timeoutMs: WAIT_CLIENT_MS });
|
|
163
168
|
if (result.status === 'ok') {
|
|
164
|
-
const text = typeof result.terminalReply?.text === 'string' ? result.terminalReply.text : last;
|
|
165
|
-
on?.({ type: 'text', text }); // the final cumulative text
|
|
166
169
|
const done = await Promise.race([final.catch(() => undefined), delay(FINAL_GRACE_MS, undefined, { ref: false })]);
|
|
167
|
-
|
|
170
|
+
ended = true; // no later stream event may supersede the final callback
|
|
171
|
+
const frame = isRecord(done) && isRecord(done.result) ? done.result : {};
|
|
172
|
+
const payloadText = Array.isArray(frame.payloads)
|
|
173
|
+
? frame.payloads.filter((p) => isRecord(p) && typeof p.text === 'string')
|
|
174
|
+
.map((p) => p.text) : [];
|
|
175
|
+
// The pin's terminal snapshot is capped display evidence, not the complete generated answer.
|
|
176
|
+
// Its silent/empty disposition still controls visibility, even after transient streamed text.
|
|
177
|
+
const terminal = result.terminalReply;
|
|
178
|
+
const text = terminal?.disposition === 'silent' || terminal?.disposition === 'empty' ? ''
|
|
179
|
+
: payloadText.length ? payloadText.join('\n\n')
|
|
180
|
+
: last ?? (typeof terminal?.text === 'string' ? terminal.text : '');
|
|
181
|
+
on?.({ type: 'text', text }); // the final cumulative text (unvalidated)
|
|
182
|
+
const parsed = output?.parse(text);
|
|
183
|
+
if (output && !parsed)
|
|
184
|
+
return { ok: false, kind: 'output', message: words('member.output') };
|
|
185
|
+
const meta = isRecord(frame.meta) ? frame.meta : {};
|
|
168
186
|
const agentMeta = isRecord(meta.agentMeta) ? meta.agentMeta : {};
|
|
169
187
|
const usage = usageOf(agentMeta);
|
|
170
188
|
const planWindow = typeof agentMeta.provider === 'string'
|
|
171
189
|
? await planWindowOf(agentId, agentMeta.provider.toLowerCase()) : undefined;
|
|
172
|
-
return { ok: true, text, ...(usage ? { usage } : {}), ...(planWindow ? { planWindow } : {}) };
|
|
190
|
+
return { ok: true, text, ...(parsed ? { data: parsed.data } : {}), ...(usage ? { usage } : {}), ...(planWindow ? { planWindow } : {}) };
|
|
173
191
|
}
|
|
174
192
|
if (result.stopReason === 'aborted' || (abortedByEngine && result.status !== 'ok'))
|
|
175
193
|
return { ok: false, aborted: true };
|
|
@@ -7,6 +7,7 @@ import { readFileSync } from 'node:fs';
|
|
|
7
7
|
import { dirname, join } from 'node:path';
|
|
8
8
|
import { ENGINE_VERSION, PROTOCOL_VERSION } from "../constants.js";
|
|
9
9
|
import { toolCalls } from "./model-stub.js";
|
|
10
|
+
import { routes } from "../routes.js";
|
|
10
11
|
// The device-code script from Crewhouse's openclaw-wizard.test.ts, ported: the step, then progress, then done.
|
|
11
12
|
const DEVICE_STEP = {
|
|
12
13
|
id: 'step-device', type: 'note', executor: 'client',
|
|
@@ -98,6 +99,7 @@ export function fakeGateway(script) {
|
|
|
98
99
|
const failures = new Map();
|
|
99
100
|
const handlers = new Map();
|
|
100
101
|
const agents = new Map();
|
|
102
|
+
const keys = new Map();
|
|
101
103
|
const wizards = new Map();
|
|
102
104
|
const runs = new Map();
|
|
103
105
|
const configBox = { hash: 'hash-1', config: {} };
|
|
@@ -220,8 +222,37 @@ export function fakeGateway(script) {
|
|
|
220
222
|
const agent = agents.get(String(p?.agentId ?? 'main'));
|
|
221
223
|
if (agent)
|
|
222
224
|
agent.providers = agent.providers.filter((provider) => provider !== p.provider);
|
|
225
|
+
if (keys.get(String(p.agentId))?.provider === p.provider)
|
|
226
|
+
keys.delete(String(p.agentId));
|
|
223
227
|
return {};
|
|
224
228
|
},
|
|
229
|
+
'byokit.keys': (p) => {
|
|
230
|
+
const id = `byokit-key-${p.member}`;
|
|
231
|
+
if (p.action === 'prepare') {
|
|
232
|
+
keys.delete(id);
|
|
233
|
+
ensureAgent(id).providers = [];
|
|
234
|
+
return { ok: true };
|
|
235
|
+
}
|
|
236
|
+
const key = keys.get(id);
|
|
237
|
+
if (!key)
|
|
238
|
+
return { ok: false };
|
|
239
|
+
if (p.action === 'seal') {
|
|
240
|
+
key.sealed = true;
|
|
241
|
+
return { ok: true };
|
|
242
|
+
}
|
|
243
|
+
return { ok: key.sealed, ...(key.sealed ? { model: key.model } : {}) };
|
|
244
|
+
},
|
|
245
|
+
'openclaw.setup.activate': (p) => {
|
|
246
|
+
if (p.kind !== 'api-key')
|
|
247
|
+
throw new Error('only the API-key setup path is simulated');
|
|
248
|
+
const route = routes().find((r) => r.choice === p.authChoice && r.keyEntry);
|
|
249
|
+
if (!route)
|
|
250
|
+
return { ok: false, status: 'unavailable' };
|
|
251
|
+
const model = `${route.provider}/fake-key-model`;
|
|
252
|
+
keys.set(p.agentId, { provider: route.provider, key: p.apiKey, model, sealed: false });
|
|
253
|
+
signInAgent(p.agentId, route.provider);
|
|
254
|
+
return { ok: true, status: 'ok', modelRef: model };
|
|
255
|
+
},
|
|
225
256
|
'openclaw.setup.auth.start': (p) => {
|
|
226
257
|
const sessionId = String(p.sessionId ?? `fake-${randomUUID()}`);
|
|
227
258
|
wizards.set(sessionId, { agentId: String(p.agentId ?? 'main'), answered: false, pulls: 0 });
|
|
@@ -338,7 +369,8 @@ export function fakeGateway(script) {
|
|
|
338
369
|
for (const [method, handler] of Object.entries(script ?? {}))
|
|
339
370
|
handlers.set(method, handler);
|
|
340
371
|
const dispatch = async (method, params, bridgeSock) => {
|
|
341
|
-
calls.push({ method, params
|
|
372
|
+
calls.push({ method, params: method === 'openclaw.setup.activate' && isPlainObject(params)
|
|
373
|
+
? { ...params, ...(params.apiKey === undefined ? {} : { apiKey: '[redacted]' }) } : params });
|
|
342
374
|
const queue = failures.get(method);
|
|
343
375
|
if (queue?.length)
|
|
344
376
|
throw new Error(queue.shift());
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { OutputSchema } from './output.ts';
|
|
2
|
+
export type { OutputSchema, SchemaOutput } from './output.ts';
|
|
1
3
|
export type Member = string;
|
|
2
4
|
export interface ToolSpec {
|
|
3
5
|
name: string;
|
|
@@ -25,8 +27,9 @@ export interface ToolHost {
|
|
|
25
27
|
}): Promise<GateResult>;
|
|
26
28
|
call(run: RunRef, tool: string, input: Record<string, unknown>, signal: AbortSignal): Promise<string>;
|
|
27
29
|
}
|
|
28
|
-
export interface RunSpec extends RunRef {
|
|
30
|
+
export interface RunSpec<S extends OutputSchema | undefined = OutputSchema | undefined> extends RunRef {
|
|
29
31
|
message: string;
|
|
32
|
+
schema?: S;
|
|
30
33
|
system?: string;
|
|
31
34
|
images?: {
|
|
32
35
|
data: string;
|
|
@@ -34,6 +37,8 @@ export interface RunSpec extends RunRef {
|
|
|
34
37
|
}[];
|
|
35
38
|
thinking?: 'off' | 'low' | 'medium' | 'high';
|
|
36
39
|
model?: string;
|
|
40
|
+
/** Explicit paid key choice. Uses a separate member-owned agent and separate session history. */
|
|
41
|
+
auth?: 'apiKey';
|
|
37
42
|
tools?: string[];
|
|
38
43
|
register?: boolean;
|
|
39
44
|
}
|
|
@@ -69,9 +74,10 @@ export type PlanWindow = {
|
|
|
69
74
|
resetAt?: number;
|
|
70
75
|
}[];
|
|
71
76
|
};
|
|
72
|
-
export type RunEnd = {
|
|
77
|
+
export type RunEnd<T = unknown> = {
|
|
73
78
|
ok: true;
|
|
74
79
|
text: string;
|
|
80
|
+
data?: T;
|
|
75
81
|
usage?: RunUsage;
|
|
76
82
|
planWindow?: PlanWindow;
|
|
77
83
|
} | {
|
|
@@ -79,7 +85,7 @@ export type RunEnd = {
|
|
|
79
85
|
aborted: true;
|
|
80
86
|
} | {
|
|
81
87
|
ok: false;
|
|
82
|
-
kind: 'signed-out' | 'resting' | 'plan' | 'network' | 'other';
|
|
88
|
+
kind: 'signed-out' | 'resting' | 'plan' | 'network' | 'other' | 'output';
|
|
83
89
|
until?: number;
|
|
84
90
|
message: string;
|
|
85
91
|
};
|
|
@@ -139,6 +145,14 @@ export type Route = {
|
|
|
139
145
|
offer: boolean;
|
|
140
146
|
reason: string;
|
|
141
147
|
source: string;
|
|
148
|
+
revision: string;
|
|
149
|
+
checked: string;
|
|
150
|
+
label: string;
|
|
151
|
+
keyEntry: boolean;
|
|
152
|
+
keyErrors: {
|
|
153
|
+
invalid: 'key.invalid';
|
|
154
|
+
not_included: 'key.notIncluded';
|
|
155
|
+
} | null;
|
|
142
156
|
};
|
|
143
157
|
export interface GatewayTransport {
|
|
144
158
|
start(): Promise<Hello>;
|
package/dist/types.js
CHANGED
package/dist/words.json
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"member.signedOut": "Sign in with {name} to start.",
|
|
12
12
|
"member.resting": "{name} needs a break until {time}.",
|
|
13
13
|
"member.plan": "Your {name} plan doesn't include this.",
|
|
14
|
+
"member.output": "The answer did not match the requested format.",
|
|
14
15
|
"member.network": "Can't reach {name} right now. This keeps trying by itself.",
|
|
15
16
|
"signin.returned": "Thanks. Finishing the sign-in — you can go back to the app now.",
|
|
16
17
|
"signin.busy": "Another sign-in is already in progress. Finish or cancel it, then try again.",
|
|
@@ -19,5 +20,12 @@
|
|
|
19
20
|
"approval.ask": "{helper} wants to {summary}. Allow it?",
|
|
20
21
|
"approval.expired": "Nobody answered in time, so this wasn't allowed.",
|
|
21
22
|
"approval.notice": "Something is waiting for your yes.",
|
|
22
|
-
"link.notAllowed": "This device can't do that. Ask the person at the computer."
|
|
23
|
+
"link.notAllowed": "This device can't do that. Ask the person at the computer.",
|
|
24
|
+
"key.label": "API key (billed per use)",
|
|
25
|
+
"key.entry": "Paste your API key. You pay the provider for each use.",
|
|
26
|
+
"key.checking": "Checking your key…",
|
|
27
|
+
"key.ok": "Your key is ready. Use it only when you choose this option.",
|
|
28
|
+
"key.invalid": "This key could not be saved. Check it and try again.",
|
|
29
|
+
"key.notIncluded": "This option is not available here.",
|
|
30
|
+
"key.missing": "Add an API key to use this option."
|
|
23
31
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@byokit/openclaw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"@byokit/link": "0.7.0",
|
|
56
|
-
"@byokit/relay": "0.5.
|
|
57
|
-
"@byokit/reach": "0.
|
|
56
|
+
"@byokit/relay": "0.5.1",
|
|
57
|
+
"@byokit/reach": "0.6.0",
|
|
58
58
|
"@byokit/seal": "0.2.0",
|
|
59
59
|
"@byokit/secrets": "0.5.0",
|
|
60
60
|
"@openclaw/gateway-client": "2026.8.1",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"ws": "8.21.3"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
|
-
"@byokit/ui-core": "0.
|
|
65
|
+
"@byokit/ui-core": "0.6.0"
|
|
66
66
|
},
|
|
67
67
|
"publishConfig": {
|
|
68
68
|
"access": "public"
|
package/plugin/index.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// schemas and descriptions stay app data. No imports beyond node builtins: nothing here needs resolving in-gateway.
|
|
4
4
|
import { connect } from 'node:net';
|
|
5
5
|
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { registerKeys } from './keys.js';
|
|
6
7
|
|
|
7
8
|
/** TypeBox's Kind is a global symbol, so these schemas are valid TypeBox without importing typebox. */
|
|
8
9
|
const Kind = Symbol.for('TypeBox.Kind');
|
|
@@ -114,6 +115,7 @@ export default {
|
|
|
114
115
|
jsonSchema: { type: 'object', additionalProperties: false, properties: {} },
|
|
115
116
|
},
|
|
116
117
|
register(api) {
|
|
118
|
+
if (api.registerGatewayMethod) registerKeys(api);
|
|
117
119
|
for (const spec of table.tools) {
|
|
118
120
|
api.registerTool({
|
|
119
121
|
name: spec.name,
|
package/plugin/keys.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Agent-local key isolation. The engine owns the SQLite transaction and publishes the changed runtime snapshot.
|
|
2
|
+
// No key value crosses this method. Only the typed setup.activate request ever carries a secret.
|
|
3
|
+
export function registerKeys(api) {
|
|
4
|
+
api.registerGatewayMethod('byokit.keys', async ({ params, respond }) => {
|
|
5
|
+
const member = params?.member;
|
|
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)) {
|
|
8
|
+
respond(true, { ok: false });
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const diagnostics = params.action === 'ready' && params.diagnostics === true ? { stage: 'imports' } : undefined;
|
|
12
|
+
const reply = (value) => respond(true, { ...value, ...(diagnostics ? { diagnostics } : {}) });
|
|
13
|
+
try {
|
|
14
|
+
const { readFile, writeFile, rm } = await import('node:fs/promises');
|
|
15
|
+
const { join } = await import('node:path');
|
|
16
|
+
const { resolveAgentDir } = await import('openclaw/plugin-sdk/agent-runtime');
|
|
17
|
+
const { updateAuthProfileStoreWithLock } = await import('openclaw/plugin-sdk/provider-auth');
|
|
18
|
+
// Read the current config: activation changes the roster and selection during the same gateway lifetime.
|
|
19
|
+
if (diagnostics) diagnostics.stage = 'config';
|
|
20
|
+
const config = JSON.parse(await readFile(process.env.OPENCLAW_CONFIG_PATH, 'utf8'));
|
|
21
|
+
const keyId = `byokit-key-${member}`;
|
|
22
|
+
if (!config.agents?.entries?.[member] || !config.agents?.entries?.[keyId]) {
|
|
23
|
+
reply({ ok: false });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const keyDir = resolveAgentDir(config, keyId);
|
|
27
|
+
const marker = join(keyDir, 'byokit-key-ready.json');
|
|
28
|
+
if (params.action === 'ready') {
|
|
29
|
+
if (diagnostics) diagnostics.stage = 'marker';
|
|
30
|
+
const selected = JSON.parse(await readFile(marker, 'utf8'));
|
|
31
|
+
let ready = false;
|
|
32
|
+
if (diagnostics) diagnostics.stage = 'store';
|
|
33
|
+
await updateAuthProfileStoreWithLock({ agentDir: keyDir, updater(store) {
|
|
34
|
+
if (diagnostics) {
|
|
35
|
+
diagnostics.profiles = Object.entries(store.profiles).map(([id, p]) => ({ id, apiKey: p.type === 'api_key', local: p.copyToAgents === false }));
|
|
36
|
+
diagnostics.order = store.order;
|
|
37
|
+
diagnostics.selected = selected.profileId;
|
|
38
|
+
diagnostics.stage = 'checked';
|
|
39
|
+
}
|
|
40
|
+
const profile = store.profiles[selected.profileId];
|
|
41
|
+
ready = profile?.type === 'api_key' && profile.copyToAgents === false
|
|
42
|
+
&& store.order?.[profile.provider]?.length === 1 && store.order[profile.provider][0] === selected.profileId;
|
|
43
|
+
return false;
|
|
44
|
+
} });
|
|
45
|
+
reply({ ok: ready, ...(ready ? { model: selected.model } : {}) });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
await rm(marker, { force: true });
|
|
49
|
+
if (params.action === 'prepare') {
|
|
50
|
+
// Only local profiles are loaded by this transaction: no shared-store read-through or CLI sync.
|
|
51
|
+
const normal = await updateAuthProfileStoreWithLock({
|
|
52
|
+
agentDir: resolveAgentDir(config, member),
|
|
53
|
+
saveOptions: { filterExternalAuthProfiles: false, syncExternalCli: false },
|
|
54
|
+
updater(store) {
|
|
55
|
+
if (Object.values(store.profiles).some((p) => p.type === 'api_key'))
|
|
56
|
+
throw new Error('The normal agent already has a key.');
|
|
57
|
+
const providers = new Set([params.provider, ...Object.values(store.profiles).map((p) => p.provider)].filter((p) => typeof p === 'string'));
|
|
58
|
+
store.order ??= {};
|
|
59
|
+
for (const provider of providers) store.order[provider] = Object.entries(store.profiles)
|
|
60
|
+
.filter(([, p]) => p.provider === provider && p.type === 'oauth').map(([id]) => id);
|
|
61
|
+
return true;
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
if (!normal) { respond(true, { ok: false }); return; }
|
|
65
|
+
// A replacement starts empty. During activation this agent is unavailable to kit.run.
|
|
66
|
+
const cleared = await updateAuthProfileStoreWithLock({ agentDir: keyDir,
|
|
67
|
+
saveOptions: { filterExternalAuthProfiles: false, syncExternalCli: false },
|
|
68
|
+
updater(store) { store.profiles = {}; store.order = {}; return true; } });
|
|
69
|
+
respond(true, { ok: Boolean(cleared) });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const ref = config.agents.entries[keyId].model;
|
|
73
|
+
const primary = typeof ref === 'string' ? ref : ref?.primary;
|
|
74
|
+
const at = typeof primary === 'string' ? primary.lastIndexOf('@') : -1;
|
|
75
|
+
if (at < 1) { respond(true, { ok: false }); return; }
|
|
76
|
+
const profileId = primary.slice(at + 1);
|
|
77
|
+
const model = primary.slice(0, at);
|
|
78
|
+
let found = false;
|
|
79
|
+
const sealed = await updateAuthProfileStoreWithLock({ agentDir: keyDir,
|
|
80
|
+
saveOptions: { filterExternalAuthProfiles: false, syncExternalCli: false },
|
|
81
|
+
updater(store) {
|
|
82
|
+
const profile = store.profiles[profileId];
|
|
83
|
+
if (profile?.type !== 'api_key') return false;
|
|
84
|
+
found = true;
|
|
85
|
+
profile.copyToAgents = false;
|
|
86
|
+
store.profiles = { [profileId]: profile };
|
|
87
|
+
// A stored agent-local order wins over global config and contains exactly this key, never a sign-in.
|
|
88
|
+
store.order = { [profile.provider]: [profileId] };
|
|
89
|
+
store.lastGood = {};
|
|
90
|
+
return true;
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
if (!sealed || !found) { respond(true, { ok: false }); return; }
|
|
94
|
+
await writeFile(marker, JSON.stringify({ profileId, model }), { mode: 0o600 });
|
|
95
|
+
respond(true, { ok: true });
|
|
96
|
+
} catch {
|
|
97
|
+
// Neither engine errors nor store contents (which contain secrets) ever leave this method.
|
|
98
|
+
reply({ ok: false });
|
|
99
|
+
}
|
|
100
|
+
}, { scope: 'operator.admin' });
|
|
101
|
+
}
|