@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.
@@ -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
+ }
@@ -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). The real
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 fromKey = key ? memberOfKey(key) : undefined;
31
- const member = agent ?? fromKey ?? asString(detail.member) ?? '';
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,3 @@
1
+ import type { Member } from './types.ts';
2
+ export declare function agentOf(ref: string): string | undefined;
3
+ export declare function memberOf(ref: string): Member | undefined;
@@ -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 = memberOfKey(key) ?? '';
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: memberOfKey(key) ?? '' };
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/*');
@@ -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 = Exclude<RunEvent, {
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 type { GatewayTransport, Member } from './types.ts';
2
- export type AddKeyResult = 'ok' | 'invalid' | 'not_included';
3
- export declare function createKeys(ctx: {
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): Promise<{
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 { keyAgentId, MEMBER_ID, KEY_PREFIX } from "./members.js";
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
- const control = (member, action, provider) => ctx.request('byokit.keys', { member, action, ...(provider ? { provider } : {}) });
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) || !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';
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 'invalid';
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
- 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';
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
- return 'invalid';
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
- // 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)
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): Promise<ModelInfo[]>;
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;