@byokit/openclaw 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/README.md +45 -7
- package/SECURITY.md +3 -2
- package/dist/accounts.d.ts +45 -1
- package/dist/accounts.js +641 -46
- package/dist/activation.d.ts +17 -0
- package/dist/activation.js +9 -0
- package/dist/approvals.d.ts +2 -0
- package/dist/approvals.js +9 -9
- package/dist/attribution.d.ts +3 -0
- package/dist/attribution.js +25 -0
- package/dist/bridge.d.ts +3 -1
- package/dist/bridge.js +4 -6
- package/dist/config.js +10 -0
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +2 -0
- package/dist/device.d.ts +5 -2
- package/dist/device.js +11 -1
- package/dist/engine.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/keys.d.ts +19 -5
- package/dist/keys.js +135 -23
- package/dist/kit.d.ts +38 -1
- package/dist/kit.js +153 -28
- package/dist/link.js +21 -11
- package/dist/members.d.ts +3 -0
- package/dist/members.js +13 -9
- package/dist/pick.d.ts +7 -3
- package/dist/pick.js +34 -5
- package/dist/routes.d.ts +1 -0
- package/dist/routes.js +5 -3
- package/dist/routes.json +31 -31
- package/dist/runs.d.ts +7 -0
- package/dist/runs.js +111 -11
- package/dist/signin.d.ts +3 -0
- package/dist/signin.js +51 -17
- package/dist/testing/fake-gateway.d.ts +18 -0
- package/dist/testing/fake-gateway.js +210 -16
- package/dist/testing/model-stub.d.ts +1 -0
- package/dist/testing/model-stub.js +2 -1
- package/dist/types.d.ts +19 -0
- package/dist/words.json +8 -1
- package/package.json +2 -2
- package/plugin/accounts.js +12 -3
- package/plugin/catalog.js +48 -0
- package/plugin/index.js +2 -1
- package/plugin/keys.js +22 -4
package/dist/routes.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"prerequisite": null,
|
|
9
9
|
"offer": false,
|
|
10
10
|
"offerPolicy": "explicit",
|
|
11
|
-
"reason": "
|
|
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": "
|
|
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
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
4033
|
+
"flow": "absent"
|
|
4034
4034
|
}
|
|
4035
4035
|
}
|
|
4036
4036
|
]
|
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>, 'bind' | 'keyRefusal' | '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
|
|
66
|
-
if (
|
|
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(
|
|
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
|
|
@@ -87,8 +90,9 @@ export function createRuns(ctx) {
|
|
|
87
90
|
return { ok: false, ...classified, message };
|
|
88
91
|
};
|
|
89
92
|
const run = async (spec, on, keyAgent, preparedOutput) => {
|
|
90
|
-
// Member boundary first: a member never speaks in another member's session, refused before any request.
|
|
91
|
-
|
|
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}--`)))
|
|
92
96
|
throw new Error(`refused: "${spec.sessionKey}" is not a session of member "${spec.member}"`);
|
|
93
97
|
// Same validation as the pin's NonEmptyString; preserve the caller's bytes (including whitespace).
|
|
94
98
|
if (spec.idempotencyKey !== undefined && (typeof spec.idempotencyKey !== 'string' || !spec.idempotencyKey.length))
|
|
@@ -97,11 +101,104 @@ export function createRuns(ctx) {
|
|
|
97
101
|
const output = preparedOutput ?? (spec.schema === undefined ? undefined : outputSchema(spec.schema));
|
|
98
102
|
const system = [spec.system, output?.prompt].filter(Boolean).join('\n\n');
|
|
99
103
|
const picked = spec.model === undefined ? undefined : account(spec.model);
|
|
104
|
+
for (const need of spec.needs ?? [])
|
|
105
|
+
account(need);
|
|
100
106
|
for (const tool of spec.tools ?? [])
|
|
101
107
|
if (!ctx.tools.has(tool))
|
|
102
108
|
throw new Error(`refused: "${tool}" is not one of this kit's tools`);
|
|
103
|
-
const
|
|
104
|
-
|
|
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());
|
|
132
|
+
}
|
|
133
|
+
if (keyAgent)
|
|
134
|
+
throw new Error('refused: an API-key run uses its own account');
|
|
135
|
+
if (!ctx.accounts || !ctx.locks)
|
|
136
|
+
throw new Error('gateway not ready');
|
|
137
|
+
const accounts = ctx.accounts;
|
|
138
|
+
const locks = ctx.locks;
|
|
139
|
+
const restAccount = async (id, until) => {
|
|
140
|
+
const at = until ?? Date.now() + REST_MS;
|
|
141
|
+
await accounts.rest(spec.member, id, at);
|
|
142
|
+
return at;
|
|
143
|
+
};
|
|
144
|
+
const signedIn = async (agentId, provider) => !!(await ctx.request('byokit.accounts', { action: 'ready', agentId, provider })).ok;
|
|
145
|
+
const skipped = [];
|
|
146
|
+
for (;;) {
|
|
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.
|
|
149
|
+
const { pick, agent } = await accounts.pick(spec.member, { account: spec.account ?? 'default',
|
|
150
|
+
...(spec.model !== undefined ? { model: spec.model } : {}), ...(spec.needs ? { needs: spec.needs } : {}) }, skipped, binding);
|
|
151
|
+
if (!pick.ok) {
|
|
152
|
+
if (pick.code === 'unknown_account')
|
|
153
|
+
throw new Error(`unknown account: ${spec.account}`); // a caller bug
|
|
154
|
+
if (pick.code === 'resting')
|
|
155
|
+
return { ok: false, kind: 'resting', until: await restAccount(pick.account, pick.until), message: pick.reason };
|
|
156
|
+
return { ok: false, kind: pick.code === 'none' ? 'signed-out' : pick.code === 'not_included' ? 'plan' : 'other', message: pick.reason };
|
|
157
|
+
}
|
|
158
|
+
const sessionKey = `agent:${agent}:${tail}`;
|
|
159
|
+
const automatic = pick.how === 'auto' || pick.why === 'first_ready';
|
|
160
|
+
// The run goes to the picked account's agent, under the key's tail. The run holds the agent's shared
|
|
161
|
+
// lock from its ready check to its end. An automatic pick not signed in is skipped before any event; an explicit
|
|
162
|
+
// one announces itself, then ends signed-out.
|
|
163
|
+
const end = await locks.shared(agent, async () => {
|
|
164
|
+
if (await moved())
|
|
165
|
+
return movedEnd;
|
|
166
|
+
let ready;
|
|
167
|
+
try {
|
|
168
|
+
ready = await signedIn(agent, pick.account.provider);
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
172
|
+
}
|
|
173
|
+
if (!ready && automatic)
|
|
174
|
+
return undefined;
|
|
175
|
+
const route = { session: sessionKey };
|
|
176
|
+
ctx.routes.set(spec.sessionKey, route);
|
|
177
|
+
try {
|
|
178
|
+
on?.({ type: 'account', account: pick.account.id, model: pick.model, sessionKey, how: pick.how, why: pick.why, considered: pick.considered });
|
|
179
|
+
if (!ready)
|
|
180
|
+
return { ok: false, kind: 'signed-out', message: `${pick.account.name} is not signed in` };
|
|
181
|
+
return await start(spec, on, { agentId: agent, sessionKey, idempotencyKey, system,
|
|
182
|
+
picked: pick.model ? account(pick.model) : undefined, output, chosen: pick }); // '' = no override
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
if (ctx.routes.get(spec.sessionKey) === route)
|
|
186
|
+
ctx.routes.delete(spec.sessionKey);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
if (end === undefined) {
|
|
190
|
+
skipped.push(pick.account.id);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
// Step 7: a resting end rests the account, so the next Auto skips it until then.
|
|
194
|
+
if (end.ok || !('kind' in end) || end.kind !== 'resting')
|
|
195
|
+
return end;
|
|
196
|
+
const until = await restAccount(pick.account.id, end.until);
|
|
197
|
+
return { ...end, until };
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
const start = async (spec, on, t) => {
|
|
201
|
+
const { agentId, sessionKey, idempotencyKey, system, picked, output, chosen } = t;
|
|
105
202
|
const release = spec.register !== false ? ctx.bridge.register({ sessionKey, member: spec.member }, spec.tools, idempotencyKey) : undefined;
|
|
106
203
|
let last;
|
|
107
204
|
let runId = ''; // gateway events for other runs carry a real runId and never match the empty one
|
|
@@ -140,7 +237,7 @@ export function createRuns(ctx) {
|
|
|
140
237
|
}
|
|
141
238
|
});
|
|
142
239
|
try {
|
|
143
|
-
if (picked) {
|
|
240
|
+
if (picked && !chosen) {
|
|
144
241
|
// The picked account must be the one called: check it is signed in for this member before the run, since
|
|
145
242
|
// the engine would otherwise fail the run only after admitting it. An explicit provider/model is strict on
|
|
146
243
|
// the pinned engine (never another provider or model), so this check plus the override is the guarantee.
|
|
@@ -207,7 +304,10 @@ export function createRuns(ctx) {
|
|
|
207
304
|
const usage = usageOf(agentMeta);
|
|
208
305
|
const planWindow = typeof agentMeta.provider === 'string'
|
|
209
306
|
? await planWindowOf(agentId, agentMeta.provider.toLowerCase()) : undefined;
|
|
210
|
-
|
|
307
|
+
const reported = typeof agentMeta.provider === 'string' && typeof agentMeta.model === 'string'
|
|
308
|
+
? `${agentMeta.provider}/${agentMeta.model}`.toLowerCase() : '';
|
|
309
|
+
return { ok: true, text, ...(parsed ? { data: parsed.data } : {}), ...(usage ? { usage } : {}), ...(planWindow ? { planWindow } : {}),
|
|
310
|
+
...(chosen ? { account: chosen.account.id, model: chosen.model || reported } : {}) };
|
|
211
311
|
}
|
|
212
312
|
if (result.stopReason === 'aborted' || (abortedByEngine && result.status !== 'ok'))
|
|
213
313
|
return { ok: false, aborted: true };
|
|
@@ -227,7 +327,7 @@ export function createRuns(ctx) {
|
|
|
227
327
|
};
|
|
228
328
|
return {
|
|
229
329
|
run,
|
|
230
|
-
steer: (k, t) => ctx.request('sessions.steer', { sessionKey: k, message: t }),
|
|
231
|
-
abort: async (k) => { await ctx.request('chat.abort', { sessionKey: k }); },
|
|
330
|
+
steer: (k, t) => ctx.request('sessions.steer', { sessionKey: ctx.routes.get(k)?.session ?? k, message: t }),
|
|
331
|
+
abort: async (k) => { await ctx.request('chat.abort', { sessionKey: ctx.routes.get(k)?.session ?? k }); },
|
|
232
332
|
};
|
|
233
333
|
}
|
package/dist/signin.d.ts
CHANGED
|
@@ -8,6 +8,9 @@ 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>;
|
|
13
|
+
fresh?: boolean;
|
|
11
14
|
};
|
|
12
15
|
/**
|
|
13
16
|
* Drive the engine's provider-owned login. `on` sees every view the person watches; `done` resolves once, and
|
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.
|
|
@@ -41,6 +45,16 @@ export function signIn(ctx, member, o, on) {
|
|
|
41
45
|
const { signal } = owner;
|
|
42
46
|
const driveOwner = new AbortController();
|
|
43
47
|
const wantsCallback = via === 'browser' && o.authChoice === 'openai';
|
|
48
|
+
// The pin builds ChatGPT's authorize URL unsigned, so a fresh sign-in can ask for the login screen itself.
|
|
49
|
+
const shown = (url) => {
|
|
50
|
+
if (!ctx.fresh || !wantsCallback)
|
|
51
|
+
return url;
|
|
52
|
+
const parsed = URL.canParse(url) ? new URL(url) : undefined;
|
|
53
|
+
if (parsed?.origin !== 'https://auth.openai.com' || parsed.pathname !== '/oauth/authorize')
|
|
54
|
+
return url;
|
|
55
|
+
parsed.searchParams.set('prompt', 'login');
|
|
56
|
+
return parsed.toString();
|
|
57
|
+
};
|
|
44
58
|
let server;
|
|
45
59
|
let sessionId = '';
|
|
46
60
|
let starting;
|
|
@@ -48,6 +62,7 @@ export function signIn(ctx, member, o, on) {
|
|
|
48
62
|
let settleStart;
|
|
49
63
|
const startingSettled = new Promise((resolve) => { settleStart = resolve; });
|
|
50
64
|
let over = false;
|
|
65
|
+
let activating = false;
|
|
51
66
|
let recovering = false;
|
|
52
67
|
let offDisconnect;
|
|
53
68
|
let agentId;
|
|
@@ -82,13 +97,21 @@ export function signIn(ctx, member, o, on) {
|
|
|
82
97
|
let pasteIn;
|
|
83
98
|
let returned;
|
|
84
99
|
// Once a secret step is seen, no later gateway prose, link or code is safe to display.
|
|
85
|
-
let sensitive = o.authChoice === 'setup-token'
|
|
86
|
-
const secretFailure =
|
|
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;
|
|
87
110
|
const done = new Promise((resolve) => { settle = resolve; });
|
|
88
111
|
const say = (v) => {
|
|
89
112
|
if (!over && !recovering && !signal.aborted)
|
|
90
113
|
on({ state: 'waiting', via, ...(sensitive
|
|
91
|
-
? { ...(v.prompt ? { prompt: v.prompt } : {}), ...(v.error ? { error: secretFailure
|
|
114
|
+
? { ...(v.prompt ? { prompt: v.prompt } : {}), ...(v.error ? { error: secretFailure } : {}) }
|
|
92
115
|
: v) });
|
|
93
116
|
};
|
|
94
117
|
const closeCallback = async () => {
|
|
@@ -109,7 +132,7 @@ export function signIn(ctx, member, o, on) {
|
|
|
109
132
|
pasteIn = undefined;
|
|
110
133
|
await closeCallback();
|
|
111
134
|
if (sensitive && view.state === 'failed' && view.why === 'failed')
|
|
112
|
-
view = { ...view, error: secretFailure
|
|
135
|
+
view = { ...view, error: secretFailure };
|
|
113
136
|
on(view);
|
|
114
137
|
settle(view);
|
|
115
138
|
};
|
|
@@ -134,11 +157,13 @@ export function signIn(ctx, member, o, on) {
|
|
|
134
157
|
};
|
|
135
158
|
const cancel = () => {
|
|
136
159
|
// Stop the drive now; the start itself is never aborted, and release waits for it to settle before cancelling,
|
|
137
|
-
// 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.
|
|
138
162
|
if (signal.aborted)
|
|
139
163
|
return;
|
|
140
164
|
owner.abort();
|
|
141
|
-
void (async () => { await release();
|
|
165
|
+
void (async () => { await release(); if (!activating)
|
|
166
|
+
await finish(cancelled()); })();
|
|
142
167
|
};
|
|
143
168
|
o.signal?.addEventListener('abort', cancel, { once: true });
|
|
144
169
|
if (o.signal?.aborted)
|
|
@@ -188,7 +213,9 @@ export function signIn(ctx, member, o, on) {
|
|
|
188
213
|
}
|
|
189
214
|
// Native login is completed by the person in unmodified Claude Code. The pin does not expose this
|
|
190
215
|
// choice via auth.start; activation verifies the CLI route and persists it only after a live test succeeds.
|
|
191
|
-
|
|
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) {
|
|
192
219
|
settleStart();
|
|
193
220
|
let apiKey;
|
|
194
221
|
if (o.authChoice === 'anthropic-cli') {
|
|
@@ -197,7 +224,7 @@ export function signIn(ctx, member, o, on) {
|
|
|
197
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.' };
|
|
198
225
|
}
|
|
199
226
|
else {
|
|
200
|
-
say({ prompt: 'API key (billed per use)' });
|
|
227
|
+
say({ prompt: keyRoute?.billing === 'api' ? 'API key (billed per use)' : 'Plan key' });
|
|
201
228
|
apiKey = returned ?? await new Promise((resolve) => {
|
|
202
229
|
const stop = (value) => { clearTimeout(timer); signal.removeEventListener('abort', abort); pasteIn = undefined; resolve(value); };
|
|
203
230
|
const abort = () => stop();
|
|
@@ -210,16 +237,23 @@ export function signIn(ctx, member, o, on) {
|
|
|
210
237
|
returned = undefined;
|
|
211
238
|
if (!apiKey)
|
|
212
239
|
return { state: 'failed', via, why: signal.aborted ? 'declined' : 'expired' };
|
|
240
|
+
secret = apiKey.trim() || undefined;
|
|
213
241
|
}
|
|
214
242
|
if (signal.aborted)
|
|
215
243
|
return { state: 'failed', via, why: 'declined' };
|
|
216
|
-
|
|
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, {
|
|
217
248
|
agentId, kind: apiKey === undefined ? 'claude-cli' : 'api-key',
|
|
218
|
-
...(apiKey === undefined ? {} : { authChoice:
|
|
219
|
-
}, { timeoutMs: PULL_MS, signal });
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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 };
|
|
223
257
|
}
|
|
224
258
|
// The client chooses the wizard id; cancellation waits for the engine to register it.
|
|
225
259
|
sessionId = `byokit-${randomUUID()}`;
|
|
@@ -295,7 +329,7 @@ export function signIn(ctx, member, o, on) {
|
|
|
295
329
|
codeTimer = setTimeout(() => { codeExpired = true; owner.abort(); }, duration);
|
|
296
330
|
const message = deviceCode.message ?? current.message;
|
|
297
331
|
// 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 } : {}),
|
|
332
|
+
say({ ...(deviceCode.code ? { code: deviceCode.code } : {}), ...(current.externalUrl ? { url: shown(current.externalUrl) } : {}),
|
|
299
333
|
...(timed ? { expiresAt: Date.now() + duration } : {}), ...(message ? { message } : {}) });
|
|
300
334
|
const next = await answer(current);
|
|
301
335
|
if (next.done) {
|
|
@@ -326,7 +360,7 @@ export function signIn(ctx, member, o, on) {
|
|
|
326
360
|
}
|
|
327
361
|
if (current.type === 'note' || current.type === 'confirm' || current.type === 'select' || current.type === 'action') {
|
|
328
362
|
if (current.externalUrl)
|
|
329
|
-
say({ url: current.externalUrl });
|
|
363
|
+
say({ url: shown(current.externalUrl) });
|
|
330
364
|
const next = await answer(current);
|
|
331
365
|
if (next.done) {
|
|
332
366
|
terminal = next;
|
|
@@ -418,7 +452,7 @@ export function signIn(ctx, member, o, on) {
|
|
|
418
452
|
else if (BUSY.test(cut(error)))
|
|
419
453
|
view = { state: 'failed', via, why: 'busy' };
|
|
420
454
|
else
|
|
421
|
-
view = { state: 'failed', via, why: 'failed', error:
|
|
455
|
+
view = { state: 'failed', via, why: 'failed', error: keyRoute ? KEY_FAILED : clean(error) };
|
|
422
456
|
}
|
|
423
457
|
settleStart();
|
|
424
458
|
if (view.state !== 'done')
|