@byokit/openclaw 0.13.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 +18 -0
- package/README.md +25 -11
- package/SECURITY.md +6 -2
- package/dist/accounts.d.ts +31 -1
- package/dist/accounts.js +429 -97
- package/dist/auth-store.d.ts +2 -0
- package/dist/auth-store.js +72 -7
- package/dist/engine.d.ts +9 -0
- package/dist/engine.js +55 -4
- 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 +11 -2
- package/dist/kit.js +60 -10
- 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.js +22 -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/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),
|
|
@@ -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,15 +973,31 @@ 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.
|
|
@@ -1049,6 +1072,32 @@ export class OpenClawKit {
|
|
|
1049
1072
|
addKey(agentId, o) {
|
|
1050
1073
|
return this.keys.add(agentId, o);
|
|
1051
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
|
+
}
|
|
1052
1101
|
async prepareKeyOwner(agentId) {
|
|
1053
1102
|
this.syncCatalogs();
|
|
1054
1103
|
await this.catalogs;
|
|
@@ -1144,6 +1193,7 @@ export class OpenClawKit {
|
|
|
1144
1193
|
});
|
|
1145
1194
|
return this.runs().run(spec, on, undefined, output);
|
|
1146
1195
|
}
|
|
1196
|
+
// An API-key run's session is on the key lane (Runs step 2).
|
|
1147
1197
|
runKey(sessionKey, o) {
|
|
1148
1198
|
if (o?.auth !== 'apiKey')
|
|
1149
1199
|
return sessionKey;
|
|
@@ -1152,7 +1202,7 @@ export class OpenClawKit {
|
|
|
1152
1202
|
throw new Error('Choose your own conversation.');
|
|
1153
1203
|
return `agent:${keyAgentId(member)}:${tail}`;
|
|
1154
1204
|
}
|
|
1155
|
-
steer(sessionKey, text, o) {
|
|
1205
|
+
async steer(sessionKey, text, o) {
|
|
1156
1206
|
return this.runs().steer(this.runKey(sessionKey, o), text);
|
|
1157
1207
|
}
|
|
1158
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
|
/**
|
package/dist/signin.js
CHANGED
|
@@ -57,12 +57,14 @@ export function signIn(ctx, member, o, on) {
|
|
|
57
57
|
};
|
|
58
58
|
let server;
|
|
59
59
|
let sessionId = '';
|
|
60
|
+
let native;
|
|
61
|
+
let nativeId;
|
|
60
62
|
let starting;
|
|
61
63
|
let releasing;
|
|
62
64
|
let settleStart;
|
|
63
65
|
const startingSettled = new Promise((resolve) => { settleStart = resolve; });
|
|
64
66
|
let over = false;
|
|
65
|
-
let
|
|
67
|
+
let committed = false; // the commit point of the native and key drives: set just before setup.activate
|
|
66
68
|
let recovering = false;
|
|
67
69
|
let offDisconnect;
|
|
68
70
|
let agentId;
|
|
@@ -127,6 +129,7 @@ export function signIn(ctx, member, o, on) {
|
|
|
127
129
|
stopApproval();
|
|
128
130
|
offDisconnect?.();
|
|
129
131
|
o.signal?.removeEventListener('abort', cancel);
|
|
132
|
+
ctx.stopSignal?.removeEventListener('abort', cancel);
|
|
130
133
|
owner.abort();
|
|
131
134
|
returned = undefined;
|
|
132
135
|
pasteIn = undefined;
|
|
@@ -142,8 +145,16 @@ export function signIn(ctx, member, o, on) {
|
|
|
142
145
|
// session only as `openclaw.setup.auth.start` settles, so a cancel sent sooner is answered `wizard not found`
|
|
143
146
|
// and leaves the person locked out of signing in. Waiting for that settlement is what frees the admission.
|
|
144
147
|
await startingSettled;
|
|
145
|
-
if (
|
|
146
|
-
await
|
|
148
|
+
if (nativeId)
|
|
149
|
+
await native?.cancel(nativeId).catch(() => { });
|
|
150
|
+
if (!sessionId)
|
|
151
|
+
return;
|
|
152
|
+
const answer = await Promise.resolve().then(() => ctx.request('wizard.cancel', { sessionId }, { timeoutMs: CANCEL_MS })).catch(() => undefined);
|
|
153
|
+
// The pin answers `cancelled` at once but frees the admission only once the cancelled runner and its setup-target
|
|
154
|
+
// lock settle, and answers wizard.status on that session only after that: done waits for it (CANCEL_MS at most).
|
|
155
|
+
// Not covered: a `running` answer (its commit under way) and a session the engine purged at the cancel.
|
|
156
|
+
if (answer?.status === 'cancelled')
|
|
157
|
+
await Promise.resolve().then(() => ctx.request('wizard.status', { sessionId }, { timeoutMs: CANCEL_MS })).catch(() => { });
|
|
147
158
|
})();
|
|
148
159
|
return releasing;
|
|
149
160
|
};
|
|
@@ -162,12 +173,17 @@ export function signIn(ctx, member, o, on) {
|
|
|
162
173
|
if (signal.aborted)
|
|
163
174
|
return;
|
|
164
175
|
owner.abort();
|
|
165
|
-
void (async () => { await release(); if (!
|
|
176
|
+
void (async () => { await release(); if (!committed)
|
|
166
177
|
await finish(cancelled()); })();
|
|
167
178
|
};
|
|
168
179
|
o.signal?.addEventListener('abort', cancel, { once: true });
|
|
169
180
|
if (o.signal?.aborted)
|
|
170
181
|
cancel();
|
|
182
|
+
// An engine shutdown before the commit point is this drive's decline too: without it a key paste would sit out
|
|
183
|
+
// its 15-minute paste wait after the engine is gone.
|
|
184
|
+
ctx.stopSignal?.addEventListener('abort', cancel, { once: true });
|
|
185
|
+
if (ctx.stopSignal?.aborted)
|
|
186
|
+
cancel();
|
|
171
187
|
/** Hold 127.0.0.1:<callbackPort> for the sign-in's life; a taken port is why the sign-in cannot start. */
|
|
172
188
|
const holdCallback = () => {
|
|
173
189
|
let took = false;
|
|
@@ -219,9 +235,53 @@ export function signIn(ctx, member, o, on) {
|
|
|
219
235
|
settleStart();
|
|
220
236
|
let apiKey;
|
|
221
237
|
if (o.authChoice === 'anthropic-cli') {
|
|
222
|
-
const detected =
|
|
223
|
-
|
|
224
|
-
return
|
|
238
|
+
const detected = async () => {
|
|
239
|
+
const value = await ctx.request('openclaw.setup.detect', { agentId }, { timeoutMs: STATUS_MS, signal });
|
|
240
|
+
return value.candidates?.some(candidate => candidate.kind === 'claude-cli' && candidate.credentials === true);
|
|
241
|
+
};
|
|
242
|
+
native = ctx.nativeClaudeLogin?.();
|
|
243
|
+
if (!await detected()) {
|
|
244
|
+
if (!native)
|
|
245
|
+
return { state: 'failed', via, why: 'failed', error: 'Claude Code sign-in is not available on this engine.' };
|
|
246
|
+
let started = await native.start('claude', { signal });
|
|
247
|
+
let ready = false;
|
|
248
|
+
if (!started.ok && started.error === 'already') {
|
|
249
|
+
ready = Boolean(await detected());
|
|
250
|
+
if (!ready && ctx.resetNativeClaudeLogin) {
|
|
251
|
+
if (signal.aborted || native.signal.aborted)
|
|
252
|
+
return cancelled();
|
|
253
|
+
await ctx.resetNativeClaudeLogin();
|
|
254
|
+
started = await native.start('claude', { signal });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (!ready) {
|
|
258
|
+
if (!started.ok)
|
|
259
|
+
return { state: 'failed', via, why: started.error === 'busy' ? 'busy' : started.error === 'cancelled' ? 'declined' : 'failed', error: started.words };
|
|
260
|
+
nativeId = started.signIn.id;
|
|
261
|
+
if (signal.aborted) {
|
|
262
|
+
await native.cancel(nativeId);
|
|
263
|
+
return cancelled();
|
|
264
|
+
}
|
|
265
|
+
let pasteFailed = false;
|
|
266
|
+
pasteIn = text => { void native.paste(nativeId, text).catch((error) => { if (error.remoteCliProblem === 'unknown')
|
|
267
|
+
return; pasteFailed = true; void native.cancel(nativeId).catch(() => { }); }); };
|
|
268
|
+
say({ url: started.signIn.verificationUri, prompt: 'Paste the code from the sign-in page' });
|
|
269
|
+
if (returned !== undefined) {
|
|
270
|
+
pasteIn(returned);
|
|
271
|
+
returned = undefined;
|
|
272
|
+
}
|
|
273
|
+
const result = await native.wait(nativeId);
|
|
274
|
+
pasteIn = undefined;
|
|
275
|
+
if (signal.aborted)
|
|
276
|
+
return cancelled();
|
|
277
|
+
if (pasteFailed)
|
|
278
|
+
return { state: 'failed', via, why: 'failed', error: 'The sign-in code could not be sent. Try again.' };
|
|
279
|
+
if (!result.ok)
|
|
280
|
+
return { state: 'failed', via, why: result.error === 'expired' ? 'expired' : result.error === 'cancelled' ? 'declined' : 'failed', error: result.words };
|
|
281
|
+
if (!await detected())
|
|
282
|
+
return { state: 'failed', via, why: 'failed', error: 'Claude Code is still signed out on this engine. Try again.' };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
225
285
|
}
|
|
226
286
|
else {
|
|
227
287
|
say({ prompt: keyRoute?.billing === 'api' ? 'API key (billed per use)' : 'Plan key' });
|
|
@@ -239,21 +299,19 @@ export function signIn(ctx, member, o, on) {
|
|
|
239
299
|
return { state: 'failed', via, why: signal.aborted ? 'declined' : 'expired' };
|
|
240
300
|
secret = apiKey.trim() || undefined;
|
|
241
301
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const result = await activate(ctx, {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}, { timeoutMs: PULL_MS, signal }).finally(() => { activating = false; });
|
|
302
|
+
// The commit point: a phone cancel or host shutdown that landed before it activates nothing. Done waits for the gateway restart an `auth` change causes (activation.ts).
|
|
303
|
+
// For Claude only host shutdown cuts that wait; a late restart has persisted the route and says saved. A key's cancel still declines its wait; a late key keeps the shared words.
|
|
304
|
+
if (signal.aborted || native?.signal.aborted)
|
|
305
|
+
return cancelled();
|
|
306
|
+
committed = true;
|
|
307
|
+
const result = await activate(ctx, { agentId, kind: apiKey === undefined ? 'claude-cli' : 'api-key', ...(apiKey === undefined ? {} : { authChoice: o.authChoice, apiKey }), }, { timeoutMs: PULL_MS, signal: apiKey === undefined ? native?.signal : signal });
|
|
308
|
+
if (apiKey !== undefined && signal.aborted)
|
|
309
|
+
return cancelled();
|
|
251
310
|
if (result.late)
|
|
252
|
-
return
|
|
311
|
+
return { state: 'failed', via, why: 'failed', error: apiKey === undefined ? words('signin.savedNotRestarted') : LATE_RESTART };
|
|
253
312
|
if (!result.ok)
|
|
254
|
-
return { state: 'failed', via, why: 'failed',
|
|
255
|
-
|
|
256
|
-
return signal.aborted ? cancelled() : { state: 'done', via };
|
|
313
|
+
return { state: 'failed', via, why: 'failed', error: apiKey === undefined ? clean(result.error ?? 'Claude Code activation failed') : keyFailed(result.status) };
|
|
314
|
+
return { state: 'done', via };
|
|
257
315
|
}
|
|
258
316
|
// The client chooses the wizard id; cancellation waits for the engine to register it.
|
|
259
317
|
sessionId = `byokit-${randomUUID()}`;
|
|
@@ -441,11 +499,11 @@ export function signIn(ctx, member, o, on) {
|
|
|
441
499
|
try {
|
|
442
500
|
const driven = await Promise.race([drive(), disconnected]);
|
|
443
501
|
view = driven ?? await recover();
|
|
444
|
-
if (signal.aborted)
|
|
502
|
+
if (!committed && (signal.aborted || native?.signal.aborted))
|
|
445
503
|
view = codeExpired ? expired() : cancelled();
|
|
446
504
|
}
|
|
447
505
|
catch (error) {
|
|
448
|
-
if (signal.aborted)
|
|
506
|
+
if (!committed && (signal.aborted || native?.signal.aborted))
|
|
449
507
|
view = codeExpired ? expired() : cancelled();
|
|
450
508
|
else if (sawDeviceCode && /expired_token|code.*expired|device.*(?:expired|timed out)/i.test(cut(error)))
|
|
451
509
|
view = expired();
|
|
@@ -385,6 +385,25 @@ export function fakeGateway(script, o) {
|
|
|
385
385
|
models.delete(id);
|
|
386
386
|
return { ok: true, agentId: id, removedBindings: 0 };
|
|
387
387
|
},
|
|
388
|
+
// model: null clears the agent's primary (config.patch cannot delete a key); a string sets it.
|
|
389
|
+
'agents.update': (p) => {
|
|
390
|
+
const id = String(p.agentId ?? '');
|
|
391
|
+
if (!id)
|
|
392
|
+
throw new Error('agents.update requires agentId');
|
|
393
|
+
if ('model' in p) {
|
|
394
|
+
const entries = configBox.config.agents?.entries;
|
|
395
|
+
if (p.model === null) {
|
|
396
|
+
models.delete(id);
|
|
397
|
+
if (entries?.[id] && 'model' in entries[id])
|
|
398
|
+
delete entries[id].model;
|
|
399
|
+
}
|
|
400
|
+
else if (typeof p.model === 'string')
|
|
401
|
+
setPrimaryOfId(id, p.model);
|
|
402
|
+
else
|
|
403
|
+
throw new Error('agents.update model must be a string or null');
|
|
404
|
+
}
|
|
405
|
+
return { ok: true, agentId: id };
|
|
406
|
+
},
|
|
388
407
|
'agents.files.get': (p) => fileAnswer(String(p.agentId), String(p.name)),
|
|
389
408
|
'agents.files.set': (p) => {
|
|
390
409
|
filesOf(String(p.agentId)).files.set(String(p.name), String(p.content));
|
|
@@ -447,9 +466,11 @@ export function fakeGateway(script, o) {
|
|
|
447
466
|
'byokit.keys': (p) => {
|
|
448
467
|
const id = `byokit-key-${p.member}`;
|
|
449
468
|
if (p.action === 'prepare' || p.action === 'clear') {
|
|
469
|
+
// As the shipped plugin: a clear of a lane that was never created has nothing to drop and reports absent.
|
|
470
|
+
const lane = keys.has(id) || agents.has(id);
|
|
450
471
|
keys.delete(id);
|
|
451
472
|
ensureAgent(id).providers = [];
|
|
452
|
-
return { ok: true };
|
|
473
|
+
return lane ? { ok: true } : { ok: false, absent: true };
|
|
453
474
|
}
|
|
454
475
|
const key = keys.get(id);
|
|
455
476
|
if (!key)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ThinkingAsked, ThinkingReport, ThinkingUsed } from './types.ts';
|
|
2
|
+
export type CatalogModel = {
|
|
3
|
+
id?: string;
|
|
4
|
+
thinkingLevelMap?: Record<string, string | null | undefined>;
|
|
5
|
+
compat?: {
|
|
6
|
+
supportedReasoningEfforts?: readonly string[] | null;
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
/** Supported thinking level ids from one catalog model row, or undefined when the catalog names none. */
|
|
10
|
+
export declare function levelsOf(model: CatalogModel | undefined): ThinkingUsed[] | undefined;
|
|
11
|
+
/** Nearest supported level for `asked`. Empty/absent catalog levels leave `asked` unchanged. */
|
|
12
|
+
export declare function clampThinking(asked: ThinkingAsked, supported: readonly ThinkingUsed[] | undefined): ThinkingReport;
|
|
13
|
+
/** The catalog row for `provider`/`modelId` in a `config.get` models.providers map. */
|
|
14
|
+
export declare function catalogModelOf(providers: Record<string, {
|
|
15
|
+
models?: CatalogModel[];
|
|
16
|
+
}> | undefined, provider: string, modelId: string): CatalogModel | undefined;
|
package/dist/thinking.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const ORDER = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
2
|
+
/** Supported thinking level ids from one catalog model row, or undefined when the catalog names none. */
|
|
3
|
+
export function levelsOf(model) {
|
|
4
|
+
if (!model)
|
|
5
|
+
return undefined;
|
|
6
|
+
const efforts = model.compat?.supportedReasoningEfforts;
|
|
7
|
+
if (Array.isArray(efforts) && efforts.length > 0) {
|
|
8
|
+
const out = [];
|
|
9
|
+
const seen = new Set();
|
|
10
|
+
for (const raw of ['off', ...efforts]) {
|
|
11
|
+
const id = raw === 'none' ? 'off' : raw;
|
|
12
|
+
if (!ORDER.includes(id) || seen.has(id))
|
|
13
|
+
continue;
|
|
14
|
+
seen.add(id);
|
|
15
|
+
out.push(id);
|
|
16
|
+
}
|
|
17
|
+
return out.length ? out : undefined;
|
|
18
|
+
}
|
|
19
|
+
const map = model.thinkingLevelMap;
|
|
20
|
+
if (!map)
|
|
21
|
+
return undefined;
|
|
22
|
+
const out = ORDER.filter((level) => {
|
|
23
|
+
const mapped = map[level];
|
|
24
|
+
if (mapped === null)
|
|
25
|
+
return false;
|
|
26
|
+
if (level === 'xhigh' || level === 'max')
|
|
27
|
+
return mapped !== undefined;
|
|
28
|
+
return true;
|
|
29
|
+
});
|
|
30
|
+
return out.length ? out : undefined;
|
|
31
|
+
}
|
|
32
|
+
/** Nearest supported level for `asked`. Empty/absent catalog levels leave `asked` unchanged. */
|
|
33
|
+
export function clampThinking(asked, supported) {
|
|
34
|
+
if (!supported?.length)
|
|
35
|
+
return { asked, used: asked };
|
|
36
|
+
if (supported.includes(asked))
|
|
37
|
+
return { asked, used: asked };
|
|
38
|
+
const candidates = asked === 'off' ? supported : supported.filter((level) => level !== 'off');
|
|
39
|
+
const pool = candidates.length ? candidates : supported;
|
|
40
|
+
const at = ORDER.indexOf(asked);
|
|
41
|
+
if (at >= 0) {
|
|
42
|
+
for (let i = at; i < ORDER.length; i++) {
|
|
43
|
+
const level = ORDER[i];
|
|
44
|
+
if (pool.includes(level))
|
|
45
|
+
return { asked, used: level };
|
|
46
|
+
}
|
|
47
|
+
for (let i = at - 1; i >= 0; i--) {
|
|
48
|
+
const level = ORDER[i];
|
|
49
|
+
if (pool.includes(level))
|
|
50
|
+
return { asked, used: level };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { asked, used: pool[0] };
|
|
54
|
+
}
|
|
55
|
+
/** The catalog row for `provider`/`modelId` in a `config.get` models.providers map. */
|
|
56
|
+
export function catalogModelOf(providers, provider, modelId) {
|
|
57
|
+
const rows = providers?.[provider]?.models;
|
|
58
|
+
if (!rows)
|
|
59
|
+
return undefined;
|
|
60
|
+
const needle = modelId.includes('/') ? modelId.slice(modelId.indexOf('/') + 1) : modelId;
|
|
61
|
+
return rows.find((row) => row.id === needle || row.id === modelId || row.id === `${provider}/${needle}`);
|
|
62
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -102,6 +102,13 @@ export type PlanWindow = {
|
|
|
102
102
|
resetAt?: number;
|
|
103
103
|
}[];
|
|
104
104
|
};
|
|
105
|
+
/** Thinking levels a run may ask for (`RunSpec.thinking`) and the level the kit actually sent. */
|
|
106
|
+
export type ThinkingAsked = 'off' | 'low' | 'medium' | 'high';
|
|
107
|
+
export type ThinkingUsed = ThinkingAsked | 'minimal' | 'xhigh' | 'max';
|
|
108
|
+
export type ThinkingReport = {
|
|
109
|
+
asked: ThinkingAsked;
|
|
110
|
+
used: ThinkingUsed;
|
|
111
|
+
};
|
|
105
112
|
export type RunEnd<T = unknown> = {
|
|
106
113
|
ok: true;
|
|
107
114
|
text: string;
|
|
@@ -110,6 +117,7 @@ export type RunEnd<T = unknown> = {
|
|
|
110
117
|
planWindow?: PlanWindow;
|
|
111
118
|
account?: AccountId;
|
|
112
119
|
model?: string;
|
|
120
|
+
thinking?: ThinkingReport;
|
|
113
121
|
} | {
|
|
114
122
|
ok: false;
|
|
115
123
|
aborted: true;
|
package/dist/words.json
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"signin.returned": "Thanks. Finishing the sign-in — you can go back to the app now.",
|
|
22
22
|
"signin.busy": "Another sign-in is already in progress. Finish or cancel it, then try again.",
|
|
23
23
|
"signin.cancelled": "Sign-in cancelled. You can start again whenever you are ready.",
|
|
24
|
+
"signin.savedNotRestarted": "Your Claude sign-in was saved, but the engine did not restart in time. Try again.",
|
|
24
25
|
"signin.expired": "The sign-in took too long. Start it again.",
|
|
25
26
|
"signin.title": "Sign in to {site}",
|
|
26
27
|
"signin.takeover": "Take over to sign in",
|
|
@@ -81,6 +82,7 @@
|
|
|
81
82
|
"auto.refillsNoTime": "All {provider} accounts are out of room. {name} refills first.",
|
|
82
83
|
"auto.terms": "Auto may use either of a provider's accounts.",
|
|
83
84
|
"auto.none": "No signed-in {provider} account.",
|
|
85
|
+
"auto.noneBound": "No account in this conversation is signed in.",
|
|
84
86
|
"room.unknown": "Room left unknown",
|
|
85
87
|
"room.session": "{left} left this session",
|
|
86
88
|
"room.week": "{left} left this week",
|
|
@@ -110,10 +112,12 @@
|
|
|
110
112
|
"account.another": "another account",
|
|
111
113
|
"account.signOutFirst": "To add a different {provider} account, sign out of {provider} in your browser first.",
|
|
112
114
|
"account.legacyMember": "This person needs a new profile before adding more accounts.",
|
|
115
|
+
"account.indexDamaged": "The account list for {member} couldn't be opened, so those accounts are paused. Add the account again to fix this.",
|
|
113
116
|
"move.unsupported": "This conversation has not started yet, so there is nothing to move.",
|
|
114
117
|
"move.busy": "Wait for this conversation to finish before moving it.",
|
|
115
118
|
"move.notReady": "Sign in to {name} first.",
|
|
116
119
|
"move.closeFailed": "This conversation now continues on {name}, but its earlier copy could not be removed.",
|
|
117
120
|
"move.taken": "{name} already holds a conversation under this name, so nothing was moved.",
|
|
118
|
-
"run.moved": "This conversation just moved to another account. Send it again to continue there."
|
|
121
|
+
"run.moved": "This conversation just moved to another account. Send it again to continue there.",
|
|
122
|
+
"run.thinkingClamped": "Thinking set to {used} because this model does not support {asked}."
|
|
119
123
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@byokit/openclaw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "The OpenClaw runtime kit: the pinned engine's full operator surface with plain-words helpers for members, sign-in, runs and approvals.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
"prepack": "tsc -b && node ../../scripts/fix-words-dts.cjs"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
+
"@byokit/accounts": "0.23.0",
|
|
63
64
|
"@byokit/pair": "0.10.0",
|
|
64
65
|
"@byokit/relay": "0.6.1",
|
|
65
66
|
"@byokit/discover": "0.8.1",
|
package/plugin/keys.js
CHANGED
|
@@ -19,8 +19,9 @@ export function registerKeys(api) {
|
|
|
19
19
|
if (diagnostics) diagnostics.stage = 'config';
|
|
20
20
|
const config = JSON.parse(await readFile(process.env.OPENCLAW_CONFIG_PATH, 'utf8'));
|
|
21
21
|
const keyId = `byokit-key-${member}`;
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
// A clear for a lane that was never created (or whose entries are gone) has nothing left to drop and reports absent.
|
|
23
|
+
if (!config.agents?.entries?.[keyId] || (params.action !== 'clear' && !config.agents?.entries?.[member])) {
|
|
24
|
+
reply(params.action === 'clear' ? { ok: false, absent: true } : { ok: false });
|
|
24
25
|
return;
|
|
25
26
|
}
|
|
26
27
|
const keyDir = resolveAgentDir(config, keyId);
|