@byokit/openclaw 0.3.6 → 0.4.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 +9 -0
- package/README.md +24 -2
- package/dist/device.d.ts +7 -2
- package/dist/device.js +5 -4
- package/dist/engine.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/keys.d.ts +20 -0
- package/dist/keys.js +73 -0
- package/dist/kit.d.ts +14 -2
- package/dist/kit.js +37 -4
- package/dist/link.js +7 -5
- package/dist/members.d.ts +2 -0
- package/dist/members.js +3 -1
- package/dist/routes.json +271 -58
- package/dist/runs.d.ts +1 -1
- package/dist/runs.js +5 -4
- package/dist/testing/fake-gateway.js +33 -1
- package/dist/types.d.ts +10 -0
- package/dist/words.json +8 -1
- package/package.json +3 -3
- package/plugin/index.js +2 -0
- package/plugin/keys.js +101 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.4.0 (2026-10-01)
|
|
6
|
+
|
|
7
|
+
- Dependency update: pins @byokit/ui-core 0.6.0.
|
|
8
|
+
- Dependency update: pins @byokit/reach 0.6.0.
|
|
9
|
+
|
|
10
|
+
- Key readiness waits through the engine's retryable restart refusal after activation; missing keys still fail without another account.
|
|
11
|
+
- API key (billed per use): `addKey` verifies and stores an explicitly entered key in a separate member-owned agent, with copying disabled and no automatic subscription fallback. `run({ auth: 'apiKey' })` selects it explicitly with separate history; key-entry labels, errors and route revision dates are included.
|
|
12
|
+
- Offer subscription sign-ins, including pasted tokens and native Claude Code, by default and allow their provider plugins. API key (billed per use) routes remain app opt-in; proxies, compatibility aliases and local routes stay off.
|
|
13
|
+
|
|
5
14
|
## 0.3.6 (2026-10-01)
|
|
6
15
|
|
|
7
16
|
- FIX: Live orphan gateways recover after the engine rewrites its process title. Recovery verifies the recorded launch pid and process start time with the executable and isolated store paths; ambiguous ownership preserves the saved sign-in state.
|
package/README.md
CHANGED
|
@@ -69,6 +69,23 @@ provider or model. If the member isn't signed in to it, the run ends `{ ok: fals
|
|
|
69
69
|
engine is never called. Leave `model` out to keep the engine's own choice. A specific sign-in (`@profile`) can't be
|
|
70
70
|
picked per run: the pinned engine may still switch to another sign-in for the same provider.
|
|
71
71
|
|
|
72
|
+
|
|
73
|
+
For an owner who explicitly offers keys, `kit.addKey('ana', { authChoice: 'openai-api-key', apiKey })` returns
|
|
74
|
+
`ok`, `invalid` or `not_included`. Label the option **API key (billed per use)**. Select only `routes()` rows with
|
|
75
|
+
`keyEntry: true`; API rows remain `offer: false`. Each route also carries its engine `revision`, `checked` date,
|
|
76
|
+
label and key error word names. Submit the input directly, clear the field afterwards, and never log or save it in
|
|
77
|
+
app state. The kit never returns engine key-check errors or the key. `ui-core/kits` supplies `keyStep`/`keyView`
|
|
78
|
+
for entry, checking, success and failure states, using the kit's `key.*` words.
|
|
79
|
+
|
|
80
|
+
To use the saved key for one run, pass `auth: 'apiKey'`. The key lives in the member's separate
|
|
81
|
+
`byokit-key-<member>` agent and workspace, with `copyToAgents: false` and only that key in its local auth order.
|
|
82
|
+
Its selected model is used; omit `model` or pass that same model. Key runs have separate history. To steer or stop
|
|
83
|
+
one, pass `{ auth: 'apiKey' }` to `steer` or `abort` too (the device helpers accept the same option).
|
|
84
|
+
Ordinary runs never enter the key agent, even when the subscription is resting. This agent boundary is necessary
|
|
85
|
+
because the engine's profile pins can rotate. The `byokit-key-` member prefix is reserved. Adding another key
|
|
86
|
+
replaces the previous key; a failed replacement leaves this option unavailable. If the normal agent already has
|
|
87
|
+
an API key added through pass-through calls, `addKey` returns `invalid` and leaves that key untouched.
|
|
88
|
+
|
|
72
89
|
A run spec also takes `system`, `images` (`{ data, mimeType }[]`), `thinking` and `tools`, a subset of the app tools
|
|
73
90
|
(`KitOptions.tools` names) this run may call; any other app tool is refused at the gate before `ToolHost.gate` sees
|
|
74
91
|
it. Tool events carry the engine's call `id`, the `input` on `start` and the `output` and `error` on `end`. A run that
|
|
@@ -120,10 +137,15 @@ route: github-copilot subscription code
|
|
|
120
137
|
route: github-copilot-enterprise subscription code
|
|
121
138
|
route: minimax-global-oauth subscription code
|
|
122
139
|
route: minimax-cn-oauth subscription code
|
|
140
|
+
route: anthropic-cli subscription browser
|
|
141
|
+
route: setup-token subscription browser
|
|
123
142
|
```
|
|
124
143
|
|
|
125
|
-
|
|
126
|
-
|
|
144
|
+
Subscription sign-ins are offered by default. API-billed routes remain in `kit.routes()` with `offer: false`;
|
|
145
|
+
apps can offer them when the app or person opts in, labelled API key (billed per use). Each `Route` carries `billing` (`subscription`, `api` or
|
|
146
|
+
`local`) so the app can label what is billed. Proxy routes, compatibility aliases, local runtimes stay off. Native Claude Code sign-in is offered by default. The pinned Gateway guides only some routes; other choices need manual paste or key entry. For an opted-in route, allow its `plugin` id
|
|
147
|
+
in the app configuration and pass its `choice` explicitly to `kit.signIn()`.
|
|
148
|
+
Offered provider plugins are allowed by default alongside the app's plugin ids.
|
|
127
149
|
|
|
128
150
|
## API at a glance
|
|
129
151
|
|
package/dist/device.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ export type SessionRow = {
|
|
|
23
23
|
export type DeviceRunOptions = {
|
|
24
24
|
sessionKey?: string;
|
|
25
25
|
model?: string;
|
|
26
|
+
auth?: 'apiKey';
|
|
26
27
|
system?: string;
|
|
27
28
|
images?: {
|
|
28
29
|
data: string;
|
|
@@ -64,8 +65,12 @@ export declare function openclawDevice(link: DeviceLink): {
|
|
|
64
65
|
type: 'end';
|
|
65
66
|
end: RunEnd;
|
|
66
67
|
}>;
|
|
67
|
-
steer(k: string, t: string
|
|
68
|
-
|
|
68
|
+
steer(k: string, t: string, o?: {
|
|
69
|
+
auth?: 'apiKey';
|
|
70
|
+
}): Promise<void>;
|
|
71
|
+
abort(k: string, o?: {
|
|
72
|
+
auth?: 'apiKey';
|
|
73
|
+
}): Promise<void>;
|
|
69
74
|
approvals(): Promise<Approval[]>;
|
|
70
75
|
decide(id: string, d: Decision): Promise<void>;
|
|
71
76
|
events(): AsyncIterable<OpenClawLinkEvent>;
|
package/dist/device.js
CHANGED
|
@@ -139,6 +139,7 @@ export function openclawDevice(link) {
|
|
|
139
139
|
run: (message, o) => {
|
|
140
140
|
const inner = liveStream(() => link.stream('oc.run', { message, ...(o?.sessionKey ? { sessionKey: o.sessionKey } : {}),
|
|
141
141
|
...(o?.model !== undefined ? { model: o.model } : {}), ...(o?.system !== undefined ? { system: o.system } : {}),
|
|
142
|
+
...(o?.auth !== undefined ? { auth: o.auth } : {}),
|
|
142
143
|
...(o?.images !== undefined ? { images: o.images } : {}), ...(o?.thinking !== undefined ? { thinking: o.thinking } : {}),
|
|
143
144
|
...(o?.tools !== undefined ? { tools: o.tools } : {}) }));
|
|
144
145
|
let finished = false;
|
|
@@ -168,11 +169,11 @@ export function openclawDevice(link) {
|
|
|
168
169
|
},
|
|
169
170
|
};
|
|
170
171
|
},
|
|
171
|
-
steer: async (k, t) => {
|
|
172
|
-
await link.request('oc.steer', { sessionKey: k, text: t });
|
|
172
|
+
steer: async (k, t, o) => {
|
|
173
|
+
await link.request('oc.steer', { sessionKey: k, text: t, ...(o?.auth ? { auth: o.auth } : {}) });
|
|
173
174
|
},
|
|
174
|
-
abort: async (k) => {
|
|
175
|
-
await link.request('oc.abort', { sessionKey: k });
|
|
175
|
+
abort: async (k, o) => {
|
|
176
|
+
await link.request('oc.abort', { sessionKey: k, ...(o?.auth ? { auth: o.auth } : {}) });
|
|
176
177
|
},
|
|
177
178
|
approvals: () => link.request('oc.approvals'),
|
|
178
179
|
decide: async (id, d) => {
|
package/dist/engine.js
CHANGED
|
@@ -148,7 +148,7 @@ export class Engine {
|
|
|
148
148
|
const pluginDir = join(this.root, 'plugin');
|
|
149
149
|
mkdirSync(pluginDir, { recursive: true, mode: 0o700 });
|
|
150
150
|
// The shipped plugin follows the kit on every prepare too: a state dir from an older kit must not keep its gate.
|
|
151
|
-
for (const f of ['package.json', 'index.js']) {
|
|
151
|
+
for (const f of ['package.json', 'index.js', 'keys.js']) {
|
|
152
152
|
const source = join(kitDir, 'plugin', f);
|
|
153
153
|
putChanged(join(pluginDir, f), readFileSync(source, 'utf8'));
|
|
154
154
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { ENGINE_VERSION, OPERATOR_SCOPES, PROTOCOL_VERSION } from './constants.ts';
|
|
2
2
|
export type { Approval, CallOptions, Decision, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, GateResult, Hello, KitState, Member, PlanWindow, Route, RunEnd, RunEvent, RunRef, RunSpec, RunUsage, SignInOptions, SignInView, ToolHost, ToolSpec, } from './types.ts';
|
|
3
3
|
export { OpenClawKit, type KitOptions, type RetainedLogin } from './kit.ts';
|
|
4
|
+
export type { AddKeyResult } from './keys.ts';
|
|
4
5
|
export { stateWords, toAccountView, words, type AccountView, type WordKey } from './words.ts';
|
|
5
6
|
export { EngineAlreadyRunningError } from './engine-status.ts';
|
package/dist/keys.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { GatewayTransport, Member } from './types.ts';
|
|
2
|
+
export type AddKeyResult = 'ok' | 'invalid' | 'not_included';
|
|
3
|
+
export declare function createKeys(ctx: {
|
|
4
|
+
root: string;
|
|
5
|
+
request: GatewayTransport['request'];
|
|
6
|
+
ensure(member: Member): Promise<{
|
|
7
|
+
agentId: string;
|
|
8
|
+
}>;
|
|
9
|
+
restarting?(): boolean;
|
|
10
|
+
}): {
|
|
11
|
+
exclusive: <T>(member: Member, work: () => Promise<T>) => Promise<T>;
|
|
12
|
+
add(member: Member, o: {
|
|
13
|
+
authChoice: string;
|
|
14
|
+
apiKey: string;
|
|
15
|
+
}): Promise<AddKeyResult>;
|
|
16
|
+
ready(member: Member): Promise<{
|
|
17
|
+
agentId: string;
|
|
18
|
+
model: string;
|
|
19
|
+
} | undefined>;
|
|
20
|
+
};
|
package/dist/keys.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { keyAgentId, MEMBER_ID, KEY_PREFIX } from "./members.js";
|
|
3
|
+
import { routes } from "./routes.js";
|
|
4
|
+
export function createKeys(ctx) {
|
|
5
|
+
// Serialize key replacement and key runs, so an in-flight run never sees a half-installed replacement.
|
|
6
|
+
const tails = new Map();
|
|
7
|
+
const exclusive = async (member, work) => {
|
|
8
|
+
const previous = tails.get(member) ?? Promise.resolve();
|
|
9
|
+
const next = previous.catch(() => { }).then(work);
|
|
10
|
+
tails.set(member, next);
|
|
11
|
+
try {
|
|
12
|
+
return await next;
|
|
13
|
+
}
|
|
14
|
+
finally {
|
|
15
|
+
if (tails.get(member) === next)
|
|
16
|
+
tails.delete(member);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const check = (member) => MEMBER_ID.test(member) && !member.startsWith(KEY_PREFIX);
|
|
20
|
+
const control = (member, action, provider) => ctx.request('byokit.keys', { member, action, ...(provider ? { provider } : {}) });
|
|
21
|
+
return {
|
|
22
|
+
exclusive,
|
|
23
|
+
async add(member, o) {
|
|
24
|
+
if (!check(member) || !o.apiKey.trim())
|
|
25
|
+
return 'invalid';
|
|
26
|
+
const route = routes().find((r) => r.choice === o.authChoice && r.keyEntry);
|
|
27
|
+
if (!route)
|
|
28
|
+
return 'not_included';
|
|
29
|
+
return exclusive(member, async () => {
|
|
30
|
+
try {
|
|
31
|
+
await ctx.ensure(member);
|
|
32
|
+
const agentId = keyAgentId(member);
|
|
33
|
+
const list = await ctx.request('agents.list');
|
|
34
|
+
if (!list.agents.some((a) => a.id === agentId))
|
|
35
|
+
await ctx.request('agents.create', { name: agentId, workspace: join(ctx.root, 'key-workspaces', member) });
|
|
36
|
+
if (!(await control(member, 'prepare', route.provider)).ok)
|
|
37
|
+
return 'invalid';
|
|
38
|
+
// The public pin's typed API-key path, never a CLI-login import. Discard the engine's lines/errors.
|
|
39
|
+
const params = {
|
|
40
|
+
kind: 'api-key', agentId, authChoice: o.authChoice, apiKey: o.apiKey,
|
|
41
|
+
};
|
|
42
|
+
const result = (await ctx.request('openclaw.setup.activate', params, { timeoutMs: 180_000 }));
|
|
43
|
+
if (!result.ok)
|
|
44
|
+
return result.status === 'unavailable' ? 'not_included' : 'invalid';
|
|
45
|
+
return (await control(member, 'seal')).ok ? 'ok' : 'invalid';
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return 'invalid';
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
async ready(member) {
|
|
53
|
+
if (!check(member))
|
|
54
|
+
return undefined;
|
|
55
|
+
const deadline = Date.now() + 60_000;
|
|
56
|
+
for (;;) {
|
|
57
|
+
try {
|
|
58
|
+
const ready = await control(member, 'ready');
|
|
59
|
+
return ready.ok && ready.model ? { agentId: keyAgentId(member), model: ready.model } : undefined;
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
// Config writes may drain/restart the gateway after activation. This read is safe to retry;
|
|
63
|
+
// an absent profile or a terminal refusal still fails closed immediately.
|
|
64
|
+
const refusal = error;
|
|
65
|
+
const transient = ctx.restarting?.() || (refusal?.code === 'UNAVAILABLE' && refusal.retryable === true);
|
|
66
|
+
if (!transient || Date.now() >= deadline)
|
|
67
|
+
return undefined;
|
|
68
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
package/dist/kit.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SealingAdapter } from '@byokit/secrets';
|
|
2
|
+
import { type AddKeyResult } from './keys.ts';
|
|
2
3
|
import type { Approval, CallOptions, Decision, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, Hello, KitState, Member, Route, RunEnd, RunEvent, RunSpec, SignInOptions, SignInView, ToolHost, ToolSpec } from './types.ts';
|
|
3
4
|
export type KitOptions = {
|
|
4
5
|
stateDir: string;
|
|
@@ -47,6 +48,7 @@ export declare class OpenClawKit {
|
|
|
47
48
|
private greeting?;
|
|
48
49
|
private members?;
|
|
49
50
|
private readonly bridge;
|
|
51
|
+
private readonly keys;
|
|
50
52
|
private readonly approvalsCtl;
|
|
51
53
|
private listeners;
|
|
52
54
|
private off;
|
|
@@ -81,14 +83,24 @@ export declare class OpenClawKit {
|
|
|
81
83
|
done: Promise<SignInView>;
|
|
82
84
|
};
|
|
83
85
|
signOut(member: Member, provider: string): Promise<void>;
|
|
86
|
+
/** Save an explicitly supplied API key (billed per use) in this member's separate key agent. */
|
|
87
|
+
addKey(agentId: Member, o: {
|
|
88
|
+
authChoice: string;
|
|
89
|
+
apiKey: string;
|
|
90
|
+
}): Promise<AddKeyResult>;
|
|
84
91
|
migrateRetainedLogin(member: Member, source: RetainedLogin): Promise<'staged' | 'nothing' | 'failed'>;
|
|
85
92
|
confirmRetainedLogin(member: Member, source: RetainedLogin): Promise<boolean>;
|
|
86
93
|
private runs;
|
|
87
94
|
/** The app tool names (`KitOptions.tools`): the only names `RunSpec.tools` may carry. */
|
|
88
95
|
toolNames(): string[];
|
|
89
96
|
run(spec: RunSpec, on?: (e: RunEvent) => void): Promise<RunEnd>;
|
|
90
|
-
|
|
91
|
-
|
|
97
|
+
private runKey;
|
|
98
|
+
steer(sessionKey: string, text: string, o?: {
|
|
99
|
+
auth?: 'apiKey';
|
|
100
|
+
}): Promise<void>;
|
|
101
|
+
abort(sessionKey: string, o?: {
|
|
102
|
+
auth?: 'apiKey';
|
|
103
|
+
}): Promise<void>;
|
|
92
104
|
approvals(member?: Member): Approval[];
|
|
93
105
|
onApproval(fn: (a: Approval, change: 'added' | 'resolved') => void): () => void;
|
|
94
106
|
decide(id: string, d: Decision): Promise<void>;
|
package/dist/kit.js
CHANGED
|
@@ -8,8 +8,10 @@ import { Bridge } from "./bridge.js";
|
|
|
8
8
|
import { Engine } from "./engine.js";
|
|
9
9
|
import { gatewayTransport } from "./transport.js";
|
|
10
10
|
import { createMembers } from "./members.js";
|
|
11
|
+
import { keyAgentId, KEY_PREFIX, MEMBER_ID } from "./members.js";
|
|
11
12
|
import { confirmRetainedLogin as confirmLogin, migrateRetainedLogin as migrateLogin } from "./migrate.js";
|
|
12
13
|
import { createRuns } from "./runs.js";
|
|
14
|
+
import { createKeys } from "./keys.js";
|
|
13
15
|
import { routes as routeTable } from "./routes.js";
|
|
14
16
|
import { providers as engineProviders, signIn as startSignIn, signOut as engineSignOut } from "./signin.js";
|
|
15
17
|
import { reconcileConfig, memoryLimited as configMemoryLimited } from "./config.js";
|
|
@@ -420,6 +422,7 @@ export class OpenClawKit {
|
|
|
420
422
|
// One bridge and one approval surface per kit: listeners and run registrations survive reconnects (B3).
|
|
421
423
|
// Only the socket itself is per-connection. Approvals always reaches the live transport through this.request().
|
|
422
424
|
bridge;
|
|
425
|
+
keys;
|
|
423
426
|
approvalsCtl;
|
|
424
427
|
listeners = new Set();
|
|
425
428
|
off = [];
|
|
@@ -439,6 +442,10 @@ export class OpenClawKit {
|
|
|
439
442
|
this.engine = new Engine({ ...o, pluginId: o.plugin?.id ?? 'byokit', tools: o.tools ?? [],
|
|
440
443
|
gateBuiltins: o.gateBuiltins !== false, spawnEngine: o.spawnEngine !== false,
|
|
441
444
|
onState: (s) => this.setState(s), onExit: () => this.closed('engine exited') });
|
|
445
|
+
this.keys = createKeys({ root: this.engine.root,
|
|
446
|
+
restarting: () => this.current.phase === 'restarting' || this.current.phase === 'starting',
|
|
447
|
+
request: (method, params, options) => this.request()(method, params, options),
|
|
448
|
+
ensure: (member) => this.ensureMember(member) });
|
|
442
449
|
const slot = {};
|
|
443
450
|
this.approvalsCtl = new Approvals({
|
|
444
451
|
request: (method, params, co) => this.request()(method, params, co),
|
|
@@ -599,6 +606,10 @@ export class OpenClawKit {
|
|
|
599
606
|
signOut(member, provider) {
|
|
600
607
|
return engineSignOut(this.signInCtx(), member, provider);
|
|
601
608
|
}
|
|
609
|
+
/** Save an explicitly supplied API key (billed per use) in this member's separate key agent. */
|
|
610
|
+
addKey(agentId, o) {
|
|
611
|
+
return this.keys.add(agentId, o);
|
|
612
|
+
}
|
|
602
613
|
migrateRetainedLogin(member, source) {
|
|
603
614
|
return migrateLogin({ root: this.engine.root, prepare: () => this.prepare(), doctor: () => this.engine.doctor(120_000),
|
|
604
615
|
seal: this.o.authSeal, log: this.o.log, withStore: (task) => this.engine.withAuthStore(task) }, member, source);
|
|
@@ -627,13 +638,35 @@ export class OpenClawKit {
|
|
|
627
638
|
return (this.o.tools ?? []).map((t) => t.name);
|
|
628
639
|
}
|
|
629
640
|
run(spec, on) {
|
|
641
|
+
if (spec.auth !== undefined && spec.auth !== 'apiKey')
|
|
642
|
+
return Promise.reject(new Error('Choose a supported account option.'));
|
|
643
|
+
if (spec.auth === 'apiKey')
|
|
644
|
+
return this.keys.exclusive(spec.member, async () => {
|
|
645
|
+
if (!spec.sessionKey.startsWith(`agent:${spec.member}:`))
|
|
646
|
+
throw new Error('This conversation belongs to another member.');
|
|
647
|
+
const selected = await this.keys.ready(spec.member);
|
|
648
|
+
if (!selected)
|
|
649
|
+
return { ok: false, kind: 'signed-out', message: 'Add an API key to use this option.' };
|
|
650
|
+
if (spec.model !== undefined && spec.model !== selected.model)
|
|
651
|
+
return { ok: false, kind: 'plan', message: 'Choose the model saved with this key.' };
|
|
652
|
+
// RunRef.member stays the person for app gates; the engine receives the isolated agent and history.
|
|
653
|
+
return this.runs().run({ ...spec, model: selected.model }, on, selected.agentId);
|
|
654
|
+
});
|
|
630
655
|
return this.runs().run(spec, on);
|
|
631
656
|
}
|
|
632
|
-
|
|
633
|
-
|
|
657
|
+
runKey(sessionKey, o) {
|
|
658
|
+
if (o?.auth !== 'apiKey')
|
|
659
|
+
return sessionKey;
|
|
660
|
+
const [, member, tail] = /^agent:([^:]+):(.*)$/.exec(sessionKey) ?? [];
|
|
661
|
+
if (!member || !MEMBER_ID.test(member) || member.startsWith(KEY_PREFIX))
|
|
662
|
+
throw new Error('Choose your own conversation.');
|
|
663
|
+
return `agent:${keyAgentId(member)}:${tail}`;
|
|
664
|
+
}
|
|
665
|
+
steer(sessionKey, text, o) {
|
|
666
|
+
return this.runs().steer(this.runKey(sessionKey, o), text);
|
|
634
667
|
}
|
|
635
|
-
abort(sessionKey) {
|
|
636
|
-
return this.runs().abort(sessionKey);
|
|
668
|
+
abort(sessionKey, o) {
|
|
669
|
+
return this.runs().abort(this.runKey(sessionKey, o));
|
|
637
670
|
}
|
|
638
671
|
// approvals (5.9)
|
|
639
672
|
approvals(member) {
|
package/dist/link.js
CHANGED
|
@@ -25,9 +25,10 @@ function memberSessions(result, member) {
|
|
|
25
25
|
}
|
|
26
26
|
/** `oc.run`'s run options, type-checked; the kit's own checks (account, tool names) still apply. */
|
|
27
27
|
function runOptions(args) {
|
|
28
|
-
const { message, model, system, images, thinking, tools } = args;
|
|
28
|
+
const { message, model, auth, system, images, thinking, tools } = args;
|
|
29
29
|
const ok = typeof message === 'string'
|
|
30
30
|
&& (model === undefined || typeof model === 'string')
|
|
31
|
+
&& (auth === undefined || auth === 'apiKey')
|
|
31
32
|
&& (system === undefined || typeof system === 'string')
|
|
32
33
|
&& (images === undefined || (Array.isArray(images) && images.every((i) => isRecord(i) && typeof i.data === 'string' && typeof i.mimeType === 'string')))
|
|
33
34
|
&& (thinking === undefined || (typeof thinking === 'string' && THINKING.has(thinking)))
|
|
@@ -37,6 +38,7 @@ function runOptions(args) {
|
|
|
37
38
|
return {
|
|
38
39
|
message: message,
|
|
39
40
|
...(model === undefined ? {} : { model: model }),
|
|
41
|
+
...(auth === undefined ? {} : { auth: 'apiKey' }),
|
|
40
42
|
...(system === undefined ? {} : { system: system }),
|
|
41
43
|
...(images === undefined ? {} : { images: images
|
|
42
44
|
.map((i) => ({ data: i.data, mimeType: i.mimeType })) }),
|
|
@@ -180,19 +182,19 @@ export function openclawLink(kit, o) {
|
|
|
180
182
|
return memberSessions(result, member);
|
|
181
183
|
}
|
|
182
184
|
case 'oc.steer': {
|
|
183
|
-
if (typeof args.sessionKey !== 'string' || typeof args.text !== 'string')
|
|
185
|
+
if (typeof args.sessionKey !== 'string' || typeof args.text !== 'string' || (args.auth !== undefined && args.auth !== 'apiKey'))
|
|
184
186
|
throw new Error('oc.steer needs { sessionKey, text }');
|
|
185
187
|
if (!memberKey(member, args.sessionKey))
|
|
186
188
|
throw refused();
|
|
187
|
-
await kit.steer(args.sessionKey, args.text);
|
|
189
|
+
await kit.steer(args.sessionKey, args.text, args.auth === 'apiKey' ? { auth: 'apiKey' } : undefined);
|
|
188
190
|
return null;
|
|
189
191
|
}
|
|
190
192
|
case 'oc.abort': {
|
|
191
|
-
if (typeof args.sessionKey !== 'string')
|
|
193
|
+
if (typeof args.sessionKey !== 'string' || (args.auth !== undefined && args.auth !== 'apiKey'))
|
|
192
194
|
throw new Error('oc.abort needs { sessionKey }');
|
|
193
195
|
if (!memberKey(member, args.sessionKey))
|
|
194
196
|
throw refused();
|
|
195
|
-
await kit.abort(args.sessionKey);
|
|
197
|
+
await kit.abort(args.sessionKey, args.auth === 'apiKey' ? { auth: 'apiKey' } : undefined);
|
|
196
198
|
return null;
|
|
197
199
|
}
|
|
198
200
|
case 'oc.approvals':
|
package/dist/members.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { GatewayTransport, Member } from './types.ts';
|
|
2
2
|
export declare const MEMBER_ID: RegExp;
|
|
3
|
+
export declare const KEY_PREFIX = "byokit-key-";
|
|
4
|
+
export declare const keyAgentId: (member: Member) => string;
|
|
3
5
|
export declare function createMembers(ctx: {
|
|
4
6
|
request: GatewayTransport['request'];
|
|
5
7
|
root: string;
|
package/dist/members.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
export const MEMBER_ID = /^[a-z][a-z0-9-]{0,31}$/;
|
|
3
|
+
export const KEY_PREFIX = 'byokit-key-';
|
|
4
|
+
export const keyAgentId = (member) => `${KEY_PREFIX}${member}`;
|
|
3
5
|
export function createMembers(ctx) {
|
|
4
6
|
const cache = new Map();
|
|
5
7
|
return { ensure(member) {
|
|
6
|
-
if (!MEMBER_ID.test(member))
|
|
8
|
+
if (!MEMBER_ID.test(member) || member.startsWith(KEY_PREFIX))
|
|
7
9
|
return Promise.reject(new Error(`invalid member id: ${member}`));
|
|
8
10
|
const existing = cache.get(member);
|
|
9
11
|
if (existing)
|
package/dist/routes.json
CHANGED
|
@@ -8,7 +8,12 @@
|
|
|
8
8
|
"prerequisite": null,
|
|
9
9
|
"offer": true,
|
|
10
10
|
"reason": "ChatGPT/Codex subscription sign-in: the engine writes the redirect to the app's callback port.",
|
|
11
|
-
"source": "dist/provider-contract-api-*.js (2026.8.1)"
|
|
11
|
+
"source": "dist/provider-contract-api-*.js (2026.8.1)",
|
|
12
|
+
"revision": "2026.8.1",
|
|
13
|
+
"checked": "2026-09-30",
|
|
14
|
+
"label": "Subscription",
|
|
15
|
+
"keyEntry": false,
|
|
16
|
+
"keyErrors": null
|
|
12
17
|
},
|
|
13
18
|
{
|
|
14
19
|
"choice": "openai-device-code",
|
|
@@ -19,7 +24,12 @@
|
|
|
19
24
|
"prerequisite": null,
|
|
20
25
|
"offer": true,
|
|
21
26
|
"reason": "ChatGPT subscription sign-in by device code: the code is shown, no browser on this computer needed.",
|
|
22
|
-
"source": "dist/provider-contract-api-*.js (2026.8.1)"
|
|
27
|
+
"source": "dist/provider-contract-api-*.js (2026.8.1)",
|
|
28
|
+
"revision": "2026.8.1",
|
|
29
|
+
"checked": "2026-09-30",
|
|
30
|
+
"label": "Subscription",
|
|
31
|
+
"keyEntry": false,
|
|
32
|
+
"keyErrors": null
|
|
23
33
|
},
|
|
24
34
|
{
|
|
25
35
|
"choice": "xai-oauth",
|
|
@@ -30,7 +40,12 @@
|
|
|
30
40
|
"prerequisite": null,
|
|
31
41
|
"offer": true,
|
|
32
42
|
"reason": "Grok subscription sign-in through the engine's own wizard.",
|
|
33
|
-
"source": "dist/extensions/xai/openclaw.plugin.json (2026.8.1)"
|
|
43
|
+
"source": "dist/extensions/xai/openclaw.plugin.json (2026.8.1)",
|
|
44
|
+
"revision": "2026.8.1",
|
|
45
|
+
"checked": "2026-09-30",
|
|
46
|
+
"label": "Subscription",
|
|
47
|
+
"keyEntry": false,
|
|
48
|
+
"keyErrors": null
|
|
34
49
|
},
|
|
35
50
|
{
|
|
36
51
|
"choice": "github-copilot",
|
|
@@ -41,7 +56,12 @@
|
|
|
41
56
|
"prerequisite": null,
|
|
42
57
|
"offer": true,
|
|
43
58
|
"reason": "GitHub Copilot subscription sign-in; the login stays in the engine's own folder.",
|
|
44
|
-
"source": "dist/extensions/github-copilot/openclaw.plugin.json (2026.8.1)"
|
|
59
|
+
"source": "dist/extensions/github-copilot/openclaw.plugin.json (2026.8.1)",
|
|
60
|
+
"revision": "2026.8.1",
|
|
61
|
+
"checked": "2026-09-30",
|
|
62
|
+
"label": "Subscription",
|
|
63
|
+
"keyEntry": false,
|
|
64
|
+
"keyErrors": null
|
|
45
65
|
},
|
|
46
66
|
{
|
|
47
67
|
"choice": "github-copilot-enterprise",
|
|
@@ -52,7 +72,12 @@
|
|
|
52
72
|
"prerequisite": null,
|
|
53
73
|
"offer": true,
|
|
54
74
|
"reason": "GitHub Copilot Enterprise sign-in; the login stays in the engine's own folder.",
|
|
55
|
-
"source": "dist/extensions/github-copilot/openclaw.plugin.json (2026.8.1)"
|
|
75
|
+
"source": "dist/extensions/github-copilot/openclaw.plugin.json (2026.8.1)",
|
|
76
|
+
"revision": "2026.8.1",
|
|
77
|
+
"checked": "2026-09-30",
|
|
78
|
+
"label": "Subscription",
|
|
79
|
+
"keyEntry": false,
|
|
80
|
+
"keyErrors": null
|
|
56
81
|
},
|
|
57
82
|
{
|
|
58
83
|
"choice": "minimax-global-oauth",
|
|
@@ -63,7 +88,12 @@
|
|
|
63
88
|
"prerequisite": null,
|
|
64
89
|
"offer": true,
|
|
65
90
|
"reason": "MiniMax subscription sign-in (global endpoint).",
|
|
66
|
-
"source": "dist/extensions/minimax/openclaw.plugin.json (2026.8.1)"
|
|
91
|
+
"source": "dist/extensions/minimax/openclaw.plugin.json (2026.8.1)",
|
|
92
|
+
"revision": "2026.8.1",
|
|
93
|
+
"checked": "2026-09-30",
|
|
94
|
+
"label": "Subscription",
|
|
95
|
+
"keyEntry": false,
|
|
96
|
+
"keyErrors": null
|
|
67
97
|
},
|
|
68
98
|
{
|
|
69
99
|
"choice": "minimax-cn-oauth",
|
|
@@ -74,7 +104,12 @@
|
|
|
74
104
|
"prerequisite": null,
|
|
75
105
|
"offer": true,
|
|
76
106
|
"reason": "MiniMax subscription sign-in (CN endpoint).",
|
|
77
|
-
"source": "dist/extensions/minimax/openclaw.plugin.json (2026.8.1)"
|
|
107
|
+
"source": "dist/extensions/minimax/openclaw.plugin.json (2026.8.1)",
|
|
108
|
+
"revision": "2026.8.1",
|
|
109
|
+
"checked": "2026-09-30",
|
|
110
|
+
"label": "Subscription",
|
|
111
|
+
"keyEntry": false,
|
|
112
|
+
"keyErrors": null
|
|
78
113
|
},
|
|
79
114
|
{
|
|
80
115
|
"choice": "openrouter-oauth",
|
|
@@ -84,8 +119,13 @@
|
|
|
84
119
|
"via": "browser",
|
|
85
120
|
"prerequisite": null,
|
|
86
121
|
"offer": false,
|
|
87
|
-
"reason": "API
|
|
88
|
-
"source": "dist/extensions/openrouter/openclaw.plugin.json (2026.8.1)"
|
|
122
|
+
"reason": "API key (billed per use); offered only when the app or person opts in.",
|
|
123
|
+
"source": "dist/extensions/openrouter/openclaw.plugin.json (2026.8.1)",
|
|
124
|
+
"revision": "2026.8.1",
|
|
125
|
+
"checked": "2026-09-30",
|
|
126
|
+
"label": "API key (billed per use)",
|
|
127
|
+
"keyEntry": false,
|
|
128
|
+
"keyErrors": null
|
|
89
129
|
},
|
|
90
130
|
{
|
|
91
131
|
"choice": "xai-device-code",
|
|
@@ -96,7 +136,12 @@
|
|
|
96
136
|
"prerequisite": null,
|
|
97
137
|
"offer": false,
|
|
98
138
|
"reason": "Compatibility alias the Gateway does not offer; use xai-oauth.",
|
|
99
|
-
"source": "dist/extensions/xai/openclaw.plugin.json (2026.8.1)"
|
|
139
|
+
"source": "dist/extensions/xai/openclaw.plugin.json (2026.8.1)",
|
|
140
|
+
"revision": "2026.8.1",
|
|
141
|
+
"checked": "2026-09-30",
|
|
142
|
+
"label": "Subscription",
|
|
143
|
+
"keyEntry": false,
|
|
144
|
+
"keyErrors": null
|
|
100
145
|
},
|
|
101
146
|
{
|
|
102
147
|
"choice": "alibaba-model-studio-api-key",
|
|
@@ -106,8 +151,13 @@
|
|
|
106
151
|
"via": "browser",
|
|
107
152
|
"prerequisite": null,
|
|
108
153
|
"offer": false,
|
|
109
|
-
"reason": "API
|
|
110
|
-
"source": "dist/extensions/alibaba/openclaw.plugin.json (2026.8.1)"
|
|
154
|
+
"reason": "API key (billed per use); offered only when the app or person opts in.",
|
|
155
|
+
"source": "dist/extensions/alibaba/openclaw.plugin.json (2026.8.1)",
|
|
156
|
+
"revision": "2026.8.1",
|
|
157
|
+
"checked": "2026-09-30",
|
|
158
|
+
"label": "API key (billed per use)",
|
|
159
|
+
"keyEntry": false,
|
|
160
|
+
"keyErrors": null
|
|
111
161
|
},
|
|
112
162
|
{
|
|
113
163
|
"choice": "anthropic-cli",
|
|
@@ -121,7 +171,12 @@
|
|
|
121
171
|
"source": "dist/extensions/anthropic/openclaw.plugin.json (2026.8.1)",
|
|
122
172
|
"auth": "cli",
|
|
123
173
|
"terms": "allowed",
|
|
124
|
-
"termsUrl": "https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use"
|
|
174
|
+
"termsUrl": "https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use",
|
|
175
|
+
"revision": "2026.8.1",
|
|
176
|
+
"checked": "2026-09-30",
|
|
177
|
+
"label": "Subscription",
|
|
178
|
+
"keyEntry": false,
|
|
179
|
+
"keyErrors": null
|
|
125
180
|
},
|
|
126
181
|
{
|
|
127
182
|
"choice": "apiKey",
|
|
@@ -135,7 +190,15 @@
|
|
|
135
190
|
"source": "dist/extensions/anthropic/openclaw.plugin.json (2026.8.1)",
|
|
136
191
|
"auth": "api_key",
|
|
137
192
|
"terms": "allowed",
|
|
138
|
-
"termsUrl": "https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use"
|
|
193
|
+
"termsUrl": "https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use",
|
|
194
|
+
"revision": "2026.8.1",
|
|
195
|
+
"checked": "2026-09-30",
|
|
196
|
+
"label": "API key (billed per use)",
|
|
197
|
+
"keyEntry": true,
|
|
198
|
+
"keyErrors": {
|
|
199
|
+
"invalid": "key.invalid",
|
|
200
|
+
"not_included": "key.notIncluded"
|
|
201
|
+
}
|
|
139
202
|
},
|
|
140
203
|
{
|
|
141
204
|
"choice": "clawrouter-api-key",
|
|
@@ -145,8 +208,13 @@
|
|
|
145
208
|
"via": "browser",
|
|
146
209
|
"prerequisite": null,
|
|
147
210
|
"offer": false,
|
|
148
|
-
"reason": "API
|
|
149
|
-
"source": "dist/extensions/clawrouter/openclaw.plugin.json (2026.8.1)"
|
|
211
|
+
"reason": "Proxy route; API key (billed per use); offered only when an app asks.",
|
|
212
|
+
"source": "dist/extensions/clawrouter/openclaw.plugin.json (2026.8.1)",
|
|
213
|
+
"revision": "2026.8.1",
|
|
214
|
+
"checked": "2026-09-30",
|
|
215
|
+
"label": "API key (billed per use)",
|
|
216
|
+
"keyEntry": false,
|
|
217
|
+
"keyErrors": null
|
|
150
218
|
},
|
|
151
219
|
{
|
|
152
220
|
"choice": "copilot-proxy",
|
|
@@ -157,7 +225,12 @@
|
|
|
157
225
|
"prerequisite": null,
|
|
158
226
|
"offer": false,
|
|
159
227
|
"reason": "Local runtime on this computer; no account and no billing.",
|
|
160
|
-
"source": "dist/extensions/copilot-proxy/openclaw.plugin.json (2026.8.1)"
|
|
228
|
+
"source": "dist/extensions/copilot-proxy/openclaw.plugin.json (2026.8.1)",
|
|
229
|
+
"revision": "2026.8.1",
|
|
230
|
+
"checked": "2026-09-30",
|
|
231
|
+
"label": "On this computer",
|
|
232
|
+
"keyEntry": false,
|
|
233
|
+
"keyErrors": null
|
|
161
234
|
},
|
|
162
235
|
{
|
|
163
236
|
"choice": "custom-api-key",
|
|
@@ -167,8 +240,13 @@
|
|
|
167
240
|
"via": "browser",
|
|
168
241
|
"prerequisite": null,
|
|
169
242
|
"offer": false,
|
|
170
|
-
"reason": "API
|
|
171
|
-
"source": "dist/auth-choice-options-BLHNSTXX.js (2026.8.1)"
|
|
243
|
+
"reason": "Proxy route; API key (billed per use); offered only when an app asks.",
|
|
244
|
+
"source": "dist/auth-choice-options-BLHNSTXX.js (2026.8.1)",
|
|
245
|
+
"revision": "2026.8.1",
|
|
246
|
+
"checked": "2026-09-30",
|
|
247
|
+
"label": "API key (billed per use)",
|
|
248
|
+
"keyEntry": false,
|
|
249
|
+
"keyErrors": null
|
|
172
250
|
},
|
|
173
251
|
{
|
|
174
252
|
"choice": "fal-api-key",
|
|
@@ -178,8 +256,13 @@
|
|
|
178
256
|
"via": "browser",
|
|
179
257
|
"prerequisite": null,
|
|
180
258
|
"offer": false,
|
|
181
|
-
"reason": "API
|
|
182
|
-
"source": "dist/extensions/fal/openclaw.plugin.json (2026.8.1)"
|
|
259
|
+
"reason": "API key (billed per use); offered only when the app or person opts in.",
|
|
260
|
+
"source": "dist/extensions/fal/openclaw.plugin.json (2026.8.1)",
|
|
261
|
+
"revision": "2026.8.1",
|
|
262
|
+
"checked": "2026-09-30",
|
|
263
|
+
"label": "API key (billed per use)",
|
|
264
|
+
"keyEntry": false,
|
|
265
|
+
"keyErrors": null
|
|
183
266
|
},
|
|
184
267
|
{
|
|
185
268
|
"choice": "gemini-api-key",
|
|
@@ -189,8 +272,16 @@
|
|
|
189
272
|
"via": "browser",
|
|
190
273
|
"prerequisite": null,
|
|
191
274
|
"offer": false,
|
|
192
|
-
"reason": "API
|
|
193
|
-
"source": "dist/provider-contract-api-*.js (2026.8.1)"
|
|
275
|
+
"reason": "API key (billed per use); offered only when the app or person opts in.",
|
|
276
|
+
"source": "dist/provider-contract-api-*.js (2026.8.1)",
|
|
277
|
+
"revision": "2026.8.1",
|
|
278
|
+
"checked": "2026-09-30",
|
|
279
|
+
"label": "API key (billed per use)",
|
|
280
|
+
"keyEntry": true,
|
|
281
|
+
"keyErrors": {
|
|
282
|
+
"invalid": "key.invalid",
|
|
283
|
+
"not_included": "key.notIncluded"
|
|
284
|
+
}
|
|
194
285
|
},
|
|
195
286
|
{
|
|
196
287
|
"choice": "huggingface-api-key",
|
|
@@ -200,8 +291,16 @@
|
|
|
200
291
|
"via": "browser",
|
|
201
292
|
"prerequisite": null,
|
|
202
293
|
"offer": false,
|
|
203
|
-
"reason": "API
|
|
204
|
-
"source": "dist/extensions/huggingface/openclaw.plugin.json (2026.8.1)"
|
|
294
|
+
"reason": "API key (billed per use); offered only when the app or person opts in.",
|
|
295
|
+
"source": "dist/extensions/huggingface/openclaw.plugin.json (2026.8.1)",
|
|
296
|
+
"revision": "2026.8.1",
|
|
297
|
+
"checked": "2026-09-30",
|
|
298
|
+
"label": "API key (billed per use)",
|
|
299
|
+
"keyEntry": true,
|
|
300
|
+
"keyErrors": {
|
|
301
|
+
"invalid": "key.invalid",
|
|
302
|
+
"not_included": "key.notIncluded"
|
|
303
|
+
}
|
|
205
304
|
},
|
|
206
305
|
{
|
|
207
306
|
"choice": "litellm-api-key",
|
|
@@ -211,8 +310,16 @@
|
|
|
211
310
|
"via": "browser",
|
|
212
311
|
"prerequisite": null,
|
|
213
312
|
"offer": false,
|
|
214
|
-
"reason": "API
|
|
215
|
-
"source": "dist/extensions/litellm/openclaw.plugin.json (2026.8.1)"
|
|
313
|
+
"reason": "Proxy route; API key (billed per use); offered only when an app asks.",
|
|
314
|
+
"source": "dist/extensions/litellm/openclaw.plugin.json (2026.8.1)",
|
|
315
|
+
"revision": "2026.8.1",
|
|
316
|
+
"checked": "2026-09-30",
|
|
317
|
+
"label": "API key (billed per use)",
|
|
318
|
+
"keyEntry": true,
|
|
319
|
+
"keyErrors": {
|
|
320
|
+
"invalid": "key.invalid",
|
|
321
|
+
"not_included": "key.notIncluded"
|
|
322
|
+
}
|
|
216
323
|
},
|
|
217
324
|
{
|
|
218
325
|
"choice": "lmstudio",
|
|
@@ -223,7 +330,12 @@
|
|
|
223
330
|
"prerequisite": null,
|
|
224
331
|
"offer": false,
|
|
225
332
|
"reason": "Local runtime on this computer; no account and no billing.",
|
|
226
|
-
"source": "dist/extensions/lmstudio/openclaw.plugin.json (2026.8.1)"
|
|
333
|
+
"source": "dist/extensions/lmstudio/openclaw.plugin.json (2026.8.1)",
|
|
334
|
+
"revision": "2026.8.1",
|
|
335
|
+
"checked": "2026-09-30",
|
|
336
|
+
"label": "On this computer",
|
|
337
|
+
"keyEntry": false,
|
|
338
|
+
"keyErrors": null
|
|
227
339
|
},
|
|
228
340
|
{
|
|
229
341
|
"choice": "microsoft-foundry-apikey",
|
|
@@ -233,8 +345,13 @@
|
|
|
233
345
|
"via": "browser",
|
|
234
346
|
"prerequisite": null,
|
|
235
347
|
"offer": false,
|
|
236
|
-
"reason": "API
|
|
237
|
-
"source": "dist/extensions/microsoft-foundry/openclaw.plugin.json (2026.8.1)"
|
|
348
|
+
"reason": "API key (billed per use); offered only when the app or person opts in.",
|
|
349
|
+
"source": "dist/extensions/microsoft-foundry/openclaw.plugin.json (2026.8.1)",
|
|
350
|
+
"revision": "2026.8.1",
|
|
351
|
+
"checked": "2026-09-30",
|
|
352
|
+
"label": "API key (billed per use)",
|
|
353
|
+
"keyEntry": false,
|
|
354
|
+
"keyErrors": null
|
|
238
355
|
},
|
|
239
356
|
{
|
|
240
357
|
"choice": "microsoft-foundry-entra",
|
|
@@ -244,8 +361,13 @@
|
|
|
244
361
|
"via": "browser",
|
|
245
362
|
"prerequisite": null,
|
|
246
363
|
"offer": false,
|
|
247
|
-
"reason": "API
|
|
248
|
-
"source": "dist/extensions/microsoft-foundry/openclaw.plugin.json (2026.8.1)"
|
|
364
|
+
"reason": "API key (billed per use); offered only when the app or person opts in.",
|
|
365
|
+
"source": "dist/extensions/microsoft-foundry/openclaw.plugin.json (2026.8.1)",
|
|
366
|
+
"revision": "2026.8.1",
|
|
367
|
+
"checked": "2026-09-30",
|
|
368
|
+
"label": "API key (billed per use)",
|
|
369
|
+
"keyEntry": false,
|
|
370
|
+
"keyErrors": null
|
|
249
371
|
},
|
|
250
372
|
{
|
|
251
373
|
"choice": "minimax-cn-api",
|
|
@@ -255,8 +377,13 @@
|
|
|
255
377
|
"via": "browser",
|
|
256
378
|
"prerequisite": null,
|
|
257
379
|
"offer": false,
|
|
258
|
-
"reason": "API
|
|
259
|
-
"source": "dist/extensions/minimax/openclaw.plugin.json (2026.8.1)"
|
|
380
|
+
"reason": "API key (billed per use); offered only when the app or person opts in.",
|
|
381
|
+
"source": "dist/extensions/minimax/openclaw.plugin.json (2026.8.1)",
|
|
382
|
+
"revision": "2026.8.1",
|
|
383
|
+
"checked": "2026-09-30",
|
|
384
|
+
"label": "API key (billed per use)",
|
|
385
|
+
"keyEntry": false,
|
|
386
|
+
"keyErrors": null
|
|
260
387
|
},
|
|
261
388
|
{
|
|
262
389
|
"choice": "minimax-global-api",
|
|
@@ -266,8 +393,13 @@
|
|
|
266
393
|
"via": "browser",
|
|
267
394
|
"prerequisite": null,
|
|
268
395
|
"offer": false,
|
|
269
|
-
"reason": "API
|
|
270
|
-
"source": "dist/extensions/minimax/openclaw.plugin.json (2026.8.1)"
|
|
396
|
+
"reason": "API key (billed per use); offered only when the app or person opts in.",
|
|
397
|
+
"source": "dist/extensions/minimax/openclaw.plugin.json (2026.8.1)",
|
|
398
|
+
"revision": "2026.8.1",
|
|
399
|
+
"checked": "2026-09-30",
|
|
400
|
+
"label": "API key (billed per use)",
|
|
401
|
+
"keyEntry": false,
|
|
402
|
+
"keyErrors": null
|
|
271
403
|
},
|
|
272
404
|
{
|
|
273
405
|
"choice": "nvidia-api-key",
|
|
@@ -277,8 +409,16 @@
|
|
|
277
409
|
"via": "browser",
|
|
278
410
|
"prerequisite": null,
|
|
279
411
|
"offer": false,
|
|
280
|
-
"reason": "API
|
|
281
|
-
"source": "dist/extensions/nvidia/openclaw.plugin.json (2026.8.1)"
|
|
412
|
+
"reason": "API key (billed per use); offered only when the app or person opts in.",
|
|
413
|
+
"source": "dist/extensions/nvidia/openclaw.plugin.json (2026.8.1)",
|
|
414
|
+
"revision": "2026.8.1",
|
|
415
|
+
"checked": "2026-09-30",
|
|
416
|
+
"label": "API key (billed per use)",
|
|
417
|
+
"keyEntry": true,
|
|
418
|
+
"keyErrors": {
|
|
419
|
+
"invalid": "key.invalid",
|
|
420
|
+
"not_included": "key.notIncluded"
|
|
421
|
+
}
|
|
282
422
|
},
|
|
283
423
|
{
|
|
284
424
|
"choice": "ollama",
|
|
@@ -289,7 +429,12 @@
|
|
|
289
429
|
"prerequisite": null,
|
|
290
430
|
"offer": false,
|
|
291
431
|
"reason": "Local runtime on this computer; no account and no billing.",
|
|
292
|
-
"source": "dist/extensions/ollama/openclaw.plugin.json (2026.8.1)"
|
|
432
|
+
"source": "dist/extensions/ollama/openclaw.plugin.json (2026.8.1)",
|
|
433
|
+
"revision": "2026.8.1",
|
|
434
|
+
"checked": "2026-09-30",
|
|
435
|
+
"label": "On this computer",
|
|
436
|
+
"keyEntry": false,
|
|
437
|
+
"keyErrors": null
|
|
293
438
|
},
|
|
294
439
|
{
|
|
295
440
|
"choice": "ollama-cloud",
|
|
@@ -299,8 +444,16 @@
|
|
|
299
444
|
"via": "browser",
|
|
300
445
|
"prerequisite": null,
|
|
301
446
|
"offer": false,
|
|
302
|
-
"reason": "API
|
|
303
|
-
"source": "dist/extensions/ollama/openclaw.plugin.json (2026.8.1)"
|
|
447
|
+
"reason": "API key (billed per use); offered only when the app or person opts in.",
|
|
448
|
+
"source": "dist/extensions/ollama/openclaw.plugin.json (2026.8.1)",
|
|
449
|
+
"revision": "2026.8.1",
|
|
450
|
+
"checked": "2026-09-30",
|
|
451
|
+
"label": "API key (billed per use)",
|
|
452
|
+
"keyEntry": true,
|
|
453
|
+
"keyErrors": {
|
|
454
|
+
"invalid": "key.invalid",
|
|
455
|
+
"not_included": "key.notIncluded"
|
|
456
|
+
}
|
|
304
457
|
},
|
|
305
458
|
{
|
|
306
459
|
"choice": "openai-api-key",
|
|
@@ -310,8 +463,16 @@
|
|
|
310
463
|
"via": "browser",
|
|
311
464
|
"prerequisite": null,
|
|
312
465
|
"offer": false,
|
|
313
|
-
"reason": "API
|
|
314
|
-
"source": "dist/provider-contract-api-*.js (2026.8.1)"
|
|
466
|
+
"reason": "API key (billed per use); offered only when the app or person opts in.",
|
|
467
|
+
"source": "dist/provider-contract-api-*.js (2026.8.1)",
|
|
468
|
+
"revision": "2026.8.1",
|
|
469
|
+
"checked": "2026-09-30",
|
|
470
|
+
"label": "API key (billed per use)",
|
|
471
|
+
"keyEntry": true,
|
|
472
|
+
"keyErrors": {
|
|
473
|
+
"invalid": "key.invalid",
|
|
474
|
+
"not_included": "key.notIncluded"
|
|
475
|
+
}
|
|
315
476
|
},
|
|
316
477
|
{
|
|
317
478
|
"choice": "opencode-go",
|
|
@@ -321,8 +482,16 @@
|
|
|
321
482
|
"via": "browser",
|
|
322
483
|
"prerequisite": null,
|
|
323
484
|
"offer": false,
|
|
324
|
-
"reason": "API
|
|
325
|
-
"source": "dist/extensions/opencode-go/openclaw.plugin.json (2026.8.1)"
|
|
485
|
+
"reason": "API key (billed per use); offered only when the app or person opts in.",
|
|
486
|
+
"source": "dist/extensions/opencode-go/openclaw.plugin.json (2026.8.1)",
|
|
487
|
+
"revision": "2026.8.1",
|
|
488
|
+
"checked": "2026-09-30",
|
|
489
|
+
"label": "API key (billed per use)",
|
|
490
|
+
"keyEntry": true,
|
|
491
|
+
"keyErrors": {
|
|
492
|
+
"invalid": "key.invalid",
|
|
493
|
+
"not_included": "key.notIncluded"
|
|
494
|
+
}
|
|
326
495
|
},
|
|
327
496
|
{
|
|
328
497
|
"choice": "openrouter-api-key",
|
|
@@ -332,8 +501,16 @@
|
|
|
332
501
|
"via": "browser",
|
|
333
502
|
"prerequisite": null,
|
|
334
503
|
"offer": false,
|
|
335
|
-
"reason": "API
|
|
336
|
-
"source": "dist/extensions/openrouter/openclaw.plugin.json (2026.8.1)"
|
|
504
|
+
"reason": "API key (billed per use); offered only when the app or person opts in.",
|
|
505
|
+
"source": "dist/extensions/openrouter/openclaw.plugin.json (2026.8.1)",
|
|
506
|
+
"revision": "2026.8.1",
|
|
507
|
+
"checked": "2026-09-30",
|
|
508
|
+
"label": "API key (billed per use)",
|
|
509
|
+
"keyEntry": true,
|
|
510
|
+
"keyErrors": {
|
|
511
|
+
"invalid": "key.invalid",
|
|
512
|
+
"not_included": "key.notIncluded"
|
|
513
|
+
}
|
|
337
514
|
},
|
|
338
515
|
{
|
|
339
516
|
"choice": "runway-api-key",
|
|
@@ -343,8 +520,13 @@
|
|
|
343
520
|
"via": "browser",
|
|
344
521
|
"prerequisite": null,
|
|
345
522
|
"offer": false,
|
|
346
|
-
"reason": "API
|
|
347
|
-
"source": "dist/extensions/runway/openclaw.plugin.json (2026.8.1)"
|
|
523
|
+
"reason": "API key (billed per use); offered only when the app or person opts in.",
|
|
524
|
+
"source": "dist/extensions/runway/openclaw.plugin.json (2026.8.1)",
|
|
525
|
+
"revision": "2026.8.1",
|
|
526
|
+
"checked": "2026-09-30",
|
|
527
|
+
"label": "API key (billed per use)",
|
|
528
|
+
"keyEntry": false,
|
|
529
|
+
"keyErrors": null
|
|
348
530
|
},
|
|
349
531
|
{
|
|
350
532
|
"choice": "setup-token",
|
|
@@ -353,12 +535,17 @@
|
|
|
353
535
|
"billing": "subscription",
|
|
354
536
|
"via": "browser",
|
|
355
537
|
"prerequisite": null,
|
|
356
|
-
"offer":
|
|
357
|
-
"reason": "
|
|
538
|
+
"offer": true,
|
|
539
|
+
"reason": "Claude subscription sign-in by pasting a token.",
|
|
358
540
|
"source": "dist/extensions/anthropic/openclaw.plugin.json (2026.8.1)",
|
|
359
541
|
"auth": "token",
|
|
360
542
|
"terms": "grey",
|
|
361
|
-
"termsUrl": "https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use"
|
|
543
|
+
"termsUrl": "https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use",
|
|
544
|
+
"revision": "2026.8.1",
|
|
545
|
+
"checked": "2026-09-30",
|
|
546
|
+
"label": "Subscription",
|
|
547
|
+
"keyEntry": false,
|
|
548
|
+
"keyErrors": null
|
|
362
549
|
},
|
|
363
550
|
{
|
|
364
551
|
"choice": "sglang",
|
|
@@ -369,7 +556,12 @@
|
|
|
369
556
|
"prerequisite": null,
|
|
370
557
|
"offer": false,
|
|
371
558
|
"reason": "Local runtime on this computer; no account and no billing.",
|
|
372
|
-
"source": "dist/extensions/sglang/openclaw.plugin.json (2026.8.1)"
|
|
559
|
+
"source": "dist/extensions/sglang/openclaw.plugin.json (2026.8.1)",
|
|
560
|
+
"revision": "2026.8.1",
|
|
561
|
+
"checked": "2026-09-30",
|
|
562
|
+
"label": "On this computer",
|
|
563
|
+
"keyEntry": false,
|
|
564
|
+
"keyErrors": null
|
|
373
565
|
},
|
|
374
566
|
{
|
|
375
567
|
"choice": "together-api-key",
|
|
@@ -379,8 +571,16 @@
|
|
|
379
571
|
"via": "browser",
|
|
380
572
|
"prerequisite": null,
|
|
381
573
|
"offer": false,
|
|
382
|
-
"reason": "API
|
|
383
|
-
"source": "dist/extensions/together/openclaw.plugin.json (2026.8.1)"
|
|
574
|
+
"reason": "API key (billed per use); offered only when the app or person opts in.",
|
|
575
|
+
"source": "dist/extensions/together/openclaw.plugin.json (2026.8.1)",
|
|
576
|
+
"revision": "2026.8.1",
|
|
577
|
+
"checked": "2026-09-30",
|
|
578
|
+
"label": "API key (billed per use)",
|
|
579
|
+
"keyEntry": true,
|
|
580
|
+
"keyErrors": {
|
|
581
|
+
"invalid": "key.invalid",
|
|
582
|
+
"not_included": "key.notIncluded"
|
|
583
|
+
}
|
|
384
584
|
},
|
|
385
585
|
{
|
|
386
586
|
"choice": "vllm",
|
|
@@ -391,7 +591,12 @@
|
|
|
391
591
|
"prerequisite": null,
|
|
392
592
|
"offer": false,
|
|
393
593
|
"reason": "Local runtime on this computer; no account and no billing.",
|
|
394
|
-
"source": "dist/extensions/vllm/openclaw.plugin.json (2026.8.1)"
|
|
594
|
+
"source": "dist/extensions/vllm/openclaw.plugin.json (2026.8.1)",
|
|
595
|
+
"revision": "2026.8.1",
|
|
596
|
+
"checked": "2026-09-30",
|
|
597
|
+
"label": "On this computer",
|
|
598
|
+
"keyEntry": false,
|
|
599
|
+
"keyErrors": null
|
|
395
600
|
},
|
|
396
601
|
{
|
|
397
602
|
"choice": "xai-api-key",
|
|
@@ -401,7 +606,15 @@
|
|
|
401
606
|
"via": "browser",
|
|
402
607
|
"prerequisite": null,
|
|
403
608
|
"offer": false,
|
|
404
|
-
"reason": "API
|
|
405
|
-
"source": "dist/extensions/xai/openclaw.plugin.json (2026.8.1)"
|
|
609
|
+
"reason": "API key (billed per use); offered only when the app or person opts in.",
|
|
610
|
+
"source": "dist/extensions/xai/openclaw.plugin.json (2026.8.1)",
|
|
611
|
+
"revision": "2026.8.1",
|
|
612
|
+
"checked": "2026-09-30",
|
|
613
|
+
"label": "API key (billed per use)",
|
|
614
|
+
"keyEntry": true,
|
|
615
|
+
"keyErrors": {
|
|
616
|
+
"invalid": "key.invalid",
|
|
617
|
+
"not_included": "key.notIncluded"
|
|
618
|
+
}
|
|
406
619
|
}
|
|
407
620
|
]
|
package/dist/runs.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export declare function createRuns(ctx: {
|
|
|
14
14
|
bridge: Pick<Bridge, 'register'>;
|
|
15
15
|
tools: ReadonlySet<string>;
|
|
16
16
|
}): {
|
|
17
|
-
run(spec: RunSpec, on?: (e: RunEvent) => void): Promise<RunEnd>;
|
|
17
|
+
run(spec: RunSpec, on?: (e: RunEvent) => void, keyAgent?: string): Promise<RunEnd>;
|
|
18
18
|
steer(k: string, t: string): Promise<void>;
|
|
19
19
|
abort(k: string): Promise<void>;
|
|
20
20
|
};
|
package/dist/runs.js
CHANGED
|
@@ -78,7 +78,7 @@ export function createRuns(ctx) {
|
|
|
78
78
|
return undefined;
|
|
79
79
|
}
|
|
80
80
|
};
|
|
81
|
-
const run = async (spec, on) => {
|
|
81
|
+
const run = async (spec, on, keyAgent) => {
|
|
82
82
|
// Member boundary first: a member never speaks in another member's session, refused before any request.
|
|
83
83
|
if (!spec.sessionKey.startsWith(`agent:${spec.member}:`))
|
|
84
84
|
throw new Error(`refused: "${spec.sessionKey}" is not a session of member "${spec.member}"`);
|
|
@@ -86,8 +86,9 @@ export function createRuns(ctx) {
|
|
|
86
86
|
for (const tool of spec.tools ?? [])
|
|
87
87
|
if (!ctx.tools.has(tool))
|
|
88
88
|
throw new Error(`refused: "${tool}" is not one of this kit's tools`);
|
|
89
|
-
const
|
|
90
|
-
const
|
|
89
|
+
const agentId = keyAgent ?? (await ctx.ensure(spec.member)).agentId;
|
|
90
|
+
const sessionKey = keyAgent ? `agent:${keyAgent}:${spec.sessionKey.slice(`agent:${spec.member}:`.length)}` : spec.sessionKey;
|
|
91
|
+
const release = spec.register !== false ? ctx.bridge.register({ sessionKey, member: spec.member }, spec.tools) : undefined;
|
|
91
92
|
let last = '';
|
|
92
93
|
let runId = ''; // gateway events for other runs carry a real runId and never match the empty one
|
|
93
94
|
let ended = false;
|
|
@@ -148,7 +149,7 @@ export function createRuns(ctx) {
|
|
|
148
149
|
};
|
|
149
150
|
const final = ctx.request('agent', {
|
|
150
151
|
agentId,
|
|
151
|
-
sessionKey
|
|
152
|
+
sessionKey,
|
|
152
153
|
message: spec.message,
|
|
153
154
|
idempotencyKey: randomUUID(),
|
|
154
155
|
...(picked ?? {}),
|
|
@@ -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
|
@@ -34,6 +34,8 @@ export interface RunSpec extends RunRef {
|
|
|
34
34
|
}[];
|
|
35
35
|
thinking?: 'off' | 'low' | 'medium' | 'high';
|
|
36
36
|
model?: string;
|
|
37
|
+
/** Explicit paid key choice. Uses a separate member-owned agent and separate session history. */
|
|
38
|
+
auth?: 'apiKey';
|
|
37
39
|
tools?: string[];
|
|
38
40
|
register?: boolean;
|
|
39
41
|
}
|
|
@@ -139,6 +141,14 @@ export type Route = {
|
|
|
139
141
|
offer: boolean;
|
|
140
142
|
reason: string;
|
|
141
143
|
source: string;
|
|
144
|
+
revision: string;
|
|
145
|
+
checked: string;
|
|
146
|
+
label: string;
|
|
147
|
+
keyEntry: boolean;
|
|
148
|
+
keyErrors: {
|
|
149
|
+
invalid: 'key.invalid';
|
|
150
|
+
not_included: 'key.notIncluded';
|
|
151
|
+
} | null;
|
|
142
152
|
};
|
|
143
153
|
export interface GatewayTransport {
|
|
144
154
|
start(): Promise<Hello>;
|
package/dist/words.json
CHANGED
|
@@ -19,5 +19,12 @@
|
|
|
19
19
|
"approval.ask": "{helper} wants to {summary}. Allow it?",
|
|
20
20
|
"approval.expired": "Nobody answered in time, so this wasn't allowed.",
|
|
21
21
|
"approval.notice": "Something is waiting for your yes.",
|
|
22
|
-
"link.notAllowed": "This device can't do that. Ask the person at the computer."
|
|
22
|
+
"link.notAllowed": "This device can't do that. Ask the person at the computer.",
|
|
23
|
+
"key.label": "API key (billed per use)",
|
|
24
|
+
"key.entry": "Paste your API key. You pay the provider for each use.",
|
|
25
|
+
"key.checking": "Checking your key…",
|
|
26
|
+
"key.ok": "Your key is ready. Use it only when you choose this option.",
|
|
27
|
+
"key.invalid": "This key could not be saved. Check it and try again.",
|
|
28
|
+
"key.notIncluded": "This option is not available here.",
|
|
29
|
+
"key.missing": "Add an API key to use this option."
|
|
23
30
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@byokit/openclaw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"@byokit/link": "0.7.0",
|
|
56
56
|
"@byokit/relay": "0.5.0",
|
|
57
|
-
"@byokit/reach": "0.
|
|
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
|
+
}
|