@byokit/openclaw 0.13.0 → 0.14.2
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 +40 -0
- package/README.md +29 -12
- package/SECURITY.md +6 -2
- package/dist/accounts.d.ts +31 -1
- package/dist/accounts.js +433 -101
- package/dist/approvals.js +1 -1
- package/dist/auth-status.js +1 -1
- package/dist/auth-store.d.ts +2 -0
- package/dist/auth-store.js +73 -8
- package/dist/bridge.js +1 -1
- package/dist/browser/host.js +3 -3
- package/dist/browser/verify.js +1 -1
- package/dist/config.js +2 -2
- package/dist/constants.d.ts +1 -1
- package/dist/day-usage.js +1 -1
- package/dist/device.js +7 -7
- package/dist/engine.d.ts +12 -0
- package/dist/engine.js +77 -13
- package/dist/index.d.ts +2 -2
- package/dist/index.js +8 -8
- package/dist/keys.d.ts +17 -2
- package/dist/keys.js +133 -64
- package/dist/kit.d.ts +15 -3
- package/dist/kit.js +89 -36
- package/dist/link.js +17 -11
- package/dist/migrate.js +3 -3
- package/dist/pick.js +5 -1
- package/dist/runs.js +28 -11
- package/dist/signin.d.ts +4 -0
- package/dist/signin.js +85 -27
- package/dist/testing/browser.js +2 -2
- package/dist/testing/contract.js +3 -3
- package/dist/testing/fake-gateway.js +28 -5
- package/dist/testing/index.js +4 -4
- package/dist/testing/model-stub.d.ts +4 -0
- package/dist/testing/model-stub.js +8 -1
- package/dist/thinking.d.ts +16 -0
- package/dist/thinking.js +62 -0
- package/dist/transport.d.ts +1 -1
- package/dist/transport.js +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/words.json +5 -1
- package/package.json +6 -5
- package/plugin/keys.js +3 -2
package/dist/keys.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
-
import { activate } from
|
|
3
|
-
import { keyAgentId, MEMBER_ID, KEY_PREFIX, untilListed } from
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
2
|
+
import { activate } from './activation.js';
|
|
3
|
+
import { keyAgentId, MEMBER_ID, KEY_PREFIX, untilListed } from './members.js';
|
|
4
|
+
import { providerOf } from './pick.js';
|
|
5
|
+
import { routes } from './routes.js';
|
|
6
|
+
import { modelsOf } from './accounts.js';
|
|
7
|
+
import { LIVE_CATALOGS } from './constants.js';
|
|
7
8
|
export function createKeys(ctx) {
|
|
8
9
|
// Serialize key replacement and key runs, so an in-flight run never sees a half-installed replacement.
|
|
9
10
|
const tails = new Map();
|
|
@@ -73,62 +74,96 @@ export function createKeys(ctx) {
|
|
|
73
74
|
return {
|
|
74
75
|
models,
|
|
75
76
|
exclusive,
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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.
|
|
77
|
+
add(member, o) {
|
|
78
|
+
const ac = new AbortController();
|
|
79
|
+
const cancel = () => { if (!ac.signal.aborted)
|
|
80
|
+
ac.abort(); };
|
|
81
|
+
const done = (async () => {
|
|
82
|
+
if (!check(member))
|
|
83
|
+
throw new Error('Choose your own member.');
|
|
84
|
+
if (!o.apiKey.trim())
|
|
85
|
+
return failure('invalid', 'empty');
|
|
86
|
+
const route = routes().find((r) => r.choice === o.authChoice && r.keyEntry && r.readiness === 'ready');
|
|
87
|
+
if (!route || (o.model !== undefined && !LIVE_CATALOGS.includes(route.provider)))
|
|
88
|
+
return failure('not_included', 'model');
|
|
89
|
+
// A workspace names a live-catalog login's own workspace only; another route takes no header, and a
|
|
90
|
+
// refusal here leaves the member's lanes untouched (it lands before any prepare or activation).
|
|
91
|
+
const orgId = typeof o.orgId === 'string' ? o.orgId.trim() : '';
|
|
92
|
+
if (orgId) {
|
|
93
|
+
if (orgId.length > 256)
|
|
94
|
+
return failure('invalid', 'auth');
|
|
95
|
+
if (!LIVE_CATALOGS.includes(route.provider))
|
|
96
|
+
return failure('not_included', 'model');
|
|
97
|
+
}
|
|
98
|
+
if (ac.signal.aborted)
|
|
99
|
+
return failure('retry', 'busy');
|
|
100
|
+
return exclusive(member, async () => {
|
|
101
|
+
let sent = false;
|
|
102
|
+
// A key the engine may have taken but not sealed is cleared (prepare already emptied the agent); if the clear
|
|
103
|
+
// does not reach the engine, the key stays unsealed and unusable until the next key add or kit start, so runs
|
|
104
|
+
// fail closed.
|
|
105
|
+
const abandon = async (result) => {
|
|
106
|
+
await control(member, 'clear').catch(() => undefined);
|
|
107
|
+
return result;
|
|
108
|
+
};
|
|
109
|
+
const cancelled = () => failure('retry', 'busy');
|
|
113
110
|
try {
|
|
114
|
-
|
|
111
|
+
if (ac.signal.aborted)
|
|
112
|
+
return cancelled();
|
|
113
|
+
await ctx.ensure(member);
|
|
114
|
+
const agentId = keyAgentId(member);
|
|
115
|
+
const list = await ctx.request('agents.list');
|
|
116
|
+
if (!list.agents.some((a) => a.id === agentId))
|
|
117
|
+
await ctx.request('agents.create', { name: agentId, workspace: join(ctx.root, 'key-workspaces', member) });
|
|
118
|
+
// Creation answers before the runtime roster/owners publish, just as for ordinary members.
|
|
119
|
+
await untilListed(ctx.request, agentId);
|
|
120
|
+
if (ac.signal.aborted)
|
|
121
|
+
return cancelled();
|
|
122
|
+
if (!(await control(member, 'prepare', route.provider)).ok)
|
|
123
|
+
return failure('retry', 'engine');
|
|
124
|
+
// The workspace rides the provider's probe and every run from its config; it is written before the
|
|
125
|
+
// activation, so the probe itself carries it. The slot is provider-wide (shared by every member's
|
|
126
|
+
// key on this engine), so a replacement with no `orgId` keeps the standing one and `removeKey` drops it.
|
|
127
|
+
if (orgId)
|
|
128
|
+
await ctx.setProviderHeaders(route.provider, { 'x-opencode-org-id': orgId });
|
|
129
|
+
// The public pin's typed API-key path, never a CLI-login import. Discard the engine's lines/errors.
|
|
130
|
+
sent = true;
|
|
131
|
+
if (ac.signal.aborted)
|
|
132
|
+
return abandon(cancelled());
|
|
133
|
+
const result = await activate(ctx, { kind: 'api-key', agentId, authChoice: o.authChoice, apiKey: o.apiKey }, { timeoutMs: 180_000, signal: ac.signal });
|
|
134
|
+
if (ac.signal.aborted)
|
|
135
|
+
return abandon(cancelled());
|
|
136
|
+
if (!result.ok && !result.late)
|
|
137
|
+
return abandon(probeResult(result.status));
|
|
138
|
+
if (result.late)
|
|
139
|
+
return abandon(failure('retry', 'busy'));
|
|
140
|
+
if (o.model !== undefined && !(await models(member, o)).some((m) => m.id === o.model && m.available))
|
|
141
|
+
return abandon(failure('not_included', 'model'));
|
|
142
|
+
// Prepare before publishing the ready marker: failed preparation remains unusable even if clear fails.
|
|
143
|
+
try {
|
|
144
|
+
await ctx.prepareOwner(agentId);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return abandon(failure('retry', 'engine'));
|
|
148
|
+
}
|
|
149
|
+
if (ac.signal.aborted)
|
|
150
|
+
return abandon(cancelled());
|
|
151
|
+
if (o.model !== undefined && LIVE_CATALOGS.includes(route.provider) && !(await registered(route.provider, o.model)))
|
|
152
|
+
return abandon(failure('retry', 'model'));
|
|
153
|
+
if (!(await control(member, 'seal', undefined, o.model)).ok)
|
|
154
|
+
return abandon(failure('retry', 'engine'));
|
|
155
|
+
await ctx.indexLane(member, route);
|
|
156
|
+
return { status: 'ok' };
|
|
115
157
|
}
|
|
116
|
-
catch {
|
|
117
|
-
|
|
158
|
+
catch (error) {
|
|
159
|
+
const reason = errorReason(error, sent);
|
|
160
|
+
if (sent)
|
|
161
|
+
return abandon(failure('retry', reason));
|
|
162
|
+
return failure('retry', reason);
|
|
118
163
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
return { status: 'ok' };
|
|
123
|
-
return abandon(failure('retry', 'engine'));
|
|
124
|
-
}
|
|
125
|
-
catch (error) {
|
|
126
|
-
const reason = errorReason(error, sent);
|
|
127
|
-
if (sent)
|
|
128
|
-
return abandon(failure('retry', reason));
|
|
129
|
-
return failure('retry', reason);
|
|
130
|
-
}
|
|
131
|
-
});
|
|
164
|
+
});
|
|
165
|
+
})();
|
|
166
|
+
return Object.assign(done, { cancel });
|
|
132
167
|
},
|
|
133
168
|
async ready(member, o = {}) {
|
|
134
169
|
if (!check(member))
|
|
@@ -147,11 +182,41 @@ export function createKeys(ctx) {
|
|
|
147
182
|
}
|
|
148
183
|
}
|
|
149
184
|
},
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
|
|
185
|
+
// Remove this member's saved key: the key agent's store and its ready marker, so the lane reads signed out and
|
|
186
|
+
// stays signed out, plus its `<provider>.key` index entry, so the accounts list stops naming the removed key.
|
|
187
|
+
// `provider` scopes it to the key actually saved for that provider (the member agent's own
|
|
188
|
+
// sign-out never reaches this lane), so signing out of one provider never drops another's key. A lane that reads
|
|
189
|
+
// not ready has nothing saved to remove; a refused read or clear is a typed refusal, never raw engine text.
|
|
190
|
+
// Serialized with adds and key runs like every other key change.
|
|
191
|
+
async remove(member, o = {}) {
|
|
192
|
+
if (!check(member))
|
|
193
|
+
throw new Error('Choose your own member.');
|
|
194
|
+
await exclusive(member, async () => {
|
|
195
|
+
const refused = () => { throw new Error('The key could not be removed.'); };
|
|
196
|
+
if (o.provider !== undefined) {
|
|
197
|
+
// An unreadable lane cannot name its provider, so removal never guesses and risks another provider's key.
|
|
198
|
+
const ready = await control(member, 'ready').catch(() => undefined);
|
|
199
|
+
if (!ready)
|
|
200
|
+
refused();
|
|
201
|
+
else if (ready.ok !== true || providerOf(ready.model ?? '') !== o.provider)
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
// The lane's own provider, when the removal can name it: the header clear below is scoped like the removal.
|
|
205
|
+
const cleared = await control(member, 'clear').catch(() => undefined);
|
|
206
|
+
if (cleared?.ok !== true && cleared?.absent !== true)
|
|
207
|
+
refused();
|
|
208
|
+
// A workspace header the removed key named must not outlive it; the next add names its own (or none).
|
|
209
|
+
if (o.provider !== undefined && LIVE_CATALOGS.includes(o.provider))
|
|
210
|
+
await ctx.setProviderHeaders(o.provider, null).catch(() => { throw new Error('The key could not be removed.'); });
|
|
211
|
+
await ctx.dropLane(member, o.provider);
|
|
212
|
+
});
|
|
213
|
+
},
|
|
214
|
+
// Kit connect calls this once the gateway is ready: a failed add whose abandon clear never reached the engine
|
|
215
|
+
// leaves an unsealed key with no ready marker, so drop it (`clear`, until a sweep completes). A single ready read
|
|
216
|
+
// per key agent, no retries; an add in flight for that member is waited for. Only an absent marker is cleared; a
|
|
217
|
+
// refused read or clear (the gateway left ready) makes the sweep incomplete. A sealed lane is backfilled on every
|
|
218
|
+
// call. Resolves true once every key agent was read.
|
|
219
|
+
async sweep(clear) {
|
|
155
220
|
let agents;
|
|
156
221
|
try {
|
|
157
222
|
agents = (await ctx.request('agents.list')).agents;
|
|
@@ -168,7 +233,11 @@ export function createKeys(ctx) {
|
|
|
168
233
|
continue;
|
|
169
234
|
await exclusive(member, async () => {
|
|
170
235
|
const ready = await control(member, 'ready').catch(() => undefined);
|
|
171
|
-
if (ready?.ok === true)
|
|
236
|
+
if (ready?.ok === true) {
|
|
237
|
+
await ctx.backfillLane(member, ready.model);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (!clear)
|
|
172
241
|
return;
|
|
173
242
|
if (ready?.absent !== true) {
|
|
174
243
|
complete = false;
|
package/dist/kit.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SealingAdapter } from '@byokit/secrets';
|
|
2
|
-
import { type
|
|
2
|
+
import { type AddKeyHandle } from './keys.ts';
|
|
3
3
|
import { type RouteView } from './routes.ts';
|
|
4
4
|
import type { BrowserHost, BrowserOptions } from './browser.ts';
|
|
5
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';
|
|
@@ -153,12 +153,24 @@ export declare class OpenClawKit {
|
|
|
153
153
|
removeAccount(member: Member, id: AccountId): Promise<void>;
|
|
154
154
|
defaults(member: Member): Promise<Defaults>;
|
|
155
155
|
setDefaults(member: Member, d: Defaults): Promise<void>;
|
|
156
|
-
/** Save an explicitly supplied API key (billed per use) in this member's separate key agent.
|
|
156
|
+
/** Save an explicitly supplied API key (billed per use) in this member's separate key agent.
|
|
157
|
+
* `orgId` names the workspace a Go plan login acts for (the pick the app captured at connect);
|
|
158
|
+
* it rides every probe and run as `x-opencode-org-id`, and replacing the key rewrites it. */
|
|
157
159
|
addKey(agentId: Member, o: {
|
|
158
160
|
authChoice: string;
|
|
159
161
|
apiKey: string;
|
|
160
162
|
model?: string;
|
|
161
|
-
|
|
163
|
+
orgId?: string;
|
|
164
|
+
}): AddKeyHandle;
|
|
165
|
+
private indexKeyLane;
|
|
166
|
+
/** Remove this member's saved key (the `byokit-key-<member>` lane: its credential store and ready marker), so
|
|
167
|
+
* `signedIn` is false for it and a restart cannot revive it. `provider` removes only that provider's saved key;
|
|
168
|
+
* with no saved key the call resolves without doing anything. */
|
|
169
|
+
removeKey(member: Member, o?: {
|
|
170
|
+
provider?: string;
|
|
171
|
+
}): Promise<void>;
|
|
172
|
+
private dropKeyLane;
|
|
173
|
+
private backfillKeyLane;
|
|
162
174
|
private prepareKeyOwner;
|
|
163
175
|
private syncCatalogs;
|
|
164
176
|
migrateRetainedLogin(member: Member, source: RetainedLogin): Promise<'staged' | 'nothing' | 'failed'>;
|
package/dist/kit.js
CHANGED
|
@@ -3,29 +3,29 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
4
4
|
import { isDeepStrictEqual } from 'node:util';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { PROTOCOL_VERSION, LIVE_CATALOGS } from
|
|
7
|
-
import { atomic } from
|
|
8
|
-
import { Approvals } from
|
|
9
|
-
import { Bridge } from
|
|
10
|
-
import { Engine, refusedOnly } from
|
|
11
|
-
import { AuthStoreSealSizeError } from
|
|
12
|
-
import { gatewayTransport } from
|
|
13
|
-
import { createMembers } from
|
|
14
|
-
import { createAccounts, memberOfAccount } from
|
|
15
|
-
import { createLocks } from
|
|
16
|
-
import { keyAgentId, KEY_PREFIX, MEMBER_ID } from
|
|
17
|
-
import { confirmRetainedLogin as confirmLogin, migrateRetainedLogin as migrateLogin } from
|
|
18
|
-
import { createRuns, signedInProviders } from
|
|
19
|
-
import { authStatus, createAuthStatus } from
|
|
20
|
-
import { outputSchema } from
|
|
21
|
-
import { createKeys } from
|
|
22
|
-
import { routes as routeTable } from
|
|
23
|
-
import { providerOf } from
|
|
24
|
-
import { signIn as startSignIn, signOut as engineSignOut } from
|
|
25
|
-
import { reconcileConfig, browserToolPolicySafe, browserProfileAcknowledged, browserSessionMayRun, memoryLimited as configMemoryLimited, configKeyPath, readConfigKey, writeConfigKey } from
|
|
26
|
-
import { createBrowserHost } from
|
|
27
|
-
import { fileSignInStore } from
|
|
28
|
-
import { launchBroker } from
|
|
6
|
+
import { PROTOCOL_VERSION, LIVE_CATALOGS } from './constants.js';
|
|
7
|
+
import { atomic } from './engine-patches.js';
|
|
8
|
+
import { Approvals } from './approvals.js';
|
|
9
|
+
import { Bridge } from './bridge.js';
|
|
10
|
+
import { Engine, canonicalStateDir, refusedOnly } from './engine.js';
|
|
11
|
+
import { AuthStoreSealSizeError } from './auth-store.js';
|
|
12
|
+
import { gatewayTransport } from './transport.js';
|
|
13
|
+
import { createMembers } from './members.js';
|
|
14
|
+
import { createAccounts, memberOfAccount } from './accounts.js';
|
|
15
|
+
import { createLocks } from './locks.js';
|
|
16
|
+
import { keyAgentId, KEY_PREFIX, MEMBER_ID } from './members.js';
|
|
17
|
+
import { confirmRetainedLogin as confirmLogin, migrateRetainedLogin as migrateLogin } from './migrate.js';
|
|
18
|
+
import { createRuns, signedInProviders } from './runs.js';
|
|
19
|
+
import { authStatus, createAuthStatus } from './auth-status.js';
|
|
20
|
+
import { outputSchema } from './output.js';
|
|
21
|
+
import { createKeys } from './keys.js';
|
|
22
|
+
import { routes as routeTable } from './routes.js';
|
|
23
|
+
import { providerOf } from './pick.js';
|
|
24
|
+
import { signIn as startSignIn, signOut as engineSignOut } from './signin.js';
|
|
25
|
+
import { reconcileConfig, browserToolPolicySafe, browserProfileAcknowledged, browserSessionMayRun, memoryLimited as configMemoryLimited, configKeyPath, readConfigKey, writeConfigKey } from './config.js';
|
|
26
|
+
import { createBrowserHost } from './browser/host.js';
|
|
27
|
+
import { fileSignInStore } from './browser/store.js';
|
|
28
|
+
import { launchBroker } from './browser/broker.js';
|
|
29
29
|
/** The engine's learning-mode config key (5.6 "Engine learning state"). */
|
|
30
30
|
const LEARNING_KEY = 'skills.workshop.autonomous.mode';
|
|
31
31
|
const RESTART_MS = 60_000;
|
|
@@ -651,7 +651,7 @@ export class OpenClawKit {
|
|
|
651
651
|
|| (o.browser && ['browser', 'request_sign_in', 'exec', 'process', 'code_execution', 'terminal', 'read', 'write', 'edit', 'apply_patch', 'gateway'].includes(t.name)))
|
|
652
652
|
throw new Error(`invalid tool name: ${t.name}`);
|
|
653
653
|
}
|
|
654
|
-
this.o = o;
|
|
654
|
+
this.o = { ...o, stateDir: canonicalStateDir(o.stateDir) };
|
|
655
655
|
this.engine = new Engine({ ...o, pluginId: o.plugin?.id ?? 'byokit', tools: [...o.tools ?? [], ...(o.browser ? [{
|
|
656
656
|
name: 'request_sign_in', description: 'Ask the person to sign in to the current browser site.',
|
|
657
657
|
parameters: { type: 'object', additionalProperties: false, properties: {
|
|
@@ -664,11 +664,15 @@ export class OpenClawKit {
|
|
|
664
664
|
this.admission = createAuthStatus(this.engine.root, (method, params, options) => this.request()(method, params, options));
|
|
665
665
|
this.keys = createKeys({ root: this.engine.root,
|
|
666
666
|
prepareOwner: (agentId) => this.prepareKeyOwner(agentId),
|
|
667
|
+
indexLane: (member, route) => this.indexKeyLane(member, route),
|
|
668
|
+
dropLane: (member, provider) => this.dropKeyLane(member, provider),
|
|
669
|
+
backfillLane: (member, model) => this.backfillKeyLane(member, model),
|
|
667
670
|
restarting: () => this.engineRestarting(),
|
|
668
671
|
request: (method, params, options) => this.request()(method, params, options),
|
|
669
672
|
ensure: (member) => this.ensureMember(member),
|
|
670
673
|
authConfig: () => this.authSnapshot(),
|
|
671
|
-
restarted: (authBefore, signal) => this.restarted(authBefore, signal)
|
|
674
|
+
restarted: (authBefore, signal) => this.restarted(authBefore, signal),
|
|
675
|
+
setProviderHeaders: (provider, headers) => this.patchConfig({ models: { providers: { [provider]: { headers: headers ?? null } } } }) });
|
|
672
676
|
const slot = {};
|
|
673
677
|
this.approvalsCtl = new Approvals({
|
|
674
678
|
request: (method, params, co) => this.request()(method, params, co),
|
|
@@ -771,10 +775,11 @@ export class OpenClawKit {
|
|
|
771
775
|
this.setState({ phase: 'ready' });
|
|
772
776
|
this.syncCatalogs();
|
|
773
777
|
// 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
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
+
// runs stay closed, so drop it now that the gateway is ready. Ready keys keep their marker and survive. Every
|
|
779
|
+
// connect backfills the key lanes' index entries; only the clear stops once a sweep completes.
|
|
780
|
+
const clear = !this.swept;
|
|
781
|
+
void this.keys.sweep(clear).then((done) => { if (clear && done)
|
|
782
|
+
this.swept = true; }).catch(() => undefined);
|
|
778
783
|
if (this.o.browser)
|
|
779
784
|
await this.checkBrowserTools();
|
|
780
785
|
// Replays the engine's native approval lists over the now-live transport (N9).
|
|
@@ -951,7 +956,10 @@ export class OpenClawKit {
|
|
|
951
956
|
// Public readiness/sign-in reads stay live; their fresh result can prepare the next run's admission.
|
|
952
957
|
authStatus: (agentId, refresh, native) => this.admission.read(agentId, refresh, native, false),
|
|
953
958
|
ensure: (member) => this.ensureMember(member), callbackPort: this.o.callbackPort ?? 1455,
|
|
959
|
+
nativeClaudeLogin: () => this.engine.nativeClaudeLogin(),
|
|
960
|
+
resetNativeClaudeLogin: () => this.engine.resetNativeClaudeLogin(),
|
|
954
961
|
onDisconnect: (fn) => { this.signInDisconnects.add(fn); return () => { this.signInDisconnects.delete(fn); }; },
|
|
962
|
+
stopSignal: this.engine.stopSignal,
|
|
955
963
|
authConfig: () => this.authSnapshot(),
|
|
956
964
|
restarted: (authBefore, signal) => this.restarted(authBefore, signal) };
|
|
957
965
|
}
|
|
@@ -966,15 +974,31 @@ export class OpenClawKit {
|
|
|
966
974
|
return this.providerStatus(member).then((names) => names ?? []);
|
|
967
975
|
}
|
|
968
976
|
/** A member sign-in to `provider`, or a ready key (5.8) whose saved model is that provider's. */
|
|
969
|
-
signedIn(member, provider) {
|
|
970
|
-
|
|
971
|
-
|
|
977
|
+
async signedIn(member, provider) {
|
|
978
|
+
// The key probe stands alone: a member-agent admission failure after an engine restart must not hide a sealed key.
|
|
979
|
+
const keyReady = async () => {
|
|
980
|
+
try {
|
|
981
|
+
return providerOf((await this.keys.ready(member, { once: true }))?.model ?? '') === provider;
|
|
982
|
+
}
|
|
983
|
+
catch {
|
|
984
|
+
return false;
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
try {
|
|
988
|
+
if ((await this.providers(member)).includes(provider))
|
|
989
|
+
return true;
|
|
990
|
+
}
|
|
991
|
+
catch { /* key probe below */ }
|
|
992
|
+
return keyReady();
|
|
972
993
|
}
|
|
973
994
|
signIn(member, o, on) {
|
|
974
995
|
return startSignIn(this.signInCtx(), member, o, on);
|
|
975
996
|
}
|
|
976
|
-
signOut(member, provider) {
|
|
977
|
-
|
|
997
|
+
async signOut(member, provider) {
|
|
998
|
+
await engineSignOut(this.signInCtx(), member, provider);
|
|
999
|
+
// The saved key lives on its own `byokit-key-<member>` agent, which the member-agent logout never reaches: without
|
|
1000
|
+
// this, a key of that provider stays sealed and ready and the member reads signed in again after a restart.
|
|
1001
|
+
await this.keys.remove(member, { provider });
|
|
978
1002
|
}
|
|
979
1003
|
// The member an agent id or session key belongs to, through the account index (5.15, O14m). Reads the index file, so it
|
|
980
1004
|
// needs no live gateway.
|
|
@@ -1045,10 +1069,38 @@ export class OpenClawKit {
|
|
|
1045
1069
|
return Promise.reject(new Error('gateway not ready'));
|
|
1046
1070
|
return this.accountsCtl.setDefaults(member, d);
|
|
1047
1071
|
}
|
|
1048
|
-
/** Save an explicitly supplied API key (billed per use) in this member's separate key agent.
|
|
1072
|
+
/** Save an explicitly supplied API key (billed per use) in this member's separate key agent.
|
|
1073
|
+
* `orgId` names the workspace a Go plan login acts for (the pick the app captured at connect);
|
|
1074
|
+
* it rides every probe and run as `x-opencode-org-id`, and replacing the key rewrites it. */
|
|
1049
1075
|
addKey(agentId, o) {
|
|
1050
1076
|
return this.keys.add(agentId, o);
|
|
1051
1077
|
}
|
|
1078
|
+
// The lane's index entry follows its sealed key. A failed write is logged and never fails the add; the start sweep repairs it.
|
|
1079
|
+
async indexKeyLane(member, route) {
|
|
1080
|
+
if (!this.accountsCtl) {
|
|
1081
|
+
this.o.log?.('addKey: the account index is not ready, so the key lane is indexed at the next connect');
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
await this.accountsCtl.key(member, route).catch((error) => this.o.log?.(`addKey: the key lane is not indexed yet: ${error instanceof Error ? error.message : String(error)}`));
|
|
1085
|
+
}
|
|
1086
|
+
/** Remove this member's saved key (the `byokit-key-<member>` lane: its credential store and ready marker), so
|
|
1087
|
+
* `signedIn` is false for it and a restart cannot revive it. `provider` removes only that provider's saved key;
|
|
1088
|
+
* with no saved key the call resolves without doing anything. */
|
|
1089
|
+
removeKey(member, o) {
|
|
1090
|
+
return this.keys.remove(member, o);
|
|
1091
|
+
}
|
|
1092
|
+
// The lane's index entry follows its sealed key down. A failed drop is logged and never fails the removal.
|
|
1093
|
+
async dropKeyLane(member, provider) {
|
|
1094
|
+
if (!this.accountsCtl) {
|
|
1095
|
+
this.o.log?.('removeKey: the account index is not ready, so the key lane entry stays until the next removal');
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
await this.accountsCtl.unkey(member, provider).catch((error) => this.o.log?.(`removeKey: the key lane entry is not dropped yet: ${error instanceof Error ? error.message : String(error)}`));
|
|
1099
|
+
}
|
|
1100
|
+
async backfillKeyLane(member, model) {
|
|
1101
|
+
const found = model === undefined ? [] : routeTable().filter((r) => r.keyEntry && r.readiness === 'ready' && r.provider === providerOf(model));
|
|
1102
|
+
await this.accountsCtl?.backfill(member, found).catch((error) => this.o.log?.(`key lane ${member}: not indexed yet: ${error instanceof Error ? error.message : String(error)}`));
|
|
1103
|
+
}
|
|
1052
1104
|
async prepareKeyOwner(agentId) {
|
|
1053
1105
|
this.syncCatalogs();
|
|
1054
1106
|
await this.catalogs;
|
|
@@ -1144,6 +1196,7 @@ export class OpenClawKit {
|
|
|
1144
1196
|
});
|
|
1145
1197
|
return this.runs().run(spec, on, undefined, output);
|
|
1146
1198
|
}
|
|
1199
|
+
// An API-key run's session is on the key lane (Runs step 2).
|
|
1147
1200
|
runKey(sessionKey, o) {
|
|
1148
1201
|
if (o?.auth !== 'apiKey')
|
|
1149
1202
|
return sessionKey;
|
|
@@ -1152,7 +1205,7 @@ export class OpenClawKit {
|
|
|
1152
1205
|
throw new Error('Choose your own conversation.');
|
|
1153
1206
|
return `agent:${keyAgentId(member)}:${tail}`;
|
|
1154
1207
|
}
|
|
1155
|
-
steer(sessionKey, text, o) {
|
|
1208
|
+
async steer(sessionKey, text, o) {
|
|
1156
1209
|
return this.runs().steer(this.runKey(sessionKey, o), text);
|
|
1157
1210
|
}
|
|
1158
1211
|
async abort(sessionKey, o) {
|
package/dist/link.js
CHANGED
|
@@ -7,11 +7,11 @@ import { createServer } from 'node:http';
|
|
|
7
7
|
import { WebSocketServer } from 'ws';
|
|
8
8
|
import { PublicLinkError, } from '@byokit/pair';
|
|
9
9
|
import { reach } from '@byokit/discover';
|
|
10
|
-
import { ENGINE_VERSION } from
|
|
11
|
-
import { b64urlDecode, b64urlEncode, sealNotice } from
|
|
12
|
-
import { routeFor } from
|
|
13
|
-
import { outputSchema } from
|
|
14
|
-
import { stateWords, words } from
|
|
10
|
+
import { ENGINE_VERSION } from './constants.js';
|
|
11
|
+
import { b64urlDecode, b64urlEncode, sealNotice } from './notices.js';
|
|
12
|
+
import { routeFor } from './routes.js';
|
|
13
|
+
import { outputSchema } from './output.js';
|
|
14
|
+
import { stateWords, words } from './words.js';
|
|
15
15
|
const VIEW_OPS = new Set(['oc.state', 'oc.routes', 'oc.signin.view', 'oc.sessions', 'oc.approvals', 'oc.events',
|
|
16
16
|
'oc.browser.state', 'oc.browser.signins', 'oc.browser.thumb']);
|
|
17
17
|
// This kit's own version, read once from its package.json (beside src/ and dist/ alike).
|
|
@@ -322,12 +322,16 @@ export function openclawLink(kit, o) {
|
|
|
322
322
|
drives.get(key)?.cancel();
|
|
323
323
|
const view = { state: 'waiting', via: args.via };
|
|
324
324
|
latest.set(key, view);
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
325
|
+
// A superseded attempt (still activating past its commit point) never writes over the newer one's view.
|
|
326
|
+
let drive;
|
|
327
|
+
const current = () => drive !== undefined && drives.get(key) === drive;
|
|
328
|
+
drive = kit.signIn(member, { authChoice: route.choice, via: args.via }, (next) => { if (current())
|
|
329
|
+
latest.set(key, next); });
|
|
328
330
|
drives.set(key, drive);
|
|
329
|
-
void drive.done.then((done) => {
|
|
330
|
-
|
|
331
|
+
void drive.done.then((done) => { if (current()) {
|
|
332
|
+
latest.set(key, done);
|
|
333
|
+
drives.delete(key);
|
|
334
|
+
} }, () => { if (current())
|
|
331
335
|
drives.delete(key); });
|
|
332
336
|
return latest.get(key);
|
|
333
337
|
}
|
|
@@ -336,7 +340,9 @@ export function openclawLink(kit, o) {
|
|
|
336
340
|
throw new Error('oc.signin.view needs { provider }');
|
|
337
341
|
// `ready` is the member's account (signed in to this provider), not the engine: `phaseOf` reads it as done.
|
|
338
342
|
const key = `${member}:${args.provider}`;
|
|
339
|
-
|
|
343
|
+
// Native Claude is ready only once this attempt's live activation settles.
|
|
344
|
+
const native = args.provider === 'claude-cli';
|
|
345
|
+
const ready = kit.state.phase === 'ready' && await kit.signedIn(member, args.provider) && (!native || (!drives.has(key) && latest.get(key)?.state !== 'failed'));
|
|
340
346
|
// A finished sign-in whose account is gone since (signed out anywhere, or dropped by the engine) is no
|
|
341
347
|
// sign-in to show: without this the view says done while `ready` says no, which phaseOf reads as opening.
|
|
342
348
|
if (!ready && !drives.has(key) && latest.get(key)?.state === 'done')
|
package/dist/migrate.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// only when the Gateway itself reports every provider signed in.
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
-
import { retireArchive } from
|
|
6
|
-
import { MEMBER_ID } from
|
|
7
|
-
import { providers } from
|
|
5
|
+
import { retireArchive } from './auth-store.js';
|
|
6
|
+
import { MEMBER_ID } from './members.js';
|
|
7
|
+
import { providers } from './signin.js';
|
|
8
8
|
const MOVED = '.moved-to-engine';
|
|
9
9
|
const MARKER = '.canonicalized';
|
|
10
10
|
const TRY_MS = 1_500;
|
package/dist/pick.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { words } from
|
|
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
4
|
/** The lowercased provider of a well-formed `provider/model` reference; undefined when the reference is malformed. */
|
|
@@ -154,6 +154,10 @@ export function resolveSelection(accounts, defaults, sel, room, nowMs, models, b
|
|
|
154
154
|
const rests = eligible.filter((c) => c.row.out === 'resting');
|
|
155
155
|
if (rests.length)
|
|
156
156
|
return restingPick(rests.reduce((a, b) => ((b.row.until ?? Infinity) < (a.row.until ?? Infinity) ? b : a)));
|
|
157
|
+
// An empty `bound` (a member-agent conversation holding no indexed account) names no provider: every account is
|
|
158
|
+
// signed in somewhere else, so the provider-specific sentence would mislead.
|
|
159
|
+
if (bound !== undefined && !bound.length)
|
|
160
|
+
return { ok: false, code: 'none', reason: words('auto.noneBound'), considered };
|
|
157
161
|
return { ok: false, code: 'none', reason: words('auto.none', { provider: provider ?? '' }), considered };
|
|
158
162
|
function finish(account, row, how, why) {
|
|
159
163
|
const listed = models?.(account);
|