@byokit/openclaw 0.11.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +38 -6
- package/SECURITY.md +3 -2
- package/dist/accounts.d.ts +15 -2
- package/dist/accounts.js +480 -162
- package/dist/activation.d.ts +17 -0
- package/dist/activation.js +9 -0
- package/dist/approvals.d.ts +2 -0
- package/dist/approvals.js +9 -9
- package/dist/attribution.d.ts +3 -0
- package/dist/attribution.js +25 -0
- package/dist/bridge.d.ts +3 -1
- package/dist/bridge.js +4 -6
- package/dist/config.js +10 -0
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +2 -0
- package/dist/device.d.ts +4 -4
- package/dist/device.js +11 -1
- package/dist/engine.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/keys.d.ts +19 -5
- package/dist/keys.js +135 -23
- package/dist/kit.d.ts +21 -2
- package/dist/kit.js +132 -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 +12 -0
- package/dist/testing/fake-gateway.js +124 -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
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { GatewayParams, GatewayTransport } from './types.ts';
|
|
2
|
+
export type ActivationCtx = {
|
|
3
|
+
request: GatewayTransport['request'];
|
|
4
|
+
authConfig(): string;
|
|
5
|
+
restarted(authBefore: string | undefined, signal?: AbortSignal): Promise<boolean>;
|
|
6
|
+
};
|
|
7
|
+
export declare const LATE_RESTART = "The engine took too long to restart after this sign-in. Nothing was kept; try again.";
|
|
8
|
+
export type Activation = {
|
|
9
|
+
ok: boolean;
|
|
10
|
+
late?: boolean;
|
|
11
|
+
status?: string;
|
|
12
|
+
error?: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function activate(ctx: ActivationCtx, params: GatewayParams<'openclaw.setup.activate'>, o: {
|
|
15
|
+
timeoutMs: number;
|
|
16
|
+
signal?: AbortSignal;
|
|
17
|
+
}): Promise<Activation>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const LATE_RESTART = 'The engine took too long to restart after this sign-in. Nothing was kept; try again.';
|
|
2
|
+
export async function activate(ctx, params, o) {
|
|
3
|
+
const authBefore = ctx.authConfig();
|
|
4
|
+
// The request is never aborted by a cancel: its answer decides whether the engine kept the key, so cleanup waits for it.
|
|
5
|
+
const result = await ctx.request('openclaw.setup.activate', params, { timeoutMs: o.timeoutMs });
|
|
6
|
+
if (!result.ok)
|
|
7
|
+
return { ok: false, status: result.status, error: result.error };
|
|
8
|
+
return await ctx.restarted(authBefore, o.signal) ? { ok: true } : { ok: false, late: true };
|
|
9
|
+
}
|
package/dist/approvals.d.ts
CHANGED
|
@@ -3,11 +3,13 @@ import type { Approval, Decision, GatewayTransport, Member } from './types.ts';
|
|
|
3
3
|
export declare class Approvals {
|
|
4
4
|
private readonly request;
|
|
5
5
|
private readonly bridge;
|
|
6
|
+
private readonly memberOf;
|
|
6
7
|
private readonly approvals;
|
|
7
8
|
private readonly listeners;
|
|
8
9
|
constructor(o: {
|
|
9
10
|
request: GatewayTransport['request'];
|
|
10
11
|
bridge: Pick<Bridge, 'resolveAsk'>;
|
|
12
|
+
memberOf: (ref: string) => Member | undefined;
|
|
11
13
|
});
|
|
12
14
|
handleEvent(e: {
|
|
13
15
|
event: string;
|
package/dist/approvals.js
CHANGED
|
@@ -14,21 +14,19 @@ const REQUESTED = {
|
|
|
14
14
|
const RESOLVED = new Set(['exec.approval.resolved', 'plugin.approval.resolved', 'question.resolved']);
|
|
15
15
|
const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
16
16
|
const asString = (v) => (typeof v === 'string' && v.length > 0 ? v : undefined);
|
|
17
|
-
const memberOfKey = (key) => {
|
|
18
|
-
const m = /^agent:([a-z][a-z0-9-]{0,31}):/.exec(key);
|
|
19
|
-
return m?.[1];
|
|
20
|
-
};
|
|
21
17
|
/**
|
|
22
|
-
* Member attribution: the request detail's agent first, else the `agent:<member>:` session key (5.9).
|
|
18
|
+
* Member attribution: the request detail's agent first, else the `agent:<member>:` session key (5.9). An
|
|
19
|
+
* indexed `<member>--<hex6>` account agent attributes to `<member>` (5.15). The real
|
|
23
20
|
* engine nests exec/plugin details under `request` (question events stay flat); unattributed approvals carry ''
|
|
24
21
|
* so they can never match a member id (B2).
|
|
25
22
|
*/
|
|
26
|
-
function attribute(payload) {
|
|
23
|
+
function attribute(payload, memberOf) {
|
|
27
24
|
const detail = isRecord(payload.request) ? payload.request : payload;
|
|
28
25
|
const agent = asString(detail.agentId);
|
|
29
26
|
const key = asString(detail.sessionKey);
|
|
30
|
-
const
|
|
31
|
-
const
|
|
27
|
+
const fromAgent = agent ? memberOf(agent) : undefined;
|
|
28
|
+
const fromKey = key ? memberOf(key) : undefined;
|
|
29
|
+
const member = fromAgent ?? fromKey ?? asString(detail.member) ?? '';
|
|
32
30
|
return { member, ...(key ? { sessionKey: key } : {}) };
|
|
33
31
|
}
|
|
34
32
|
/** Exec policy modes (`off|on-miss|always`) ride in `request.ask`; they are not something a person said (B8). */
|
|
@@ -58,11 +56,13 @@ function expiresOf(payload, fallbackMs) {
|
|
|
58
56
|
export class Approvals {
|
|
59
57
|
request;
|
|
60
58
|
bridge;
|
|
59
|
+
memberOf;
|
|
61
60
|
approvals = new Map();
|
|
62
61
|
listeners = new Set();
|
|
63
62
|
constructor(o) {
|
|
64
63
|
this.request = o.request;
|
|
65
64
|
this.bridge = o.bridge;
|
|
65
|
+
this.memberOf = o.memberOf;
|
|
66
66
|
}
|
|
67
67
|
handleEvent(e) {
|
|
68
68
|
const payload = isRecord(e.payload) ? e.payload : {};
|
|
@@ -71,7 +71,7 @@ export class Approvals {
|
|
|
71
71
|
const id = asString(payload.id) ?? randomBytes(12).toString('base64url');
|
|
72
72
|
if (this.approvals.has(id))
|
|
73
73
|
return;
|
|
74
|
-
const { member, sessionKey } = attribute(payload);
|
|
74
|
+
const { member, sessionKey } = attribute(payload, this.memberOf);
|
|
75
75
|
const detail = isRecord(payload.request) ? payload.request : payload;
|
|
76
76
|
const now = Date.now();
|
|
77
77
|
const approval = {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const MEMBER = /^[a-z][a-z0-9-]{0,31}$/;
|
|
2
|
+
const HEX6 = /^[0-9a-f]{6}$/;
|
|
3
|
+
const KEY_PREFIX = 'byokit-key-';
|
|
4
|
+
export function agentOf(ref) {
|
|
5
|
+
if (!ref.startsWith('agent:'))
|
|
6
|
+
return ref;
|
|
7
|
+
return /^agent:([^:]+):/.exec(ref)?.[1];
|
|
8
|
+
}
|
|
9
|
+
export function memberOf(ref) {
|
|
10
|
+
const agent = agentOf(ref);
|
|
11
|
+
if (agent === undefined)
|
|
12
|
+
return undefined;
|
|
13
|
+
if (agent.startsWith(KEY_PREFIX)) {
|
|
14
|
+
const inner = agent.slice(KEY_PREFIX.length);
|
|
15
|
+
return MEMBER.test(inner) ? inner : undefined;
|
|
16
|
+
}
|
|
17
|
+
const dash = agent.lastIndexOf('--');
|
|
18
|
+
if (dash >= 0) {
|
|
19
|
+
const head = agent.slice(0, dash);
|
|
20
|
+
const tail = agent.slice(dash + 2);
|
|
21
|
+
if (MEMBER.test(head) && HEX6.test(tail))
|
|
22
|
+
return head;
|
|
23
|
+
}
|
|
24
|
+
return MEMBER.test(agent) ? agent : undefined;
|
|
25
|
+
}
|
package/dist/bridge.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Approval, Decision, RunRef, ToolHost, ToolSpec } from './types.ts';
|
|
1
|
+
import type { Approval, Decision, Member, RunRef, ToolHost, ToolSpec } from './types.ts';
|
|
2
2
|
/**
|
|
3
3
|
* Asks must expire before the plugin's 195 s gate timeout, or the run gets a generic block while the approval
|
|
4
4
|
* lingers (N3). The kit passes its `approvalTimeoutMs` through; anything above is clamped to this.
|
|
@@ -32,6 +32,7 @@ export declare class Bridge {
|
|
|
32
32
|
private readonly tools;
|
|
33
33
|
private readonly permitted;
|
|
34
34
|
private readonly approvalTimeoutMs;
|
|
35
|
+
private readonly memberOf;
|
|
35
36
|
private readonly onAsk;
|
|
36
37
|
private readonly onAskGone;
|
|
37
38
|
private readonly beforeAgentRun?;
|
|
@@ -52,6 +53,7 @@ export declare class Bridge {
|
|
|
52
53
|
tools: ReadonlySet<string>;
|
|
53
54
|
permitted: (tool: string) => boolean;
|
|
54
55
|
approvalTimeoutMs: number;
|
|
56
|
+
memberOf(ref: string): Member | undefined;
|
|
55
57
|
onAsk(a: Approval): void;
|
|
56
58
|
onAskGone(id: string): void;
|
|
57
59
|
beforeAgentRun?: (key: string, runId?: string) => Promise<boolean>;
|
package/dist/bridge.js
CHANGED
|
@@ -12,10 +12,6 @@ const FRAME_CAP = 1_000_000;
|
|
|
12
12
|
*/
|
|
13
13
|
export const MAX_APPROVAL_TIMEOUT_MS = 190_000;
|
|
14
14
|
const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
15
|
-
const memberOfKey = (key) => {
|
|
16
|
-
const m = /^agent:([a-z][a-z0-9-]{0,31}):/.exec(key);
|
|
17
|
-
return m?.[1];
|
|
18
|
-
};
|
|
19
15
|
export const DEFAULT_SOCKET_NAME = 'bridge.sock';
|
|
20
16
|
export const DEFAULT_PARAM_PREFIX = '__byokit';
|
|
21
17
|
/** Resolve the bridge naming options, validating explicit values; defaults keep prior behavior. */
|
|
@@ -66,6 +62,7 @@ export class Bridge {
|
|
|
66
62
|
tools;
|
|
67
63
|
permitted;
|
|
68
64
|
approvalTimeoutMs;
|
|
65
|
+
memberOf;
|
|
69
66
|
onAsk;
|
|
70
67
|
onAskGone;
|
|
71
68
|
beforeAgentRun;
|
|
@@ -88,6 +85,7 @@ export class Bridge {
|
|
|
88
85
|
this.tools = o.tools;
|
|
89
86
|
this.permitted = o.permitted;
|
|
90
87
|
this.approvalTimeoutMs = Math.min(o.approvalTimeoutMs, MAX_APPROVAL_TIMEOUT_MS);
|
|
88
|
+
this.memberOf = o.memberOf;
|
|
91
89
|
this.onAsk = o.onAsk;
|
|
92
90
|
this.onAskGone = o.onAskGone;
|
|
93
91
|
this.beforeAgentRun = o.beforeAgentRun;
|
|
@@ -293,7 +291,7 @@ export class Bridge {
|
|
|
293
291
|
(!armed.input || armed.input(input))) {
|
|
294
292
|
this.armed = undefined;
|
|
295
293
|
// '' when unattributable: never a member id (B2).
|
|
296
|
-
const member =
|
|
294
|
+
const member = this.memberOf(key) ?? '';
|
|
297
295
|
const runRef = { sessionKey: key, member };
|
|
298
296
|
return void this.gateRun(socket, runRef, tool, input);
|
|
299
297
|
}
|
|
@@ -389,7 +387,7 @@ export class Bridge {
|
|
|
389
387
|
}
|
|
390
388
|
if (!this.host)
|
|
391
389
|
return this.reply(socket, { ok: false, reason: "can't run this action right now" });
|
|
392
|
-
const run = this.runs.get(key) ?? { sessionKey: key, member:
|
|
390
|
+
const run = this.runs.get(key) ?? { sessionKey: key, member: this.memberOf(key) ?? '' };
|
|
393
391
|
// The plugin destroys its socket when the run aborts (run abort, hook timeout); without this the host's
|
|
394
392
|
// sandboxed command keeps running after the tool is gone.
|
|
395
393
|
const controller = new AbortController();
|
package/dist/config.js
CHANGED
|
@@ -52,6 +52,16 @@ export function reconcileConfig(saved, o) {
|
|
|
52
52
|
c.agents.defaults.authInheritance = { agentId: BASE_AGENT };
|
|
53
53
|
c.agents.defaults.models ??= {};
|
|
54
54
|
c.agents.defaults.models['openai/*'] = { ...c.agents.defaults.models['openai/*'], agentRuntime: { id: 'openclaw' } };
|
|
55
|
+
// With no policy record the pin reads the `models` keys above as a legacy allowlist and materializes
|
|
56
|
+
// `modelPolicy.allow: ['openai/*']` on boot, so a member's OpenCode Go (any non-OpenAI) model fell back to
|
|
57
|
+
// openai. An empty record restricts nothing; the exact list that migration wrote is dropped unless the app sets one.
|
|
58
|
+
if (!o.app?.agents?.defaults?.modelPolicy) {
|
|
59
|
+
const policy = c.agents.defaults.modelPolicy;
|
|
60
|
+
if (!object(policy))
|
|
61
|
+
c.agents.defaults.modelPolicy = {};
|
|
62
|
+
else if (Array.isArray(policy.allow) && policy.allow.length === 1 && policy.allow[0] === 'openai/*')
|
|
63
|
+
delete policy.allow;
|
|
64
|
+
}
|
|
55
65
|
const allow = c.agents.defaults.modelPolicy?.allow;
|
|
56
66
|
if (Array.isArray(allow) && allow.length && !allow.includes('openai/*'))
|
|
57
67
|
allow.push('openai/*');
|
package/dist/constants.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export declare const ENGINE_VERSION = "2026.8.35";
|
|
2
2
|
export declare const PROTOCOL_VERSION = 4;
|
|
3
|
+
export declare const LIVE_CATALOGS: string[];
|
|
3
4
|
export declare const OPERATOR_SCOPES: readonly ["operator.read", "operator.write", "operator.admin", "operator.approvals", "operator.questions", "operator.pairing", "operator.talk"];
|
package/dist/constants.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// `needs-update`, never silently run.
|
|
3
3
|
export const ENGINE_VERSION = '2026.8.35';
|
|
4
4
|
export const PROTOCOL_VERSION = 4;
|
|
5
|
+
// Providers whose live catalog the kit registers (5.6); key choice uses the same catalog hook.
|
|
6
|
+
export const LIVE_CATALOGS = ['opencode-go'];
|
|
5
7
|
export const OPERATOR_SCOPES = [
|
|
6
8
|
'operator.read',
|
|
7
9
|
'operator.write',
|
package/dist/device.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { BrowserDevice, SignInRefusedWhy } from './browser.ts';
|
|
|
2
2
|
import type { DeviceLink } from '@byokit/pair';
|
|
3
3
|
import type { RouteView } from './routes.ts';
|
|
4
4
|
import { type AccountView } from './words.ts';
|
|
5
|
-
import type { Approval, Decision, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, KitState, RunEnd, OutputSchema, SchemaOutput, RunEvent, SignInView } from './types.ts';
|
|
5
|
+
import type { Approval, Decision, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, KitState, RunEnd, OutputSchema, SchemaOutput, RunEvent, SignInView, AccountRef } from './types.ts';
|
|
6
6
|
export type { GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult } from './types.ts';
|
|
7
7
|
export type { Approval, Decision, KitState, PlanWindow, Route, RunEnd, RunEvent, RunUsage, SignInView, OutputSchema, SchemaOutput } from './types.ts';
|
|
8
8
|
/** One `oc.events` frame: a member's Gateway event, typed by name, or an approval add/resolve (7.2). */
|
|
@@ -36,6 +36,8 @@ export type DeviceEndFrame<T = unknown> = {
|
|
|
36
36
|
export type DeviceRunOptions<S extends OutputSchema | undefined = OutputSchema | undefined> = {
|
|
37
37
|
schema?: S;
|
|
38
38
|
sessionKey?: string;
|
|
39
|
+
account?: AccountRef | 'default' | 'auto';
|
|
40
|
+
needs?: string[];
|
|
39
41
|
model?: string;
|
|
40
42
|
auth?: 'apiKey';
|
|
41
43
|
system?: string;
|
|
@@ -79,9 +81,7 @@ export type SignInNotice = {
|
|
|
79
81
|
export declare function openSignInNotice(data: Record<string, unknown>, seed: Uint8Array): SignInNotice | null;
|
|
80
82
|
/** Transient streams bypass link's durable request/answer cache: no lease nonce or frame is persisted. */
|
|
81
83
|
export declare function browserDevice(link: DeviceLink): BrowserDevice;
|
|
82
|
-
type DeviceRunEvent =
|
|
83
|
-
type: 'account';
|
|
84
|
-
}>;
|
|
84
|
+
type DeviceRunEvent = RunEvent;
|
|
85
85
|
/** `openclawDevice`'s device surface: every `oc.*` op the computer answers, typed. */
|
|
86
86
|
export type OpenClawDevice = {
|
|
87
87
|
browser: BrowserDevice;
|
package/dist/device.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { openBoxFromSeed, boxKeyPairFromSeed } from '@byokit/seal';
|
|
2
|
+
import { memberOf } from "./attribution.js";
|
|
2
3
|
import { toAccountView } from "./words.js";
|
|
3
4
|
import { b64urlDecode, b64urlEncode, openNotice } from "./notices.js";
|
|
4
5
|
/**
|
|
@@ -19,6 +20,13 @@ export class BrowserRefused extends LinkRefused {
|
|
|
19
20
|
constructor(why) { super(`Sign-in refused: ${why}`); this.why = why; }
|
|
20
21
|
}
|
|
21
22
|
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
23
|
+
// A session key this device sends must be shaped like one: `agent:<member>:` or an
|
|
24
|
+
// `<member>--<hex6>` account agent's key (5.15). Anything else is refused before it reaches the computer.
|
|
25
|
+
const checkKey = (sessionKey) => {
|
|
26
|
+
if (typeof sessionKey !== 'string' || !sessionKey.startsWith('agent:') || memberOf(sessionKey) === undefined)
|
|
27
|
+
throw new LinkRefused('Invalid session key');
|
|
28
|
+
return sessionKey;
|
|
29
|
+
};
|
|
22
30
|
/**
|
|
23
31
|
* Newline-delimited JSON frames off a link stream, in order. A host end with an error becomes a
|
|
24
32
|
* LinkRefused with the host's words (a refusal arrives after the stream opens, so it cannot ride
|
|
@@ -354,7 +362,9 @@ export function openclawDevice(link) {
|
|
|
354
362
|
},
|
|
355
363
|
sessions: () => link.request('oc.sessions'),
|
|
356
364
|
run: (message, o) => {
|
|
357
|
-
const inner = liveStream(() => link.stream('oc.run', { message, ...(o?.sessionKey ? { sessionKey: o.sessionKey } : {}),
|
|
365
|
+
const inner = liveStream(() => link.stream('oc.run', { message, ...(o?.sessionKey ? { sessionKey: checkKey(o.sessionKey) } : {}),
|
|
366
|
+
...(o?.account !== undefined ? { account: o.account } : {}),
|
|
367
|
+
...(o?.needs !== undefined ? { needs: o.needs } : {}),
|
|
358
368
|
...(o?.model !== undefined ? { model: o.model } : {}), ...(o?.system !== undefined ? { system: o.system } : {}),
|
|
359
369
|
...(o?.auth !== undefined ? { auth: o.auth } : {}),
|
|
360
370
|
...(o?.images !== undefined ? { images: o.images } : {}), ...(o?.thinking !== undefined ? { thinking: o.thinking } : {}),
|
package/dist/engine.js
CHANGED
|
@@ -256,7 +256,7 @@ export class Engine {
|
|
|
256
256
|
const pluginDir = join(this.root, 'plugin');
|
|
257
257
|
mkdirSync(pluginDir, { recursive: true, mode: 0o700 });
|
|
258
258
|
// The shipped plugin follows the kit on every prepare too: a state dir from an older kit must not keep its gate.
|
|
259
|
-
for (const f of ['package.json', 'index.js', 'keys.js', 'usage.js', 'accounts.js']) {
|
|
259
|
+
for (const f of ['package.json', 'index.js', 'keys.js', 'usage.js', 'accounts.js', 'catalog.js']) {
|
|
260
260
|
const source = join(kitDir, 'plugin', f);
|
|
261
261
|
putChanged(join(pluginDir, f), readFileSync(source, 'utf8'));
|
|
262
262
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export type { GatewayMethods } from './generated/methods.ts';
|
|
|
4
4
|
export type { JsonValue } from './output.ts';
|
|
5
5
|
export type { RouteFacts, RouteView } from './routes.ts';
|
|
6
6
|
export { OpenClawKit, type KitOptions, type RetainedLogin } from './kit.ts';
|
|
7
|
-
export type { AddKeyResult } from './keys.ts';
|
|
7
|
+
export type { AddKeyResult, KeyFailureReason } from './keys.ts';
|
|
8
8
|
export { stateWords, toAccountView, words, type AccountView, type WordKey } from './words.ts';
|
|
9
9
|
export { chooseAccount, consider, resolveSelection, type RoomOf } from './pick.ts';
|
|
10
10
|
export { AuthStoreUnreadableError, AuthStoreSealSizeError } from './auth-store.ts';
|
package/dist/keys.d.ts
CHANGED
|
@@ -1,20 +1,34 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
export
|
|
1
|
+
import { type ActivationCtx } from './activation.ts';
|
|
2
|
+
import type { Member, ModelInfo } from './types.ts';
|
|
3
|
+
export type KeyFailureReason = 'auth' | 'billing' | 'empty' | 'model' | 'network' | 'engine' | 'inference' | 'busy';
|
|
4
|
+
export type AddKeyResult = {
|
|
5
|
+
status: 'ok';
|
|
6
|
+
} | {
|
|
7
|
+
status: 'invalid' | 'not_included' | 'retry';
|
|
8
|
+
reason: KeyFailureReason;
|
|
9
|
+
};
|
|
10
|
+
export declare function createKeys(ctx: ActivationCtx & {
|
|
4
11
|
root: string;
|
|
5
|
-
request: GatewayTransport['request'];
|
|
6
12
|
ensure(member: Member): Promise<{
|
|
7
13
|
agentId: string;
|
|
8
14
|
}>;
|
|
9
15
|
restarting?(): boolean;
|
|
16
|
+
prepareOwner(agentId: string): Promise<void>;
|
|
10
17
|
}): {
|
|
18
|
+
models: (member: Member, o: {
|
|
19
|
+
authChoice: string;
|
|
20
|
+
}) => Promise<ModelInfo[]>;
|
|
11
21
|
exclusive: <T>(member: Member, work: () => Promise<T>) => Promise<T>;
|
|
12
22
|
add(member: Member, o: {
|
|
13
23
|
authChoice: string;
|
|
14
24
|
apiKey: string;
|
|
25
|
+
model?: string;
|
|
15
26
|
}): Promise<AddKeyResult>;
|
|
16
|
-
ready(member: Member
|
|
27
|
+
ready(member: Member, o?: {
|
|
28
|
+
once?: boolean;
|
|
29
|
+
}): Promise<{
|
|
17
30
|
agentId: string;
|
|
18
31
|
model: string;
|
|
19
32
|
} | undefined>;
|
|
33
|
+
sweep(): Promise<boolean>;
|
|
20
34
|
};
|
package/dist/keys.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
-
import {
|
|
2
|
+
import { activate } from "./activation.js";
|
|
3
|
+
import { keyAgentId, MEMBER_ID, KEY_PREFIX, untilListed } from "./members.js";
|
|
3
4
|
import { routes } from "./routes.js";
|
|
5
|
+
import { modelsOf } from "./accounts.js";
|
|
6
|
+
import { LIVE_CATALOGS } from "./constants.js";
|
|
4
7
|
export function createKeys(ctx) {
|
|
5
8
|
// Serialize key replacement and key runs, so an in-flight run never sees a half-installed replacement.
|
|
6
9
|
const tails = new Map();
|
|
@@ -17,39 +20,117 @@ export function createKeys(ctx) {
|
|
|
17
20
|
}
|
|
18
21
|
};
|
|
19
22
|
const check = (member) => MEMBER_ID.test(member) && !member.startsWith(KEY_PREFIX);
|
|
20
|
-
|
|
23
|
+
// Config writes may drain/restart the gateway: a restart, or a retryable refusal, means the call may simply run again.
|
|
24
|
+
const transient = (error) => {
|
|
25
|
+
const refusal = error;
|
|
26
|
+
return Boolean(ctx.restarting?.() || (refusal?.code === 'UNAVAILABLE' && refusal.retryable === true));
|
|
27
|
+
};
|
|
28
|
+
const control = (member, action, provider, model) => ctx.request('byokit.keys', { member, action, ...(provider ? { provider } : {}), ...(model ? { model } : {}) });
|
|
29
|
+
const failure = (status, reason) => ({ status, reason });
|
|
30
|
+
// Only a provider refusal is `invalid`; an unavailable route or model is `not_included`; everything else may be retried.
|
|
31
|
+
const probeResult = (status) => {
|
|
32
|
+
switch (status) {
|
|
33
|
+
case 'auth': return failure('invalid', 'auth');
|
|
34
|
+
case 'billing': return failure('invalid', 'billing');
|
|
35
|
+
case 'unavailable':
|
|
36
|
+
case 'no_model': return failure('not_included', 'model');
|
|
37
|
+
case 'timeout':
|
|
38
|
+
case 'network': return failure('retry', 'network');
|
|
39
|
+
case 'rate_limit':
|
|
40
|
+
case 'busy': return failure('retry', 'busy');
|
|
41
|
+
case 'format':
|
|
42
|
+
case 'unknown': return failure('retry', 'inference');
|
|
43
|
+
default: return failure('retry', 'engine');
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const registered = async (provider, model) => {
|
|
47
|
+
const { config } = await ctx.request('config.get');
|
|
48
|
+
return Boolean(config?.models?.providers?.[provider]?.models?.some((m) => `${provider}/${m.id}` === model));
|
|
49
|
+
};
|
|
50
|
+
const errorReason = (error, sent) => {
|
|
51
|
+
if (transient(error))
|
|
52
|
+
return 'busy';
|
|
53
|
+
const e = error;
|
|
54
|
+
if (sent && e?.code === 'INVALID_REQUEST')
|
|
55
|
+
return 'inference';
|
|
56
|
+
return ['ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'EAI_AGAIN', 'ETIMEDOUT', 'CLIENT_TIMEOUT', 'NETWORK_ERROR'].includes(e?.code ?? '')
|
|
57
|
+
|| e?.name === 'TimeoutError' ? 'network' : 'engine';
|
|
58
|
+
};
|
|
59
|
+
const models = async (member, o) => {
|
|
60
|
+
if (!check(member))
|
|
61
|
+
throw new Error('Choose your own member.');
|
|
62
|
+
const route = routes().find((r) => r.choice === o.authChoice && r.keyEntry && r.readiness === 'ready');
|
|
63
|
+
if (!route || !LIVE_CATALOGS.includes(route.provider))
|
|
64
|
+
return [];
|
|
65
|
+
await ctx.ensure(member);
|
|
66
|
+
const catalog = await ctx.request('byokit.catalog', { provider: route.provider });
|
|
67
|
+
if (catalog.ok)
|
|
68
|
+
return modelsOf(route.provider, { models: catalog.models?.map((m) => ({ ...m, provider: route.provider })) });
|
|
69
|
+
if (catalog.reason !== 'no-key')
|
|
70
|
+
throw new Error('Model catalog is unavailable.');
|
|
71
|
+
return [];
|
|
72
|
+
};
|
|
21
73
|
return {
|
|
74
|
+
models,
|
|
22
75
|
exclusive,
|
|
23
76
|
async add(member, o) {
|
|
24
|
-
if (!check(member)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
77
|
+
if (!check(member))
|
|
78
|
+
throw new Error('Choose your own member.');
|
|
79
|
+
if (!o.apiKey.trim())
|
|
80
|
+
return failure('invalid', 'empty');
|
|
81
|
+
const route = routes().find((r) => r.choice === o.authChoice && r.keyEntry && r.readiness === 'ready');
|
|
82
|
+
if (!route || (o.model !== undefined && !LIVE_CATALOGS.includes(route.provider)))
|
|
83
|
+
return failure('not_included', 'model');
|
|
29
84
|
return exclusive(member, async () => {
|
|
85
|
+
let sent = false;
|
|
86
|
+
// A key the engine may have taken but not sealed is cleared (prepare already emptied the agent); if the clear
|
|
87
|
+
// does not reach the engine, the key stays unsealed and unusable until the next key add or kit start, so runs
|
|
88
|
+
// fail closed.
|
|
89
|
+
const abandon = async (result) => {
|
|
90
|
+
await control(member, 'clear').catch(() => undefined);
|
|
91
|
+
return result;
|
|
92
|
+
};
|
|
30
93
|
try {
|
|
31
94
|
await ctx.ensure(member);
|
|
32
95
|
const agentId = keyAgentId(member);
|
|
33
96
|
const list = await ctx.request('agents.list');
|
|
34
97
|
if (!list.agents.some((a) => a.id === agentId))
|
|
35
98
|
await ctx.request('agents.create', { name: agentId, workspace: join(ctx.root, 'key-workspaces', member) });
|
|
99
|
+
// Creation answers before the runtime roster/owners publish, just as for ordinary members.
|
|
100
|
+
await untilListed(ctx.request, agentId);
|
|
36
101
|
if (!(await control(member, 'prepare', route.provider)).ok)
|
|
37
|
-
return '
|
|
102
|
+
return failure('retry', 'engine');
|
|
38
103
|
// The public pin's typed API-key path, never a CLI-login import. Discard the engine's lines/errors.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (
|
|
44
|
-
return
|
|
45
|
-
|
|
104
|
+
sent = true;
|
|
105
|
+
const result = await activate(ctx, { kind: 'api-key', agentId, authChoice: o.authChoice, apiKey: o.apiKey }, { timeoutMs: 180_000 });
|
|
106
|
+
if (!result.ok && !result.late)
|
|
107
|
+
return abandon(probeResult(result.status));
|
|
108
|
+
if (result.late)
|
|
109
|
+
return abandon(failure('retry', 'busy'));
|
|
110
|
+
if (o.model !== undefined && !(await models(member, o)).some((m) => m.id === o.model && m.available))
|
|
111
|
+
return abandon(failure('not_included', 'model'));
|
|
112
|
+
// Prepare before publishing the ready marker: failed preparation remains unusable even if clear fails.
|
|
113
|
+
try {
|
|
114
|
+
await ctx.prepareOwner(agentId);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return abandon(failure('retry', 'engine'));
|
|
118
|
+
}
|
|
119
|
+
if (o.model !== undefined && LIVE_CATALOGS.includes(route.provider) && !(await registered(route.provider, o.model)))
|
|
120
|
+
return abandon(failure('retry', 'model'));
|
|
121
|
+
if ((await control(member, 'seal', undefined, o.model)).ok)
|
|
122
|
+
return { status: 'ok' };
|
|
123
|
+
return abandon(failure('retry', 'engine'));
|
|
46
124
|
}
|
|
47
|
-
catch {
|
|
48
|
-
|
|
125
|
+
catch (error) {
|
|
126
|
+
const reason = errorReason(error, sent);
|
|
127
|
+
if (sent)
|
|
128
|
+
return abandon(failure('retry', reason));
|
|
129
|
+
return failure('retry', reason);
|
|
49
130
|
}
|
|
50
131
|
});
|
|
51
132
|
},
|
|
52
|
-
async ready(member) {
|
|
133
|
+
async ready(member, o = {}) {
|
|
53
134
|
if (!check(member))
|
|
54
135
|
return undefined;
|
|
55
136
|
const deadline = Date.now() + 60_000;
|
|
@@ -59,15 +140,46 @@ export function createKeys(ctx) {
|
|
|
59
140
|
return ready.ok && ready.model ? { agentId: keyAgentId(member), model: ready.model } : undefined;
|
|
60
141
|
}
|
|
61
142
|
catch (error) {
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
const refusal = error;
|
|
65
|
-
const transient = ctx.restarting?.() || (refusal?.code === 'UNAVAILABLE' && refusal.retryable === true);
|
|
66
|
-
if (!transient || Date.now() >= deadline)
|
|
143
|
+
// This read is safe to retry; an absent profile or a terminal refusal still fails closed immediately.
|
|
144
|
+
if (o.once || !transient(error) || Date.now() >= deadline)
|
|
67
145
|
return undefined;
|
|
68
146
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
69
147
|
}
|
|
70
148
|
}
|
|
71
149
|
},
|
|
150
|
+
// Kit start calls this once the gateway is ready: a failed add whose abandon clear never reached the engine
|
|
151
|
+
// leaves an unsealed key with no ready marker, so drop it. A single ready read per key agent, no retries; an add
|
|
152
|
+
// in flight for that member is waited for. Only an absent marker is cleared; a refused read or clear (the gateway
|
|
153
|
+
// left ready) makes the sweep incomplete. Resolves true once every key agent was read.
|
|
154
|
+
async sweep() {
|
|
155
|
+
let agents;
|
|
156
|
+
try {
|
|
157
|
+
agents = (await ctx.request('agents.list')).agents;
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
let complete = true;
|
|
163
|
+
for (const agent of agents) {
|
|
164
|
+
if (!agent.id.startsWith(KEY_PREFIX))
|
|
165
|
+
continue;
|
|
166
|
+
const member = agent.id.slice(KEY_PREFIX.length);
|
|
167
|
+
if (!check(member))
|
|
168
|
+
continue;
|
|
169
|
+
await exclusive(member, async () => {
|
|
170
|
+
const ready = await control(member, 'ready').catch(() => undefined);
|
|
171
|
+
if (ready?.ok === true)
|
|
172
|
+
return;
|
|
173
|
+
if (ready?.absent !== true) {
|
|
174
|
+
complete = false;
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const cleared = await control(member, 'clear').catch(() => undefined);
|
|
178
|
+
if (cleared?.ok !== true)
|
|
179
|
+
complete = false;
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return complete;
|
|
183
|
+
},
|
|
72
184
|
};
|
|
73
185
|
}
|
package/dist/kit.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { SealingAdapter } from '@byokit/secrets';
|
|
|
2
2
|
import { type AddKeyResult } from './keys.ts';
|
|
3
3
|
import { type RouteView } from './routes.ts';
|
|
4
4
|
import type { BrowserHost, BrowserOptions } from './browser.ts';
|
|
5
|
-
import type { Account, AccountId, Approval, CallOptions, Decision, Defaults, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, Hello, KitState, KitEventName, KitEventPayload, LearningCapture, LearningMode, Member, ModelInfo, Room, RunEnd, RunEvent, RunSpec, OutputSchema, SchemaOutput, SignInOptions, SignInView, ToolHost, ToolSpec } from './types.ts';
|
|
5
|
+
import type { Account, AccountId, AccountPick, Approval, CallOptions, Decision, Defaults, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, Hello, KitState, KitEventName, KitEventPayload, LearningCapture, LearningMode, Member, ModelInfo, MoveResult, Room, RunEnd, RunEvent, RunRef, RunSelection, RunSpec, OutputSchema, SchemaOutput, SignInOptions, SignInView, ToolHost, ToolSpec } from './types.ts';
|
|
6
6
|
export type KitOptions = {
|
|
7
7
|
stateDir: string;
|
|
8
8
|
authSeal?: SealingAdapter;
|
|
@@ -64,9 +64,12 @@ export declare class OpenClawKit {
|
|
|
64
64
|
private listeners;
|
|
65
65
|
private off;
|
|
66
66
|
private signInDisconnects;
|
|
67
|
+
private readonly phaseListeners;
|
|
67
68
|
private starting?;
|
|
68
69
|
private stopping;
|
|
69
70
|
private failures;
|
|
71
|
+
private swept;
|
|
72
|
+
private catalogs;
|
|
70
73
|
browser?: BrowserHost;
|
|
71
74
|
private browserHost?;
|
|
72
75
|
private readonly brokers;
|
|
@@ -90,6 +93,9 @@ export declare class OpenClawKit {
|
|
|
90
93
|
start(): Promise<void>;
|
|
91
94
|
private connect;
|
|
92
95
|
private disconnect;
|
|
96
|
+
private engineRestarting;
|
|
97
|
+
private authSnapshot;
|
|
98
|
+
private restarted;
|
|
93
99
|
private closed;
|
|
94
100
|
stop(): Promise<void>;
|
|
95
101
|
private closeBrowsers;
|
|
@@ -106,6 +112,7 @@ export declare class OpenClawKit {
|
|
|
106
112
|
routes(): RouteView[];
|
|
107
113
|
providerStatus(member: Member): Promise<string[] | undefined>;
|
|
108
114
|
providers(member: Member): Promise<string[]>;
|
|
115
|
+
/** A member sign-in to `provider`, or a ready key (5.8) whose saved model is that provider's. */
|
|
109
116
|
signedIn(member: Member, provider: string): Promise<boolean>;
|
|
110
117
|
signIn(member: Member, o: SignInOptions, on: (v: SignInView) => void): {
|
|
111
118
|
paste(text: string): void;
|
|
@@ -113,9 +120,18 @@ export declare class OpenClawKit {
|
|
|
113
120
|
done: Promise<SignInView>;
|
|
114
121
|
};
|
|
115
122
|
signOut(member: Member, provider: string): Promise<void>;
|
|
123
|
+
memberOf(ref: string): Member | undefined;
|
|
116
124
|
accounts(member: Member): Promise<Account[]>;
|
|
117
|
-
models(member: Member, id: AccountId
|
|
125
|
+
models(member: Member, id: AccountId | {
|
|
126
|
+
authChoice: string;
|
|
127
|
+
}): Promise<ModelInfo[]>;
|
|
118
128
|
room(member: Member, id: AccountId, demand?: string[]): Promise<Room>;
|
|
129
|
+
/** What `run` would decide now with this selection and key, refusals included; decides nothing and runs nothing. */
|
|
130
|
+
pick(member: Member, sel: RunSelection, o?: {
|
|
131
|
+
sessionKey?: string;
|
|
132
|
+
}): Promise<AccountPick>;
|
|
133
|
+
/** Carry a conversation to another account: its whole transcript forks onto that account, then the old session goes. */
|
|
134
|
+
move(ref: RunRef, to: AccountId): Promise<MoveResult>;
|
|
119
135
|
/** Sign in one more account: on the member agent while it holds none of the provider, else on a new sealed agent. */
|
|
120
136
|
addAccount(member: Member, o: {
|
|
121
137
|
authChoice: string;
|
|
@@ -138,7 +154,10 @@ export declare class OpenClawKit {
|
|
|
138
154
|
addKey(agentId: Member, o: {
|
|
139
155
|
authChoice: string;
|
|
140
156
|
apiKey: string;
|
|
157
|
+
model?: string;
|
|
141
158
|
}): Promise<AddKeyResult>;
|
|
159
|
+
private prepareKeyOwner;
|
|
160
|
+
private syncCatalogs;
|
|
142
161
|
migrateRetainedLogin(member: Member, source: RetainedLogin): Promise<'staged' | 'nothing' | 'failed'>;
|
|
143
162
|
confirmRetainedLogin(member: Member, source: RetainedLogin): Promise<boolean>;
|
|
144
163
|
private runs;
|