@byokit/openclaw 0.11.0 → 0.11.1

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 CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.11.1 (2026-10-12)
6
+
7
+ - Dependency update: pins @byokit/discover 0.8.1.
8
+ - FIX: (from @byokit/discover 0.8.1) `browse()` and `scan()` accept resolved services whose `fullName` carries the service type without a trailing dot (`host._type._tcp`, as Android's react-native-zeroconf delivers) or with a trailing `.local` domain suffix, case-insensitive, instead of only the `host.local._type._tcp.` shape. Android phones now discover computers over mDNS instead of always scanning empty.
9
+
10
+ - FIX: restoring an engine home no longer rewrites a live file whose bytes already match the sealed object. `restore()` compares each decrypted object against the live file at the restore mode (0600) — size first, then bytes — and skips the temp write, file fsync, atomic rename and directory fsync when they match, so a start over an untouched tree pays no per-object writes. A missing, changed, different-mode or unreadable file restores exactly as before, and a skipped file changes nothing on disk, so the crash-durability contract is unchanged; nothing new is written to disk.
11
+ - FEAT: OpenClaw kit S5 account spine: per-member `accounts/<member>.json` index (0600, atomic, no email/token/engine line) behind `kit.accounts`, `renameAccount` and `defaults`/`setDefaults`; a pre-O14 sign-in synthesizes an `addedAt: 0` entry, a default naming a removed account drops on read, and an unindexed `<member>--<hex6>` agent is never listed. `src/locks.ts` adds the per-agent shared/exclusive/live locks later steps use for runs, removal, moves and sealing.
12
+ - FEAT: OpenClaw kit S6 `kit.addAccount(member, { authChoice, via?, name?, again? }, on)`: a member's first sign-in of a provider stays on its own agent, and each further one (or `again`) gets a new sealed agent `<member>--<hex6>` (drawn again on a collision) with account id `<provider>.<hex6>`, its index entry written before the agent and IDENTITY.md kept byte-identical. The ChatGPT authorize URL carries `prompt=login` so a second account can be chosen; the row reads `signing` until the sign-in ends; `done` comes only after the agent is sealed to that one sign-in and its `who` hash is indexed; a sign-in that fails, ends busy or is cancelled removes the new agent and its entry. `authChoice: 'openai-codex'` (the engine's id for ChatGPT) signs in on the `openai` route.
13
+ - FEAT: OpenClaw kit S9 Auto input: `kit.models(member, id)` maps the account provider's `models.list { agentId, view: 'default' }` rows to `provider/model` (`missing-auth`/`auth-failed` → `why: 'signed_out'`, `cooldown` → `why: 'resting'` with `until = unavailableUntil`), and `kit.room(member, id, demand?)` reads the `models.authStatus { agentId }` row for the provider's tightest applying usage window (`left`, `span`, `resetsAt`; no `at`, no kit cache, unknown for API-key accounts and unanswered reads). Both hold the account agent's shared lock for the read. The fake gateway serves `models.list { agentId }` per agent and a cold per-provider usage cache (`setUsage`; no `usage` on the first answer carrying the provider).
14
+ - FEAT: OpenClaw kit S10 account runs: `kit.run({ member, sessionKey, account: 'auto' | 'default' | id, model?, needs? })` picks one account once, before any request (Choosing, 5.15), checks its own sealed sign-in with `byokit.accounts ready` and runs on its agent; an unbound key that picks an account agent runs as `agent:<account agent>:<tail>`. The first event is `{ type: 'account', account, model, sessionKey, how, why, considered }`, an ok end carries `account` and `model`, a chosen account that fails its ready check ends `signed-out` after its account event, with no agent request (Auto skips such an account for the next one), an empty pick ends `signed-out`, a missing model ends `plan`, and a resting end writes `until` (now + 5 min when the message names none) so the next Auto skips the account. `needs` alone resolves as `account: 'default'`; with `account` absent and `needs` absent or empty a run is unchanged. Account runs hold the agent's shared lock to their end, and index edits now take their own per-member lock, so a live run never blocks a rename or a listing. A model the engine reports unavailable only because its account is signed out or resting still counts as that account's plan. Session binding arrives in the next step.
15
+
5
16
  ## 0.11.0 (2026-10-12)
6
17
 
7
18
  - Dependency update: pins @byokit/ui 0.7.0.
package/README.md CHANGED
@@ -124,12 +124,18 @@ engine text that could echo the token. It is sent only as this member's wizard a
124
124
  results or kit logs. Ordinary browser paste and typed `kit.call('wizard.next', …)` pass-through are unchanged.
125
125
  API key (billed per use) entry still requires explicit selection; there is no subscription-to-key fallback.
126
126
 
127
- To choose which of a member's accounts a run uses, pass `model: 'provider/model'` in the run spec (for example
127
+ To choose the model for a run, pass `model: 'provider/model'` in the run spec (for example
128
128
  `'openai/gpt-5.1'`, where `provider` is an id `kit.providers(member)` lists). That provider is the one called and billed for this run only, with no fallback to another
129
129
  provider or model. If the member isn't signed in to it, the run ends `{ ok: false, kind: 'signed-out' }` and the
130
130
  engine is never called. Leave `model` out to keep the engine's own choice. A specific sign-in (`@profile`) can't be
131
131
  picked per run: the pinned engine may still switch to another sign-in for the same provider.
132
132
 
133
+ To let the kit choose among a member's accounts, pass `account: 'auto'` (the account with the most room left, decided
134
+ once at run start), `'default'` (the saved default) or an id from `kit.accounts(member)`, with `needs: ['provider/model', …]`
135
+ for other models the run may call. The first event is `{ type: 'account', account, model, sessionKey, how, why, considered }`,
136
+ and an ok end carries `account` and `model`. A chosen account that isn't signed in ends `signed-out` with no agent request;
137
+ when every account is resting, the end is `resting` with `until`, and Auto skips a resting account until then.
138
+
133
139
 
134
140
  For an owner who explicitly offers keys, `kit.addKey('ana', { authChoice: 'openai-api-key', apiKey })` returns
135
141
  `ok`, `invalid` or `not_included`. Label the option **API key (billed per use)**. Select only `routes()` rows with
@@ -1,5 +1,25 @@
1
1
  import { createLocks } from './locks.ts';
2
- import type { Account, AccountId, Defaults, GatewayTransport, Member } from './types.ts';
2
+ import type { Account, AccountId, AccountPick, Defaults, GatewayTransport, Member, ModelInfo, Room, RunSelection, SignInOptions, SignInView } from './types.ts';
3
+ type AddOptions = {
4
+ authChoice: string;
5
+ via?: 'browser' | 'code';
6
+ name?: string;
7
+ again?: AccountId;
8
+ };
9
+ type AccountSignIn = {
10
+ id: AccountId;
11
+ paste(text: string): void;
12
+ cancel(): void;
13
+ done: Promise<{
14
+ view: SignInView;
15
+ id: AccountId;
16
+ }>;
17
+ };
18
+ type StartSignIn = (agentId: string, o: SignInOptions, on: (v: SignInView) => void, fresh: boolean) => {
19
+ paste(text: string): void;
20
+ cancel(): void;
21
+ done: Promise<SignInView>;
22
+ };
3
23
  export declare function createAccounts(ctx: {
4
24
  request: GatewayTransport['request'];
5
25
  root: string;
@@ -8,9 +28,20 @@ export declare function createAccounts(ctx: {
8
28
  workspace: string;
9
29
  }>;
10
30
  locks: ReturnType<typeof createLocks>;
31
+ signIn: StartSignIn;
32
+ log?: (line: string) => void;
11
33
  }): {
12
34
  list(member: Member): Promise<Account[]>;
35
+ add(member: Member, o: AddOptions, on: (v: SignInView) => void): Promise<AccountSignIn>;
13
36
  rename(member: Member, id: AccountId, name: string): Promise<Account>;
14
37
  defaults(member: Member): Promise<Defaults>;
15
38
  setDefaults(member: Member, d: Defaults): Promise<void>;
39
+ models(member: Member, id: AccountId): Promise<ModelInfo[]>;
40
+ room(member: Member, id: AccountId, demand?: string[]): Promise<Room>;
41
+ pick(member: Member, sel: RunSelection, skip?: readonly AccountId[]): Promise<{
42
+ pick: AccountPick;
43
+ agent?: string;
44
+ }>;
45
+ rest(member: Member, id: AccountId, until: number): Promise<void>;
16
46
  };
47
+ export {};
package/dist/accounts.js CHANGED
@@ -1,14 +1,23 @@
1
1
  // The per-member account index (5.15, O14 step S5): `<root>/accounts/<member>.json` (0600, whole-file temp
2
2
  // plus rename) is the only kit state: `{ v: 1, accounts: { [id]: { agent, name, route, addedAt, until?, model?,
3
- // who? } }, defaults }`. No credential, token, email or engine line. Listing, rename and defaults live here;
4
- // add, remove, models, room, pick and move arrive in later steps.
3
+ // who? } }, defaults }`. No credential, token, email or engine line. Listing, rename, defaults, models, room,
4
+ // adding (O14 step S6) and the start-of-run pick (S10) live here; remove and move arrive in later steps.
5
5
  import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
6
- import { randomUUID } from 'node:crypto';
6
+ import { randomBytes, randomUUID } from 'node:crypto';
7
7
  import { join } from 'node:path';
8
- import { KEY_PREFIX, MEMBER_ID } from "./members.js";
8
+ import { providerOf, resolveSelection } from "./pick.js";
9
+ import { KEY_PREFIX, MEMBER_ID, NEW_MEMBER_ID, untilListed } from "./members.js";
9
10
  import { routes, routeFor } from "./routes.js";
11
+ import { words } from "./words.js";
10
12
  const ACCOUNT_AGENT = /^[a-z][a-z0-9-]{0,31}--[0-9a-f]{6}$/;
11
13
  const usable = (status) => status === 'ok' || status === 'expiring' || status === 'static';
14
+ const IDENTITY = 'IDENTITY.md';
15
+ const named = (name, op) => {
16
+ const trimmed = typeof name === 'string' ? name.trim() : '';
17
+ if (!trimmed || trimmed.length > 100 || /[\u0000-\u001f\u007f]/.test(trimmed))
18
+ throw new Error(`${op}: name is empty`);
19
+ return trimmed;
20
+ };
12
21
  const empty = () => ({ v: 1, accounts: {}, defaults: {} });
13
22
  const validEntry = (v) => !!v && typeof v === 'object'
14
23
  && typeof v.agent === 'string' && typeof v.name === 'string'
@@ -18,6 +27,10 @@ const validEntry = (v) => !!v && typeof v === 'object'
18
27
  && (v.who === undefined || typeof v.who === 'string');
19
28
  export function createAccounts(ctx) {
20
29
  const path = (member) => join(ctx.root, 'accounts', `${member}.json`);
30
+ // Accounts whose sign-in is running, as `<member>\n<id>`, counted per add: listed `signing` until the last one ends.
31
+ const signing = new Map();
32
+ // The index file's own lock, apart from any agent's: a run's shared hold on the member agent never blocks an edit.
33
+ const indexLock = (member) => `${member}#index`;
21
34
  const check = (member) => {
22
35
  if (!MEMBER_ID.test(member) || member.startsWith(KEY_PREFIX))
23
36
  throw new Error(`invalid member id: ${member}`);
@@ -141,6 +154,9 @@ export function createAccounts(ctx) {
141
154
  memberRows.sort((a, b) => a.addedAt - b.addedAt || (a.id < b.id ? -1 : 1));
142
155
  agentRows.sort((a, b) => a.addedAt - b.addedAt);
143
156
  const rows = [...memberRows, ...agentRows];
157
+ for (const row of rows)
158
+ if (signing.has(`${member}\n${row.id}`))
159
+ row.state = 'signing';
144
160
  if (file.defaults.account !== undefined && !rows.some((r) => r.id === file.defaults.account)) {
145
161
  delete file.defaults.account;
146
162
  dirty = true;
@@ -153,15 +169,101 @@ export function createAccounts(ctx) {
153
169
  const [roster, profiles] = await gather(agentId);
154
170
  return { ...(await build(member, agentId, file, roster, profiles)), file };
155
171
  };
172
+ // The account's index entry, only when `list` names it: an unindexed agent is never reachable, whatever the
173
+ // file holds.
174
+ const entryOf = async (member, id) => {
175
+ const row = (await list(member)).find((r) => r.id === id);
176
+ const entry = read(member).accounts[id];
177
+ if (!row || !entry)
178
+ throw new Error(`unknown account: ${id}`);
179
+ return { row, entry };
180
+ };
181
+ const record = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
182
+ // `models.list { agentId, view: 'default' }` rows of the account's provider, mapped to `provider/model`. The pin
183
+ // reports no per-plan availability, so `why: 'plan'` only comes from a run ending `plan`, never from here.
184
+ const modelsOf = (provider, answer) => {
185
+ const rows = record(answer) && Array.isArray(answer.models) ? answer.models : [];
186
+ const out = [];
187
+ for (const candidate of rows) {
188
+ if (!record(candidate) || typeof candidate.provider !== 'string'
189
+ || candidate.provider.toLowerCase() !== provider.toLowerCase())
190
+ continue;
191
+ if (typeof candidate.id !== 'string' || !candidate.id || typeof candidate.name !== 'string')
192
+ continue;
193
+ const id = candidate.id.includes('/') ? candidate.id : `${candidate.provider}/${candidate.id}`;
194
+ if (providerOf(id) !== provider.toLowerCase())
195
+ continue;
196
+ const available = candidate.available === undefined
197
+ ? candidate.unavailableReason === undefined : candidate.available === true;
198
+ const info = { id, name: candidate.name, available };
199
+ if ((candidate.tier === 'strong' || candidate.tier === 'fast'))
200
+ info.tier = candidate.tier;
201
+ if (!available) {
202
+ if (candidate.unavailableReason === 'missing-auth' || candidate.unavailableReason === 'auth-failed')
203
+ info.why = 'signed_out';
204
+ else if (candidate.unavailableReason === 'cooldown') {
205
+ info.why = 'resting';
206
+ if (typeof candidate.unavailableUntil === 'number' && Number.isFinite(candidate.unavailableUntil))
207
+ info.until = candidate.unavailableUntil;
208
+ }
209
+ }
210
+ out.push(info);
211
+ }
212
+ return out;
213
+ };
214
+ const spanOf = (label) => {
215
+ const lower = label.toLowerCase();
216
+ if (lower === 'session')
217
+ return 'session';
218
+ if (lower === 'weekly')
219
+ return 'week';
220
+ if (lower === 'monthly')
221
+ return 'month';
222
+ return 'tightest';
223
+ };
224
+ // The `models.authStatus { agentId }` row for the account's provider: of its `usage.windows` entries the one
225
+ // with the highest numeric `usedPercent` gives `left`, `resetsAt` and `span`. A window applies to every model
226
+ // unless the engine scopes it to models; the pin's rows carry no scope, so the demand never narrows them today
227
+ // (the tightest known). A window without a numeric `usedPercent` is skipped, never read as unused. `at` is left
228
+ // out: the pin exposes no reading age, so the kit never stamps receipt time as reading time. The kit adds no
229
+ // cache: every call reads live.
230
+ const roomOf = (provider, answer) => {
231
+ const unknown = { left: 'unknown' };
232
+ const rows = record(answer) && Array.isArray(answer.providers) ? answer.providers : undefined;
233
+ if (!rows)
234
+ return unknown;
235
+ const row = rows.find((r) => record(r) && typeof r.provider === 'string'
236
+ && r.provider.toLowerCase() === provider.toLowerCase());
237
+ if (!row || !record(row))
238
+ return unknown;
239
+ const windows = record(row.usage) && Array.isArray(row.usage.windows) ? row.usage.windows : undefined;
240
+ if (!windows)
241
+ return unknown;
242
+ let best;
243
+ for (const candidate of windows) {
244
+ if (!record(candidate))
245
+ continue;
246
+ if (typeof candidate.usedPercent !== 'number' || !Number.isFinite(candidate.usedPercent))
247
+ continue;
248
+ const span = spanOf(typeof candidate.label === 'string' ? candidate.label : '');
249
+ const resetAt = typeof candidate.resetAt === 'number' && Number.isFinite(candidate.resetAt)
250
+ ? candidate.resetAt : undefined;
251
+ if (!best || candidate.usedPercent > best.used)
252
+ best = { used: candidate.usedPercent, ...(resetAt !== undefined ? { resetAt } : {}), span };
253
+ }
254
+ if (!best)
255
+ return unknown;
256
+ return { left: 100 - best.used, span: best.span, ...(best.resetAt !== undefined ? { resetsAt: best.resetAt } : {}) };
257
+ };
156
258
  async function list(member) {
157
259
  check(member);
158
260
  const { agentId } = await ctx.ensure(member);
159
- const first = await ctx.locks.shared(agentId, () => buildRows(member, agentId));
261
+ const first = await ctx.locks.shared(indexLock(member), () => buildRows(member, agentId));
160
262
  if (!first.dirty)
161
263
  return first.rows;
162
264
  // A write is needed (synthesized entry, `who` backfill or dropped default): redo it under the
163
265
  // exclusive lock so two first-lists cannot interleave their read-modify-write.
164
- return ctx.locks.exclusive([agentId], async () => {
266
+ return ctx.locks.exclusive([indexLock(member)], async () => {
165
267
  const { rows, dirty, file } = await buildRows(member, agentId);
166
268
  if (dirty)
167
269
  write(member, file);
@@ -170,15 +272,10 @@ export function createAccounts(ctx) {
170
272
  }
171
273
  async function rename(member, id, name) {
172
274
  check(member);
173
- if (typeof name !== 'string')
174
- throw new Error('renameAccount: name is empty');
175
- const trimmed = name.trim();
176
- if (!trimmed || trimmed.length > 100 || /[\u0000-\u001f\u007f]/.test(trimmed))
177
- throw new Error('renameAccount: name is empty');
178
- const { agentId } = await ctx.ensure(member);
275
+ const trimmed = named(name, 'renameAccount');
179
276
  if (!(await list(member)).some((r) => r.id === id))
180
277
  throw new Error(`unknown account: ${id}`);
181
- await ctx.locks.exclusive([agentId], async () => {
278
+ await ctx.locks.exclusive([indexLock(member)], async () => {
182
279
  const file = read(member);
183
280
  const entry = file.accounts[id];
184
281
  if (!entry)
@@ -205,8 +302,8 @@ export function createAccounts(ctx) {
205
302
  throw new Error('setDefaults: defaults are invalid');
206
303
  if (d.auto !== undefined && typeof d.auto !== 'boolean')
207
304
  throw new Error('setDefaults: defaults are invalid');
208
- const { agentId } = await ctx.ensure(member);
209
- return ctx.locks.exclusive([agentId], async () => {
305
+ await ctx.ensure(member);
306
+ return ctx.locks.exclusive([indexLock(member)], async () => {
210
307
  const file = read(member);
211
308
  const next = {};
212
309
  if (d.account !== undefined)
@@ -218,5 +315,185 @@ export function createAccounts(ctx) {
218
315
  write(member, { ...file, defaults: next });
219
316
  });
220
317
  }
221
- return { list, rename, defaults, setDefaults };
318
+ async function models(member, id) {
319
+ check(member);
320
+ await ctx.ensure(member);
321
+ const { row, entry } = await entryOf(member, id);
322
+ return readModels(row, entry);
323
+ }
324
+ // The shared hold covers the read, so no kit read of the agent is in flight while it is removed.
325
+ const readModels = (row, entry) => ctx.locks.shared(entry.agent, async () => modelsOf(row.provider, await ctx.request('models.list', { agentId: entry.agent, view: 'default' })));
326
+ async function room(member, id, demand) {
327
+ check(member);
328
+ await ctx.ensure(member);
329
+ const { row, entry } = await entryOf(member, id);
330
+ return readRoom(row, entry);
331
+ }
332
+ // An API-key account is used only when chosen, never by Auto: its room is always unknown, with no read. A demand
333
+ // never narrows the windows (see `roomOf`), so the read takes none.
334
+ const readRoom = (row, entry) => row.billing === 'api'
335
+ ? Promise.resolve({ left: 'unknown' })
336
+ : ctx.locks.shared(entry.agent, async () => {
337
+ let answer;
338
+ try {
339
+ answer = await ctx.request('models.authStatus', { agentId: entry.agent }, { timeoutMs: 20_000 });
340
+ }
341
+ catch {
342
+ return { left: 'unknown' };
343
+ }
344
+ return roomOf(row.provider, answer);
345
+ });
346
+ // Choosing (5.15) for an unbound key: every listed account's room and models are read live once, then the pure
347
+ // `resolveSelection` decides. `agent` is the picked account's engine agent. A model unavailable only because the
348
+ // account is signed out or resting is still in its plan: the account's state and the run's ready check judge that.
349
+ // The pinned engine may hold no prepared model catalog for an agent added since it started, and refuses its models; that
350
+ // row's models are then unknown (`undefined`), and `resolveSelection` host-validates its membership by provider.
351
+ async function pick(member, sel, skip = []) {
352
+ const rows = (await list(member)).filter((r) => !skip.includes(r.id));
353
+ const file = read(member);
354
+ const reads = new Map(await Promise.all(rows.map(async (row) => {
355
+ const entry = file.accounts[row.id];
356
+ const models = await readModels(row, entry).then((ms) => ms.map((m) => (m.why ? { ...m, available: true } : m)), () => undefined);
357
+ return [row.id, { room: await readRoom(row, entry), models }];
358
+ })));
359
+ const chosen = resolveSelection(rows, file.defaults, sel, (a) => reads.get(a.id).room, Date.now(), (a) => reads.get(a.id).models);
360
+ return { pick: chosen, ...(chosen.ok ? { agent: file.accounts[chosen.account.id].agent } : {}) };
361
+ }
362
+ // A run on the account ended resting: Auto skips it until `until` (its state reads `resting` until then).
363
+ const rest = (member, id, until) => edit(member, (file) => { const entry = file.accounts[id]; if (entry)
364
+ entry.until = until; });
365
+ // Index edits take the index lock, as listing's own write does.
366
+ const edit = (member, fn) => ctx.locks.exclusive([indexLock(member)], async () => { const file = read(member); fn(file); write(member, file); });
367
+ const identityOf = async (agentId) => {
368
+ const { file } = await ctx.request('agents.files.get', { agentId, name: IDENTITY });
369
+ return file.missing ? undefined : file.content;
370
+ };
371
+ const restoreIdentity = async (memberAgent, before) => {
372
+ if (before !== undefined && await identityOf(memberAgent) !== before)
373
+ await ctx.request('agents.files.set', { agentId: memberAgent, name: IDENTITY, content: before });
374
+ };
375
+ const onGateway = (agent) => untilListed(ctx.request, agent)
376
+ .then(() => true, () => ctx.request('agents.list')
377
+ .then((r) => r.agents.some((a) => a.id === agent)));
378
+ // A new agent whose sign-in did not finish goes with its entry (5.15); the shared workspace stays. The entry is
379
+ // dropped only once the agent's directory is gone, so a later removal can finish what this one could not.
380
+ const discard = (member, id, agent, created) => (async () => {
381
+ if (created) {
382
+ await ctx.request('byokit.accounts', { action: 'clear', agentId: agent });
383
+ await ctx.request('agents.delete', { agentId: agent, deleteFiles: false });
384
+ }
385
+ rmSync(join(ctx.root, 'state', 'agents', agent), { recursive: true, force: true });
386
+ await edit(member, (file) => { delete file.accounts[id]; });
387
+ })().catch((error) => ctx.log?.(`addAccount: ${agent} is not removed yet: ${error instanceof Error ? error.message : String(error)}`));
388
+ async function add(member, o, on) {
389
+ check(member);
390
+ if (!NEW_MEMBER_ID.test(member))
391
+ throw new Error(words('account.legacyMember'));
392
+ const choice = o?.authChoice === 'openai-codex' ? 'openai' : o?.authChoice;
393
+ const route = routes().find((r) => r.choice === choice && r.billing === 'subscription');
394
+ if (!route)
395
+ throw new Error(`addAccount: ${String(o?.authChoice)} is not a subscription sign-in`);
396
+ const name = o.name === undefined ? undefined : named(o.name, 'addAccount');
397
+ const provider = route.provider;
398
+ const { agentId: memberAgent, workspace } = await ctx.ensure(member);
399
+ if (o.again !== undefined && (!read(member).accounts[o.again] || o.again.split('.')[0] !== provider))
400
+ throw new Error(`unknown account: ${o.again}`);
401
+ // The member agent is the target only while it holds no local profile of the provider (5.15).
402
+ const own = await ctx.request('byokit.accounts', { action: 'list', agentId: memberAgent });
403
+ if (!own.ok || !Array.isArray(own.profiles))
404
+ throw new Error('addAccount: the member\'s sign-ins could not be read');
405
+ const fresh = o.again !== undefined || own.profiles.some((p) => p.provider === provider);
406
+ const entryOf = (file, agent) => ({ agent, route: route.choice, addedAt: Date.now(),
407
+ name: name ?? `${route.label} ${Object.values(file.accounts).filter((e) => e.route === route.choice).length + 1}` });
408
+ let id = provider;
409
+ let agent = memberAgent;
410
+ let added = false;
411
+ if (!fresh) {
412
+ await edit(member, (file) => { if (!file.accounts[id]) {
413
+ file.accounts[id] = entryOf(file, agent);
414
+ added = true;
415
+ } });
416
+ }
417
+ else {
418
+ // `<member>--<hex6>`, drawn again while that agent exists; its entry is written before the agent.
419
+ const roster = new Set((await ctx.request('agents.list')).agents.map((a) => a.id));
420
+ await edit(member, (file) => {
421
+ const taken = new Set([...roster, ...Object.values(file.accounts).map((e) => e.agent)]);
422
+ do {
423
+ const hex = randomBytes(3).toString('hex');
424
+ id = `${provider}.${hex}`;
425
+ agent = `${member}--${hex}`;
426
+ } while (taken.has(agent) || file.accounts[id]);
427
+ file.accounts[id] = entryOf(file, agent);
428
+ });
429
+ added = true;
430
+ let created = false;
431
+ // The pin rewrites the shared workspace's IDENTITY.md Name line on create, so the create window is one lock per member.
432
+ await ctx.locks.exclusive([`${member}#identity`], async () => {
433
+ let before;
434
+ try {
435
+ before = await identityOf(memberAgent);
436
+ const made = await ctx.request('agents.create', { name: agent, workspace });
437
+ created = true;
438
+ if (made.agentId !== agent)
439
+ throw new Error(`addAccount: agents.create answered ${String(made.agentId)}, not ${agent}`);
440
+ await untilListed(ctx.request, agent);
441
+ await restoreIdentity(memberAgent, before);
442
+ }
443
+ catch (error) {
444
+ await restoreIdentity(memberAgent, before)
445
+ .catch((restoreError) => ctx.log?.(`addAccount: IDENTITY.md is not restored: ${restoreError instanceof Error ? restoreError.message : String(restoreError)}`));
446
+ const exists = created || await onGateway(agent).catch((listError) => {
447
+ ctx.log?.(`addAccount: ${agent} is not removed yet: ${listError instanceof Error ? listError.message : String(listError)}`);
448
+ return undefined;
449
+ });
450
+ if (exists !== undefined)
451
+ await discard(member, id, agent, exists);
452
+ throw error;
453
+ }
454
+ });
455
+ }
456
+ const key = `${member}\n${id}`;
457
+ signing.set(key, (signing.get(key) ?? 0) + 1);
458
+ const handle = ctx.signIn(agent, { authChoice: route.choice, ...(o.via ? { via: o.via } : {}) }, (v) => { if (v.state === 'waiting')
459
+ on(v); }, fresh);
460
+ // Sealed before the person sees done: the agent keeps only this sign-in, and its `who` goes to the index.
461
+ const settle = async (view) => {
462
+ if (view.state === 'done') {
463
+ const sealed = await ctx.locks.exclusive([memberAgent, agent, indexLock(member)], async () => {
464
+ const answer = await ctx.request('byokit.accounts', { action: 'seal', agentId: agent, provider });
465
+ if (!answer.ok)
466
+ return 'Sign-in could not be saved. Try again.';
467
+ const file = read(member);
468
+ const entry = file.accounts[id];
469
+ if (entry && answer.who) {
470
+ entry.who = answer.who;
471
+ write(member, file);
472
+ }
473
+ return true;
474
+ }).catch((error) => (error instanceof Error ? error.message : String(error)).slice(0, 200));
475
+ if (sealed === true)
476
+ return view;
477
+ view = { state: 'failed', via: view.via, why: 'failed', error: sealed };
478
+ }
479
+ if (agent !== memberAgent)
480
+ await discard(member, id, agent, true);
481
+ else if (added)
482
+ await edit(member, (file) => { delete file.accounts[id]; })
483
+ .catch((error) => ctx.log?.(`addAccount: ${id} entry is kept: ${error instanceof Error ? error.message : String(error)}`));
484
+ return view;
485
+ };
486
+ const done = handle.done.then(settle).finally(() => {
487
+ const left = signing.get(key) - 1;
488
+ if (left)
489
+ signing.set(key, left);
490
+ else
491
+ signing.delete(key);
492
+ }).then((view) => {
493
+ on(view);
494
+ return { view, id };
495
+ });
496
+ return { id, paste: handle.paste, cancel: handle.cancel, done };
497
+ }
498
+ return { list, add, rename, defaults, setDefaults, models, room, pick, rest };
222
499
  }
package/dist/device.d.ts CHANGED
@@ -79,6 +79,9 @@ export type SignInNotice = {
79
79
  export declare function openSignInNotice(data: Record<string, unknown>, seed: Uint8Array): SignInNotice | null;
80
80
  /** Transient streams bypass link's durable request/answer cache: no lease nonce or frame is persisted. */
81
81
  export declare function browserDevice(link: DeviceLink): BrowserDevice;
82
+ type DeviceRunEvent = Exclude<RunEvent, {
83
+ type: 'account';
84
+ }>;
82
85
  /** `openclawDevice`'s device surface: every `oc.*` op the computer answers, typed. */
83
86
  export type OpenClawDevice = {
84
87
  browser: BrowserDevice;
@@ -92,7 +95,7 @@ export type OpenClawDevice = {
92
95
  };
93
96
  signOut(p: string): Promise<void>;
94
97
  sessions(): Promise<SessionRow[]>;
95
- run<const S extends OutputSchema | undefined = undefined>(message: string, o?: DeviceRunOptions<S>): AsyncIterable<RunEvent | DeviceEndFrame<SchemaOutput<S>>>;
98
+ run<const S extends OutputSchema | undefined = undefined>(message: string, o?: DeviceRunOptions<S>): AsyncIterable<DeviceRunEvent | DeviceEndFrame<SchemaOutput<S>>>;
96
99
  steer(k: string, t: string, o?: {
97
100
  auth?: 'apiKey';
98
101
  }): Promise<void>;
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, RunEnd, RunEvent, RunSpec, OutputSchema, SchemaOutput, SignInOptions, SignInView, ToolHost, ToolSpec } from './types.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';
6
6
  export type KitOptions = {
7
7
  stateDir: string;
8
8
  authSeal?: SealingAdapter;
@@ -55,6 +55,7 @@ export declare class OpenClawKit {
55
55
  private greeting?;
56
56
  private members?;
57
57
  private readonly locks;
58
+ private readonly routeMap;
58
59
  private accountsCtl?;
59
60
  private readonly bridge;
60
61
  private readonly keys;
@@ -113,6 +114,23 @@ export declare class OpenClawKit {
113
114
  };
114
115
  signOut(member: Member, provider: string): Promise<void>;
115
116
  accounts(member: Member): Promise<Account[]>;
117
+ models(member: Member, id: AccountId): Promise<ModelInfo[]>;
118
+ room(member: Member, id: AccountId, demand?: string[]): Promise<Room>;
119
+ /** Sign in one more account: on the member agent while it holds none of the provider, else on a new sealed agent. */
120
+ addAccount(member: Member, o: {
121
+ authChoice: string;
122
+ via?: 'browser' | 'code';
123
+ name?: string;
124
+ again?: AccountId;
125
+ }, on: (v: SignInView) => void): Promise<{
126
+ id: AccountId;
127
+ paste(text: string): void;
128
+ cancel(): void;
129
+ done: Promise<{
130
+ view: SignInView;
131
+ id: AccountId;
132
+ }>;
133
+ }>;
116
134
  renameAccount(member: Member, id: AccountId, name: string): Promise<Account>;
117
135
  defaults(member: Member): Promise<Defaults>;
118
136
  setDefaults(member: Member, d: Defaults): Promise<void>;
package/dist/kit.js CHANGED
@@ -432,6 +432,7 @@ export class OpenClawKit {
432
432
  greeting;
433
433
  members;
434
434
  locks = createLocks();
435
+ routeMap = new Map();
435
436
  accountsCtl;
436
437
  // One bridge and one approval surface per kit: listeners and run registrations survive reconnects (B3).
437
438
  // Only the socket itself is per-connection. Approvals always reaches the live transport through this.request().
@@ -746,7 +747,9 @@ export class OpenClawKit {
746
747
  this.o.log?.(`gateway has unknown method: ${name}`);
747
748
  this.members = createMembers({ request: transport.request.bind(transport), root: this.engine.root });
748
749
  this.accountsCtl = createAccounts({ request: transport.request.bind(transport), root: this.engine.root,
749
- ensure: (member) => this.ensureMember(member), locks: this.locks });
750
+ ensure: (member) => this.ensureMember(member), locks: this.locks,
751
+ signIn: (agentId, o, on, fresh) => startSignIn({ ...this.signInCtx(), ensure: async () => ({ agentId }), fresh }, agentId, o, on),
752
+ log: this.o.log });
750
753
  // The fail-closed tool bridge feeds the kit's single Approval surface (5.9, O5).
751
754
  await this.bridge.start();
752
755
  this.off.push(transport.onEvent((e) => this.approvalsCtl.handleEvent(e)));
@@ -926,13 +929,29 @@ export class OpenClawKit {
926
929
  signOut(member, provider) {
927
930
  return engineSignOut(this.signInCtx(), member, provider);
928
931
  }
929
- // accounts (5.15, O14 step S5): the per-member index with listing, defaults and rename. Adding,
930
- // removing, models, room, pick and move arrive in later steps.
932
+ // accounts (5.15, O14 steps S5, S6, S9 and S10): the per-member index with listing, adding, defaults, rename, models
933
+ // and room; `run({ account })` picks among them. Removing, `pick` and move arrive in later steps.
931
934
  accounts(member) {
932
935
  if (!this.accountsCtl)
933
936
  return Promise.reject(new Error('gateway not ready'));
934
937
  return this.accountsCtl.list(member);
935
938
  }
939
+ models(member, id) {
940
+ if (!this.accountsCtl)
941
+ return Promise.reject(new Error('gateway not ready'));
942
+ return this.accountsCtl.models(member, id);
943
+ }
944
+ room(member, id, demand) {
945
+ if (!this.accountsCtl)
946
+ return Promise.reject(new Error('gateway not ready'));
947
+ return this.accountsCtl.room(member, id, demand);
948
+ }
949
+ /** Sign in one more account: on the member agent while it holds none of the provider, else on a new sealed agent. */
950
+ addAccount(member, o, on) {
951
+ if (!this.accountsCtl)
952
+ return Promise.reject(new Error('gateway not ready'));
953
+ return this.accountsCtl.add(member, o, on);
954
+ }
936
955
  renameAccount(member, id, name) {
937
956
  if (!this.accountsCtl)
938
957
  return Promise.reject(new Error('gateway not ready'));
@@ -975,6 +994,9 @@ export class OpenClawKit {
975
994
  ensure: (member) => this.ensureMember(member),
976
995
  bridge: { register: (run, tools, runId) => this.bridge.register(run, tools && this.o.browser ? [...tools, 'request_sign_in'] : tools, runId) },
977
996
  tools: new Set(this.toolNames()),
997
+ ...(this.accountsCtl ? { accounts: this.accountsCtl } : {}),
998
+ locks: this.locks,
999
+ routes: this.routeMap,
978
1000
  });
979
1001
  }
980
1002
  /** The app tool names (`KitOptions.tools`): the only names `RunSpec.tools` may carry. */
package/dist/members.d.ts CHANGED
@@ -1,8 +1,11 @@
1
1
  import type { GatewayTransport, Member } from './types.ts';
2
2
  export declare const MEMBER_ID: RegExp;
3
+ export declare const NEW_MEMBER_ID: RegExp;
3
4
  export declare const KEY_PREFIX = "byokit-key-";
4
5
  export declare const BASE_AGENT = "byokit-base";
5
6
  export declare const keyAgentId: (member: Member) => string;
7
+ /** Resolves once agents.list shows `id`, which agents.create answers before. */
8
+ export declare function untilListed(request: GatewayTransport['request'], id: string): Promise<void>;
6
9
  export declare function createMembers(ctx: {
7
10
  request: GatewayTransport['request'];
8
11
  root: string;
package/dist/members.js CHANGED
@@ -2,7 +2,7 @@ import { join } from 'node:path';
2
2
  // Any member that may run: D9's older rule, which a member created before O14 still matches (D17).
3
3
  export const MEMBER_ID = /^[a-z][a-z0-9-]{0,31}$/;
4
4
  // D9 for a member the kit creates: no `--` (account agents are `<member>--<6 hex>`) and at most 24 characters.
5
- const NEW_MEMBER_ID = /^[a-z](?!.*--)[a-z0-9-]{0,23}$/;
5
+ export const NEW_MEMBER_ID = /^[a-z](?!.*--)[a-z0-9-]{0,23}$/;
6
6
  export const KEY_PREFIX = 'byokit-key-';
7
7
  // The inherited auth base every agent reads through (5.15): the kit never creates, signs in to or runs it.
8
8
  export const BASE_AGENT = 'byokit-base';
@@ -13,9 +13,18 @@ export const keyAgentId = (member) => `${KEY_PREFIX}${member}`;
13
13
  // at once fails as 'unknown agent id'. ensure resolves only once agents.list shows the member.
14
14
  const VISIBLE_TIMEOUT_MS = 5_000;
15
15
  const VISIBLE_POLL_MS = 25;
16
+ const listed = (request, id) => request('agents.list').then((list) => list.agents.some((agent) => agent.id === id));
17
+ /** Resolves once agents.list shows `id`, which agents.create answers before. */
18
+ export async function untilListed(request, id) {
19
+ const deadline = Date.now() + VISIBLE_TIMEOUT_MS;
20
+ while (!await listed(request, id)) {
21
+ if (Date.now() >= deadline)
22
+ throw new Error(`agent ${id} is not listed by agents.list after agents.create`);
23
+ await new Promise((resolve) => setTimeout(resolve, VISIBLE_POLL_MS));
24
+ }
25
+ }
16
26
  export function createMembers(ctx) {
17
27
  const cache = new Map();
18
- const listed = (member) => ctx.request('agents.list').then((list) => list.agents.some((agent) => agent.id === member));
19
28
  return { ensure(member) {
20
29
  const invalid = () => new Error(`invalid member id: ${member}`);
21
30
  if (!MEMBER_ID.test(member) || member.startsWith(KEY_PREFIX) || RESERVED.has(member))
@@ -25,16 +34,11 @@ export function createMembers(ctx) {
25
34
  return existing;
26
35
  const workspace = join(ctx.root, 'workspaces', member);
27
36
  const result = (async () => {
28
- if (!await listed(member)) {
37
+ if (!await listed(ctx.request, member)) {
29
38
  if (!NEW_MEMBER_ID.test(member) || member.startsWith('byokit-'))
30
39
  throw invalid();
31
40
  await ctx.request('agents.create', { name: member, workspace });
32
- const deadline = Date.now() + VISIBLE_TIMEOUT_MS;
33
- while (!await listed(member)) {
34
- if (Date.now() >= deadline)
35
- throw new Error(`agent ${member} is not listed by agents.list after agents.create`);
36
- await new Promise((resolve) => setTimeout(resolve, VISIBLE_POLL_MS));
37
- }
41
+ await untilListed(ctx.request, member);
38
42
  }
39
43
  return { agentId: member, workspace };
40
44
  })();
package/dist/pick.d.ts CHANGED
@@ -1,11 +1,15 @@
1
1
  import type { Account, AccountId, AccountPick, Considered, Defaults, ModelInfo, PickWhy, Room, RunSelection } from './types.ts';
2
2
  export type RoomOf = (a: Account, demand: readonly string[]) => Room;
3
+ /** The lowercased provider of a well-formed `provider/model` reference; undefined when the reference is malformed. */
4
+ export declare function providerOf(model: string): string | undefined;
3
5
  /** Every account's `Considered` row, in list order, with the kit-only `bound` applied. */
4
- export declare function consider(accounts: readonly Account[], defaults: Defaults, sel: RunSelection, room: RoomOf, nowMs: number, models?: (a: Account) => readonly ModelInfo[], bound?: readonly AccountId[]): Considered[];
6
+ export declare function consider(accounts: readonly Account[], defaults: Defaults, sel: RunSelection, room: RoomOf, nowMs: number, models?: (a: Account) => readonly ModelInfo[] | undefined, bound?: readonly AccountId[]): Considered[];
5
7
  /** Auto's winner only; `candidates` are the eligible accounts. Pure and portable; no timers, reads or mid-run switching. */
6
8
  export declare function chooseAccount(candidates: readonly Account[], room: (a: Account) => Room, nowMs: number): {
7
9
  account: Account;
8
10
  why: PickWhy;
9
11
  } | undefined;
10
- /** Resolve once before starting. Without `models`, membership is host-validated and a missing model is ''. */
11
- export declare function resolveSelection(accounts: readonly Account[], defaults: Defaults, sel: RunSelection, room: RoomOf, nowMs: number, models?: (a: Account) => readonly ModelInfo[], bound?: readonly AccountId[]): AccountPick;
12
+ /** Resolve once before starting. Without `models`, membership is host-validated and a missing model is ''. A row whose
13
+ * models are unknown (`undefined`) host-validates membership by provider; with no model asked for it runs the saved
14
+ * default, else model '' (no override: the engine's own default). */
15
+ export declare function resolveSelection(accounts: readonly Account[], defaults: Defaults, sel: RunSelection, room: RoomOf, nowMs: number, models?: (a: Account) => readonly ModelInfo[] | undefined, bound?: readonly AccountId[]): AccountPick;
package/dist/pick.js CHANGED
@@ -1,6 +1,11 @@
1
1
  import { words } from "./words.js";
2
2
  const DAY_MS = 24 * 60 * 60 * 1000;
3
3
  const finite = (n) => typeof n === 'number' && Number.isFinite(n);
4
+ /** The lowercased provider of a well-formed `provider/model` reference; undefined when the reference is malformed. */
5
+ export function providerOf(model) {
6
+ const at = model.indexOf('/');
7
+ return at <= 0 || at === model.length - 1 || /[\s@]/.test(model) ? undefined : model.slice(0, at).toLowerCase();
8
+ }
4
9
  const reset = (r) => r.resetsAt ?? Infinity;
5
10
  const roomWords = (left, span) => words(`room.${span}`, { left: `${Math.round(left)}%` });
6
11
  // "3:40pm", or "Fri 3:40pm" when it isn't today (matches @byokit/accounts' clock).
@@ -35,8 +40,18 @@ function considerRows(accounts, room, nowMs, demand = [], models, provider, name
35
40
  else if (account.billing !== 'subscription')
36
41
  row.out = 'billing';
37
42
  }
43
+ if (!row.out && models) {
44
+ const listed = models(account) ?? [];
45
+ if (listed.length && listed.every((m) => m.why === 'resting')) {
46
+ row.out = 'resting';
47
+ const untils = listed.map((m) => m.until).filter(finite);
48
+ if (untils.length)
49
+ row.until = Math.min(...untils);
50
+ }
51
+ }
38
52
  if (!row.out && demand.length && models) {
39
- const available = models(account);
53
+ const available = models(account) ?? demand.filter((id) => id.split('/')[0].toLowerCase() === account.provider.toLowerCase())
54
+ .map((id) => ({ id, name: id, available: true }));
40
55
  const missing = demand.find((id) => !available.some((m) => m.id === id && m.available));
41
56
  if (missing !== undefined) {
42
57
  row.out = 'model';
@@ -105,7 +120,9 @@ function nameOf(accounts, defaults, sel, bound) {
105
120
  (bound === undefined || bound.includes(defaultAccount.id));
106
121
  return sel.account === 'default' ? (usable ? defaultAccount.id : undefined) : sel.account === 'auto' ? undefined : sel.account;
107
122
  }
108
- /** Resolve once before starting. Without `models`, membership is host-validated and a missing model is ''. */
123
+ /** Resolve once before starting. Without `models`, membership is host-validated and a missing model is ''. A row whose
124
+ * models are unknown (`undefined`) host-validates membership by provider; with no model asked for it runs the saved
125
+ * default, else model '' (no override: the engine's own default). */
109
126
  export function resolveSelection(accounts, defaults, sel, room, nowMs, models, bound) {
110
127
  const demand = [...new Set([...(sel.model ? [sel.model] : []), ...(sel.needs ?? [])])];
111
128
  const defaultAccount = accounts.find((a) => a.id === defaults.account);
@@ -114,10 +131,14 @@ export function resolveSelection(accounts, defaults, sel, room, nowMs, models, b
114
131
  const rows = considerRows(accounts, (a) => room(a, demand), nowMs, demand, models, provider, named, bound);
115
132
  const considered = rows.map((c) => c.row);
116
133
  const auto = rank(rows); // also fills explanations for every eligible row, even on explicit selection
134
+ const restingPick = (c) => ({ ok: false, code: 'resting', account: c.account.id, until: c.row.until,
135
+ reason: words('pick.out.resting', { time: finite(c.row.until) ? clock(c.row.until) : '' }), considered });
117
136
  if (named !== undefined) {
118
137
  const chosen = rows.find((c) => c.account.id === named);
119
138
  if (!chosen)
120
139
  return { ok: false, code: 'unknown_account', reason: words('auto.none', { provider: provider ?? '' }), considered };
140
+ if (chosen.row.out === 'resting')
141
+ return restingPick(chosen);
121
142
  if (chosen.row.out)
122
143
  return { ok: false, code: 'not_included', reason: words(`pick.out.${chosen.row.out}`, { model: chosen.row.missing ?? '' }), considered };
123
144
  return finish(chosen.account, chosen.row, sel.account === 'default' ? 'default' : 'chosen', sel.account === 'default' ? 'default' : 'chosen');
@@ -129,11 +150,19 @@ export function resolveSelection(accounts, defaults, sel, room, nowMs, models, b
129
150
  }
130
151
  else if (auto)
131
152
  return finish(auto.account, auto.row, 'auto', auto.why);
153
+ const eligible = rows.filter((c) => !c.row.out || c.row.out === 'resting');
154
+ const rests = eligible.filter((c) => c.row.out === 'resting');
155
+ if (rests.length)
156
+ return restingPick(rests.reduce((a, b) => ((b.row.until ?? Infinity) < (a.row.until ?? Infinity) ? b : a)));
132
157
  return { ok: false, code: 'none', reason: words('auto.none', { provider: provider ?? '' }), considered };
133
158
  function finish(account, row, how, why) {
134
- const available = models?.(account).filter((m) => m.available);
135
- const model = sel.model ?? (defaults.model && (!available || available.some((m) => m.id === defaults.model)) ? defaults.model : available?.[0]?.id) ?? '';
136
- if (models && !model)
159
+ const listed = models?.(account);
160
+ const ready = listed?.filter((m) => m.available && !m.why);
161
+ const usable = ready?.length ? ready : listed?.filter((m) => m.available && m.why === 'signed_out');
162
+ const saved = defaults.model && (usable ? usable.some((m) => m.id === defaults.model)
163
+ : providerOf(defaults.model) === account.provider.toLowerCase()) ? defaults.model : undefined;
164
+ const model = sel.model ?? saved ?? usable?.[0]?.id ?? '';
165
+ if (listed && !model)
137
166
  return { ok: false, code: 'not_included', reason: words('pick.out.model', { model: sel.model ?? '' }), considered };
138
167
  row.reason = why;
139
168
  const slots = { name: account.name, provider: account.provider };
package/dist/runs.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import type { Bridge } from './bridge.ts';
2
+ import type { createAccounts } from './accounts.ts';
3
+ import type { createLocks } from './locks.ts';
2
4
  import { authStatus } from './auth-status.ts';
3
5
  import { outputSchema, type OutputSchema, type SchemaOutput } from './output.ts';
4
6
  import type { GatewayTransport, Member, RunEnd, RunEvent, RunSpec } from './types.ts';
@@ -18,6 +20,11 @@ export declare function createRuns(ctx: {
18
20
  }>;
19
21
  bridge: Pick<Bridge, 'register'>;
20
22
  tools: ReadonlySet<string>;
23
+ accounts?: Pick<ReturnType<typeof createAccounts>, 'pick' | 'rest'>;
24
+ locks?: ReturnType<typeof createLocks>;
25
+ routes: Map<string, {
26
+ session: string;
27
+ }>;
21
28
  }): {
22
29
  run<const S extends OutputSchema | undefined = undefined>(spec: RunSpec<S>, on?: (e: RunEvent) => void, keyAgent?: string, preparedOutput?: ReturnType<typeof outputSchema>): Promise<RunEnd<SchemaOutput<S>>>;
23
30
  steer(k: string, t: string): Promise<void>;
package/dist/runs.js CHANGED
@@ -5,12 +5,15 @@ import { authStatus } from "./auth-status.js";
5
5
  import { outputSchema } from "./output.js";
6
6
  import { words } from "./words.js";
7
7
  import { classify } from "./classify.js";
8
+ import { providerOf } from "./pick.js";
8
9
  // The gateway gives up on a silent run after an hour; we wait ten minutes longer for the reply frame itself.
9
10
  const WAIT_MS = 3_600_000;
10
11
  const WAIT_CLIENT_MS = 3_610_000;
11
12
  // The `agent` request's final frame lands with the run's end; a run already over waits no longer than this for it.
12
13
  const FINAL_GRACE_MS = 5_000;
13
14
  const STATUS_MS = 5_000;
15
+ // A resting end whose message names no time rests the account this long (5.15 Runs step 7).
16
+ const REST_MS = 300_000;
14
17
  export const USABLE = new Set(['ok', 'expiring', 'static']);
15
18
  const usable = (row) => typeof row === 'string'
16
19
  || (Array.isArray(row.profiles) && row.profiles.length > 0 ? row.profiles.some((p) => USABLE.has(String(p?.status)))
@@ -62,10 +65,10 @@ function planOf(provider, row) {
62
65
  * a session preference it may rotate away from (docs/runtime-kits.md 5.8), so the kit never sends one.
63
66
  */
64
67
  function account(model) {
65
- const at = model.indexOf('/');
66
- if (at <= 0 || at === model.length - 1 || /[\s@]/.test(model))
68
+ const provider = providerOf(model);
69
+ if (provider === undefined)
67
70
  throw new Error(`refused: "${model}" is not a provider/model reference`);
68
- return { provider: model.slice(0, at).toLowerCase(), model: model.slice(at + 1) }; // the engine lowercases ids
71
+ return { provider, model: model.slice(model.indexOf('/') + 1) }; // the engine lowercases ids
69
72
  }
70
73
  export function createRuns(ctx) {
71
74
  // The plan window the engine last read for the run's provider: `models.authStatus` answers from its cache without
@@ -97,11 +100,83 @@ export function createRuns(ctx) {
97
100
  const output = preparedOutput ?? (spec.schema === undefined ? undefined : outputSchema(spec.schema));
98
101
  const system = [spec.system, output?.prompt].filter(Boolean).join('\n\n');
99
102
  const picked = spec.model === undefined ? undefined : account(spec.model);
103
+ for (const need of spec.needs ?? [])
104
+ account(need);
100
105
  for (const tool of spec.tools ?? [])
101
106
  if (!ctx.tools.has(tool))
102
107
  throw new Error(`refused: "${tool}" is not one of this kit's tools`);
103
- const agentId = keyAgent ?? (await ctx.ensure(spec.member)).agentId;
104
- const sessionKey = keyAgent ? `agent:${keyAgent}:${spec.sessionKey.slice(`agent:${spec.member}:`.length)}` : spec.sessionKey;
108
+ const tail = spec.sessionKey.slice(`agent:${spec.member}:`.length);
109
+ // Runs step 3 (5.15): with `account` and `needs` both absent, exactly the run on the member agent it always was.
110
+ if (spec.account === undefined && !spec.needs?.length) {
111
+ const agentId = keyAgent ?? (await ctx.ensure(spec.member)).agentId;
112
+ return start(spec, on, { agentId, sessionKey: keyAgent ? `agent:${keyAgent}:${tail}` : spec.sessionKey, idempotencyKey, system, picked, output });
113
+ }
114
+ if (keyAgent)
115
+ throw new Error('refused: an API-key run uses its own account');
116
+ if (!ctx.accounts || !ctx.locks)
117
+ throw new Error('gateway not ready');
118
+ const accounts = ctx.accounts;
119
+ const locks = ctx.locks;
120
+ const restAccount = async (id, until) => {
121
+ const at = until ?? Date.now() + REST_MS;
122
+ await accounts.rest(spec.member, id, at);
123
+ return at;
124
+ };
125
+ const signedIn = async (agentId, provider) => !!(await ctx.request('byokit.accounts', { action: 'ready', agentId, provider })).ok;
126
+ const skipped = [];
127
+ for (;;) {
128
+ // Step 4: the selection is resolved once per attempt, before any request; `needs` alone resolves as 'default'.
129
+ const { pick, agent } = await accounts.pick(spec.member, { account: spec.account ?? 'default',
130
+ ...(spec.model !== undefined ? { model: spec.model } : {}), ...(spec.needs ? { needs: spec.needs } : {}) }, skipped);
131
+ if (!pick.ok) {
132
+ if (pick.code === 'unknown_account')
133
+ throw new Error(`unknown account: ${spec.account}`); // a caller bug
134
+ if (pick.code === 'resting')
135
+ return { ok: false, kind: 'resting', until: await restAccount(pick.account, pick.until), message: pick.reason };
136
+ return { ok: false, kind: pick.code === 'none' ? 'signed-out' : pick.code === 'not_included' ? 'plan' : 'other', message: pick.reason };
137
+ }
138
+ const sessionKey = `agent:${agent}:${tail}`;
139
+ const automatic = pick.how === 'auto' || pick.why === 'first_ready';
140
+ // An unbound key runs on the picked account's agent, under the app key's tail. The run holds the agent's shared
141
+ // lock from its ready check to its end. An automatic pick not signed in is skipped before any event; an explicit
142
+ // one announces itself, then ends signed-out.
143
+ const end = await locks.shared(agent, async () => {
144
+ let ready;
145
+ try {
146
+ ready = await signedIn(agent, pick.account.provider);
147
+ }
148
+ catch (error) {
149
+ return failure(error instanceof Error ? error.message : String(error));
150
+ }
151
+ if (!ready && automatic)
152
+ return undefined;
153
+ const route = { session: sessionKey };
154
+ ctx.routes.set(spec.sessionKey, route);
155
+ try {
156
+ on?.({ type: 'account', account: pick.account.id, model: pick.model, sessionKey, how: pick.how, why: pick.why, considered: pick.considered });
157
+ if (!ready)
158
+ return { ok: false, kind: 'signed-out', message: `${pick.account.name} is not signed in` };
159
+ return await start(spec, on, { agentId: agent, sessionKey, idempotencyKey, system,
160
+ picked: pick.model ? account(pick.model) : undefined, output, chosen: pick }); // '' = no override
161
+ }
162
+ finally {
163
+ if (ctx.routes.get(spec.sessionKey) === route)
164
+ ctx.routes.delete(spec.sessionKey);
165
+ }
166
+ });
167
+ if (end === undefined) {
168
+ skipped.push(pick.account.id);
169
+ continue;
170
+ }
171
+ // Step 7: a resting end rests the account, so the next Auto skips it until then.
172
+ if (end.ok || !('kind' in end) || end.kind !== 'resting')
173
+ return end;
174
+ const until = await restAccount(pick.account.id, end.until);
175
+ return { ...end, until };
176
+ }
177
+ };
178
+ const start = async (spec, on, t) => {
179
+ const { agentId, sessionKey, idempotencyKey, system, picked, output, chosen } = t;
105
180
  const release = spec.register !== false ? ctx.bridge.register({ sessionKey, member: spec.member }, spec.tools, idempotencyKey) : undefined;
106
181
  let last;
107
182
  let runId = ''; // gateway events for other runs carry a real runId and never match the empty one
@@ -140,7 +215,7 @@ export function createRuns(ctx) {
140
215
  }
141
216
  });
142
217
  try {
143
- if (picked) {
218
+ if (picked && !chosen) {
144
219
  // The picked account must be the one called: check it is signed in for this member before the run, since
145
220
  // the engine would otherwise fail the run only after admitting it. An explicit provider/model is strict on
146
221
  // the pinned engine (never another provider or model), so this check plus the override is the guarantee.
@@ -207,7 +282,10 @@ export function createRuns(ctx) {
207
282
  const usage = usageOf(agentMeta);
208
283
  const planWindow = typeof agentMeta.provider === 'string'
209
284
  ? await planWindowOf(agentId, agentMeta.provider.toLowerCase()) : undefined;
210
- return { ok: true, text, ...(parsed ? { data: parsed.data } : {}), ...(usage ? { usage } : {}), ...(planWindow ? { planWindow } : {}) };
285
+ const reported = typeof agentMeta.provider === 'string' && typeof agentMeta.model === 'string'
286
+ ? `${agentMeta.provider}/${agentMeta.model}`.toLowerCase() : '';
287
+ return { ok: true, text, ...(parsed ? { data: parsed.data } : {}), ...(usage ? { usage } : {}), ...(planWindow ? { planWindow } : {}),
288
+ ...(chosen ? { account: chosen.account.id, model: chosen.model || reported } : {}) };
211
289
  }
212
290
  if (result.stopReason === 'aborted' || (abortedByEngine && result.status !== 'ok'))
213
291
  return { ok: false, aborted: true };
@@ -227,7 +305,7 @@ export function createRuns(ctx) {
227
305
  };
228
306
  return {
229
307
  run,
230
- steer: (k, t) => ctx.request('sessions.steer', { sessionKey: k, message: t }),
231
- abort: async (k) => { await ctx.request('chat.abort', { sessionKey: k }); },
308
+ steer: (k, t) => ctx.request('sessions.steer', { sessionKey: ctx.routes.get(k)?.session ?? k, message: t }),
309
+ abort: async (k) => { await ctx.request('chat.abort', { sessionKey: ctx.routes.get(k)?.session ?? k }); },
232
310
  };
233
311
  }
package/dist/signin.d.ts CHANGED
@@ -8,6 +8,7 @@ export type SignInCtx = {
8
8
  }>;
9
9
  callbackPort: number;
10
10
  onDisconnect?(fn: () => void): () => void;
11
+ fresh?: boolean;
11
12
  };
12
13
  /**
13
14
  * Drive the engine's provider-owned login. `on` sees every view the person watches; `done` resolves once, and
package/dist/signin.js CHANGED
@@ -41,6 +41,16 @@ export function signIn(ctx, member, o, on) {
41
41
  const { signal } = owner;
42
42
  const driveOwner = new AbortController();
43
43
  const wantsCallback = via === 'browser' && o.authChoice === 'openai';
44
+ // The pin builds ChatGPT's authorize URL unsigned, so a fresh sign-in can ask for the login screen itself.
45
+ const shown = (url) => {
46
+ if (!ctx.fresh || !wantsCallback)
47
+ return url;
48
+ const parsed = URL.canParse(url) ? new URL(url) : undefined;
49
+ if (parsed?.origin !== 'https://auth.openai.com' || parsed.pathname !== '/oauth/authorize')
50
+ return url;
51
+ parsed.searchParams.set('prompt', 'login');
52
+ return parsed.toString();
53
+ };
44
54
  let server;
45
55
  let sessionId = '';
46
56
  let starting;
@@ -295,7 +305,7 @@ export function signIn(ctx, member, o, on) {
295
305
  codeTimer = setTimeout(() => { codeExpired = true; owner.abort(); }, duration);
296
306
  const message = deviceCode.message ?? current.message;
297
307
  // The code is on the card; acknowledging it lets the engine poll the provider itself.
298
- say({ ...(deviceCode.code ? { code: deviceCode.code } : {}), ...(current.externalUrl ? { url: current.externalUrl } : {}),
308
+ say({ ...(deviceCode.code ? { code: deviceCode.code } : {}), ...(current.externalUrl ? { url: shown(current.externalUrl) } : {}),
299
309
  ...(timed ? { expiresAt: Date.now() + duration } : {}), ...(message ? { message } : {}) });
300
310
  const next = await answer(current);
301
311
  if (next.done) {
@@ -326,7 +336,7 @@ export function signIn(ctx, member, o, on) {
326
336
  }
327
337
  if (current.type === 'note' || current.type === 'confirm' || current.type === 'select' || current.type === 'action') {
328
338
  if (current.externalUrl)
329
- say({ url: current.externalUrl });
339
+ say({ url: shown(current.externalUrl) });
330
340
  const next = await answer(current);
331
341
  if (next.done) {
332
342
  terminal = next;
@@ -20,6 +20,12 @@ export type FakeGateway = {
20
20
  failNext(method: string, message: string): void;
21
21
  drop(why: string): void;
22
22
  handle(method: string, fn: FakeHandler): void;
23
+ /** Usage windows a later `models.authStatus` read reports for the agent's provider (O14: the Auto input). */
24
+ setUsage(agentId: string, provider: string, windows: {
25
+ label: string;
26
+ usedPercent?: number;
27
+ resetAt?: number;
28
+ }[]): void;
23
29
  } & {
24
30
  transport: GatewayTransport;
25
31
  };
@@ -14,6 +14,13 @@ const DEVICE_STEP = {
14
14
  deviceCode: { code: 'CREW-2026', expiresInMinutes: 15 }, externalUrl: 'https://auth.openai.com/codex/device',
15
15
  };
16
16
  const PROGRESS_STEP = { id: 'step-wait', type: 'progress', message: 'Waiting for authorization', executor: 'gateway' };
17
+ // ChatGPT's browser route: the pin hands its unsigned authorize URL to the next step as `externalUrl`.
18
+ const BROWSER_STEP = {
19
+ id: 'step-browser', type: 'note', executor: 'client', message: 'Open this URL in your LOCAL browser.',
20
+ externalUrl: 'https://auth.openai.com/oauth/authorize?response_type=code&client_id=fake-client&redirect_uri=http%3A%2F%2Flocalhost%3A1455%2Fauth%2Fcallback&state=fake-state',
21
+ };
22
+ // The pin's agents.create into an existing workspace rewrites IDENTITY.md's Name line (mergeIdentityMarkdownContent).
23
+ const IDENTITY_NAME = /^- \*\*Name:\*\*.*$/m;
17
24
  const EVENTS = [
18
25
  'agent',
19
26
  'exec.approval.requested', 'exec.approval.resolved',
@@ -110,7 +117,32 @@ export function fakeGateway(script, o) {
110
117
  const orders = new Map();
111
118
  const models = new Map();
112
119
  const wizards = new Map();
120
+ // Each workspace's files by name, as `agents.files.get/set` and the create-time IDENTITY.md rewrite see them.
121
+ const workspaces = new Map();
122
+ const filesOf = (agentId) => {
123
+ const agent = agents.get(agentId);
124
+ if (!agent)
125
+ throw new Error(`unknown agent id "${agentId}"`);
126
+ const workspace = agent.workspace ?? `workspace-${agentId}`;
127
+ let files = workspaces.get(workspace);
128
+ if (!files) {
129
+ files = new Map();
130
+ workspaces.set(workspace, files);
131
+ }
132
+ return { workspace, files };
133
+ };
134
+ const fileAnswer = (agentId, name) => {
135
+ const { workspace, files } = filesOf(agentId);
136
+ const content = files.get(name);
137
+ return { agentId, workspace, file: { name, path: join(workspace, name), missing: content === undefined,
138
+ ...(content === undefined ? {} : { size: Buffer.byteLength(content), content }) } };
139
+ };
113
140
  const runs = new Map();
141
+ // The engine's own cached read of each provider's usage endpoint (O14): cold until a `models.authStatus`
142
+ // answer has carried the provider once (no `usage` on that first answer), then whatever `setUsage` stored.
143
+ // A pre-sign-in read names no provider, so it warms nothing: the journey's first room read stays cold.
144
+ const usage = new Map();
145
+ const warmed = new Set();
114
146
  const configBox = { hash: 'hash-1', config: {} };
115
147
  let hashSeq = 1;
116
148
  const ensureAgent = (id, workspace) => {
@@ -304,16 +336,41 @@ export function fakeGateway(script, o) {
304
336
  }),
305
337
  'agents.create': (p) => {
306
338
  const id = String(p.name);
339
+ if (agents.has(id))
340
+ throw new Error(`agent "${id}" already exists`);
307
341
  const workspace = p.workspace ? String(p.workspace) : undefined;
342
+ if (workspace) {
343
+ const files = workspaces.get(workspace) ?? new Map();
344
+ workspaces.set(workspace, files);
345
+ const identity = files.get('IDENTITY.md') ?? '# IDENTITY.md - Agent Identity\n\n';
346
+ files.set('IDENTITY.md', IDENTITY_NAME.test(identity) ? identity.replace(IDENTITY_NAME, `- **Name:** ${id}`) : `${identity}- **Name:** ${id}\n`);
347
+ }
308
348
  // With rosterApplyMs the fake mirrors the pinned engine: create answers before the roster hot-reload
309
349
  // makes the agent visible to agents.list (Crewhouse saw ~113 ms).
310
350
  if (o?.rosterApplyMs)
311
351
  setTimeout(() => ensureAgent(id, workspace), o.rosterApplyMs);
312
352
  else
313
353
  ensureAgent(id, workspace);
314
- return { id };
354
+ return { ok: true, agentId: id, name: id, workspace: workspace ?? `workspace-${id}` };
355
+ },
356
+ // deleteFiles: false keeps the workspace, as the kit always asks; the agent's own store and model go.
357
+ 'agents.delete': (p) => {
358
+ const id = String(p.agentId);
359
+ if (!agents.delete(id))
360
+ throw new Error(`agent "${id}" not found`);
361
+ profiles.delete(id);
362
+ orders.delete(id);
363
+ models.delete(id);
364
+ return { ok: true, agentId: id, removedBindings: 0 };
365
+ },
366
+ 'agents.files.get': (p) => fileAnswer(String(p.agentId), String(p.name)),
367
+ 'agents.files.set': (p) => {
368
+ filesOf(String(p.agentId)).files.set(String(p.name), String(p.content));
369
+ return { ok: true, ...fileAnswer(String(p.agentId), String(p.name)) };
315
370
  },
316
371
  // One profile per signed-in provider, `<provider>:default`, as the pin lists an agent's own profiles.
372
+ // Plus its cached usage once warm: cold on the first answer carrying the provider (no `usage`), then the
373
+ // stored windows.
317
374
  'models.authStatus': (p) => {
318
375
  const id = String(p?.agentId ?? 'main');
319
376
  const store = storeOf(id);
@@ -322,12 +379,26 @@ export function fakeGateway(script, o) {
322
379
  providers: (agents.get(id)?.providers ?? []).map((provider) => {
323
380
  const profileId = `${provider}:default`;
324
381
  const credential = store[profileId];
382
+ const key = `${id}\n${provider}`;
383
+ const cold = !warmed.has(key);
384
+ warmed.add(key);
385
+ const windows = cold ? undefined : usage.get(id)?.get(provider);
325
386
  return { provider,
326
387
  profiles: [{ profileId, type: credential?.type ?? 'oauth',
327
- status: credential ? statusOf(credential, now) : 'ok' }] };
388
+ status: credential ? statusOf(credential, now) : 'ok' }],
389
+ ...(windows !== undefined ? { usage: { windows } } : {}) };
328
390
  }),
329
391
  };
330
392
  },
393
+ // The runtime-allowed catalog for the agent's signed-in providers (O14): one available row each. A test
394
+ // scripting exact availability scripts this method with `handle`.
395
+ 'models.list': (p) => {
396
+ const id = String(p?.agentId ?? 'main');
397
+ return {
398
+ models: (agents.get(id)?.providers ?? []).map((provider) => ({ id: `${provider}/default`,
399
+ name: 'Default', provider, available: true })),
400
+ };
401
+ },
331
402
  'models.authLogout': (p) => {
332
403
  const agent = agents.get(String(p?.agentId ?? 'main'));
333
404
  // The pin refuses a profileIds list naming a profile the agent does not hold.
@@ -475,8 +546,11 @@ export function fakeGateway(script, o) {
475
546
  return { ok: true, status: 'ok', modelRef: model };
476
547
  },
477
548
  'openclaw.setup.auth.start': (p) => {
549
+ // The pin's single setup admission: a second start while a session runs is refused.
550
+ if (wizards.size)
551
+ throw new Error('setup is already in progress');
478
552
  const sessionId = String(p.sessionId ?? `fake-${randomUUID()}`);
479
- wizards.set(sessionId, { agentId: String(p.agentId ?? 'main'), answered: false, pulls: 0 });
553
+ wizards.set(sessionId, { agentId: String(p.agentId ?? 'main'), authChoice: String(p.authChoice ?? ''), answered: false, pulls: 0 });
480
554
  return { sessionId, done: false, status: 'running' };
481
555
  },
482
556
  'wizard.next': (p) => {
@@ -488,7 +562,7 @@ export function fakeGateway(script, o) {
488
562
  return { done: false, step: PROGRESS_STEP };
489
563
  }
490
564
  if (!session.answered)
491
- return { done: false, step: DEVICE_STEP };
565
+ return { done: false, step: session.authChoice === 'openai' ? BROWSER_STEP : DEVICE_STEP };
492
566
  if (session.pulls++ === 0)
493
567
  return { done: false, step: PROGRESS_STEP };
494
568
  wizards.delete(String(p.sessionId));
@@ -664,6 +738,14 @@ export function fakeGateway(script, o) {
664
738
  failNext,
665
739
  drop,
666
740
  handle: (method, fn) => { handlers.set(method, fn); },
741
+ setUsage: (agentId, provider, windows) => {
742
+ let perAgent = usage.get(agentId);
743
+ if (!perAgent) {
744
+ perAgent = new Map();
745
+ usage.set(agentId, perAgent);
746
+ }
747
+ perAgent.set(provider, windows);
748
+ },
667
749
  transport: factory({ port: 0, token: 'test', identityPath: '', bridgeSock: '' }),
668
750
  };
669
751
  }
package/dist/types.d.ts CHANGED
@@ -50,6 +50,8 @@ export interface RunSpec<S extends OutputSchema | undefined = OutputSchema | und
50
50
  }[];
51
51
  thinking?: 'off' | 'low' | 'medium' | 'high';
52
52
  model?: string;
53
+ account?: AccountRef | 'default' | 'auto';
54
+ needs?: string[];
53
55
  /** Explicit paid key choice. Uses a separate member-owned agent and separate session history. */
54
56
  auth?: 'apiKey';
55
57
  tools?: string[];
@@ -60,6 +62,14 @@ export type RunEvent = {
60
62
  } | {
61
63
  type: 'thinking';
62
64
  tokens: number;
65
+ } | {
66
+ type: 'account';
67
+ account: AccountId;
68
+ model: string;
69
+ sessionKey: string;
70
+ how: 'chosen' | 'default' | 'auto';
71
+ why: PickWhy;
72
+ considered: Considered[];
63
73
  } | {
64
74
  type: 'text';
65
75
  text: string;
@@ -98,6 +108,8 @@ export type RunEnd<T = unknown> = {
98
108
  data?: T;
99
109
  usage?: RunUsage;
100
110
  planWindow?: PlanWindow;
111
+ account?: AccountId;
112
+ model?: string;
101
113
  } | {
102
114
  ok: false;
103
115
  aborted: true;
@@ -319,6 +331,13 @@ export type AccountPick = {
319
331
  code: 'none' | 'not_included' | 'unknown_account' | 'bound' | 'paid';
320
332
  reason: string;
321
333
  considered: Considered[];
334
+ } | {
335
+ ok: false;
336
+ code: 'resting';
337
+ account: AccountId;
338
+ until?: number;
339
+ reason: string;
340
+ considered: Considered[];
322
341
  };
323
342
  export type MoveResult = {
324
343
  ok: true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byokit/openclaw",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
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",
@@ -62,7 +62,7 @@
62
62
  "dependencies": {
63
63
  "@byokit/pair": "0.10.0",
64
64
  "@byokit/relay": "0.6.1",
65
- "@byokit/discover": "0.8.0",
65
+ "@byokit/discover": "0.8.1",
66
66
  "@byokit/seal": "0.3.0",
67
67
  "@byokit/secrets": "0.5.1",
68
68
  "@openclaw/gateway-client": "2026.8.35",