@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/CHANGELOG.md +22 -0
- package/README.md +38 -6
- package/SECURITY.md +3 -2
- package/dist/accounts.d.ts +19 -2
- package/dist/accounts.js +618 -165
- 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/auth-store.d.ts +6 -0
- package/dist/auth-store.js +135 -23
- 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 +4 -4
- package/dist/device.js +11 -1
- package/dist/engine.d.ts +1 -0
- package/dist/engine.js +16 -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 +24 -2
- package/dist/kit.js +139 -29
- package/dist/link.js +21 -11
- package/dist/routes.d.ts +1 -0
- package/dist/routes.js +5 -3
- package/dist/routes.json +31 -31
- package/dist/runs.d.ts +1 -1
- package/dist/runs.js +32 -10
- package/dist/signin.d.ts +2 -0
- package/dist/signin.js +39 -15
- package/dist/testing/fake-gateway.d.ts +19 -0
- package/dist/testing/fake-gateway.js +137 -12
- package/dist/testing/model-stub.d.ts +1 -0
- package/dist/testing/model-stub.js +2 -1
- package/dist/words.json +8 -1
- package/package.json +1 -1
- 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/keys.d.ts
CHANGED
|
@@ -1,20 +1,34 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
export
|
|
1
|
+
import { type ActivationCtx } from './activation.ts';
|
|
2
|
+
import type { Member, ModelInfo } from './types.ts';
|
|
3
|
+
export type KeyFailureReason = 'auth' | 'billing' | 'empty' | 'model' | 'network' | 'engine' | 'inference' | 'busy';
|
|
4
|
+
export type AddKeyResult = {
|
|
5
|
+
status: 'ok';
|
|
6
|
+
} | {
|
|
7
|
+
status: 'invalid' | 'not_included' | 'retry';
|
|
8
|
+
reason: KeyFailureReason;
|
|
9
|
+
};
|
|
10
|
+
export declare function createKeys(ctx: ActivationCtx & {
|
|
4
11
|
root: string;
|
|
5
|
-
request: GatewayTransport['request'];
|
|
6
12
|
ensure(member: Member): Promise<{
|
|
7
13
|
agentId: string;
|
|
8
14
|
}>;
|
|
9
15
|
restarting?(): boolean;
|
|
16
|
+
prepareOwner(agentId: string): Promise<void>;
|
|
10
17
|
}): {
|
|
18
|
+
models: (member: Member, o: {
|
|
19
|
+
authChoice: string;
|
|
20
|
+
}) => Promise<ModelInfo[]>;
|
|
11
21
|
exclusive: <T>(member: Member, work: () => Promise<T>) => Promise<T>;
|
|
12
22
|
add(member: Member, o: {
|
|
13
23
|
authChoice: string;
|
|
14
24
|
apiKey: string;
|
|
25
|
+
model?: string;
|
|
15
26
|
}): Promise<AddKeyResult>;
|
|
16
|
-
ready(member: Member
|
|
27
|
+
ready(member: Member, o?: {
|
|
28
|
+
once?: boolean;
|
|
29
|
+
}): Promise<{
|
|
17
30
|
agentId: string;
|
|
18
31
|
model: string;
|
|
19
32
|
} | undefined>;
|
|
33
|
+
sweep(): Promise<boolean>;
|
|
20
34
|
};
|
package/dist/keys.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
-
import {
|
|
2
|
+
import { activate } from "./activation.js";
|
|
3
|
+
import { keyAgentId, MEMBER_ID, KEY_PREFIX, untilListed } from "./members.js";
|
|
3
4
|
import { routes } from "./routes.js";
|
|
5
|
+
import { modelsOf } from "./accounts.js";
|
|
6
|
+
import { LIVE_CATALOGS } from "./constants.js";
|
|
4
7
|
export function createKeys(ctx) {
|
|
5
8
|
// Serialize key replacement and key runs, so an in-flight run never sees a half-installed replacement.
|
|
6
9
|
const tails = new Map();
|
|
@@ -17,39 +20,117 @@ export function createKeys(ctx) {
|
|
|
17
20
|
}
|
|
18
21
|
};
|
|
19
22
|
const check = (member) => MEMBER_ID.test(member) && !member.startsWith(KEY_PREFIX);
|
|
20
|
-
|
|
23
|
+
// Config writes may drain/restart the gateway: a restart, or a retryable refusal, means the call may simply run again.
|
|
24
|
+
const transient = (error) => {
|
|
25
|
+
const refusal = error;
|
|
26
|
+
return Boolean(ctx.restarting?.() || (refusal?.code === 'UNAVAILABLE' && refusal.retryable === true));
|
|
27
|
+
};
|
|
28
|
+
const control = (member, action, provider, model) => ctx.request('byokit.keys', { member, action, ...(provider ? { provider } : {}), ...(model ? { model } : {}) });
|
|
29
|
+
const failure = (status, reason) => ({ status, reason });
|
|
30
|
+
// Only a provider refusal is `invalid`; an unavailable route or model is `not_included`; everything else may be retried.
|
|
31
|
+
const probeResult = (status) => {
|
|
32
|
+
switch (status) {
|
|
33
|
+
case 'auth': return failure('invalid', 'auth');
|
|
34
|
+
case 'billing': return failure('invalid', 'billing');
|
|
35
|
+
case 'unavailable':
|
|
36
|
+
case 'no_model': return failure('not_included', 'model');
|
|
37
|
+
case 'timeout':
|
|
38
|
+
case 'network': return failure('retry', 'network');
|
|
39
|
+
case 'rate_limit':
|
|
40
|
+
case 'busy': return failure('retry', 'busy');
|
|
41
|
+
case 'format':
|
|
42
|
+
case 'unknown': return failure('retry', 'inference');
|
|
43
|
+
default: return failure('retry', 'engine');
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const registered = async (provider, model) => {
|
|
47
|
+
const { config } = await ctx.request('config.get');
|
|
48
|
+
return Boolean(config?.models?.providers?.[provider]?.models?.some((m) => `${provider}/${m.id}` === model));
|
|
49
|
+
};
|
|
50
|
+
const errorReason = (error, sent) => {
|
|
51
|
+
if (transient(error))
|
|
52
|
+
return 'busy';
|
|
53
|
+
const e = error;
|
|
54
|
+
if (sent && e?.code === 'INVALID_REQUEST')
|
|
55
|
+
return 'inference';
|
|
56
|
+
return ['ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'EAI_AGAIN', 'ETIMEDOUT', 'CLIENT_TIMEOUT', 'NETWORK_ERROR'].includes(e?.code ?? '')
|
|
57
|
+
|| e?.name === 'TimeoutError' ? 'network' : 'engine';
|
|
58
|
+
};
|
|
59
|
+
const models = async (member, o) => {
|
|
60
|
+
if (!check(member))
|
|
61
|
+
throw new Error('Choose your own member.');
|
|
62
|
+
const route = routes().find((r) => r.choice === o.authChoice && r.keyEntry && r.readiness === 'ready');
|
|
63
|
+
if (!route || !LIVE_CATALOGS.includes(route.provider))
|
|
64
|
+
return [];
|
|
65
|
+
await ctx.ensure(member);
|
|
66
|
+
const catalog = await ctx.request('byokit.catalog', { provider: route.provider });
|
|
67
|
+
if (catalog.ok)
|
|
68
|
+
return modelsOf(route.provider, { models: catalog.models?.map((m) => ({ ...m, provider: route.provider })) });
|
|
69
|
+
if (catalog.reason !== 'no-key')
|
|
70
|
+
throw new Error('Model catalog is unavailable.');
|
|
71
|
+
return [];
|
|
72
|
+
};
|
|
21
73
|
return {
|
|
74
|
+
models,
|
|
22
75
|
exclusive,
|
|
23
76
|
async add(member, o) {
|
|
24
|
-
if (!check(member)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
77
|
+
if (!check(member))
|
|
78
|
+
throw new Error('Choose your own member.');
|
|
79
|
+
if (!o.apiKey.trim())
|
|
80
|
+
return failure('invalid', 'empty');
|
|
81
|
+
const route = routes().find((r) => r.choice === o.authChoice && r.keyEntry && r.readiness === 'ready');
|
|
82
|
+
if (!route || (o.model !== undefined && !LIVE_CATALOGS.includes(route.provider)))
|
|
83
|
+
return failure('not_included', 'model');
|
|
29
84
|
return exclusive(member, async () => {
|
|
85
|
+
let sent = false;
|
|
86
|
+
// A key the engine may have taken but not sealed is cleared (prepare already emptied the agent); if the clear
|
|
87
|
+
// does not reach the engine, the key stays unsealed and unusable until the next key add or kit start, so runs
|
|
88
|
+
// fail closed.
|
|
89
|
+
const abandon = async (result) => {
|
|
90
|
+
await control(member, 'clear').catch(() => undefined);
|
|
91
|
+
return result;
|
|
92
|
+
};
|
|
30
93
|
try {
|
|
31
94
|
await ctx.ensure(member);
|
|
32
95
|
const agentId = keyAgentId(member);
|
|
33
96
|
const list = await ctx.request('agents.list');
|
|
34
97
|
if (!list.agents.some((a) => a.id === agentId))
|
|
35
98
|
await ctx.request('agents.create', { name: agentId, workspace: join(ctx.root, 'key-workspaces', member) });
|
|
99
|
+
// Creation answers before the runtime roster/owners publish, just as for ordinary members.
|
|
100
|
+
await untilListed(ctx.request, agentId);
|
|
36
101
|
if (!(await control(member, 'prepare', route.provider)).ok)
|
|
37
|
-
return '
|
|
102
|
+
return failure('retry', 'engine');
|
|
38
103
|
// The public pin's typed API-key path, never a CLI-login import. Discard the engine's lines/errors.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (
|
|
44
|
-
return
|
|
45
|
-
|
|
104
|
+
sent = true;
|
|
105
|
+
const result = await activate(ctx, { kind: 'api-key', agentId, authChoice: o.authChoice, apiKey: o.apiKey }, { timeoutMs: 180_000 });
|
|
106
|
+
if (!result.ok && !result.late)
|
|
107
|
+
return abandon(probeResult(result.status));
|
|
108
|
+
if (result.late)
|
|
109
|
+
return abandon(failure('retry', 'busy'));
|
|
110
|
+
if (o.model !== undefined && !(await models(member, o)).some((m) => m.id === o.model && m.available))
|
|
111
|
+
return abandon(failure('not_included', 'model'));
|
|
112
|
+
// Prepare before publishing the ready marker: failed preparation remains unusable even if clear fails.
|
|
113
|
+
try {
|
|
114
|
+
await ctx.prepareOwner(agentId);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return abandon(failure('retry', 'engine'));
|
|
118
|
+
}
|
|
119
|
+
if (o.model !== undefined && LIVE_CATALOGS.includes(route.provider) && !(await registered(route.provider, o.model)))
|
|
120
|
+
return abandon(failure('retry', 'model'));
|
|
121
|
+
if ((await control(member, 'seal', undefined, o.model)).ok)
|
|
122
|
+
return { status: 'ok' };
|
|
123
|
+
return abandon(failure('retry', 'engine'));
|
|
46
124
|
}
|
|
47
|
-
catch {
|
|
48
|
-
|
|
125
|
+
catch (error) {
|
|
126
|
+
const reason = errorReason(error, sent);
|
|
127
|
+
if (sent)
|
|
128
|
+
return abandon(failure('retry', reason));
|
|
129
|
+
return failure('retry', reason);
|
|
49
130
|
}
|
|
50
131
|
});
|
|
51
132
|
},
|
|
52
|
-
async ready(member) {
|
|
133
|
+
async ready(member, o = {}) {
|
|
53
134
|
if (!check(member))
|
|
54
135
|
return undefined;
|
|
55
136
|
const deadline = Date.now() + 60_000;
|
|
@@ -59,15 +140,46 @@ export function createKeys(ctx) {
|
|
|
59
140
|
return ready.ok && ready.model ? { agentId: keyAgentId(member), model: ready.model } : undefined;
|
|
60
141
|
}
|
|
61
142
|
catch (error) {
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
const refusal = error;
|
|
65
|
-
const transient = ctx.restarting?.() || (refusal?.code === 'UNAVAILABLE' && refusal.retryable === true);
|
|
66
|
-
if (!transient || Date.now() >= deadline)
|
|
143
|
+
// This read is safe to retry; an absent profile or a terminal refusal still fails closed immediately.
|
|
144
|
+
if (o.once || !transient(error) || Date.now() >= deadline)
|
|
67
145
|
return undefined;
|
|
68
146
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
69
147
|
}
|
|
70
148
|
}
|
|
71
149
|
},
|
|
150
|
+
// Kit start calls this once the gateway is ready: a failed add whose abandon clear never reached the engine
|
|
151
|
+
// leaves an unsealed key with no ready marker, so drop it. A single ready read per key agent, no retries; an add
|
|
152
|
+
// in flight for that member is waited for. Only an absent marker is cleared; a refused read or clear (the gateway
|
|
153
|
+
// left ready) makes the sweep incomplete. Resolves true once every key agent was read.
|
|
154
|
+
async sweep() {
|
|
155
|
+
let agents;
|
|
156
|
+
try {
|
|
157
|
+
agents = (await ctx.request('agents.list')).agents;
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
let complete = true;
|
|
163
|
+
for (const agent of agents) {
|
|
164
|
+
if (!agent.id.startsWith(KEY_PREFIX))
|
|
165
|
+
continue;
|
|
166
|
+
const member = agent.id.slice(KEY_PREFIX.length);
|
|
167
|
+
if (!check(member))
|
|
168
|
+
continue;
|
|
169
|
+
await exclusive(member, async () => {
|
|
170
|
+
const ready = await control(member, 'ready').catch(() => undefined);
|
|
171
|
+
if (ready?.ok === true)
|
|
172
|
+
return;
|
|
173
|
+
if (ready?.absent !== true) {
|
|
174
|
+
complete = false;
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const cleared = await control(member, 'clear').catch(() => undefined);
|
|
178
|
+
if (cleared?.ok !== true)
|
|
179
|
+
complete = false;
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return complete;
|
|
183
|
+
},
|
|
72
184
|
};
|
|
73
185
|
}
|
package/dist/kit.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { SealingAdapter } from '@byokit/secrets';
|
|
|
2
2
|
import { type AddKeyResult } from './keys.ts';
|
|
3
3
|
import { type RouteView } from './routes.ts';
|
|
4
4
|
import type { BrowserHost, BrowserOptions } from './browser.ts';
|
|
5
|
-
import type { Account, AccountId, Approval, CallOptions, Decision, Defaults, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, Hello, KitState, KitEventName, KitEventPayload, LearningCapture, LearningMode, Member, ModelInfo, Room, RunEnd, RunEvent, RunSpec, OutputSchema, SchemaOutput, SignInOptions, SignInView, ToolHost, ToolSpec } from './types.ts';
|
|
5
|
+
import type { Account, AccountId, AccountPick, Approval, CallOptions, Decision, Defaults, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, Hello, KitState, KitEventName, KitEventPayload, LearningCapture, LearningMode, Member, ModelInfo, MoveResult, Room, RunEnd, RunEvent, RunRef, RunSelection, RunSpec, OutputSchema, SchemaOutput, SignInOptions, SignInView, ToolHost, ToolSpec } from './types.ts';
|
|
6
6
|
export type KitOptions = {
|
|
7
7
|
stateDir: string;
|
|
8
8
|
authSeal?: SealingAdapter;
|
|
@@ -64,9 +64,12 @@ export declare class OpenClawKit {
|
|
|
64
64
|
private listeners;
|
|
65
65
|
private off;
|
|
66
66
|
private signInDisconnects;
|
|
67
|
+
private readonly phaseListeners;
|
|
67
68
|
private starting?;
|
|
68
69
|
private stopping;
|
|
69
70
|
private failures;
|
|
71
|
+
private swept;
|
|
72
|
+
private catalogs;
|
|
70
73
|
browser?: BrowserHost;
|
|
71
74
|
private browserHost?;
|
|
72
75
|
private readonly brokers;
|
|
@@ -90,6 +93,9 @@ export declare class OpenClawKit {
|
|
|
90
93
|
start(): Promise<void>;
|
|
91
94
|
private connect;
|
|
92
95
|
private disconnect;
|
|
96
|
+
private engineRestarting;
|
|
97
|
+
private authSnapshot;
|
|
98
|
+
private restarted;
|
|
93
99
|
private closed;
|
|
94
100
|
stop(): Promise<void>;
|
|
95
101
|
private closeBrowsers;
|
|
@@ -106,6 +112,7 @@ export declare class OpenClawKit {
|
|
|
106
112
|
routes(): RouteView[];
|
|
107
113
|
providerStatus(member: Member): Promise<string[] | undefined>;
|
|
108
114
|
providers(member: Member): Promise<string[]>;
|
|
115
|
+
/** A member sign-in to `provider`, or a ready key (5.8) whose saved model is that provider's. */
|
|
109
116
|
signedIn(member: Member, provider: string): Promise<boolean>;
|
|
110
117
|
signIn(member: Member, o: SignInOptions, on: (v: SignInView) => void): {
|
|
111
118
|
paste(text: string): void;
|
|
@@ -113,9 +120,18 @@ export declare class OpenClawKit {
|
|
|
113
120
|
done: Promise<SignInView>;
|
|
114
121
|
};
|
|
115
122
|
signOut(member: Member, provider: string): Promise<void>;
|
|
123
|
+
memberOf(ref: string): Member | undefined;
|
|
116
124
|
accounts(member: Member): Promise<Account[]>;
|
|
117
|
-
models(member: Member, id: AccountId
|
|
125
|
+
models(member: Member, id: AccountId | {
|
|
126
|
+
authChoice: string;
|
|
127
|
+
}): Promise<ModelInfo[]>;
|
|
118
128
|
room(member: Member, id: AccountId, demand?: string[]): Promise<Room>;
|
|
129
|
+
/** What `run` would decide now with this selection and key, refusals included; decides nothing and runs nothing. */
|
|
130
|
+
pick(member: Member, sel: RunSelection, o?: {
|
|
131
|
+
sessionKey?: string;
|
|
132
|
+
}): Promise<AccountPick>;
|
|
133
|
+
/** Carry a conversation to another account: its whole transcript forks onto that account, then the old session goes. */
|
|
134
|
+
move(ref: RunRef, to: AccountId): Promise<MoveResult>;
|
|
119
135
|
/** Sign in one more account: on the member agent while it holds none of the provider, else on a new sealed agent. */
|
|
120
136
|
addAccount(member: Member, o: {
|
|
121
137
|
authChoice: string;
|
|
@@ -132,13 +148,19 @@ export declare class OpenClawKit {
|
|
|
132
148
|
}>;
|
|
133
149
|
}>;
|
|
134
150
|
renameAccount(member: Member, id: AccountId, name: string): Promise<Account>;
|
|
151
|
+
/** Remove one account: a member-agent account through `authLogout` with its profileIds, an account agent through
|
|
152
|
+
* `clear`, an empty `list`, `agents.delete` with `deleteFiles: false`, removal of its state dir, then the entry. */
|
|
153
|
+
removeAccount(member: Member, id: AccountId): Promise<void>;
|
|
135
154
|
defaults(member: Member): Promise<Defaults>;
|
|
136
155
|
setDefaults(member: Member, d: Defaults): Promise<void>;
|
|
137
156
|
/** Save an explicitly supplied API key (billed per use) in this member's separate key agent. */
|
|
138
157
|
addKey(agentId: Member, o: {
|
|
139
158
|
authChoice: string;
|
|
140
159
|
apiKey: string;
|
|
160
|
+
model?: string;
|
|
141
161
|
}): Promise<AddKeyResult>;
|
|
162
|
+
private prepareKeyOwner;
|
|
163
|
+
private syncCatalogs;
|
|
142
164
|
migrateRetainedLogin(member: Member, source: RetainedLogin): Promise<'staged' | 'nothing' | 'failed'>;
|
|
143
165
|
confirmRetainedLogin(member: Member, source: RetainedLogin): Promise<boolean>;
|
|
144
166
|
private runs;
|
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',
|
|
@@ -443,9 +446,12 @@ export class OpenClawKit {
|
|
|
443
446
|
listeners = new Set();
|
|
444
447
|
off = [];
|
|
445
448
|
signInDisconnects = new Set();
|
|
449
|
+
phaseListeners = new Set();
|
|
446
450
|
starting;
|
|
447
451
|
stopping = false;
|
|
448
452
|
failures = 0;
|
|
453
|
+
swept = false;
|
|
454
|
+
catalogs = Promise.resolve();
|
|
449
455
|
browser;
|
|
450
456
|
browserHost;
|
|
451
457
|
brokers = new Map();
|
|
@@ -657,13 +663,17 @@ export class OpenClawKit {
|
|
|
657
663
|
onState: (s) => this.setState(s), onExit: () => this.closed('engine exited') });
|
|
658
664
|
this.admission = createAuthStatus(this.engine.root, (method, params, options) => this.request()(method, params, options));
|
|
659
665
|
this.keys = createKeys({ root: this.engine.root,
|
|
660
|
-
|
|
666
|
+
prepareOwner: (agentId) => this.prepareKeyOwner(agentId),
|
|
667
|
+
restarting: () => this.engineRestarting(),
|
|
661
668
|
request: (method, params, options) => this.request()(method, params, options),
|
|
662
|
-
ensure: (member) => this.ensureMember(member)
|
|
669
|
+
ensure: (member) => this.ensureMember(member),
|
|
670
|
+
authConfig: () => this.authSnapshot(),
|
|
671
|
+
restarted: (authBefore, signal) => this.restarted(authBefore, signal) });
|
|
663
672
|
const slot = {};
|
|
664
673
|
this.approvalsCtl = new Approvals({
|
|
665
674
|
request: (method, params, co) => this.request()(method, params, co),
|
|
666
675
|
bridge: { resolveAsk: (id, d) => slot.bridge?.resolveAsk(id, d) ?? false },
|
|
676
|
+
memberOf: (ref) => this.memberOf(ref),
|
|
667
677
|
});
|
|
668
678
|
const host = o.browser ? {
|
|
669
679
|
gate: async (run, tool, input, info) => {
|
|
@@ -692,6 +702,7 @@ export class OpenClawKit {
|
|
|
692
702
|
return browserSessionMayRun(this.browserHost.signIns(), key, this.bridge.isRegisteredRun(key, runId));
|
|
693
703
|
} } : {}),
|
|
694
704
|
permitted: o.permitted ?? (() => true), approvalTimeoutMs: o.approvalTimeoutMs ?? 180_000,
|
|
705
|
+
memberOf: (ref) => this.memberOf(ref),
|
|
695
706
|
onAsk: (a) => this.approvalsCtl.add(a), onAskGone: (id) => this.approvalsCtl.remove(id) });
|
|
696
707
|
slot.bridge = this.bridge;
|
|
697
708
|
}
|
|
@@ -700,6 +711,8 @@ export class OpenClawKit {
|
|
|
700
711
|
this.admission?.invalidate();
|
|
701
712
|
this.current = { ...s, ...(this.engine.patchSet !== undefined ? { patchSet: this.engine.patchSet } : {}) };
|
|
702
713
|
this.o.onState?.(this.current);
|
|
714
|
+
for (const fn of this.phaseListeners)
|
|
715
|
+
fn();
|
|
703
716
|
}
|
|
704
717
|
get state() { return this.current; }
|
|
705
718
|
async prepare() {
|
|
@@ -746,15 +759,22 @@ export class OpenClawKit {
|
|
|
746
759
|
if (!operatorMethods.has(name) && !nodeMethods.has(name))
|
|
747
760
|
this.o.log?.(`gateway has unknown method: ${name}`);
|
|
748
761
|
this.members = createMembers({ request: transport.request.bind(transport), root: this.engine.root });
|
|
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,
|
|
750
764
|
ensure: (member) => this.ensureMember(member), locks: this.locks,
|
|
751
765
|
signIn: (agentId, o, on, fresh) => startSignIn({ ...this.signInCtx(), ensure: async () => ({ agentId }), fresh }, agentId, o, on),
|
|
752
|
-
log: this.o.log });
|
|
766
|
+
routes: this.routeMap, log: this.o.log, reseal: () => this.engine.authStore.reseal() });
|
|
753
767
|
// The fail-closed tool bridge feeds the kit's single Approval surface (5.9, O5).
|
|
754
768
|
await this.bridge.start();
|
|
755
769
|
this.off.push(transport.onEvent((e) => this.approvalsCtl.handleEvent(e)));
|
|
756
770
|
this.failures = 0;
|
|
757
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);
|
|
758
778
|
if (this.o.browser)
|
|
759
779
|
await this.checkBrowserTools();
|
|
760
780
|
// Replays the engine's native approval lists over the now-live transport (N9).
|
|
@@ -803,6 +823,39 @@ export class OpenClawKit {
|
|
|
803
823
|
this.bridge.stop();
|
|
804
824
|
await transport?.stop();
|
|
805
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
|
+
}
|
|
806
859
|
closed(_why) {
|
|
807
860
|
const engineFailed = _why === 'engine exited' && this.current.phase === 'failed' && this.current.why === 'exited';
|
|
808
861
|
if (this.stopping || (this.current.phase !== 'ready' && !engineFailed))
|
|
@@ -839,9 +892,9 @@ export class OpenClawKit {
|
|
|
839
892
|
delete this.browserProfiles[profile];
|
|
840
893
|
}
|
|
841
894
|
request() {
|
|
842
|
-
if (!this.transport || this.current.phase !== 'ready')
|
|
843
|
-
throw new Error('gateway not ready');
|
|
844
895
|
const transport = this.transport;
|
|
896
|
+
if (!transport || this.current.phase !== 'ready')
|
|
897
|
+
return () => Promise.reject(new Error('gateway not ready'));
|
|
845
898
|
return (method, params, options) => {
|
|
846
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);
|
|
847
900
|
if (!changesAuth || (method === 'byokit.keys' && params?.action === 'ready'))
|
|
@@ -853,22 +906,12 @@ export class OpenClawKit {
|
|
|
853
906
|
call(method, params, o) {
|
|
854
907
|
if (this.o.browser && /^(config\.(apply|patch|set)|agents\.(create|update|delete)|plugins\.(install|uninstall|setEnabled|refresh))$/.test(method))
|
|
855
908
|
return Promise.reject(new Error('browser policy changes require guarded patchConfig'));
|
|
856
|
-
|
|
857
|
-
return this.request()(method, params, o);
|
|
858
|
-
}
|
|
859
|
-
catch (error) {
|
|
860
|
-
return Promise.reject(error);
|
|
861
|
-
}
|
|
909
|
+
return this.request()(method, params, o);
|
|
862
910
|
}
|
|
863
911
|
callDynamic(method, params, o) {
|
|
864
912
|
if (operatorMethods.has(method) || nodeMethods.has(method))
|
|
865
913
|
return Promise.reject(new Error(`use typed call for generated method: ${method}`));
|
|
866
|
-
|
|
867
|
-
return this.request()(method, params, o);
|
|
868
|
-
}
|
|
869
|
-
catch (error) {
|
|
870
|
-
return Promise.reject(error);
|
|
871
|
-
}
|
|
914
|
+
return this.request()(method, params, o);
|
|
872
915
|
}
|
|
873
916
|
get hello() { return this.greeting; }
|
|
874
917
|
onEvent(event, fn) {
|
|
@@ -908,7 +951,9 @@ export class OpenClawKit {
|
|
|
908
951
|
// Public readiness/sign-in reads stay live; their fresh result can prepare the next run's admission.
|
|
909
952
|
authStatus: (agentId, refresh, native) => this.admission.read(agentId, refresh, native, false),
|
|
910
953
|
ensure: (member) => this.ensureMember(member), callbackPort: this.o.callbackPort ?? 1455,
|
|
911
|
-
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) };
|
|
912
957
|
}
|
|
913
958
|
routes() {
|
|
914
959
|
return routeTable();
|
|
@@ -920,8 +965,10 @@ export class OpenClawKit {
|
|
|
920
965
|
providers(member) {
|
|
921
966
|
return this.providerStatus(member).then((names) => names ?? []);
|
|
922
967
|
}
|
|
968
|
+
/** A member sign-in to `provider`, or a ready key (5.8) whose saved model is that provider's. */
|
|
923
969
|
signedIn(member, provider) {
|
|
924
|
-
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);
|
|
925
972
|
}
|
|
926
973
|
signIn(member, o, on) {
|
|
927
974
|
return startSignIn(this.signInCtx(), member, o, on);
|
|
@@ -929,14 +976,22 @@ export class OpenClawKit {
|
|
|
929
976
|
signOut(member, provider) {
|
|
930
977
|
return engineSignOut(this.signInCtx(), member, provider);
|
|
931
978
|
}
|
|
932
|
-
//
|
|
933
|
-
//
|
|
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 S8-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, `move`
|
|
986
|
+
// carries a conversation to another account, and `removeAccount` deletes one.
|
|
934
987
|
accounts(member) {
|
|
935
988
|
if (!this.accountsCtl)
|
|
936
989
|
return Promise.reject(new Error('gateway not ready'));
|
|
937
990
|
return this.accountsCtl.list(member);
|
|
938
991
|
}
|
|
939
992
|
models(member, id) {
|
|
993
|
+
if (typeof id !== 'string')
|
|
994
|
+
return this.keys.models(member, id);
|
|
940
995
|
if (!this.accountsCtl)
|
|
941
996
|
return Promise.reject(new Error('gateway not ready'));
|
|
942
997
|
return this.accountsCtl.models(member, id);
|
|
@@ -946,17 +1001,40 @@ export class OpenClawKit {
|
|
|
946
1001
|
return Promise.reject(new Error('gateway not ready'));
|
|
947
1002
|
return this.accountsCtl.room(member, id, demand);
|
|
948
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
|
+
}
|
|
949
1018
|
/** Sign in one more account: on the member agent while it holds none of the provider, else on a new sealed agent. */
|
|
950
1019
|
addAccount(member, o, on) {
|
|
951
1020
|
if (!this.accountsCtl)
|
|
952
1021
|
return Promise.reject(new Error('gateway not ready'));
|
|
953
|
-
return this.accountsCtl.add(member, o, on)
|
|
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; }) }));
|
|
954
1025
|
}
|
|
955
1026
|
renameAccount(member, id, name) {
|
|
956
1027
|
if (!this.accountsCtl)
|
|
957
1028
|
return Promise.reject(new Error('gateway not ready'));
|
|
958
1029
|
return this.accountsCtl.rename(member, id, name);
|
|
959
1030
|
}
|
|
1031
|
+
/** Remove one account: a member-agent account through `authLogout` with its profileIds, an account agent through
|
|
1032
|
+
* `clear`, an empty `list`, `agents.delete` with `deleteFiles: false`, removal of its state dir, then the entry. */
|
|
1033
|
+
removeAccount(member, id) {
|
|
1034
|
+
if (!this.accountsCtl)
|
|
1035
|
+
return Promise.reject(new Error('gateway not ready'));
|
|
1036
|
+
return this.accountsCtl.remove(member, id);
|
|
1037
|
+
}
|
|
960
1038
|
defaults(member) {
|
|
961
1039
|
if (!this.accountsCtl)
|
|
962
1040
|
return Promise.reject(new Error('gateway not ready'));
|
|
@@ -971,6 +1049,37 @@ export class OpenClawKit {
|
|
|
971
1049
|
addKey(agentId, o) {
|
|
972
1050
|
return this.keys.add(agentId, o);
|
|
973
1051
|
}
|
|
1052
|
+
async prepareKeyOwner(agentId) {
|
|
1053
|
+
this.syncCatalogs();
|
|
1054
|
+
await this.catalogs;
|
|
1055
|
+
if ((await authStatus(this.request(), agentId, true)).unavailable)
|
|
1056
|
+
throw new Error('Key owner unavailable.');
|
|
1057
|
+
}
|
|
1058
|
+
// Live provider catalogs (5.6): registers each live model with its api from the provider's catalog data, so
|
|
1059
|
+
// embedded runs resolve it. Serialized and in the background; a failure is logged and keeps the registered list.
|
|
1060
|
+
syncCatalogs() {
|
|
1061
|
+
for (const provider of LIVE_CATALOGS) {
|
|
1062
|
+
if (this.o.config?.models?.providers?.[provider]?.models !== undefined)
|
|
1063
|
+
continue;
|
|
1064
|
+
this.catalogs = this.catalogs.then(async () => {
|
|
1065
|
+
const answer = await this.request()('byokit.catalog', { provider });
|
|
1066
|
+
if (!answer.ok) {
|
|
1067
|
+
if (answer.reason !== 'no-key')
|
|
1068
|
+
this.o.log?.(`catalog sync ${provider}: ${answer.reason ?? 'failed'}`);
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
const { config } = await this.request()('config.get');
|
|
1072
|
+
const registered = config?.models?.providers?.[provider]?.models ?? [];
|
|
1073
|
+
const byId = new Map(registered.map((m) => [m.id, m]));
|
|
1074
|
+
for (const m of answer.models ?? [])
|
|
1075
|
+
byId.set(m.id, m);
|
|
1076
|
+
const models = [...byId.values()];
|
|
1077
|
+
if (!isDeepStrictEqual(models, registered))
|
|
1078
|
+
await this.patchConfig({ models: { providers: { [provider]: { models } } } });
|
|
1079
|
+
}).catch(() => { if (this.current.phase === 'ready')
|
|
1080
|
+
this.o.log?.(`catalog sync ${provider}: failed`); });
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
974
1083
|
migrateRetainedLogin(member, source) {
|
|
975
1084
|
return migrateLogin({ root: this.engine.root, prepare: () => this.prepare(), doctor: () => this.engine.doctor(120_000),
|
|
976
1085
|
seal: this.o.authSeal, log: this.o.log, withStore: (task) => this.engine.withAuthStore(task) }, member, source);
|
|
@@ -1027,10 +1136,11 @@ export class OpenClawKit {
|
|
|
1027
1136
|
const selected = await this.keys.ready(spec.member);
|
|
1028
1137
|
if (!selected)
|
|
1029
1138
|
return { ok: false, kind: 'signed-out', message: 'Add an API key to use this option.' };
|
|
1030
|
-
|
|
1031
|
-
|
|
1139
|
+
// A key pays for its provider's models (5.8); the engine refuses one its catalog does not list.
|
|
1140
|
+
if (spec.model !== undefined && providerOf(spec.model) !== providerOf(selected.model))
|
|
1141
|
+
return { ok: false, kind: 'plan', message: 'Choose a model of the provider saved with this key.' };
|
|
1032
1142
|
// RunRef.member stays the person for app gates; the engine receives the isolated agent and history.
|
|
1033
|
-
return this.runs().run({ ...spec, model: selected.model }, on, selected.agentId, output);
|
|
1143
|
+
return this.runs().run({ ...spec, model: spec.model ?? selected.model }, on, selected.agentId, output);
|
|
1034
1144
|
});
|
|
1035
1145
|
return this.runs().run(spec, on, undefined, output);
|
|
1036
1146
|
}
|