@byokit/openclaw 0.11.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/link.js CHANGED
@@ -24,29 +24,32 @@ const isRecord = (value) => typeof value === 'object' && value !== null && !Arra
24
24
  const browserTag = (event) => event === 'byokit.browser';
25
25
  const browserEvent = (event, payload) => event === 'byokit.browser' && isRecord(payload) && typeof payload.member === 'string'
26
26
  && (payload.kind === 'state' || payload.kind === 'signin');
27
- const memberKey = (member, sessionKey) => sessionKey.startsWith(`agent:${member}:`);
28
- function memberSessions(result, member) {
27
+ function memberSessions(result, owns) {
29
28
  const rows = Array.isArray(result) ? result : isRecord(result) && Array.isArray(result.sessions) ? result.sessions : [];
30
- return rows.filter((row) => isRecord(row) && typeof row.sessionKey === 'string' && memberKey(member, row.sessionKey));
29
+ return rows.filter((row) => isRecord(row) && typeof row.sessionKey === 'string' && owns(row.sessionKey));
31
30
  }
32
31
  /** `oc.run`'s run options, type-checked; the kit's own checks (account, tool names) still apply. */
33
32
  function runOptions(args) {
34
- const { message, model, auth, system, images, thinking, tools, schema } = args;
33
+ const { message, model, account, needs, auth, system, images, thinking, tools, schema } = args;
35
34
  const ok = typeof message === 'string'
36
35
  && (model === undefined || typeof model === 'string')
36
+ && (account === undefined || typeof account === 'string')
37
+ && (needs === undefined || (Array.isArray(needs) && needs.every((n) => typeof n === 'string')))
37
38
  && (auth === undefined || auth === 'apiKey')
38
39
  && (system === undefined || typeof system === 'string')
39
40
  && (images === undefined || (Array.isArray(images) && images.every((i) => isRecord(i) && typeof i.data === 'string' && typeof i.mimeType === 'string')))
40
41
  && (thinking === undefined || (typeof thinking === 'string' && THINKING.has(thinking)))
41
42
  && (tools === undefined || (Array.isArray(tools) && tools.every((t) => typeof t === 'string')));
42
43
  if (!ok)
43
- throw new Error('oc.run needs { message, sessionKey?, model?, auth?, system?, images?, thinking?, tools?, schema? }');
44
+ throw new Error('oc.run needs { message, sessionKey?, model?, account?, needs?, auth?, system?, images?, thinking?, tools?, schema? }');
44
45
  if (schema !== undefined)
45
46
  outputSchema(schema);
46
47
  return {
47
48
  message: message,
48
49
  ...(schema === undefined ? {} : { schema: schema }),
49
50
  ...(model === undefined ? {} : { model: model }),
51
+ ...(account === undefined ? {} : { account: account }),
52
+ ...(needs === undefined ? {} : { needs: [...needs] }),
50
53
  ...(auth === undefined ? {} : { auth: 'apiKey' }),
51
54
  ...(system === undefined ? {} : { system: system }),
52
55
  ...(images === undefined ? {} : { images: images
@@ -74,6 +77,7 @@ function browserInput(value) {
74
77
  }
75
78
  export function openclawLink(kit, o) {
76
79
  const passThrough = o.passThrough ?? (() => false);
80
+ const memberKey = (member, sessionKey) => kit.memberOf(sessionKey) === member;
77
81
  // The host lifecycle extension is supplied by the browser host. Without it control fails closed.
78
82
  const browser = () => {
79
83
  if (!kit.browser)
@@ -120,10 +124,14 @@ export function openclawLink(kit, o) {
120
124
  if (grant.expires !== undefined) {
121
125
  const timer = setTimeout(() => {
122
126
  const current = seen.get(grant.id);
123
- if (current?.expires !== undefined && current.expires > Date.now())
124
- memberOf(current);
125
- else
126
- void revoke(grant.id).catch(() => { });
127
+ if (current?.expires !== undefined && current.expires > Date.now()) {
128
+ try {
129
+ memberOf(current);
130
+ return;
131
+ }
132
+ catch { /* refused at the boundary: revoke below, never throw from a timer */ }
133
+ }
134
+ void revoke(grant.id).catch(() => { });
127
135
  }, Math.min(grant.expires - Date.now(), 2 ** 31 - 1));
128
136
  timer.unref();
129
137
  expiry.set(grant.id, timer);
@@ -355,7 +363,7 @@ export function openclawLink(kit, o) {
355
363
  }
356
364
  case 'oc.sessions': {
357
365
  const result = await kit.call('sessions.list', {});
358
- return memberSessions(result, member);
366
+ return memberSessions(result, (key) => memberKey(member, key));
359
367
  }
360
368
  case 'oc.steer': {
361
369
  if (typeof args.sessionKey !== 'string' || typeof args.text !== 'string' || (args.auth !== undefined && args.auth !== 'apiKey'))
@@ -584,9 +592,11 @@ export function openclawLink(kit, o) {
584
592
  void s.write(`${JSON.stringify({ event: 'byokit.browser', payload: { member, kind: record.kind } })}\n`).catch(() => { });
585
593
  return;
586
594
  }
595
+ // Attribution (5.15): an `<member>--<hex6>` account agent's events are the member's.
587
596
  const agent = record?.agentId;
588
597
  const key = record?.sessionKey;
589
- if (agent !== member && !(typeof key === 'string' && memberKey(member, key)))
598
+ if (!(typeof agent === 'string' && kit.memberOf(agent) === member)
599
+ && !(typeof key === 'string' && memberKey(member, key)))
590
600
  return;
591
601
  void s.write(`${JSON.stringify({ event, payload })}\n`).catch(() => { });
592
602
  });
package/dist/routes.d.ts CHANGED
@@ -17,5 +17,6 @@ export type RouteView = Route & {
17
17
  export declare function routes(facts?: RouteFacts): RouteView[];
18
18
  /** Retain explicit legacy selectors alongside corrected discovery metadata. A native CLI selector still
19
19
  * reaches engine detection (which checks its own login); it does not make an unavailable route default.
20
+ * An offered plan key is served whichever way was asked: it is always the pasted key.
20
21
  * API billing is never picked as a fallback. */
21
22
  export declare function routeFor(provider: string, via: 'browser' | 'code'): Route | undefined;
package/dist/routes.js CHANGED
@@ -41,9 +41,11 @@ export function routes(facts = {}) {
41
41
  }
42
42
  /** Retain explicit legacy selectors alongside corrected discovery metadata. A native CLI selector still
43
43
  * reaches engine detection (which checks its own login); it does not make an unavailable route default.
44
+ * An offered plan key is served whichever way was asked: it is always the pasted key.
44
45
  * API billing is never picked as a fallback. */
45
46
  export function routeFor(provider, via) {
46
- return table.find(route => route.billing === 'subscription' && route.offerPolicy === 'default'
47
- && ((route.offer && route.provider === provider && route.via === via)
48
- || (route.legacy?.provider === provider && route.legacy.via === via)));
47
+ const served = table.filter(route => route.billing === 'subscription' && route.offerPolicy === 'default');
48
+ return served.find(route => (route.offer && route.provider === provider && route.via === via)
49
+ || (route.legacy?.provider === provider && route.legacy.via === via))
50
+ ?? served.find(route => route.offer && route.provider === provider && route.keyEntry);
49
51
  }
package/dist/routes.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "prerequisite": null,
9
9
  "offer": false,
10
10
  "offerPolicy": "explicit",
11
- "reason": "Alibaba Model Studio: API key (billed per use).",
11
+ "reason": "The pinned Gateway has no app-guided setup for this choice.",
12
12
  "source": "extensions/alibaba/openclaw.plugin.json (2026.8.1)",
13
13
  "revision": "2026.8.1",
14
14
  "checked": "2026-10-02",
@@ -33,7 +33,7 @@
33
33
  "id": "alibaba",
34
34
  "method": "api-key",
35
35
  "revision": "ea806575e6450e4d1efdfc72c19f04be982a1b9b",
36
- "flow": "present"
36
+ "flow": "absent"
37
37
  }
38
38
  },
39
39
  {
@@ -176,7 +176,7 @@
176
176
  "prerequisite": null,
177
177
  "offer": false,
178
178
  "offerPolicy": "explicit",
179
- "reason": "The pinned Gateway has no app-guided sign-in for this choice.",
179
+ "reason": "The pinned Gateway has no app-guided setup for this choice.",
180
180
  "source": "extensions/anthropic/openclaw.plugin.json (2026.8.1)",
181
181
  "revision": "2026.8.1",
182
182
  "checked": "2026-10-02",
@@ -610,7 +610,7 @@
610
610
  "prerequisite": null,
611
611
  "offer": false,
612
612
  "offerPolicy": "explicit",
613
- "reason": "ClawRouter: API key (billed per use).",
613
+ "reason": "The pinned Gateway has no app-guided setup for this choice.",
614
614
  "source": "extensions/clawrouter/openclaw.plugin.json (2026.8.1)",
615
615
  "revision": "2026.8.1",
616
616
  "checked": "2026-10-02",
@@ -635,7 +635,7 @@
635
635
  "id": "clawrouter",
636
636
  "method": "api-key",
637
637
  "revision": "ea806575e6450e4d1efdfc72c19f04be982a1b9b",
638
- "flow": "present"
638
+ "flow": "absent"
639
639
  }
640
640
  },
641
641
  {
@@ -647,7 +647,7 @@
647
647
  "prerequisite": null,
648
648
  "offer": false,
649
649
  "offerPolicy": "explicit",
650
- "reason": "Cloudflare AI Gateway: API key (billed per use).",
650
+ "reason": "The pinned Gateway has no app-guided setup for this choice.",
651
651
  "source": "extensions/cloudflare-ai-gateway/openclaw.plugin.json (2026.8.1)",
652
652
  "revision": "2026.8.1",
653
653
  "checked": "2026-10-02",
@@ -681,7 +681,7 @@
681
681
  "id": "cloudflare-ai-gateway",
682
682
  "method": "api-key",
683
683
  "revision": "ea806575e6450e4d1efdfc72c19f04be982a1b9b",
684
- "flow": "present"
684
+ "flow": "absent"
685
685
  }
686
686
  },
687
687
  {
@@ -739,7 +739,7 @@
739
739
  "prerequisite": null,
740
740
  "offer": false,
741
741
  "offerPolicy": "explicit",
742
- "reason": "ComfyUI: API key (billed per use).",
742
+ "reason": "The pinned Gateway has no app-guided setup for this choice.",
743
743
  "source": "extensions/comfy/openclaw.plugin.json (2026.8.1)",
744
744
  "revision": "2026.8.1",
745
745
  "checked": "2026-10-02",
@@ -773,7 +773,7 @@
773
773
  "id": "comfy",
774
774
  "method": "cloud-api-key",
775
775
  "revision": "ea806575e6450e4d1efdfc72c19f04be982a1b9b",
776
- "flow": "present"
776
+ "flow": "absent"
777
777
  }
778
778
  },
779
779
  {
@@ -785,7 +785,7 @@
785
785
  "prerequisite": null,
786
786
  "offer": false,
787
787
  "offerPolicy": "explicit",
788
- "reason": "Billing must be supplied by the host; a server address does not determine billing.",
788
+ "reason": "The pinned Gateway has no app-guided setup for this choice.",
789
789
  "source": "extensions/copilot-proxy/openclaw.plugin.json (2026.8.1)",
790
790
  "revision": "2026.8.1",
791
791
  "checked": "2026-10-02",
@@ -807,7 +807,7 @@
807
807
  "id": "copilot-proxy",
808
808
  "method": "local",
809
809
  "revision": "ea806575e6450e4d1efdfc72c19f04be982a1b9b",
810
- "flow": "present"
810
+ "flow": "absent"
811
811
  }
812
812
  },
813
813
  {
@@ -911,7 +911,7 @@
911
911
  "prerequisite": null,
912
912
  "offer": false,
913
913
  "offerPolicy": "explicit",
914
- "reason": "fal: API key (billed per use).",
914
+ "reason": "The pinned Gateway has no app-guided setup for this choice.",
915
915
  "source": "extensions/fal/openclaw.plugin.json (2026.8.1)",
916
916
  "revision": "2026.8.1",
917
917
  "checked": "2026-10-02",
@@ -936,7 +936,7 @@
936
936
  "id": "fal",
937
937
  "method": "api-key",
938
938
  "revision": "ea806575e6450e4d1efdfc72c19f04be982a1b9b",
939
- "flow": "present"
939
+ "flow": "absent"
940
940
  }
941
941
  },
942
942
  {
@@ -1585,7 +1585,7 @@
1585
1585
  "prerequisite": null,
1586
1586
  "offer": false,
1587
1587
  "offerPolicy": "explicit",
1588
- "reason": "LongCat: API key (billed per use).",
1588
+ "reason": "The pinned Gateway has no app-guided setup for this choice.",
1589
1589
  "source": "extensions/longcat/openclaw.plugin.json (2026.8.1)",
1590
1590
  "revision": "2026.8.1",
1591
1591
  "checked": "2026-10-02",
@@ -1619,7 +1619,7 @@
1619
1619
  "id": "longcat",
1620
1620
  "method": "api-key",
1621
1621
  "revision": "ea806575e6450e4d1efdfc72c19f04be982a1b9b",
1622
- "flow": "present"
1622
+ "flow": "absent"
1623
1623
  }
1624
1624
  },
1625
1625
  {
@@ -1677,7 +1677,7 @@
1677
1677
  "prerequisite": null,
1678
1678
  "offer": false,
1679
1679
  "offerPolicy": "explicit",
1680
- "reason": "Microsoft Foundry: Cloud credentials (billed per use).",
1680
+ "reason": "The pinned Gateway has no app-guided setup for this choice.",
1681
1681
  "source": "extensions/microsoft-foundry/openclaw.plugin.json (2026.8.1)",
1682
1682
  "revision": "2026.8.1",
1683
1683
  "checked": "2026-10-02",
@@ -1699,7 +1699,7 @@
1699
1699
  "id": "microsoft-foundry",
1700
1700
  "method": "entra-id",
1701
1701
  "revision": "ea806575e6450e4d1efdfc72c19f04be982a1b9b",
1702
- "flow": "present"
1702
+ "flow": "absent"
1703
1703
  }
1704
1704
  },
1705
1705
  {
@@ -1711,7 +1711,7 @@
1711
1711
  "prerequisite": null,
1712
1712
  "offer": false,
1713
1713
  "offerPolicy": "explicit",
1714
- "reason": "Microsoft Foundry: API key (billed per use).",
1714
+ "reason": "The pinned Gateway has no app-guided setup for this choice.",
1715
1715
  "source": "extensions/microsoft-foundry/openclaw.plugin.json (2026.8.1)",
1716
1716
  "revision": "2026.8.1",
1717
1717
  "checked": "2026-10-02",
@@ -1736,7 +1736,7 @@
1736
1736
  "id": "microsoft-foundry",
1737
1737
  "method": "api-key",
1738
1738
  "revision": "ea806575e6450e4d1efdfc72c19f04be982a1b9b",
1739
- "flow": "present"
1739
+ "flow": "absent"
1740
1740
  }
1741
1741
  },
1742
1742
  {
@@ -2458,7 +2458,7 @@
2458
2458
  "prerequisite": null,
2459
2459
  "offer": false,
2460
2460
  "offerPolicy": "explicit",
2461
- "reason": "PixVerse: API key (billed per use).",
2461
+ "reason": "The pinned Gateway has no app-guided setup for this choice.",
2462
2462
  "source": "extensions/pixverse/openclaw.plugin.json (2026.8.1)",
2463
2463
  "revision": "2026.8.1",
2464
2464
  "checked": "2026-10-02",
@@ -2492,7 +2492,7 @@
2492
2492
  "id": "pixverse",
2493
2493
  "method": "api-key",
2494
2494
  "revision": "ea806575e6450e4d1efdfc72c19f04be982a1b9b",
2495
- "flow": "present"
2495
+ "flow": "absent"
2496
2496
  }
2497
2497
  },
2498
2498
  {
@@ -2838,7 +2838,7 @@
2838
2838
  "prerequisite": null,
2839
2839
  "offer": false,
2840
2840
  "offerPolicy": "explicit",
2841
- "reason": "Runway: API key (billed per use).",
2841
+ "reason": "The pinned Gateway has no app-guided setup for this choice.",
2842
2842
  "source": "extensions/runway/openclaw.plugin.json (2026.8.1)",
2843
2843
  "revision": "2026.8.1",
2844
2844
  "checked": "2026-10-02",
@@ -2863,7 +2863,7 @@
2863
2863
  "id": "runway",
2864
2864
  "method": "api-key",
2865
2865
  "revision": "ea806575e6450e4d1efdfc72c19f04be982a1b9b",
2866
- "flow": "present"
2866
+ "flow": "absent"
2867
2867
  }
2868
2868
  },
2869
2869
  {
@@ -2875,7 +2875,7 @@
2875
2875
  "prerequisite": null,
2876
2876
  "offer": false,
2877
2877
  "offerPolicy": "explicit",
2878
- "reason": "Local runtime; explicitly select this route.",
2878
+ "reason": "The pinned Gateway has no app-guided setup for this choice.",
2879
2879
  "source": "extensions/sglang/openclaw.plugin.json (2026.8.1)",
2880
2880
  "revision": "2026.8.1",
2881
2881
  "checked": "2026-10-02",
@@ -2897,7 +2897,7 @@
2897
2897
  "id": "sglang",
2898
2898
  "method": "custom",
2899
2899
  "revision": "ea806575e6450e4d1efdfc72c19f04be982a1b9b",
2900
- "flow": "present"
2900
+ "flow": "absent"
2901
2901
  }
2902
2902
  },
2903
2903
  {
@@ -3360,7 +3360,7 @@
3360
3360
  "prerequisite": null,
3361
3361
  "offer": false,
3362
3362
  "offerPolicy": "explicit",
3363
- "reason": "Local runtime; explicitly select this route.",
3363
+ "reason": "The pinned Gateway has no app-guided setup for this choice.",
3364
3364
  "source": "extensions/vllm/openclaw.plugin.json (2026.8.1)",
3365
3365
  "revision": "2026.8.1",
3366
3366
  "checked": "2026-10-02",
@@ -3382,7 +3382,7 @@
3382
3382
  "id": "vllm",
3383
3383
  "method": "custom",
3384
3384
  "revision": "ea806575e6450e4d1efdfc72c19f04be982a1b9b",
3385
- "flow": "present"
3385
+ "flow": "absent"
3386
3386
  }
3387
3387
  },
3388
3388
  {
@@ -3440,7 +3440,7 @@
3440
3440
  "prerequisite": null,
3441
3441
  "offer": false,
3442
3442
  "offerPolicy": "explicit",
3443
- "reason": "Vydra: API key (billed per use).",
3443
+ "reason": "The pinned Gateway has no app-guided setup for this choice.",
3444
3444
  "source": "extensions/vydra/openclaw.plugin.json (2026.8.1)",
3445
3445
  "revision": "2026.8.1",
3446
3446
  "checked": "2026-10-02",
@@ -3474,7 +3474,7 @@
3474
3474
  "id": "vydra",
3475
3475
  "method": "api-key",
3476
3476
  "revision": "ea806575e6450e4d1efdfc72c19f04be982a1b9b",
3477
- "flow": "present"
3477
+ "flow": "absent"
3478
3478
  }
3479
3479
  },
3480
3480
  {
@@ -4005,7 +4005,7 @@
4005
4005
  "prerequisite": null,
4006
4006
  "offer": false,
4007
4007
  "offerPolicy": "explicit",
4008
- "reason": "Billing must be supplied by the host; a server address does not determine billing.",
4008
+ "reason": "The pinned Gateway has no app-guided setup for this choice.",
4009
4009
  "source": "src/commands/auth-choice-options.static.ts (2026.8.1)",
4010
4010
  "revision": "2026.8.1",
4011
4011
  "checked": "2026-10-02",
@@ -4030,7 +4030,7 @@
4030
4030
  "id": "custom",
4031
4031
  "method": "custom",
4032
4032
  "revision": "ea806575e6450e4d1efdfc72c19f04be982a1b9b",
4033
- "flow": "present"
4033
+ "flow": "absent"
4034
4034
  }
4035
4035
  }
4036
4036
  ]
package/dist/runs.d.ts CHANGED
@@ -20,7 +20,7 @@ export declare function createRuns(ctx: {
20
20
  }>;
21
21
  bridge: Pick<Bridge, 'register'>;
22
22
  tools: ReadonlySet<string>;
23
- accounts?: Pick<ReturnType<typeof createAccounts>, 'pick' | 'rest'>;
23
+ accounts?: Pick<ReturnType<typeof createAccounts>, 'bind' | 'keyRefusal' | 'pick' | 'rest'>;
24
24
  locks?: ReturnType<typeof createLocks>;
25
25
  routes: Map<string, {
26
26
  session: string;
package/dist/runs.js CHANGED
@@ -90,8 +90,9 @@ export function createRuns(ctx) {
90
90
  return { ok: false, ...classified, message };
91
91
  };
92
92
  const run = async (spec, on, keyAgent, preparedOutput) => {
93
- // Member boundary first: a member never speaks in another member's session, refused before any request.
94
- if (!spec.sessionKey.startsWith(`agent:${spec.member}:`))
93
+ // Member boundary first: a member never speaks in another member's session, refused before any request. A key
94
+ // of one of the member's account agents is the member's too; binding below checks the index names it.
95
+ if (!spec.sessionKey.startsWith(`agent:${spec.member}:`) && !(ctx.accounts && !keyAgent && spec.sessionKey.startsWith(`agent:${spec.member}--`)))
95
96
  throw new Error(`refused: "${spec.sessionKey}" is not a session of member "${spec.member}"`);
96
97
  // Same validation as the pin's NonEmptyString; preserve the caller's bytes (including whitespace).
97
98
  if (spec.idempotencyKey !== undefined && (typeof spec.idempotencyKey !== 'string' || !spec.idempotencyKey.length))
@@ -105,11 +106,29 @@ export function createRuns(ctx) {
105
106
  for (const tool of spec.tools ?? [])
106
107
  if (!ctx.tools.has(tool))
107
108
  throw new Error(`refused: "${tool}" is not one of this kit's tools`);
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 });
109
+ const tail = spec.sessionKey.slice(spec.sessionKey.indexOf(':', 'agent:'.length) + 1);
110
+ // Runs step 1 (5.15): every run first finds the account its session is bound to, the API-key lane included.
111
+ const binding = ctx.accounts ? await ctx.accounts.bind(spec.member, spec.sessionKey) : undefined;
112
+ // Step 2 on the API-key lane: only an API-billed conversation continues on its key; any other binding is refused.
113
+ if (keyAgent && ctx.accounts && binding) {
114
+ const message = await ctx.accounts.keyRefusal(spec.member, binding);
115
+ if (message !== undefined)
116
+ return { ok: false, kind: 'other', message };
117
+ }
118
+ // A run that waited for its agent's lock across a move (5.15) binds again under it: a changed binding means its
119
+ // session went to another account, so it ends before any event and the caller's retry lands there.
120
+ const moved = async () => !!binding && (await ctx.accounts.bind(spec.member, spec.sessionKey))?.agentId !== binding.agentId;
121
+ const movedEnd = { ok: false, kind: 'other', message: words('run.moved') };
122
+ // Step 3: with `account` and `needs` both absent, on a key no account agent holds (or an API-key run, whose
123
+ // binding is API-billed), exactly the run on the agent it always was (a bound key under its shared lock).
124
+ if ((keyAgent || binding?.id === undefined) && spec.account === undefined && !spec.needs?.length) {
125
+ if (keyAgent)
126
+ return start(spec, on, { agentId: keyAgent, sessionKey: `agent:${keyAgent}:${tail}`, idempotencyKey, system, picked, output });
127
+ const { agentId } = await ctx.ensure(spec.member);
128
+ const member = () => start(spec, on, { agentId, sessionKey: spec.sessionKey, idempotencyKey, system, picked, output });
129
+ if (!binding || !ctx.locks)
130
+ return member();
131
+ return ctx.locks.shared(agentId, async () => await moved() ? movedEnd : member());
113
132
  }
114
133
  if (keyAgent)
115
134
  throw new Error('refused: an API-key run uses its own account');
@@ -125,9 +144,10 @@ export function createRuns(ctx) {
125
144
  const signedIn = async (agentId, provider) => !!(await ctx.request('byokit.accounts', { action: 'ready', agentId, provider })).ok;
126
145
  const skipped = [];
127
146
  for (;;) {
128
- // Step 4: the selection is resolved once per attempt, before any request; `needs` alone resolves as 'default'.
147
+ // Steps 2 and 4: the selection is resolved once per attempt, before any request, narrowed by the binding; `needs`
148
+ // alone resolves as 'default'. A refusal (`bound`, `paid`) ends the run with its words before any room, model or agent request.
129
149
  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);
150
+ ...(spec.model !== undefined ? { model: spec.model } : {}), ...(spec.needs ? { needs: spec.needs } : {}) }, skipped, binding);
131
151
  if (!pick.ok) {
132
152
  if (pick.code === 'unknown_account')
133
153
  throw new Error(`unknown account: ${spec.account}`); // a caller bug
@@ -137,10 +157,12 @@ export function createRuns(ctx) {
137
157
  }
138
158
  const sessionKey = `agent:${agent}:${tail}`;
139
159
  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
160
+ // The run goes to the picked account's agent, under the key's tail. The run holds the agent's shared
141
161
  // lock from its ready check to its end. An automatic pick not signed in is skipped before any event; an explicit
142
162
  // one announces itself, then ends signed-out.
143
163
  const end = await locks.shared(agent, async () => {
164
+ if (await moved())
165
+ return movedEnd;
144
166
  let ready;
145
167
  try {
146
168
  ready = await signedIn(agent, pick.account.provider);
package/dist/signin.d.ts CHANGED
@@ -8,6 +8,8 @@ export type SignInCtx = {
8
8
  }>;
9
9
  callbackPort: number;
10
10
  onDisconnect?(fn: () => void): () => void;
11
+ authConfig(): string;
12
+ restarted(authBefore: string | undefined, signal?: AbortSignal): Promise<boolean>;
11
13
  fresh?: boolean;
12
14
  };
13
15
  /**
package/dist/signin.js CHANGED
@@ -2,7 +2,9 @@
2
2
  // sign-in; OAuth credentials stay in the engine/CLI. Explicit API keys go only to engine activation (D11).
3
3
  import { randomUUID } from 'node:crypto';
4
4
  import { createServer } from 'node:http';
5
+ import { activate, LATE_RESTART } from "./activation.js";
5
6
  import { authStatus } from "./auth-status.js";
7
+ import { routes } from "./routes.js";
6
8
  import { signedInProviders } from "./runs.js";
7
9
  import { words } from "./words.js";
8
10
  // The pin's wizard contract (2026.8.1): steps arrive only from `wizard.next` (wizard.status answers {status, error}
@@ -15,6 +17,8 @@ const CANCEL_MS = 10_000;
15
17
  const RECONFIRM_MS = 60_000;
16
18
  const TURNS = 200;
17
19
  const PASTE_MS = 15 * 60_000;
20
+ const KEY_UNAVAILABLE = 'This key type is not available on this engine.';
21
+ const KEY_FAILED = 'API key activation failed';
18
22
  // Two of the pin's code routes print their code only into a note's text ("Code: X" with
19
23
  // "Code expires in N minutes", or "enter the code X." with "Expires at: <ISO>"), never as `deviceCode`.
20
24
  // Read it back so every code route reaches the caller in the one structured shape.
@@ -58,6 +62,7 @@ export function signIn(ctx, member, o, on) {
58
62
  let settleStart;
59
63
  const startingSettled = new Promise((resolve) => { settleStart = resolve; });
60
64
  let over = false;
65
+ let activating = false;
61
66
  let recovering = false;
62
67
  let offDisconnect;
63
68
  let agentId;
@@ -92,13 +97,21 @@ export function signIn(ctx, member, o, on) {
92
97
  let pasteIn;
93
98
  let returned;
94
99
  // Once a secret step is seen, no later gateway prose, link or code is safe to display.
95
- let sensitive = o.authChoice === 'setup-token' || o.authChoice === 'apiKey';
96
- const secretFailure = () => o.authChoice === 'apiKey' ? 'API key activation failed' : 'Sign-in failed. Try again.';
100
+ let sensitive = o.authChoice === 'setup-token';
101
+ const secretFailure = 'Sign-in failed. Try again.';
102
+ // A pasted key is scrubbed from every failure on top of the engine's own redaction.
103
+ const keyRoute = routes().find((r) => r.choice === o.authChoice && r.keyEntry);
104
+ let secret;
105
+ const clean = (value) => {
106
+ const text = value instanceof Error ? value.message : String(value);
107
+ return cut(secret ? text.split(secret).join('…') : text);
108
+ };
109
+ const keyFailed = (status) => status === 'unavailable' ? KEY_UNAVAILABLE : KEY_FAILED;
97
110
  const done = new Promise((resolve) => { settle = resolve; });
98
111
  const say = (v) => {
99
112
  if (!over && !recovering && !signal.aborted)
100
113
  on({ state: 'waiting', via, ...(sensitive
101
- ? { ...(v.prompt ? { prompt: v.prompt } : {}), ...(v.error ? { error: secretFailure() } : {}) }
114
+ ? { ...(v.prompt ? { prompt: v.prompt } : {}), ...(v.error ? { error: secretFailure } : {}) }
102
115
  : v) });
103
116
  };
104
117
  const closeCallback = async () => {
@@ -119,7 +132,7 @@ export function signIn(ctx, member, o, on) {
119
132
  pasteIn = undefined;
120
133
  await closeCallback();
121
134
  if (sensitive && view.state === 'failed' && view.why === 'failed')
122
- view = { ...view, error: secretFailure() };
135
+ view = { ...view, error: secretFailure };
123
136
  on(view);
124
137
  settle(view);
125
138
  };
@@ -144,11 +157,13 @@ export function signIn(ctx, member, o, on) {
144
157
  };
145
158
  const cancel = () => {
146
159
  // Stop the drive now; the start itself is never aborted, and release waits for it to settle before cancelling,
147
- // so the gateway's one setup admission is freed by a cancel that the engine can actually find.
160
+ // so the gateway's one setup admission is freed by a cancel that the engine can actually find. An activation in
161
+ // flight is not finished here: the drive returns once the engine answers, so cleanup follows the engine's answer.
148
162
  if (signal.aborted)
149
163
  return;
150
164
  owner.abort();
151
- void (async () => { await release(); await finish(cancelled()); })();
165
+ void (async () => { await release(); if (!activating)
166
+ await finish(cancelled()); })();
152
167
  };
153
168
  o.signal?.addEventListener('abort', cancel, { once: true });
154
169
  if (o.signal?.aborted)
@@ -198,7 +213,9 @@ export function signIn(ctx, member, o, on) {
198
213
  }
199
214
  // Native login is completed by the person in unmodified Claude Code. The pin does not expose this
200
215
  // choice via auth.start; activation verifies the CLI route and persists it only after a live test succeeds.
201
- if (o.authChoice === 'anthropic-cli' || o.authChoice === 'apiKey') {
216
+ // A key-entry choice (API key or plan key) is not an auth.start login either: the pin's Gateway sets it up only
217
+ // from the pasted key through typed activation, and refuses it there ("provider setup is not available").
218
+ if (o.authChoice === 'anthropic-cli' || keyRoute) {
202
219
  settleStart();
203
220
  let apiKey;
204
221
  if (o.authChoice === 'anthropic-cli') {
@@ -207,7 +224,7 @@ export function signIn(ctx, member, o, on) {
207
224
  return { state: 'failed', via, why: 'failed', error: 'Sign in with Claude Code on this machine in the kit’s isolated HOME, then try again. The login stays in Claude Code.' };
208
225
  }
209
226
  else {
210
- say({ prompt: 'API key (billed per use)' });
227
+ say({ prompt: keyRoute?.billing === 'api' ? 'API key (billed per use)' : 'Plan key' });
211
228
  apiKey = returned ?? await new Promise((resolve) => {
212
229
  const stop = (value) => { clearTimeout(timer); signal.removeEventListener('abort', abort); pasteIn = undefined; resolve(value); };
213
230
  const abort = () => stop();
@@ -220,16 +237,23 @@ export function signIn(ctx, member, o, on) {
220
237
  returned = undefined;
221
238
  if (!apiKey)
222
239
  return { state: 'failed', via, why: signal.aborted ? 'declined' : 'expired' };
240
+ secret = apiKey.trim() || undefined;
223
241
  }
224
242
  if (signal.aborted)
225
243
  return { state: 'failed', via, why: 'declined' };
226
- const result = await ctx.request('openclaw.setup.activate', {
244
+ // Done waits for the gateway restart an `auth` change causes (activation.ts); a run started before it runs on the
245
+ // old config. A restart that runs late or is cancelled ends failed, and the account is cleaned up by the caller.
246
+ activating = true;
247
+ const result = await activate(ctx, {
227
248
  agentId, kind: apiKey === undefined ? 'claude-cli' : 'api-key',
228
- ...(apiKey === undefined ? {} : { authChoice: 'apiKey', apiKey }),
229
- }, { timeoutMs: PULL_MS, signal });
230
- // API secrets must never be echoed in a view, even if the gateway returns an error containing one.
231
- return result.ok ? { state: 'done', via } : { state: 'failed', via, why: 'failed',
232
- error: apiKey === undefined ? cut(result.error ?? 'Claude Code activation failed') : 'API key activation failed' };
249
+ ...(apiKey === undefined ? {} : { authChoice: o.authChoice, apiKey }),
250
+ }, { timeoutMs: PULL_MS, signal }).finally(() => { activating = false; });
251
+ if (result.late)
252
+ return signal.aborted ? cancelled() : { state: 'failed', via, why: 'failed', error: LATE_RESTART };
253
+ if (!result.ok)
254
+ return { state: 'failed', via, why: 'failed',
255
+ error: apiKey === undefined ? clean(result.error ?? 'Claude Code activation failed') : keyFailed(result.status) };
256
+ return signal.aborted ? cancelled() : { state: 'done', via };
233
257
  }
234
258
  // The client chooses the wizard id; cancellation waits for the engine to register it.
235
259
  sessionId = `byokit-${randomUUID()}`;
@@ -428,7 +452,7 @@ export function signIn(ctx, member, o, on) {
428
452
  else if (BUSY.test(cut(error)))
429
453
  view = { state: 'failed', via, why: 'busy' };
430
454
  else
431
- view = { state: 'failed', via, why: 'failed', error: o.authChoice === 'apiKey' ? 'API key activation failed' : cut(error) };
455
+ view = { state: 'failed', via, why: 'failed', error: keyRoute ? KEY_FAILED : clean(error) };
432
456
  }
433
457
  settleStart();
434
458
  if (view.state !== 'done')
@@ -26,6 +26,25 @@ export type FakeGateway = {
26
26
  usedPercent?: number;
27
27
  resetAt?: number;
28
28
  }[]): void;
29
+ /** Profile fields every later wizard sign-in stores (O14 identity): `accountId: undefined` signs in with no account
30
+ * id, so no `who`. With no call, or `signInAs()`, the account id is `fake-<agentId>`. */
31
+ signInAs(fields?: {
32
+ accountId?: string;
33
+ email?: string;
34
+ expires?: number;
35
+ }): void;
36
+ /** A sealed profile already in an agent's store, as a sign-in of another provider leaves it; the agent's model points at it. */
37
+ seedProfile(agentId: string, profileId: string, profile: {
38
+ provider: string;
39
+ type: string;
40
+ } & Record<string, unknown>): void;
41
+ /** A late engine refresh racing a removal (O14 S8): writes one profile of `provider` onto `agentId` whole, as the
42
+ * pin would under its store lock. With `order: false` it lands with no order entry; `expires` (default: valid)
43
+ * may name an expired credential. */
44
+ refresh(agentId: string, provider: string, o?: {
45
+ order?: boolean;
46
+ expires?: number;
47
+ }): void;
29
48
  } & {
30
49
  transport: GatewayTransport;
31
50
  };