@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/kit.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
4
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { PROTOCOL_VERSION } from "./constants.js";
|
|
6
|
+
import { PROTOCOL_VERSION, LIVE_CATALOGS } from "./constants.js";
|
|
6
7
|
import { atomic } from "./engine-patches.js";
|
|
7
8
|
import { Approvals } from "./approvals.js";
|
|
8
9
|
import { Bridge } from "./bridge.js";
|
|
@@ -10,15 +11,16 @@ import { Engine, refusedOnly } from "./engine.js";
|
|
|
10
11
|
import { AuthStoreSealSizeError } from "./auth-store.js";
|
|
11
12
|
import { gatewayTransport } from "./transport.js";
|
|
12
13
|
import { createMembers } from "./members.js";
|
|
13
|
-
import { createAccounts } from "./accounts.js";
|
|
14
|
+
import { createAccounts, memberOfAccount } from "./accounts.js";
|
|
14
15
|
import { createLocks } from "./locks.js";
|
|
15
16
|
import { keyAgentId, KEY_PREFIX, MEMBER_ID } from "./members.js";
|
|
16
17
|
import { confirmRetainedLogin as confirmLogin, migrateRetainedLogin as migrateLogin } from "./migrate.js";
|
|
17
18
|
import { createRuns, signedInProviders } from "./runs.js";
|
|
18
|
-
import { createAuthStatus } from "./auth-status.js";
|
|
19
|
+
import { authStatus, createAuthStatus } from "./auth-status.js";
|
|
19
20
|
import { outputSchema } from "./output.js";
|
|
20
21
|
import { createKeys } from "./keys.js";
|
|
21
22
|
import { routes as routeTable } from "./routes.js";
|
|
23
|
+
import { providerOf } from "./pick.js";
|
|
22
24
|
import { signIn as startSignIn, signOut as engineSignOut } from "./signin.js";
|
|
23
25
|
import { reconcileConfig, browserToolPolicySafe, browserProfileAcknowledged, browserSessionMayRun, memoryLimited as configMemoryLimited, configKeyPath, readConfigKey, writeConfigKey } from "./config.js";
|
|
24
26
|
import { createBrowserHost } from "./browser/host.js";
|
|
@@ -26,6 +28,7 @@ import { fileSignInStore } from "./browser/store.js";
|
|
|
26
28
|
import { launchBroker } from "./browser/broker.js";
|
|
27
29
|
/** The engine's learning-mode config key (5.6 "Engine learning state"). */
|
|
28
30
|
const LEARNING_KEY = 'skills.workshop.autonomous.mode';
|
|
31
|
+
const RESTART_MS = 60_000;
|
|
29
32
|
const operatorMethods = new Set([
|
|
30
33
|
'agent',
|
|
31
34
|
'agent.identity.get',
|
|
@@ -432,6 +435,7 @@ export class OpenClawKit {
|
|
|
432
435
|
greeting;
|
|
433
436
|
members;
|
|
434
437
|
locks = createLocks();
|
|
438
|
+
routeMap = new Map();
|
|
435
439
|
accountsCtl;
|
|
436
440
|
// One bridge and one approval surface per kit: listeners and run registrations survive reconnects (B3).
|
|
437
441
|
// Only the socket itself is per-connection. Approvals always reaches the live transport through this.request().
|
|
@@ -442,9 +446,12 @@ export class OpenClawKit {
|
|
|
442
446
|
listeners = new Set();
|
|
443
447
|
off = [];
|
|
444
448
|
signInDisconnects = new Set();
|
|
449
|
+
phaseListeners = new Set();
|
|
445
450
|
starting;
|
|
446
451
|
stopping = false;
|
|
447
452
|
failures = 0;
|
|
453
|
+
swept = false;
|
|
454
|
+
catalogs = Promise.resolve();
|
|
448
455
|
browser;
|
|
449
456
|
browserHost;
|
|
450
457
|
brokers = new Map();
|
|
@@ -656,13 +663,17 @@ export class OpenClawKit {
|
|
|
656
663
|
onState: (s) => this.setState(s), onExit: () => this.closed('engine exited') });
|
|
657
664
|
this.admission = createAuthStatus(this.engine.root, (method, params, options) => this.request()(method, params, options));
|
|
658
665
|
this.keys = createKeys({ root: this.engine.root,
|
|
659
|
-
|
|
666
|
+
prepareOwner: (agentId) => this.prepareKeyOwner(agentId),
|
|
667
|
+
restarting: () => this.engineRestarting(),
|
|
660
668
|
request: (method, params, options) => this.request()(method, params, options),
|
|
661
|
-
ensure: (member) => this.ensureMember(member)
|
|
669
|
+
ensure: (member) => this.ensureMember(member),
|
|
670
|
+
authConfig: () => this.authSnapshot(),
|
|
671
|
+
restarted: (authBefore, signal) => this.restarted(authBefore, signal) });
|
|
662
672
|
const slot = {};
|
|
663
673
|
this.approvalsCtl = new Approvals({
|
|
664
674
|
request: (method, params, co) => this.request()(method, params, co),
|
|
665
675
|
bridge: { resolveAsk: (id, d) => slot.bridge?.resolveAsk(id, d) ?? false },
|
|
676
|
+
memberOf: (ref) => this.memberOf(ref),
|
|
666
677
|
});
|
|
667
678
|
const host = o.browser ? {
|
|
668
679
|
gate: async (run, tool, input, info) => {
|
|
@@ -691,6 +702,7 @@ export class OpenClawKit {
|
|
|
691
702
|
return browserSessionMayRun(this.browserHost.signIns(), key, this.bridge.isRegisteredRun(key, runId));
|
|
692
703
|
} } : {}),
|
|
693
704
|
permitted: o.permitted ?? (() => true), approvalTimeoutMs: o.approvalTimeoutMs ?? 180_000,
|
|
705
|
+
memberOf: (ref) => this.memberOf(ref),
|
|
694
706
|
onAsk: (a) => this.approvalsCtl.add(a), onAskGone: (id) => this.approvalsCtl.remove(id) });
|
|
695
707
|
slot.bridge = this.bridge;
|
|
696
708
|
}
|
|
@@ -699,6 +711,8 @@ export class OpenClawKit {
|
|
|
699
711
|
this.admission?.invalidate();
|
|
700
712
|
this.current = { ...s, ...(this.engine.patchSet !== undefined ? { patchSet: this.engine.patchSet } : {}) };
|
|
701
713
|
this.o.onState?.(this.current);
|
|
714
|
+
for (const fn of this.phaseListeners)
|
|
715
|
+
fn();
|
|
702
716
|
}
|
|
703
717
|
get state() { return this.current; }
|
|
704
718
|
async prepare() {
|
|
@@ -745,13 +759,22 @@ export class OpenClawKit {
|
|
|
745
759
|
if (!operatorMethods.has(name) && !nodeMethods.has(name))
|
|
746
760
|
this.o.log?.(`gateway has unknown method: ${name}`);
|
|
747
761
|
this.members = createMembers({ request: transport.request.bind(transport), root: this.engine.root });
|
|
748
|
-
|
|
749
|
-
|
|
762
|
+
// An add outlives a restart (activation restarts the gateway before it settles): requests use the kit's current transport.
|
|
763
|
+
this.accountsCtl = createAccounts({ request: (method, params, o) => this.request()(method, params, o), root: this.engine.root,
|
|
764
|
+
ensure: (member) => this.ensureMember(member), locks: this.locks,
|
|
765
|
+
signIn: (agentId, o, on, fresh) => startSignIn({ ...this.signInCtx(), ensure: async () => ({ agentId }), fresh }, agentId, o, on),
|
|
766
|
+
routes: this.routeMap, log: this.o.log });
|
|
750
767
|
// The fail-closed tool bridge feeds the kit's single Approval surface (5.9, O5).
|
|
751
768
|
await this.bridge.start();
|
|
752
769
|
this.off.push(transport.onEvent((e) => this.approvalsCtl.handleEvent(e)));
|
|
753
770
|
this.failures = 0;
|
|
754
771
|
this.setState({ phase: 'ready' });
|
|
772
|
+
this.syncCatalogs();
|
|
773
|
+
// A failed key add whose abandon clear never reached the engine leaves an unsealed key with no ready marker;
|
|
774
|
+
// runs stay closed, so drop it now that the gateway is ready. Ready keys keep their marker and survive.
|
|
775
|
+
if (!this.swept)
|
|
776
|
+
void this.keys.sweep().then((done) => { if (done)
|
|
777
|
+
this.swept = true; }).catch(() => undefined);
|
|
755
778
|
if (this.o.browser)
|
|
756
779
|
await this.checkBrowserTools();
|
|
757
780
|
// Replays the engine's native approval lists over the now-live transport (N9).
|
|
@@ -800,6 +823,39 @@ export class OpenClawKit {
|
|
|
800
823
|
this.bridge.stop();
|
|
801
824
|
await transport?.stop();
|
|
802
825
|
}
|
|
826
|
+
engineRestarting() {
|
|
827
|
+
return this.current.phase === 'restarting' || this.current.phase === 'starting';
|
|
828
|
+
}
|
|
829
|
+
authSnapshot() {
|
|
830
|
+
return JSON.stringify(this.getConfigKey('auth')) ?? '';
|
|
831
|
+
}
|
|
832
|
+
// An activation that changed config `auth` makes the pin restart the gateway; then this resolves true once that
|
|
833
|
+
// restart has begun and the kit is ready again, false if it fails, is aborted or runs past RESTART_MS. No change: true.
|
|
834
|
+
restarted(authBefore, signal) {
|
|
835
|
+
if (this.o.spawnEngine === false || this.authSnapshot() === authBefore)
|
|
836
|
+
return Promise.resolve(true);
|
|
837
|
+
if (signal?.aborted)
|
|
838
|
+
return Promise.resolve(false);
|
|
839
|
+
return new Promise((resolve) => {
|
|
840
|
+
let began = this.engineRestarting();
|
|
841
|
+
const end = (ok) => {
|
|
842
|
+
clearTimeout(timer);
|
|
843
|
+
this.phaseListeners.delete(check);
|
|
844
|
+
signal?.removeEventListener('abort', abort);
|
|
845
|
+
resolve(ok);
|
|
846
|
+
};
|
|
847
|
+
const abort = () => end(false);
|
|
848
|
+
const check = () => {
|
|
849
|
+
if (this.engineRestarting())
|
|
850
|
+
began = true;
|
|
851
|
+
else if (began)
|
|
852
|
+
end(this.current.phase === 'ready');
|
|
853
|
+
};
|
|
854
|
+
const timer = setTimeout(() => end(false), RESTART_MS);
|
|
855
|
+
this.phaseListeners.add(check);
|
|
856
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
857
|
+
});
|
|
858
|
+
}
|
|
803
859
|
closed(_why) {
|
|
804
860
|
const engineFailed = _why === 'engine exited' && this.current.phase === 'failed' && this.current.why === 'exited';
|
|
805
861
|
if (this.stopping || (this.current.phase !== 'ready' && !engineFailed))
|
|
@@ -836,9 +892,9 @@ export class OpenClawKit {
|
|
|
836
892
|
delete this.browserProfiles[profile];
|
|
837
893
|
}
|
|
838
894
|
request() {
|
|
839
|
-
if (!this.transport || this.current.phase !== 'ready')
|
|
840
|
-
throw new Error('gateway not ready');
|
|
841
895
|
const transport = this.transport;
|
|
896
|
+
if (!transport || this.current.phase !== 'ready')
|
|
897
|
+
return () => Promise.reject(new Error('gateway not ready'));
|
|
842
898
|
return (method, params, options) => {
|
|
843
899
|
const changesAuth = /^(models\.authLogout|openclaw\.setup\.(auth\.start|activate)|wizard\.(next|cancel)|config\.(apply|patch|set)|agents\.(create|update|delete)|secrets\.(reload|store\.(set|delete))|byokit\.keys)$/.test(method);
|
|
844
900
|
if (!changesAuth || (method === 'byokit.keys' && params?.action === 'ready'))
|
|
@@ -850,22 +906,12 @@ export class OpenClawKit {
|
|
|
850
906
|
call(method, params, o) {
|
|
851
907
|
if (this.o.browser && /^(config\.(apply|patch|set)|agents\.(create|update|delete)|plugins\.(install|uninstall|setEnabled|refresh))$/.test(method))
|
|
852
908
|
return Promise.reject(new Error('browser policy changes require guarded patchConfig'));
|
|
853
|
-
|
|
854
|
-
return this.request()(method, params, o);
|
|
855
|
-
}
|
|
856
|
-
catch (error) {
|
|
857
|
-
return Promise.reject(error);
|
|
858
|
-
}
|
|
909
|
+
return this.request()(method, params, o);
|
|
859
910
|
}
|
|
860
911
|
callDynamic(method, params, o) {
|
|
861
912
|
if (operatorMethods.has(method) || nodeMethods.has(method))
|
|
862
913
|
return Promise.reject(new Error(`use typed call for generated method: ${method}`));
|
|
863
|
-
|
|
864
|
-
return this.request()(method, params, o);
|
|
865
|
-
}
|
|
866
|
-
catch (error) {
|
|
867
|
-
return Promise.reject(error);
|
|
868
|
-
}
|
|
914
|
+
return this.request()(method, params, o);
|
|
869
915
|
}
|
|
870
916
|
get hello() { return this.greeting; }
|
|
871
917
|
onEvent(event, fn) {
|
|
@@ -905,7 +951,9 @@ export class OpenClawKit {
|
|
|
905
951
|
// Public readiness/sign-in reads stay live; their fresh result can prepare the next run's admission.
|
|
906
952
|
authStatus: (agentId, refresh, native) => this.admission.read(agentId, refresh, native, false),
|
|
907
953
|
ensure: (member) => this.ensureMember(member), callbackPort: this.o.callbackPort ?? 1455,
|
|
908
|
-
onDisconnect: (fn) => { this.signInDisconnects.add(fn); return () => { this.signInDisconnects.delete(fn); }; }
|
|
954
|
+
onDisconnect: (fn) => { this.signInDisconnects.add(fn); return () => { this.signInDisconnects.delete(fn); }; },
|
|
955
|
+
authConfig: () => this.authSnapshot(),
|
|
956
|
+
restarted: (authBefore, signal) => this.restarted(authBefore, signal) };
|
|
909
957
|
}
|
|
910
958
|
routes() {
|
|
911
959
|
return routeTable();
|
|
@@ -917,8 +965,10 @@ export class OpenClawKit {
|
|
|
917
965
|
providers(member) {
|
|
918
966
|
return this.providerStatus(member).then((names) => names ?? []);
|
|
919
967
|
}
|
|
968
|
+
/** A member sign-in to `provider`, or a ready key (5.8) whose saved model is that provider's. */
|
|
920
969
|
signedIn(member, provider) {
|
|
921
|
-
return this.providers(member).then((names) => names.includes(provider)
|
|
970
|
+
return this.providers(member).then(async (names) => names.includes(provider)
|
|
971
|
+
|| providerOf((await this.keys.ready(member, { once: true }))?.model ?? '') === provider).catch(() => false);
|
|
922
972
|
}
|
|
923
973
|
signIn(member, o, on) {
|
|
924
974
|
return startSignIn(this.signInCtx(), member, o, on);
|
|
@@ -926,13 +976,53 @@ export class OpenClawKit {
|
|
|
926
976
|
signOut(member, provider) {
|
|
927
977
|
return engineSignOut(this.signInCtx(), member, provider);
|
|
928
978
|
}
|
|
929
|
-
//
|
|
930
|
-
//
|
|
979
|
+
// The member an agent id or session key belongs to, through the account index (5.15, O14m). Reads the index file, so it
|
|
980
|
+
// needs no live gateway.
|
|
981
|
+
memberOf(ref) {
|
|
982
|
+
return memberOfAccount(this.engine.root, ref);
|
|
983
|
+
}
|
|
984
|
+
// accounts (5.15, O14 steps S5, S6 and S9-S12): the per-member index with listing, adding, defaults, rename, models
|
|
985
|
+
// and room; `run({ account })` and `pick` choose among them, held to the account a session is bound to, and `move`
|
|
986
|
+
// carries a conversation to another account. Removing arrives in a later step.
|
|
931
987
|
accounts(member) {
|
|
932
988
|
if (!this.accountsCtl)
|
|
933
989
|
return Promise.reject(new Error('gateway not ready'));
|
|
934
990
|
return this.accountsCtl.list(member);
|
|
935
991
|
}
|
|
992
|
+
models(member, id) {
|
|
993
|
+
if (typeof id !== 'string')
|
|
994
|
+
return this.keys.models(member, id);
|
|
995
|
+
if (!this.accountsCtl)
|
|
996
|
+
return Promise.reject(new Error('gateway not ready'));
|
|
997
|
+
return this.accountsCtl.models(member, id);
|
|
998
|
+
}
|
|
999
|
+
room(member, id, demand) {
|
|
1000
|
+
if (!this.accountsCtl)
|
|
1001
|
+
return Promise.reject(new Error('gateway not ready'));
|
|
1002
|
+
return this.accountsCtl.room(member, id, demand);
|
|
1003
|
+
}
|
|
1004
|
+
/** What `run` would decide now with this selection and key, refusals included; decides nothing and runs nothing. */
|
|
1005
|
+
async pick(member, sel, o) {
|
|
1006
|
+
const accounts = this.accountsCtl;
|
|
1007
|
+
if (!accounts)
|
|
1008
|
+
throw new Error('gateway not ready');
|
|
1009
|
+
const binding = o?.sessionKey === undefined ? undefined : await accounts.bind(member, o.sessionKey);
|
|
1010
|
+
return (await accounts.pick(member, sel, [], binding)).pick;
|
|
1011
|
+
}
|
|
1012
|
+
/** Carry a conversation to another account: its whole transcript forks onto that account, then the old session goes. */
|
|
1013
|
+
move(ref, to) {
|
|
1014
|
+
if (!this.accountsCtl)
|
|
1015
|
+
return Promise.reject(new Error('gateway not ready'));
|
|
1016
|
+
return this.accountsCtl.move(ref.member, ref.sessionKey, to);
|
|
1017
|
+
}
|
|
1018
|
+
/** Sign in one more account: on the member agent while it holds none of the provider, else on a new sealed agent. */
|
|
1019
|
+
addAccount(member, o, on) {
|
|
1020
|
+
if (!this.accountsCtl)
|
|
1021
|
+
return Promise.reject(new Error('gateway not ready'));
|
|
1022
|
+
return this.accountsCtl.add(member, o, on).then((added) => ({ ...added,
|
|
1023
|
+
done: added.done.then((r) => { if (r.view.state === 'done')
|
|
1024
|
+
this.syncCatalogs(); return r; }) }));
|
|
1025
|
+
}
|
|
936
1026
|
renameAccount(member, id, name) {
|
|
937
1027
|
if (!this.accountsCtl)
|
|
938
1028
|
return Promise.reject(new Error('gateway not ready'));
|
|
@@ -952,6 +1042,37 @@ export class OpenClawKit {
|
|
|
952
1042
|
addKey(agentId, o) {
|
|
953
1043
|
return this.keys.add(agentId, o);
|
|
954
1044
|
}
|
|
1045
|
+
async prepareKeyOwner(agentId) {
|
|
1046
|
+
this.syncCatalogs();
|
|
1047
|
+
await this.catalogs;
|
|
1048
|
+
if ((await authStatus(this.request(), agentId, true)).unavailable)
|
|
1049
|
+
throw new Error('Key owner unavailable.');
|
|
1050
|
+
}
|
|
1051
|
+
// Live provider catalogs (5.6): registers each live model with its api from the provider's catalog data, so
|
|
1052
|
+
// embedded runs resolve it. Serialized and in the background; a failure is logged and keeps the registered list.
|
|
1053
|
+
syncCatalogs() {
|
|
1054
|
+
for (const provider of LIVE_CATALOGS) {
|
|
1055
|
+
if (this.o.config?.models?.providers?.[provider]?.models !== undefined)
|
|
1056
|
+
continue;
|
|
1057
|
+
this.catalogs = this.catalogs.then(async () => {
|
|
1058
|
+
const answer = await this.request()('byokit.catalog', { provider });
|
|
1059
|
+
if (!answer.ok) {
|
|
1060
|
+
if (answer.reason !== 'no-key')
|
|
1061
|
+
this.o.log?.(`catalog sync ${provider}: ${answer.reason ?? 'failed'}`);
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
const { config } = await this.request()('config.get');
|
|
1065
|
+
const registered = config?.models?.providers?.[provider]?.models ?? [];
|
|
1066
|
+
const byId = new Map(registered.map((m) => [m.id, m]));
|
|
1067
|
+
for (const m of answer.models ?? [])
|
|
1068
|
+
byId.set(m.id, m);
|
|
1069
|
+
const models = [...byId.values()];
|
|
1070
|
+
if (!isDeepStrictEqual(models, registered))
|
|
1071
|
+
await this.patchConfig({ models: { providers: { [provider]: { models } } } });
|
|
1072
|
+
}).catch(() => { if (this.current.phase === 'ready')
|
|
1073
|
+
this.o.log?.(`catalog sync ${provider}: failed`); });
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
955
1076
|
migrateRetainedLogin(member, source) {
|
|
956
1077
|
return migrateLogin({ root: this.engine.root, prepare: () => this.prepare(), doctor: () => this.engine.doctor(120_000),
|
|
957
1078
|
seal: this.o.authSeal, log: this.o.log, withStore: (task) => this.engine.withAuthStore(task) }, member, source);
|
|
@@ -975,6 +1096,9 @@ export class OpenClawKit {
|
|
|
975
1096
|
ensure: (member) => this.ensureMember(member),
|
|
976
1097
|
bridge: { register: (run, tools, runId) => this.bridge.register(run, tools && this.o.browser ? [...tools, 'request_sign_in'] : tools, runId) },
|
|
977
1098
|
tools: new Set(this.toolNames()),
|
|
1099
|
+
...(this.accountsCtl ? { accounts: this.accountsCtl } : {}),
|
|
1100
|
+
locks: this.locks,
|
|
1101
|
+
routes: this.routeMap,
|
|
978
1102
|
});
|
|
979
1103
|
}
|
|
980
1104
|
/** The app tool names (`KitOptions.tools`): the only names `RunSpec.tools` may carry. */
|
|
@@ -1005,10 +1129,11 @@ export class OpenClawKit {
|
|
|
1005
1129
|
const selected = await this.keys.ready(spec.member);
|
|
1006
1130
|
if (!selected)
|
|
1007
1131
|
return { ok: false, kind: 'signed-out', message: 'Add an API key to use this option.' };
|
|
1008
|
-
|
|
1009
|
-
|
|
1132
|
+
// A key pays for its provider's models (5.8); the engine refuses one its catalog does not list.
|
|
1133
|
+
if (spec.model !== undefined && providerOf(spec.model) !== providerOf(selected.model))
|
|
1134
|
+
return { ok: false, kind: 'plan', message: 'Choose a model of the provider saved with this key.' };
|
|
1010
1135
|
// RunRef.member stays the person for app gates; the engine receives the isolated agent and history.
|
|
1011
|
-
return this.runs().run({ ...spec, model: selected.model }, on, selected.agentId, output);
|
|
1136
|
+
return this.runs().run({ ...spec, model: spec.model ?? selected.model }, on, selected.agentId, output);
|
|
1012
1137
|
});
|
|
1013
1138
|
return this.runs().run(spec, on, undefined, output);
|
|
1014
1139
|
}
|
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
|
-
|
|
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' &&
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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 (
|
|
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/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
|
-
|
|
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
|
-
|
|
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
|
|
135
|
-
const
|
|
136
|
-
|
|
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/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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
}
|