@byokit/openclaw 0.12.0 → 0.14.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 +24 -0
- package/README.md +25 -11
- package/SECURITY.md +6 -2
- package/dist/accounts.d.ts +34 -0
- package/dist/accounts.js +533 -66
- package/dist/auth-store.d.ts +8 -0
- package/dist/auth-store.js +202 -25
- package/dist/engine.d.ts +10 -0
- package/dist/engine.js +67 -1
- package/dist/index.d.ts +2 -2
- package/dist/keys.d.ts +13 -2
- package/dist/keys.js +110 -59
- package/dist/kit.d.ts +14 -2
- package/dist/kit.js +71 -14
- package/dist/link.js +12 -6
- package/dist/pick.js +4 -0
- package/dist/runs.js +23 -6
- package/dist/signin.d.ts +4 -0
- package/dist/signin.js +80 -22
- package/dist/testing/fake-gateway.d.ts +7 -0
- package/dist/testing/fake-gateway.js +35 -1
- package/dist/thinking.d.ts +16 -0
- package/dist/thinking.js +62 -0
- package/dist/types.d.ts +8 -0
- package/dist/words.json +5 -1
- package/package.json +2 -1
- package/plugin/keys.js +3 -2
package/dist/keys.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { activate } from "./activation.js";
|
|
3
3
|
import { keyAgentId, MEMBER_ID, KEY_PREFIX, untilListed } from "./members.js";
|
|
4
|
+
import { providerOf } from "./pick.js";
|
|
4
5
|
import { routes } from "./routes.js";
|
|
5
6
|
import { modelsOf } from "./accounts.js";
|
|
6
7
|
import { LIVE_CATALOGS } from "./constants.js";
|
|
@@ -73,62 +74,82 @@ 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
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
await untilListed(ctx.request, agentId);
|
|
101
|
-
if (!(await control(member, 'prepare', route.provider)).ok)
|
|
102
|
-
return failure('retry', 'engine');
|
|
103
|
-
// The public pin's typed API-key path, never a CLI-login import. Discard the engine's lines/errors.
|
|
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.
|
|
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
|
+
if (ac.signal.aborted)
|
|
90
|
+
return failure('retry', 'busy');
|
|
91
|
+
return exclusive(member, async () => {
|
|
92
|
+
let sent = false;
|
|
93
|
+
// A key the engine may have taken but not sealed is cleared (prepare already emptied the agent); if the clear
|
|
94
|
+
// does not reach the engine, the key stays unsealed and unusable until the next key add or kit start, so runs
|
|
95
|
+
// fail closed.
|
|
96
|
+
const abandon = async (result) => {
|
|
97
|
+
await control(member, 'clear').catch(() => undefined);
|
|
98
|
+
return result;
|
|
99
|
+
};
|
|
100
|
+
const cancelled = () => failure('retry', 'busy');
|
|
113
101
|
try {
|
|
114
|
-
|
|
102
|
+
if (ac.signal.aborted)
|
|
103
|
+
return cancelled();
|
|
104
|
+
await ctx.ensure(member);
|
|
105
|
+
const agentId = keyAgentId(member);
|
|
106
|
+
const list = await ctx.request('agents.list');
|
|
107
|
+
if (!list.agents.some((a) => a.id === agentId))
|
|
108
|
+
await ctx.request('agents.create', { name: agentId, workspace: join(ctx.root, 'key-workspaces', member) });
|
|
109
|
+
// Creation answers before the runtime roster/owners publish, just as for ordinary members.
|
|
110
|
+
await untilListed(ctx.request, agentId);
|
|
111
|
+
if (ac.signal.aborted)
|
|
112
|
+
return cancelled();
|
|
113
|
+
if (!(await control(member, 'prepare', route.provider)).ok)
|
|
114
|
+
return failure('retry', 'engine');
|
|
115
|
+
// The public pin's typed API-key path, never a CLI-login import. Discard the engine's lines/errors.
|
|
116
|
+
sent = true;
|
|
117
|
+
if (ac.signal.aborted)
|
|
118
|
+
return abandon(cancelled());
|
|
119
|
+
const result = await activate(ctx, { kind: 'api-key', agentId, authChoice: o.authChoice, apiKey: o.apiKey }, { timeoutMs: 180_000, signal: ac.signal });
|
|
120
|
+
if (ac.signal.aborted)
|
|
121
|
+
return abandon(cancelled());
|
|
122
|
+
if (!result.ok && !result.late)
|
|
123
|
+
return abandon(probeResult(result.status));
|
|
124
|
+
if (result.late)
|
|
125
|
+
return abandon(failure('retry', 'busy'));
|
|
126
|
+
if (o.model !== undefined && !(await models(member, o)).some((m) => m.id === o.model && m.available))
|
|
127
|
+
return abandon(failure('not_included', 'model'));
|
|
128
|
+
// Prepare before publishing the ready marker: failed preparation remains unusable even if clear fails.
|
|
129
|
+
try {
|
|
130
|
+
await ctx.prepareOwner(agentId);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return abandon(failure('retry', 'engine'));
|
|
134
|
+
}
|
|
135
|
+
if (ac.signal.aborted)
|
|
136
|
+
return abandon(cancelled());
|
|
137
|
+
if (o.model !== undefined && LIVE_CATALOGS.includes(route.provider) && !(await registered(route.provider, o.model)))
|
|
138
|
+
return abandon(failure('retry', 'model'));
|
|
139
|
+
if (!(await control(member, 'seal', undefined, o.model)).ok)
|
|
140
|
+
return abandon(failure('retry', 'engine'));
|
|
141
|
+
await ctx.indexLane(member, route);
|
|
142
|
+
return { status: 'ok' };
|
|
115
143
|
}
|
|
116
|
-
catch {
|
|
117
|
-
|
|
144
|
+
catch (error) {
|
|
145
|
+
const reason = errorReason(error, sent);
|
|
146
|
+
if (sent)
|
|
147
|
+
return abandon(failure('retry', reason));
|
|
148
|
+
return failure('retry', reason);
|
|
118
149
|
}
|
|
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
|
-
});
|
|
150
|
+
});
|
|
151
|
+
})();
|
|
152
|
+
return Object.assign(done, { cancel });
|
|
132
153
|
},
|
|
133
154
|
async ready(member, o = {}) {
|
|
134
155
|
if (!check(member))
|
|
@@ -147,11 +168,37 @@ export function createKeys(ctx) {
|
|
|
147
168
|
}
|
|
148
169
|
}
|
|
149
170
|
},
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
|
|
171
|
+
// Remove this member's saved key: the key agent's store and its ready marker, so the lane reads signed out and
|
|
172
|
+
// stays signed out, plus its `<provider>.key` index entry, so the accounts list stops naming the removed key.
|
|
173
|
+
// `provider` scopes it to the key actually saved for that provider (the member agent's own
|
|
174
|
+
// sign-out never reaches this lane), so signing out of one provider never drops another's key. A lane that reads
|
|
175
|
+
// not ready has nothing saved to remove; a refused read or clear is a typed refusal, never raw engine text.
|
|
176
|
+
// Serialized with adds and key runs like every other key change.
|
|
177
|
+
async remove(member, o = {}) {
|
|
178
|
+
if (!check(member))
|
|
179
|
+
throw new Error('Choose your own member.');
|
|
180
|
+
await exclusive(member, async () => {
|
|
181
|
+
const refused = () => { throw new Error('The key could not be removed.'); };
|
|
182
|
+
if (o.provider !== undefined) {
|
|
183
|
+
// An unreadable lane cannot name its provider, so removal never guesses and risks another provider's key.
|
|
184
|
+
const ready = await control(member, 'ready').catch(() => undefined);
|
|
185
|
+
if (!ready)
|
|
186
|
+
refused();
|
|
187
|
+
else if (ready.ok !== true || providerOf(ready.model ?? '') !== o.provider)
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const cleared = await control(member, 'clear').catch(() => undefined);
|
|
191
|
+
if (cleared?.ok !== true && cleared?.absent !== true)
|
|
192
|
+
refused();
|
|
193
|
+
await ctx.dropLane(member, o.provider);
|
|
194
|
+
});
|
|
195
|
+
},
|
|
196
|
+
// Kit connect calls this once the gateway is ready: a failed add whose abandon clear never reached the engine
|
|
197
|
+
// leaves an unsealed key with no ready marker, so drop it (`clear`, until a sweep completes). A single ready read
|
|
198
|
+
// per key agent, no retries; an add in flight for that member is waited for. Only an absent marker is cleared; a
|
|
199
|
+
// refused read or clear (the gateway left ready) makes the sweep incomplete. A sealed lane is backfilled on every
|
|
200
|
+
// call. Resolves true once every key agent was read.
|
|
201
|
+
async sweep(clear) {
|
|
155
202
|
let agents;
|
|
156
203
|
try {
|
|
157
204
|
agents = (await ctx.request('agents.list')).agents;
|
|
@@ -168,7 +215,11 @@ export function createKeys(ctx) {
|
|
|
168
215
|
continue;
|
|
169
216
|
await exclusive(member, async () => {
|
|
170
217
|
const ready = await control(member, 'ready').catch(() => undefined);
|
|
171
|
-
if (ready?.ok === true)
|
|
218
|
+
if (ready?.ok === true) {
|
|
219
|
+
await ctx.backfillLane(member, ready.model);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (!clear)
|
|
172
223
|
return;
|
|
173
224
|
if (ready?.absent !== true) {
|
|
174
225
|
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';
|
|
@@ -148,6 +148,9 @@ export declare class OpenClawKit {
|
|
|
148
148
|
}>;
|
|
149
149
|
}>;
|
|
150
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>;
|
|
151
154
|
defaults(member: Member): Promise<Defaults>;
|
|
152
155
|
setDefaults(member: Member, d: Defaults): Promise<void>;
|
|
153
156
|
/** Save an explicitly supplied API key (billed per use) in this member's separate key agent. */
|
|
@@ -155,7 +158,16 @@ export declare class OpenClawKit {
|
|
|
155
158
|
authChoice: string;
|
|
156
159
|
apiKey: string;
|
|
157
160
|
model?: string;
|
|
158
|
-
}):
|
|
161
|
+
}): AddKeyHandle;
|
|
162
|
+
private indexKeyLane;
|
|
163
|
+
/** Remove this member's saved key (the `byokit-key-<member>` lane: its credential store and ready marker), so
|
|
164
|
+
* `signedIn` is false for it and a restart cannot revive it. `provider` removes only that provider's saved key;
|
|
165
|
+
* with no saved key the call resolves without doing anything. */
|
|
166
|
+
removeKey(member: Member, o?: {
|
|
167
|
+
provider?: string;
|
|
168
|
+
}): Promise<void>;
|
|
169
|
+
private dropKeyLane;
|
|
170
|
+
private backfillKeyLane;
|
|
159
171
|
private prepareKeyOwner;
|
|
160
172
|
private syncCatalogs;
|
|
161
173
|
migrateRetainedLogin(member: Member, source: RetainedLogin): Promise<'staged' | 'nothing' | 'failed'>;
|
package/dist/kit.js
CHANGED
|
@@ -664,6 +664,9 @@ 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),
|
|
@@ -763,7 +766,7 @@ export class OpenClawKit {
|
|
|
763
766
|
this.accountsCtl = createAccounts({ request: (method, params, o) => this.request()(method, params, o), root: this.engine.root,
|
|
764
767
|
ensure: (member) => this.ensureMember(member), locks: this.locks,
|
|
765
768
|
signIn: (agentId, o, on, fresh) => startSignIn({ ...this.signInCtx(), ensure: async () => ({ agentId }), fresh }, agentId, o, on),
|
|
766
|
-
routes: this.routeMap, log: this.o.log });
|
|
769
|
+
routes: this.routeMap, log: this.o.log, reseal: () => this.engine.authStore.reseal() });
|
|
767
770
|
// The fail-closed tool bridge feeds the kit's single Approval surface (5.9, O5).
|
|
768
771
|
await this.bridge.start();
|
|
769
772
|
this.off.push(transport.onEvent((e) => this.approvalsCtl.handleEvent(e)));
|
|
@@ -771,10 +774,11 @@ export class OpenClawKit {
|
|
|
771
774
|
this.setState({ phase: 'ready' });
|
|
772
775
|
this.syncCatalogs();
|
|
773
776
|
// 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
|
-
|
|
777
|
+
// runs stay closed, so drop it now that the gateway is ready. Ready keys keep their marker and survive. Every
|
|
778
|
+
// connect backfills the key lanes' index entries; only the clear stops once a sweep completes.
|
|
779
|
+
const clear = !this.swept;
|
|
780
|
+
void this.keys.sweep(clear).then((done) => { if (clear && done)
|
|
781
|
+
this.swept = true; }).catch(() => undefined);
|
|
778
782
|
if (this.o.browser)
|
|
779
783
|
await this.checkBrowserTools();
|
|
780
784
|
// Replays the engine's native approval lists over the now-live transport (N9).
|
|
@@ -951,7 +955,10 @@ export class OpenClawKit {
|
|
|
951
955
|
// Public readiness/sign-in reads stay live; their fresh result can prepare the next run's admission.
|
|
952
956
|
authStatus: (agentId, refresh, native) => this.admission.read(agentId, refresh, native, false),
|
|
953
957
|
ensure: (member) => this.ensureMember(member), callbackPort: this.o.callbackPort ?? 1455,
|
|
958
|
+
nativeClaudeLogin: () => this.engine.nativeClaudeLogin(),
|
|
959
|
+
resetNativeClaudeLogin: () => this.engine.resetNativeClaudeLogin(),
|
|
954
960
|
onDisconnect: (fn) => { this.signInDisconnects.add(fn); return () => { this.signInDisconnects.delete(fn); }; },
|
|
961
|
+
stopSignal: this.engine.stopSignal,
|
|
955
962
|
authConfig: () => this.authSnapshot(),
|
|
956
963
|
restarted: (authBefore, signal) => this.restarted(authBefore, signal) };
|
|
957
964
|
}
|
|
@@ -966,24 +973,40 @@ export class OpenClawKit {
|
|
|
966
973
|
return this.providerStatus(member).then((names) => names ?? []);
|
|
967
974
|
}
|
|
968
975
|
/** 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
|
-
|
|
976
|
+
async signedIn(member, provider) {
|
|
977
|
+
// The key probe stands alone: a member-agent admission failure after an engine restart must not hide a sealed key.
|
|
978
|
+
const keyReady = async () => {
|
|
979
|
+
try {
|
|
980
|
+
return providerOf((await this.keys.ready(member, { once: true }))?.model ?? '') === provider;
|
|
981
|
+
}
|
|
982
|
+
catch {
|
|
983
|
+
return false;
|
|
984
|
+
}
|
|
985
|
+
};
|
|
986
|
+
try {
|
|
987
|
+
if ((await this.providers(member)).includes(provider))
|
|
988
|
+
return true;
|
|
989
|
+
}
|
|
990
|
+
catch { /* key probe below */ }
|
|
991
|
+
return keyReady();
|
|
972
992
|
}
|
|
973
993
|
signIn(member, o, on) {
|
|
974
994
|
return startSignIn(this.signInCtx(), member, o, on);
|
|
975
995
|
}
|
|
976
|
-
signOut(member, provider) {
|
|
977
|
-
|
|
996
|
+
async signOut(member, provider) {
|
|
997
|
+
await engineSignOut(this.signInCtx(), member, provider);
|
|
998
|
+
// The saved key lives on its own `byokit-key-<member>` agent, which the member-agent logout never reaches: without
|
|
999
|
+
// this, a key of that provider stays sealed and ready and the member reads signed in again after a restart.
|
|
1000
|
+
await this.keys.remove(member, { provider });
|
|
978
1001
|
}
|
|
979
1002
|
// The member an agent id or session key belongs to, through the account index (5.15, O14m). Reads the index file, so it
|
|
980
1003
|
// needs no live gateway.
|
|
981
1004
|
memberOf(ref) {
|
|
982
1005
|
return memberOfAccount(this.engine.root, ref);
|
|
983
1006
|
}
|
|
984
|
-
// accounts (5.15, O14 steps S5, S6 and
|
|
985
|
-
// and room; `run({ account })` and `pick` choose among them, held to the account a session is bound to,
|
|
986
|
-
// carries a conversation to another account
|
|
1007
|
+
// accounts (5.15, O14 steps S5, S6 and S8-S12): the per-member index with listing, adding, defaults, rename, models
|
|
1008
|
+
// and room; `run({ account })` and `pick` choose among them, held to the account a session is bound to, `move`
|
|
1009
|
+
// carries a conversation to another account, and `removeAccount` deletes one.
|
|
987
1010
|
accounts(member) {
|
|
988
1011
|
if (!this.accountsCtl)
|
|
989
1012
|
return Promise.reject(new Error('gateway not ready'));
|
|
@@ -1028,6 +1051,13 @@ export class OpenClawKit {
|
|
|
1028
1051
|
return Promise.reject(new Error('gateway not ready'));
|
|
1029
1052
|
return this.accountsCtl.rename(member, id, name);
|
|
1030
1053
|
}
|
|
1054
|
+
/** Remove one account: a member-agent account through `authLogout` with its profileIds, an account agent through
|
|
1055
|
+
* `clear`, an empty `list`, `agents.delete` with `deleteFiles: false`, removal of its state dir, then the entry. */
|
|
1056
|
+
removeAccount(member, id) {
|
|
1057
|
+
if (!this.accountsCtl)
|
|
1058
|
+
return Promise.reject(new Error('gateway not ready'));
|
|
1059
|
+
return this.accountsCtl.remove(member, id);
|
|
1060
|
+
}
|
|
1031
1061
|
defaults(member) {
|
|
1032
1062
|
if (!this.accountsCtl)
|
|
1033
1063
|
return Promise.reject(new Error('gateway not ready'));
|
|
@@ -1042,6 +1072,32 @@ export class OpenClawKit {
|
|
|
1042
1072
|
addKey(agentId, o) {
|
|
1043
1073
|
return this.keys.add(agentId, o);
|
|
1044
1074
|
}
|
|
1075
|
+
// The lane's index entry follows its sealed key. A failed write is logged and never fails the add; the start sweep repairs it.
|
|
1076
|
+
async indexKeyLane(member, route) {
|
|
1077
|
+
if (!this.accountsCtl) {
|
|
1078
|
+
this.o.log?.('addKey: the account index is not ready, so the key lane is indexed at the next connect');
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
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)}`));
|
|
1082
|
+
}
|
|
1083
|
+
/** Remove this member's saved key (the `byokit-key-<member>` lane: its credential store and ready marker), so
|
|
1084
|
+
* `signedIn` is false for it and a restart cannot revive it. `provider` removes only that provider's saved key;
|
|
1085
|
+
* with no saved key the call resolves without doing anything. */
|
|
1086
|
+
removeKey(member, o) {
|
|
1087
|
+
return this.keys.remove(member, o);
|
|
1088
|
+
}
|
|
1089
|
+
// The lane's index entry follows its sealed key down. A failed drop is logged and never fails the removal.
|
|
1090
|
+
async dropKeyLane(member, provider) {
|
|
1091
|
+
if (!this.accountsCtl) {
|
|
1092
|
+
this.o.log?.('removeKey: the account index is not ready, so the key lane entry stays until the next removal');
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
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)}`));
|
|
1096
|
+
}
|
|
1097
|
+
async backfillKeyLane(member, model) {
|
|
1098
|
+
const found = model === undefined ? [] : routeTable().filter((r) => r.keyEntry && r.readiness === 'ready' && r.provider === providerOf(model));
|
|
1099
|
+
await this.accountsCtl?.backfill(member, found).catch((error) => this.o.log?.(`key lane ${member}: not indexed yet: ${error instanceof Error ? error.message : String(error)}`));
|
|
1100
|
+
}
|
|
1045
1101
|
async prepareKeyOwner(agentId) {
|
|
1046
1102
|
this.syncCatalogs();
|
|
1047
1103
|
await this.catalogs;
|
|
@@ -1137,6 +1193,7 @@ export class OpenClawKit {
|
|
|
1137
1193
|
});
|
|
1138
1194
|
return this.runs().run(spec, on, undefined, output);
|
|
1139
1195
|
}
|
|
1196
|
+
// An API-key run's session is on the key lane (Runs step 2).
|
|
1140
1197
|
runKey(sessionKey, o) {
|
|
1141
1198
|
if (o?.auth !== 'apiKey')
|
|
1142
1199
|
return sessionKey;
|
|
@@ -1145,7 +1202,7 @@ export class OpenClawKit {
|
|
|
1145
1202
|
throw new Error('Choose your own conversation.');
|
|
1146
1203
|
return `agent:${keyAgentId(member)}:${tail}`;
|
|
1147
1204
|
}
|
|
1148
|
-
steer(sessionKey, text, o) {
|
|
1205
|
+
async steer(sessionKey, text, o) {
|
|
1149
1206
|
return this.runs().steer(this.runKey(sessionKey, o), text);
|
|
1150
1207
|
}
|
|
1151
1208
|
async abort(sessionKey, o) {
|
package/dist/link.js
CHANGED
|
@@ -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/pick.js
CHANGED
|
@@ -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);
|
package/dist/runs.js
CHANGED
|
@@ -6,6 +6,7 @@ import { outputSchema } from "./output.js";
|
|
|
6
6
|
import { words } from "./words.js";
|
|
7
7
|
import { classify } from "./classify.js";
|
|
8
8
|
import { providerOf } from "./pick.js";
|
|
9
|
+
import { catalogModelOf, clampThinking, levelsOf } from "./thinking.js";
|
|
9
10
|
// The gateway gives up on a silent run after an hour; we wait ten minutes longer for the reply frame itself.
|
|
10
11
|
const WAIT_MS = 3_600_000;
|
|
11
12
|
const WAIT_CLIENT_MS = 3_610_000;
|
|
@@ -109,7 +110,7 @@ export function createRuns(ctx) {
|
|
|
109
110
|
const tail = spec.sessionKey.slice(spec.sessionKey.indexOf(':', 'agent:'.length) + 1);
|
|
110
111
|
// Runs step 1 (5.15): every run first finds the account its session is bound to, the API-key lane included.
|
|
111
112
|
const binding = ctx.accounts ? await ctx.accounts.bind(spec.member, spec.sessionKey) : undefined;
|
|
112
|
-
// Step 2 on the API-key lane:
|
|
113
|
+
// Step 2 on the API-key lane: its own lane binding continues; any other binding is refused.
|
|
113
114
|
if (keyAgent && ctx.accounts && binding) {
|
|
114
115
|
const message = await ctx.accounts.keyRefusal(spec.member, binding);
|
|
115
116
|
if (message !== undefined)
|
|
@@ -119,8 +120,8 @@ export function createRuns(ctx) {
|
|
|
119
120
|
// session went to another account, so it ends before any event and the caller's retry lands there.
|
|
120
121
|
const moved = async () => !!binding && (await ctx.accounts.bind(spec.member, spec.sessionKey))?.agentId !== binding.agentId;
|
|
121
122
|
const movedEnd = { ok: false, kind: 'other', message: words('run.moved') };
|
|
122
|
-
// Step 3: with `account` and `needs` both absent, on a key no account agent holds (or an API-key run,
|
|
123
|
-
//
|
|
123
|
+
// Step 3: with `account` and `needs` both absent, on a key no account agent holds (or an API-key run, which binds only
|
|
124
|
+
// to its lane), exactly the run on the agent it always was (a bound key under its shared lock).
|
|
124
125
|
if ((keyAgent || binding?.id === undefined) && spec.account === undefined && !spec.needs?.length) {
|
|
125
126
|
if (keyAgent)
|
|
126
127
|
return start(spec, on, { agentId: keyAgent, sessionKey: `agent:${keyAgent}:${tail}`, idempotencyKey, system, picked, output });
|
|
@@ -197,6 +198,19 @@ export function createRuns(ctx) {
|
|
|
197
198
|
return { ...end, until };
|
|
198
199
|
}
|
|
199
200
|
};
|
|
201
|
+
const thinkingOf = async (picked, asked) => {
|
|
202
|
+
if (asked === undefined)
|
|
203
|
+
return undefined;
|
|
204
|
+
if (!picked)
|
|
205
|
+
return { asked, used: asked };
|
|
206
|
+
try {
|
|
207
|
+
const snap = await ctx.request('config.get', {});
|
|
208
|
+
return clampThinking(asked, levelsOf(catalogModelOf(snap.config?.models?.providers, picked.provider, picked.model)));
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return { asked, used: asked };
|
|
212
|
+
}
|
|
213
|
+
};
|
|
200
214
|
const start = async (spec, on, t) => {
|
|
201
215
|
const { agentId, sessionKey, idempotencyKey, system, picked, output, chosen } = t;
|
|
202
216
|
const release = spec.register !== false ? ctx.bridge.register({ sessionKey, member: spec.member }, spec.tools, idempotencyKey) : undefined;
|
|
@@ -253,7 +267,9 @@ export function createRuns(ctx) {
|
|
|
253
267
|
return { ok: false, kind: 'signed-out', message: `${picked.provider} is not signed in for ${spec.member}` };
|
|
254
268
|
}
|
|
255
269
|
// One `agent` request: its interim `accepted` frame names the run, its final frame (after the run) carries the
|
|
256
|
-
// run's usage. The wait below still decides how the run ended.
|
|
270
|
+
// run's usage. The wait below still decides how the run ended. Thinking is clamped to the catalog before the
|
|
271
|
+
// request so an unsupported level (e.g. Muse's off/high/max) never fails the engine.
|
|
272
|
+
const thinking = await thinkingOf(picked, spec.thinking);
|
|
257
273
|
let accepted;
|
|
258
274
|
const ack = new Promise((resolve) => { accepted = resolve; });
|
|
259
275
|
// The run id is taken as the accepted frame lands, so no event read after it is missed.
|
|
@@ -274,7 +290,7 @@ export function createRuns(ctx) {
|
|
|
274
290
|
...(picked ?? {}),
|
|
275
291
|
...(system ? { extraSystemPrompt: system } : {}),
|
|
276
292
|
...(spec.images ? { attachments: spec.images.map((image) => ({ mimeType: image.mimeType, content: image.data })) } : {}),
|
|
277
|
-
...(
|
|
293
|
+
...(thinking !== undefined ? { thinking: thinking.used } : {}),
|
|
278
294
|
}, { expectFinal: true, timeoutMs: WAIT_CLIENT_MS, onAccepted });
|
|
279
295
|
final.catch(() => { }); // a late failure is the wait's to report
|
|
280
296
|
const started = await Promise.race([ack, final]);
|
|
@@ -307,7 +323,8 @@ export function createRuns(ctx) {
|
|
|
307
323
|
const reported = typeof agentMeta.provider === 'string' && typeof agentMeta.model === 'string'
|
|
308
324
|
? `${agentMeta.provider}/${agentMeta.model}`.toLowerCase() : '';
|
|
309
325
|
return { ok: true, text, ...(parsed ? { data: parsed.data } : {}), ...(usage ? { usage } : {}), ...(planWindow ? { planWindow } : {}),
|
|
310
|
-
...(chosen ? { account: chosen.account.id, model: chosen.model || reported } : {})
|
|
326
|
+
...(chosen ? { account: chosen.account.id, model: chosen.model || reported } : {}),
|
|
327
|
+
...(thinking ? { thinking } : {}) };
|
|
311
328
|
}
|
|
312
329
|
if (result.stopReason === 'aborted' || (abortedByEngine && result.status !== 'ok'))
|
|
313
330
|
return { ok: false, aborted: true };
|
package/dist/signin.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { remoteCliHost } from '@byokit/accounts/cli';
|
|
1
2
|
import { authStatus } from './auth-status.ts';
|
|
2
3
|
import type { GatewayTransport, Member, SignInOptions, SignInView } from './types.ts';
|
|
3
4
|
export type SignInCtx = {
|
|
@@ -7,9 +8,12 @@ export type SignInCtx = {
|
|
|
7
8
|
agentId: string;
|
|
8
9
|
}>;
|
|
9
10
|
callbackPort: number;
|
|
11
|
+
nativeClaudeLogin?(): ReturnType<typeof remoteCliHost>;
|
|
12
|
+
resetNativeClaudeLogin?(): Promise<void>;
|
|
10
13
|
onDisconnect?(fn: () => void): () => void;
|
|
11
14
|
authConfig(): string;
|
|
12
15
|
restarted(authBefore: string | undefined, signal?: AbortSignal): Promise<boolean>;
|
|
16
|
+
stopSignal?: AbortSignal;
|
|
13
17
|
fresh?: boolean;
|
|
14
18
|
};
|
|
15
19
|
/**
|