@byokit/openclaw 0.26.0 → 0.26.1
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 +17 -0
- package/README.md +5 -0
- package/dist/accounts.d.ts +6 -0
- package/dist/accounts.js +47 -3
- package/dist/auth-status.d.ts +13 -3
- package/dist/auth-status.js +33 -10
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +8 -2
- package/dist/detect.d.ts +30 -0
- package/dist/detect.js +39 -0
- package/dist/keys.js +17 -9
- package/dist/kit.d.ts +2 -0
- package/dist/kit.js +38 -8
- package/dist/signin.d.ts +2 -0
- package/dist/signin.js +12 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.26.1 (2026-10-21)
|
|
6
|
+
|
|
7
|
+
- FIX: a key sign-in through `@byokit/openclaw` no longer fires one slow `openclaw.setup.detect` per reader: the
|
|
8
|
+
readiness read, the account list and a sign-in share one detection per agent (one probe in flight, its answer reused
|
|
9
|
+
briefly, and every sign-in, sign-out, key change and account change drops it), and the pin's own status and the
|
|
10
|
+
detection now run together. The app's poll after a sign-in costs one probe instead of fifteen.
|
|
11
|
+
- FIX: a key lane lists the models the provider's live catalog offers, so a model the picker showed (MiniMax Flash)
|
|
12
|
+
stays listed and pickable instead of being refused as "not included" while the engine's own list still lags the
|
|
13
|
+
registration. The kit keeps no model list of its own: the rows are the ones it registered from the live catalog.
|
|
14
|
+
- FIX: an OpenRouter key sign-in now registers the provider's live list beside the model the person chose, so the
|
|
15
|
+
member keeps every model the vendor lists to pick from (the MiniMax models, `minimax/minimax-m3.1-flash` and its
|
|
16
|
+
free variant, among them) and `openrouter/auto` stays beside them. The engine's catalog stops at a registered list,
|
|
17
|
+
so an add that registered the chosen model alone left the member with that one row and nothing else to pick from.
|
|
18
|
+
The rows are deduped by the id the engine keeps, so the Google preview pair is one row and the list is never
|
|
19
|
+
refused, and the chosen model is registered on its own as well, so a list the engine refuses never costs the member
|
|
20
|
+
their own choice.
|
|
21
|
+
|
|
5
22
|
## 0.26.0 (2026-10-20)
|
|
6
23
|
|
|
7
24
|
- Dependency update: pins @byokit/accounts 0.30.0.
|
package/README.md
CHANGED
|
@@ -192,6 +192,11 @@ pay-as-you-go with no plan, so it takes any model its own live catalog lists, na
|
|
|
192
192
|
prefix (`openrouter/auto`). A Google preview id is named and saved as the canonical id the engine keeps
|
|
193
193
|
(`openrouter/google/gemini-3.1-flash-lite-preview` becomes `openrouter/google/gemini-3.1-flash-lite`); the add
|
|
194
194
|
registers that one model with the engine (every provider but OpenCode Go and MiniMax, whose whole lists are registered).
|
|
195
|
+
An OpenRouter key add registers the provider's live list beside the chosen model, so the member keeps every model the
|
|
196
|
+
vendor lists to pick from (the MiniMax models, `minimax/minimax-m3.1-flash` and its free variant, among them) and
|
|
197
|
+
`openrouter/auto` stays beside them; the engine's catalog stops at a registered list, so registering the chosen model
|
|
198
|
+
alone would leave the member with that one row. The rows are deduped by the id the engine keeps, so the Google preview
|
|
199
|
+
pair is one row and the list is never refused.
|
|
195
200
|
A vendor's list can lag a model it already runs, so an id the catalog lacks is taken, not refused: it rides the route's
|
|
196
201
|
own models (the richest row's api, base URL and context), unpriced, and the app sees it missing from `models()` to warn.
|
|
197
202
|
A route with no catalog rows (its plugin has none) lists nothing, and a `model` there returns
|
package/dist/accounts.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { createLocks } from './locks.ts';
|
|
2
|
+
import type { Detector } from './detect.ts';
|
|
2
3
|
import { type RouteView } from './routes.ts';
|
|
3
4
|
import type { Account, AccountId, AccountPick, Defaults, GatewayTransport, Member, ModelInfo, MoveResult, Room, RunSelection, SignInOptions, SignInView } from './types.ts';
|
|
4
5
|
export declare const kitId: (provider: string, id: string) => string;
|
|
6
|
+
/** The id the engine keeps for a row of `provider` (5.6): a Google preview id (a row of a provider that carries
|
|
7
|
+
* Google's models) and a bare id of a provider that names it. Every other id is the vendor's own, kept as it is. */
|
|
8
|
+
export declare const engineModelId: (provider: string, id: string) => string;
|
|
5
9
|
export declare const engineId: (provider: string, model: string, rows: readonly unknown[] | undefined) => string;
|
|
6
10
|
export declare const modelsOf: (provider: string, answer: unknown) => ModelInfo[];
|
|
7
11
|
export declare const modelsOfAccount: (row: Pick<Account, 'provider' | 'state'>, answer: unknown) => ModelInfo[];
|
|
@@ -73,6 +77,8 @@ export declare function createAccounts(ctx: {
|
|
|
73
77
|
}>;
|
|
74
78
|
log?: (line: string) => void;
|
|
75
79
|
reseal?: () => Promise<void>;
|
|
80
|
+
detect?: Detector;
|
|
81
|
+
catalog?: (provider: string) => ModelInfo[];
|
|
76
82
|
}): {
|
|
77
83
|
list(member: Member): Promise<Account[]>;
|
|
78
84
|
add(member: Member, o: AddOptions, on: (v: SignInView) => void): Promise<AccountSignIn>;
|
package/dist/accounts.js
CHANGED
|
@@ -10,6 +10,7 @@ import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync
|
|
|
10
10
|
import { randomBytes, randomUUID } from 'node:crypto';
|
|
11
11
|
import { join } from 'node:path';
|
|
12
12
|
import { DatabaseSync } from 'node:sqlite';
|
|
13
|
+
import { detection } from './auth-status.js';
|
|
13
14
|
import { providerOf, resolveSelection } from './pick.js';
|
|
14
15
|
import { agentOf, memberOf as baseMemberOf } from './attribution.js';
|
|
15
16
|
import { KEY_PREFIX, MEMBER_ID, NEW_MEMBER_ID, keyAgentId, untilListed, untilUnlisted } from './members.js';
|
|
@@ -19,6 +20,35 @@ const record = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
|
19
20
|
// A provider-local engine model id as the kit's `provider/model` reference; an id already prefixed with its own
|
|
20
21
|
// provider is left alone.
|
|
21
22
|
export const kitId = (provider, id) => providerOf(id) === provider.toLowerCase() ? id : `${provider}/${id}`;
|
|
23
|
+
// The id the pinned engine keeps for a catalog row: it rewrites a row id when it writes the config. A Google
|
|
24
|
+
// preview id becomes the id Google calls it (`google/gemini-3.1-flash-lite-preview` becomes
|
|
25
|
+
// `google/gemini-3.1-flash-lite`), and a bare id of a provider that names it carries that provider. The kit names
|
|
26
|
+
// every row by the id the engine keeps, so the engine's list and the kit's never disagree, and two rows the engine
|
|
27
|
+
// would keep under one id are one row: a list with both is refused ("Ambiguous duplicate ID").
|
|
28
|
+
const googlePreviewIds = {
|
|
29
|
+
'gemini-3-pro': 'gemini-3.1-pro-preview',
|
|
30
|
+
'gemini-3-pro-preview': 'gemini-3.1-pro-preview',
|
|
31
|
+
'gemini-3.1-pro': 'gemini-3.1-pro-preview',
|
|
32
|
+
'gemini-3-flash': 'gemini-3-flash-preview',
|
|
33
|
+
'gemini-3.1-flash': 'gemini-3-flash-preview',
|
|
34
|
+
'gemini-3.1-flash-preview': 'gemini-3-flash-preview',
|
|
35
|
+
'gemini-3.1-flash-lite-preview': 'gemini-3.1-flash-lite',
|
|
36
|
+
'gemma-4-26b': 'gemma-4-26b-a4b-it',
|
|
37
|
+
};
|
|
38
|
+
/** The id the engine keeps for a row of `provider` (5.6): a Google preview id (a row of a provider that carries
|
|
39
|
+
* Google's models) and a bare id of a provider that names it. Every other id is the vendor's own, kept as it is. */
|
|
40
|
+
export const engineModelId = (provider, id) => {
|
|
41
|
+
const at = id.indexOf('/');
|
|
42
|
+
const maker = at < 0 ? '' : id.slice(0, at);
|
|
43
|
+
const model = at < 0 ? id : id.slice(at + 1);
|
|
44
|
+
if (maker === 'google') {
|
|
45
|
+
const kept = googlePreviewIds[model] ?? model;
|
|
46
|
+
return kept === model ? id : `${maker}/${kept}`;
|
|
47
|
+
}
|
|
48
|
+
if (provider === 'openrouter' && at < 0)
|
|
49
|
+
return `${provider}/${id}`;
|
|
50
|
+
return id;
|
|
51
|
+
};
|
|
22
52
|
// The engine's provider-local id for a kit `provider/model` reference, resolved against that provider's rows.
|
|
23
53
|
export const engineId = (provider, model, rows) => {
|
|
24
54
|
for (const row of rows ?? []) {
|
|
@@ -538,8 +568,9 @@ export function createAccounts(ctx) {
|
|
|
538
568
|
return { rows, dirty };
|
|
539
569
|
};
|
|
540
570
|
// A detected Claude Code login has no profile in the kit: it stays in Claude Code. The engine's detection says whether
|
|
541
|
-
// it is there, and a detected one is the member's signed-in claude-cli profile for this read.
|
|
542
|
-
|
|
571
|
+
// it is there, and a detected one is the member's signed-in claude-cli profile for this read. The shared detection
|
|
572
|
+
// answers it for every reader (one probe per agent, reused briefly), so a listing and a readiness read cost one probe.
|
|
573
|
+
const claudeDetected = (agentId) => detection(ctx.request, agentId, ctx.detect)
|
|
543
574
|
.then((r) => r.candidates?.some((c) => c.kind === 'claude-cli' && c.credentials === true) === true, () => false);
|
|
544
575
|
const gather = async (agentId) => {
|
|
545
576
|
const [roster, profiles, claude] = await Promise.all([rosterOf(), profilesOf(agentId), claudeDetected(agentId)]);
|
|
@@ -669,8 +700,21 @@ export function createAccounts(ctx) {
|
|
|
669
700
|
const { row, entry } = await entryOf(member, id);
|
|
670
701
|
return readModels(row, entry);
|
|
671
702
|
}
|
|
703
|
+
// The account's models: the engine's own rows, joined with the catalog rows the kit registered for that provider
|
|
704
|
+
// (the provider's live list). The engine's list can lag the registration, and a model the picker offered must
|
|
705
|
+
// stay pickable: the kit registered it, so the engine can run it. The engine's own row wins where both name it.
|
|
706
|
+
const withCatalog = (row, own) => {
|
|
707
|
+
const registered = ctx.catalog?.(row.provider) ?? [];
|
|
708
|
+
if (!registered.length)
|
|
709
|
+
return own;
|
|
710
|
+
const byId = new Map(own.map((m) => [m.id, m]));
|
|
711
|
+
for (const model of registered)
|
|
712
|
+
if (!byId.has(model.id))
|
|
713
|
+
byId.set(model.id, model);
|
|
714
|
+
return [...byId.values()];
|
|
715
|
+
};
|
|
672
716
|
// The shared hold covers the read, so no kit read of the agent is in flight while it is removed.
|
|
673
|
-
const readModels = (row, entry) => ctx.locks.shared(entry.agent, async () => modelsOfAccount(row, await ctx.request('models.list', { agentId: entry.agent, view: 'all' })));
|
|
717
|
+
const readModels = (row, entry) => ctx.locks.shared(entry.agent, async () => withCatalog(row, modelsOfAccount(row, await ctx.request('models.list', { agentId: entry.agent, view: 'all' }))));
|
|
674
718
|
async function room(member, id, demand) {
|
|
675
719
|
check(member);
|
|
676
720
|
await ctx.ensure(member);
|
package/dist/auth-status.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Detection, Detector } from './detect.ts';
|
|
1
2
|
import type { GatewayTransport } from './types.ts';
|
|
2
3
|
export type AuthStatus = {
|
|
3
4
|
providers?: (string | {
|
|
@@ -14,14 +15,23 @@ export type AuthStatus = {
|
|
|
14
15
|
message?: unknown;
|
|
15
16
|
};
|
|
16
17
|
};
|
|
18
|
+
/** The engine's setup detection, through the shared detector when the caller has one: one probe per agent, reused
|
|
19
|
+
* briefly, so a poll that reads readiness and the account list does not pay for the probe twice. A failed probe reads
|
|
20
|
+
* as no detection: the caller decides what an unknown answer means. */
|
|
21
|
+
export declare const detection: (request: GatewayTransport['request'], agentId: string, detect?: Detector, o?: {
|
|
22
|
+
fresh?: boolean;
|
|
23
|
+
signal?: AbortSignal;
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
}) => Promise<Detection>;
|
|
17
26
|
/** Native Claude login stays in Claude Code. The pin's profile status does not include this synthetic auth;
|
|
18
|
-
* setup detection asks the CLI itself, without handing credentials to the kit.
|
|
19
|
-
|
|
27
|
+
* setup detection asks the CLI itself, without handing credentials to the kit. The detection and the pin's own status
|
|
28
|
+
* are independent reads, so they run together rather than one after the other. */
|
|
29
|
+
export declare function authStatus(request: GatewayTransport['request'], agentId: string, refresh?: boolean, native?: boolean, detect?: Detector): Promise<AuthStatus>;
|
|
20
30
|
/** Admission snapshots, never credentials: per kit/agent/native route, at most 30s and never past reported expiry.
|
|
21
31
|
* Witness only app-owned file metadata (absence included); changed/unreadable state and auth/lifecycle mutations recheck.
|
|
22
32
|
* Remote revocation still comes from the real run's signed-out error (a local status read cannot prove it either).
|
|
23
33
|
*/
|
|
24
|
-
export declare function createAuthStatus(root: string, request: GatewayTransport['request'], codex?: () => Promise<boolean
|
|
34
|
+
export declare function createAuthStatus(root: string, request: GatewayTransport['request'], codex?: () => Promise<boolean>, detect?: Detector): {
|
|
25
35
|
read: (agentId: string, refresh?: boolean, native?: boolean, reuse?: boolean) => Promise<AuthStatus>;
|
|
26
36
|
invalidate: () => void;
|
|
27
37
|
};
|
package/dist/auth-status.js
CHANGED
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
import { statSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { signedInProviders, USABLE } from './runs.js';
|
|
4
|
+
/** The engine's setup detection, through the shared detector when the caller has one: one probe per agent, reused
|
|
5
|
+
* briefly, so a poll that reads readiness and the account list does not pay for the probe twice. A failed probe reads
|
|
6
|
+
* as no detection: the caller decides what an unknown answer means. */
|
|
7
|
+
export const detection = async (request, agentId, detect, o) => {
|
|
8
|
+
if (detect)
|
|
9
|
+
return detect.detect(agentId, { fresh: o?.fresh, timeoutMs: o?.timeoutMs, ...(o?.signal ? { signal: o.signal } : {}) });
|
|
10
|
+
return await request('openclaw.setup.detect', { agentId }, { timeoutMs: o?.timeoutMs ?? 20_000, ...(o?.signal ? { signal: o.signal } : {}) });
|
|
11
|
+
};
|
|
4
12
|
/** Native Claude login stays in Claude Code. The pin's profile status does not include this synthetic auth;
|
|
5
|
-
* setup detection asks the CLI itself, without handing credentials to the kit.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
13
|
+
* setup detection asks the CLI itself, without handing credentials to the kit. The detection and the pin's own status
|
|
14
|
+
* are independent reads, so they run together rather than one after the other. */
|
|
15
|
+
export async function authStatus(request, agentId, refresh = false, native = false, detect) {
|
|
16
|
+
const [status, detected] = await Promise.all([
|
|
17
|
+
request('models.authStatus', { agentId, ...(refresh ? { refresh: true } : {}) }, { timeoutMs: 20_000 }),
|
|
18
|
+
native ? detection(request, agentId, detect).catch(() => ({})) : undefined,
|
|
19
|
+
]);
|
|
20
|
+
return native ? nativeStatus(status, detected) : status;
|
|
9
21
|
}
|
|
10
|
-
|
|
11
|
-
const detected = await request('openclaw.setup.detect', { agentId }, { timeoutMs: 20_000 }).catch(() => ({}));
|
|
22
|
+
function nativeStatus(status, detected) {
|
|
12
23
|
const rows = (status.providers ?? []).filter((row) => (typeof row === 'string' ? row : row.provider) !== 'claude-cli');
|
|
13
|
-
if (detected
|
|
24
|
+
if (detected?.candidates?.some((candidate) => candidate.kind === 'claude-cli' && candidate.credentials === true)) {
|
|
14
25
|
return { providers: [...rows, { provider: 'claude-cli', status: 'ok' }] };
|
|
15
26
|
}
|
|
16
27
|
return { ...status, providers: rows };
|
|
@@ -19,8 +30,9 @@ async function nativeStatus(request, agentId, status) {
|
|
|
19
30
|
* Witness only app-owned file metadata (absence included); changed/unreadable state and auth/lifecycle mutations recheck.
|
|
20
31
|
* Remote revocation still comes from the real run's signed-out error (a local status read cannot prove it either).
|
|
21
32
|
*/
|
|
22
|
-
export function createAuthStatus(root, request, codex) {
|
|
33
|
+
export function createAuthStatus(root, request, codex, detect) {
|
|
23
34
|
const snapshots = new Map();
|
|
35
|
+
const witnesses = new Map();
|
|
24
36
|
let generation = 0;
|
|
25
37
|
const invalidate = () => { generation++; snapshots.clear(); };
|
|
26
38
|
const witness = (agentId) => {
|
|
@@ -54,13 +66,24 @@ export function createAuthStatus(root, request, codex) {
|
|
|
54
66
|
return cached.status;
|
|
55
67
|
snapshots.delete(key);
|
|
56
68
|
snapshots.delete(`${agentId}:false`);
|
|
57
|
-
|
|
69
|
+
// The detection answers about the same state this witness covers (the engine's own logins), so a changed witness
|
|
70
|
+
// drops it: the next reader asks the engine again instead of reading an earlier answer.
|
|
71
|
+
const seen = witnesses.get(agentId);
|
|
72
|
+
if (before !== undefined)
|
|
73
|
+
witnesses.set(agentId, before);
|
|
74
|
+
if (before !== undefined && seen !== undefined && seen !== before)
|
|
75
|
+
detect?.invalidate(agentId);
|
|
76
|
+
// The pin's own status and the setup detection are independent reads: they run together, not one after the other.
|
|
77
|
+
const [profiles, detected] = await Promise.all([
|
|
78
|
+
authStatus(request, agentId, refresh),
|
|
79
|
+
native ? detection(request, agentId, detect).catch(() => undefined) : undefined,
|
|
80
|
+
]);
|
|
58
81
|
if (codex) {
|
|
59
82
|
profiles.providers = (profiles.providers ?? []).filter(row => (typeof row === 'string' ? row : row.provider) !== 'openai-codex');
|
|
60
83
|
if (await codex())
|
|
61
84
|
profiles.providers.push({ provider: 'openai-codex', status: 'ok' });
|
|
62
85
|
}
|
|
63
|
-
const status = native ?
|
|
86
|
+
const status = native ? nativeStatus(profiles, detected) : profiles;
|
|
64
87
|
if (before !== undefined && epoch === generation && witness(agentId) === before) {
|
|
65
88
|
for (const [route, answer] of (native ? [[false, profiles], [true, status]] : [[false, profiles]])) {
|
|
66
89
|
const usable = signedInProviders(answer);
|
package/dist/constants.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export declare const ENGINE_VERSION = "2026.8.35";
|
|
2
2
|
export declare const PROTOCOL_VERSION = 4;
|
|
3
3
|
export declare const REGISTERED_CATALOGS: string[];
|
|
4
|
+
export declare const LIVE_LIST_ADDS: string[];
|
|
4
5
|
export declare const OPERATOR_SCOPES: readonly ['operator.read', 'operator.write', 'operator.admin', 'operator.approvals', 'operator.questions', 'operator.pairing', 'operator.talk'];
|
package/dist/constants.js
CHANGED
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
// `needs-update`, never silently run.
|
|
3
3
|
export const ENGINE_VERSION = '2026.8.35';
|
|
4
4
|
export const PROTOCOL_VERSION = 4;
|
|
5
|
-
// Live catalogs (5.6) the kit registers whole; every other key route registers its chosen model alone. The engine
|
|
6
|
-
//
|
|
5
|
+
// Live catalogs (5.6) the kit registers whole; every other key route registers its chosen model alone. The engine
|
|
6
|
+
// canonicalizes Google preview ids on config write, so the rows are deduped by the id it keeps (`engineModelId`)
|
|
7
|
+
// before they are written.
|
|
7
8
|
export const REGISTERED_CATALOGS = ['opencode-go', 'minimax'];
|
|
9
|
+
// The providers whose key add registers the provider's live list beside the chosen model, so the member keeps every
|
|
10
|
+
// model the vendor lists to pick from (the chosen model id sits beside the provider's own default, `openrouter/auto`).
|
|
11
|
+
// The engine's catalog stops at a registered list, so an add that registered the chosen model alone would leave the
|
|
12
|
+
// member with that one row and nothing else to pick.
|
|
13
|
+
export const LIVE_LIST_ADDS = ['openrouter'];
|
|
8
14
|
export const OPERATOR_SCOPES = [
|
|
9
15
|
'operator.read',
|
|
10
16
|
'operator.write',
|
package/dist/detect.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { GatewayTransport } from './types.ts';
|
|
2
|
+
export type Detection = {
|
|
3
|
+
candidates?: {
|
|
4
|
+
kind?: string;
|
|
5
|
+
credentials?: boolean;
|
|
6
|
+
}[];
|
|
7
|
+
};
|
|
8
|
+
export type DetectOptions = {
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
fresh?: boolean;
|
|
12
|
+
};
|
|
13
|
+
export type Detector = {
|
|
14
|
+
/** The engine's setup detection for one agent. `fresh` skips the cache and the in-flight probe: a sign-in that
|
|
15
|
+
* verifies what the person just did must read the engine, not an earlier answer. */
|
|
16
|
+
detect(agentId: string, o?: DetectOptions): Promise<Detection>;
|
|
17
|
+
/** Drops the cached answer (one agent, or every agent). */
|
|
18
|
+
invalidate(agentId?: string): void;
|
|
19
|
+
};
|
|
20
|
+
/** How long a detection answer is reused. The answer is a CLI probe the kit cannot witness (the login lives in the
|
|
21
|
+
* person's own home), so the window bounds how long an outside sign-in stays unseen; every sign-in, sign-out and
|
|
22
|
+
* account change the kit sees drops it at once. */
|
|
23
|
+
export declare const DETECT_TTL_MS = 10000;
|
|
24
|
+
/**
|
|
25
|
+
* The shared detection. One probe per agent at a time, its answer reused briefly, a failure never cached (the next
|
|
26
|
+
* reader tries again) and a fresh read never served from the cache.
|
|
27
|
+
*/
|
|
28
|
+
export declare function createDetect(request: GatewayTransport['request'], o?: {
|
|
29
|
+
ttlMs?: number;
|
|
30
|
+
}): Detector;
|
package/dist/detect.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** How long a detection answer is reused. The answer is a CLI probe the kit cannot witness (the login lives in the
|
|
2
|
+
* person's own home), so the window bounds how long an outside sign-in stays unseen; every sign-in, sign-out and
|
|
3
|
+
* account change the kit sees drops it at once. */
|
|
4
|
+
export const DETECT_TTL_MS = 10_000;
|
|
5
|
+
/**
|
|
6
|
+
* The shared detection. One probe per agent at a time, its answer reused briefly, a failure never cached (the next
|
|
7
|
+
* reader tries again) and a fresh read never served from the cache.
|
|
8
|
+
*/
|
|
9
|
+
export function createDetect(request, o) {
|
|
10
|
+
const ttl = o?.ttlMs ?? DETECT_TTL_MS;
|
|
11
|
+
const cache = new Map();
|
|
12
|
+
const inflight = new Map();
|
|
13
|
+
const detect = (agentId, options = {}) => {
|
|
14
|
+
const hit = cache.get(agentId);
|
|
15
|
+
if (!options.fresh && hit && hit.until > Date.now())
|
|
16
|
+
return Promise.resolve(hit.value);
|
|
17
|
+
const running = options.fresh ? undefined : inflight.get(agentId);
|
|
18
|
+
if (running)
|
|
19
|
+
return running;
|
|
20
|
+
const call = request('openclaw.setup.detect', { agentId }, { timeoutMs: options.timeoutMs ?? 20_000, ...(options.signal ? { signal: options.signal } : {}) })
|
|
21
|
+
.then((value) => { cache.set(agentId, { until: Date.now() + ttl, value }); return value; })
|
|
22
|
+
.finally(() => { if (inflight.get(agentId) === call)
|
|
23
|
+
inflight.delete(agentId); });
|
|
24
|
+
inflight.set(agentId, call);
|
|
25
|
+
return call;
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
detect,
|
|
29
|
+
invalidate: (agentId) => {
|
|
30
|
+
if (agentId === undefined) {
|
|
31
|
+
cache.clear();
|
|
32
|
+
inflight.clear();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
cache.delete(agentId);
|
|
36
|
+
inflight.delete(agentId);
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
package/dist/keys.js
CHANGED
|
@@ -4,8 +4,8 @@ import { openCodeWorkspaces } from '@byokit/accounts/opencode-go';
|
|
|
4
4
|
import { activate } from './activation.js';
|
|
5
5
|
import { keyAgentId, MEMBER_ID, KEY_PREFIX, untilListed } from './members.js';
|
|
6
6
|
import { providerOf } from './pick.js';
|
|
7
|
-
import { kitId, modelsOf } from './accounts.js';
|
|
8
|
-
import {
|
|
7
|
+
import { kitId, modelsOf, engineModelId } from './accounts.js';
|
|
8
|
+
import { LIVE_LIST_ADDS } from './constants.js';
|
|
9
9
|
export function createKeys(ctx) {
|
|
10
10
|
// Serialize key replacement and key runs, so an in-flight run never sees a half-installed replacement.
|
|
11
11
|
const tails = new Map();
|
|
@@ -58,12 +58,14 @@ export function createKeys(ctx) {
|
|
|
58
58
|
return ['ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'EAI_AGAIN', 'ETIMEDOUT', 'CLIENT_TIMEOUT', 'NETWORK_ERROR'].includes(e?.code ?? '')
|
|
59
59
|
|| e?.name === 'TimeoutError' ? 'network' : 'engine';
|
|
60
60
|
};
|
|
61
|
-
// The
|
|
62
|
-
|
|
63
|
-
? id.slice(0, -'-preview'.length) : id;
|
|
61
|
+
// The kit's name for a model: the provider and the row id the engine keeps (a model reference already carries its
|
|
62
|
+
// provider, so the row id is never prefixed twice).
|
|
64
63
|
const canonicalModel = (model) => {
|
|
65
64
|
const at = model.indexOf('/');
|
|
66
|
-
|
|
65
|
+
if (at < 0)
|
|
66
|
+
return model;
|
|
67
|
+
const provider = model.slice(0, at);
|
|
68
|
+
return kitId(provider, engineModelId(provider, model.slice(at + 1)));
|
|
67
69
|
};
|
|
68
70
|
// The live catalog's rows, as the engine registers them (provider-local ids, deduped by the id it keeps).
|
|
69
71
|
const catalog = async (member, provider) => {
|
|
@@ -74,7 +76,7 @@ export function createKeys(ctx) {
|
|
|
74
76
|
throw new Error('Model catalog is unavailable.');
|
|
75
77
|
return [];
|
|
76
78
|
}
|
|
77
|
-
const rows = (answer.models ?? []).map((m) => ({ ...m, id:
|
|
79
|
+
const rows = (answer.models ?? []).map((m) => ({ ...m, id: engineModelId(provider, String(m.id)) }));
|
|
78
80
|
return [...new Map(rows.map((m) => [String(m.id), m])).values()];
|
|
79
81
|
};
|
|
80
82
|
// The key route a choice names: its own, or the only key route of the provider whose other sign-in it names, billed
|
|
@@ -240,8 +242,14 @@ export function createKeys(ctx) {
|
|
|
240
242
|
}
|
|
241
243
|
if (ac.signal.aborted)
|
|
242
244
|
return abandon(cancelled());
|
|
243
|
-
//
|
|
244
|
-
|
|
245
|
+
// The provider's live list, for a provider whose add registers it (the engine's catalog stops at a
|
|
246
|
+
// registered list, so the chosen model alone would leave the member with that one row to pick): the
|
|
247
|
+
// chosen model id sits beside the provider's own default, `openrouter/auto`.
|
|
248
|
+
if (model !== undefined && LIVE_LIST_ADDS.includes(route.provider))
|
|
249
|
+
await ctx.registerModels(route.provider, rows).catch(() => { });
|
|
250
|
+
// The chosen model is registered on its own, whatever the provider's catalog is: a list the engine
|
|
251
|
+
// refuses (two rows it would keep under one id) must never cost the member's own choice.
|
|
252
|
+
if (model !== undefined)
|
|
245
253
|
await ctx.registerModels(route.provider, rows.filter((m) => kitId(route.provider, String(m.id)) === model)).catch(() => { });
|
|
246
254
|
if (model !== undefined && !(await registered(route.provider, model)))
|
|
247
255
|
return abandon(failure('retry', 'model'));
|
package/dist/kit.d.ts
CHANGED
|
@@ -73,6 +73,7 @@ export declare class OpenClawKit {
|
|
|
73
73
|
private readonly delegations;
|
|
74
74
|
private readonly keys;
|
|
75
75
|
private readonly admission;
|
|
76
|
+
private readonly detect;
|
|
76
77
|
private readonly approvalsCtl;
|
|
77
78
|
private listeners;
|
|
78
79
|
private off;
|
|
@@ -88,6 +89,7 @@ export declare class OpenClawKit {
|
|
|
88
89
|
private engineExit?;
|
|
89
90
|
private swept;
|
|
90
91
|
private catalogs;
|
|
92
|
+
private readonly registered;
|
|
91
93
|
browser?: BrowserHost;
|
|
92
94
|
private browserHost?;
|
|
93
95
|
private readonly brokers;
|
package/dist/kit.js
CHANGED
|
@@ -15,12 +15,13 @@ import { AuthStoreSealSizeError } from './auth-store.js';
|
|
|
15
15
|
import { gatewayTransport } from './transport.js';
|
|
16
16
|
import { createMembers } from './members.js';
|
|
17
17
|
import { createHelpers, HELPER_TAIL, HelperError } from './helpers.js';
|
|
18
|
-
import { createAccounts, memberOfAccount } from './accounts.js';
|
|
18
|
+
import { createAccounts, memberOfAccount, modelsOf, engineModelId } from './accounts.js';
|
|
19
19
|
import { createLocks } from './locks.js';
|
|
20
20
|
import { keyAgentId, KEY_PREFIX, MEMBER_ID } from './members.js';
|
|
21
21
|
import { confirmRetainedLogin as confirmLogin, migrateRetainedLogin as migrateLogin } from './migrate.js';
|
|
22
22
|
import { createRuns, RunReplay, signedInProviders } from './runs.js';
|
|
23
23
|
import { authStatus, createAuthStatus } from './auth-status.js';
|
|
24
|
+
import { createDetect } from './detect.js';
|
|
24
25
|
import { outputSchema } from './output.js';
|
|
25
26
|
import { words } from './words.js';
|
|
26
27
|
import { createKeys } from './keys.js';
|
|
@@ -457,6 +458,9 @@ export class OpenClawKit {
|
|
|
457
458
|
delegations;
|
|
458
459
|
keys;
|
|
459
460
|
admission;
|
|
461
|
+
// One setup detection for every reader (readiness, the account list, a sign-in): the engine's probe asks the
|
|
462
|
+
// installed CLI itself and takes seconds, so a poll that reads both pays for it once, not once per reader.
|
|
463
|
+
detect;
|
|
460
464
|
approvalsCtl;
|
|
461
465
|
listeners = new Set();
|
|
462
466
|
off = [];
|
|
@@ -472,6 +476,8 @@ export class OpenClawKit {
|
|
|
472
476
|
engineExit;
|
|
473
477
|
swept = false;
|
|
474
478
|
catalogs = Promise.resolve();
|
|
479
|
+
// The catalog rows the kit registered, per provider: the provider's live list, as the engine can run it.
|
|
480
|
+
registered = new Map();
|
|
475
481
|
browser;
|
|
476
482
|
browserHost;
|
|
477
483
|
brokers = new Map();
|
|
@@ -679,6 +685,9 @@ export class OpenClawKit {
|
|
|
679
685
|
throw new Error(`invalid tool name: ${t.name}`);
|
|
680
686
|
}
|
|
681
687
|
this.o = { ...o, stateDir: canonicalStateDir(o.stateDir) };
|
|
688
|
+
// The shared detection, above the request path: the witness check and the engine's probe run only when a probe is
|
|
689
|
+
// really made, so a cached answer costs nothing.
|
|
690
|
+
this.detect = createDetect((method, params, options) => this.request()(method, params, options));
|
|
682
691
|
this.engine = new Engine({ ...o, pluginId: o.plugin?.id ?? 'byokit', tools: [...o.tools ?? [], ...(o.browser ? [{
|
|
683
692
|
name: 'request_sign_in', description: 'Ask the person to sign in to the current browser site.',
|
|
684
693
|
parameters: { type: 'object', additionalProperties: false, properties: {
|
|
@@ -688,7 +697,7 @@ export class OpenClawKit {
|
|
|
688
697
|
...(o.browser ? { browserConfig: () => this.browserConfig() } : {}),
|
|
689
698
|
gateBuiltins: o.gateBuiltins !== false, spawnEngine: o.spawnEngine !== false,
|
|
690
699
|
onState: (s) => this.setState(s), onExit: () => this.closed('engine exited') });
|
|
691
|
-
this.admission = createAuthStatus(this.engine.root, (method, params, options) => this.request()(method, params, options), () => this.engine.installedCodexSignedIn());
|
|
700
|
+
this.admission = createAuthStatus(this.engine.root, (method, params, options) => this.request()(method, params, options), () => this.engine.installedCodexSignedIn(), this.detect);
|
|
692
701
|
this.keys = createKeys({ root: this.engine.root,
|
|
693
702
|
prepareOwner: (agentId) => this.prepareKeyOwner(agentId),
|
|
694
703
|
indexLane: (member, route) => this.indexKeyLane(member, route),
|
|
@@ -760,8 +769,10 @@ export class OpenClawKit {
|
|
|
760
769
|
slot.bridge = this.bridge;
|
|
761
770
|
}
|
|
762
771
|
setState(s) {
|
|
763
|
-
if (s.phase !== 'ready')
|
|
772
|
+
if (s.phase !== 'ready') {
|
|
764
773
|
this.admission?.invalidate();
|
|
774
|
+
this.detect?.invalidate();
|
|
775
|
+
}
|
|
765
776
|
this.current = { ...s, ...(this.engine.patchSet !== undefined ? { patchSet: this.engine.patchSet } : {}) };
|
|
766
777
|
this.o.onState?.(this.current);
|
|
767
778
|
for (const fn of this.phaseListeners)
|
|
@@ -850,7 +861,8 @@ export class OpenClawKit {
|
|
|
850
861
|
this.accountsCtl = createAccounts({ request: (method, params, o) => this.request()(method, params, o), root: this.engine.root,
|
|
851
862
|
ensure: (member) => this.ensureMember(member), locks: this.locks,
|
|
852
863
|
signIn: (agentId, o, on, fresh) => startSignIn({ ...this.signInCtx(), ensure: async () => ({ agentId }), fresh }, agentId, o, on),
|
|
853
|
-
routes: this.routeMap, log: this.o.log, reseal: () => this.engine.authStore.reseal()
|
|
864
|
+
routes: this.routeMap, log: this.o.log, reseal: () => this.engine.authStore.reseal(), detect: this.detect,
|
|
865
|
+
catalog: (provider) => this.registered.get(provider) ?? [] });
|
|
854
866
|
this.helpersCtl = createHelpers({ root: this.engine.root, list: (member) => this.accounts(member) });
|
|
855
867
|
// The fail-closed tool bridge feeds the kit's single Approval surface (5.9, O5).
|
|
856
868
|
await Promise.race([this.bridge.start(), exit]);
|
|
@@ -1038,11 +1050,23 @@ export class OpenClawKit {
|
|
|
1038
1050
|
await this.start();
|
|
1039
1051
|
return this.request()(method, params, options);
|
|
1040
1052
|
}
|
|
1053
|
+
// An account change (a sealed, cleared or adopted account) moves the detection too: a Claude Code login the
|
|
1054
|
+
// person just made is an account the kit now knows about. A read of the accounts changes nothing.
|
|
1055
|
+
if (method === 'byokit.accounts') {
|
|
1056
|
+
const action = params?.action;
|
|
1057
|
+
if (action !== 'list' && action !== 'ready')
|
|
1058
|
+
this.detect.invalidate();
|
|
1059
|
+
}
|
|
1041
1060
|
const changesAuth = /^(models\.authLogout|openclaw\.setup\.(auth\.start|activate)|wizard\.(next|cancel)|config\.(apply|patch|set)|agents\.(create|update|delete)|secrets\.(reload|store\.(set|delete))|byokit\.keys)$/.test(method);
|
|
1042
1061
|
if (!changesAuth || (method === 'byokit.keys' && params?.action === 'ready'))
|
|
1043
1062
|
return transport.request(method, params, options);
|
|
1044
1063
|
this.admission.invalidate();
|
|
1045
|
-
|
|
1064
|
+
// A sign-in, a sign-out or a key change moves the detection too: its answer is about this engine's logins.
|
|
1065
|
+
this.detect.invalidate();
|
|
1066
|
+
return Promise.resolve().then(() => transport.request(method, params, options)).finally(() => {
|
|
1067
|
+
this.admission.invalidate();
|
|
1068
|
+
this.detect.invalidate();
|
|
1069
|
+
});
|
|
1046
1070
|
};
|
|
1047
1071
|
}
|
|
1048
1072
|
call(method, params, o) {
|
|
@@ -1117,6 +1141,7 @@ export class OpenClawKit {
|
|
|
1117
1141
|
return { request: (method, params, o) => this.request()(method, params, o),
|
|
1118
1142
|
// Public readiness/sign-in reads stay live; their fresh result can prepare the next run's admission.
|
|
1119
1143
|
authStatus: (agentId, refresh, native) => this.admission.read(agentId, refresh, native, false),
|
|
1144
|
+
detect: this.detect,
|
|
1120
1145
|
ensure: (member) => this.ensureMember(member), callbackPort: this.o.callbackPort ?? 1455,
|
|
1121
1146
|
nativeClaudeLogin: () => this.engine.nativeClaudeLogin(),
|
|
1122
1147
|
resetNativeClaudeLogin: () => this.engine.resetNativeClaudeLogin(),
|
|
@@ -1361,14 +1386,19 @@ export class OpenClawKit {
|
|
|
1361
1386
|
this.o.log?.(`catalog sync ${provider}: failed`); });
|
|
1362
1387
|
}
|
|
1363
1388
|
}
|
|
1364
|
-
// Merges catalog rows into the registered list by id
|
|
1389
|
+
// Merges catalog rows into the registered list by the id the engine keeps (it rewrites a Google preview id when it
|
|
1390
|
+
// writes the config, so two rows it would keep under one id are one row and never a refused write); an unchanged
|
|
1391
|
+
// list is never rewritten.
|
|
1365
1392
|
async registerModels(provider, rows) {
|
|
1366
1393
|
const { config } = await this.request()('config.get');
|
|
1367
1394
|
const registered = config?.models?.providers?.[provider]?.models ?? [];
|
|
1368
|
-
const byId = new Map(registered.map((m) =>
|
|
1395
|
+
const byId = new Map(registered.map((m) => { const id = engineModelId(provider, m.id); return [id, { ...m, id }]; }));
|
|
1369
1396
|
for (const m of rows)
|
|
1370
|
-
byId.set(m.id, m);
|
|
1397
|
+
byId.set(engineModelId(provider, m.id), { ...m, id: engineModelId(provider, m.id) });
|
|
1371
1398
|
const models = [...byId.values()];
|
|
1399
|
+
// What the kit registered is what the engine can run for that provider: the account list joins it, so a model
|
|
1400
|
+
// the live catalog offers is pickable even while the engine's own list still lags the registration.
|
|
1401
|
+
this.registered.set(provider, modelsOf(provider, { models: models.map((m) => ({ ...m, provider })) }));
|
|
1372
1402
|
if (!isDeepStrictEqual(models, registered))
|
|
1373
1403
|
await this.patchConfig({ models: { providers: { [provider]: { models } } } });
|
|
1374
1404
|
}
|
package/dist/signin.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { remoteCliHost } from '@byokit/accounts/cli';
|
|
2
2
|
import { authStatus } from './auth-status.ts';
|
|
3
|
+
import type { Detector } from './detect.ts';
|
|
3
4
|
import type { CliLoginCause, GatewayTransport, Member, SignInOptions, SignInView } from './types.ts';
|
|
4
5
|
export type SignInCtx = {
|
|
5
6
|
request: GatewayTransport['request'];
|
|
6
7
|
authStatus?: (agentId: string, refresh: boolean, native: boolean) => ReturnType<typeof authStatus>;
|
|
8
|
+
detect?: Detector;
|
|
7
9
|
ensure(member: Member): Promise<{
|
|
8
10
|
agentId: string;
|
|
9
11
|
}>;
|
package/dist/signin.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { createServer } from 'node:http';
|
|
5
5
|
import { activate, LATE_RESTART } from './activation.js';
|
|
6
|
-
import { authStatus } from './auth-status.js';
|
|
6
|
+
import { authStatus, detection } from './auth-status.js';
|
|
7
7
|
import { routes } from './routes.js';
|
|
8
8
|
import { signedInProviders } from './runs.js';
|
|
9
9
|
import { words } from './words.js';
|
|
@@ -134,6 +134,9 @@ export function signIn(ctx, member, o, on) {
|
|
|
134
134
|
returned = undefined;
|
|
135
135
|
pasteIn = undefined;
|
|
136
136
|
await closeCallback();
|
|
137
|
+
// A drive moves the login state, so the shared detection is dropped: the next reader asks the engine.
|
|
138
|
+
if (agentId !== undefined)
|
|
139
|
+
ctx.detect?.invalidate(agentId);
|
|
137
140
|
if (sensitive && view.state === 'failed' && view.why === 'failed')
|
|
138
141
|
view = { ...view, error: secretFailure };
|
|
139
142
|
on(view);
|
|
@@ -222,6 +225,10 @@ export function signIn(ctx, member, o, on) {
|
|
|
222
225
|
return { state: 'failed', via, why: 'busy' };
|
|
223
226
|
}
|
|
224
227
|
({ agentId } = await ctx.ensure(member));
|
|
228
|
+
// A drive moves the login state, so the shared detection is dropped before it reads one: the drive's own decision
|
|
229
|
+
// (is a Claude Code login there?) must be the engine's answer now, not an earlier one.
|
|
230
|
+
if (agentId !== undefined)
|
|
231
|
+
ctx.detect?.invalidate(agentId);
|
|
225
232
|
if (reconfirm && ctx.onDisconnect) {
|
|
226
233
|
before = profiles(await authStatus(ctx.request, agentId).catch(() => undefined), false);
|
|
227
234
|
if (signal.aborted)
|
|
@@ -235,8 +242,11 @@ export function signIn(ctx, member, o, on) {
|
|
|
235
242
|
settleStart();
|
|
236
243
|
let apiKey;
|
|
237
244
|
if (o.authChoice === 'anthropic-cli') {
|
|
245
|
+
// The drive reads the engine, never a reused answer: what it decides (is a Claude Code login there? did the
|
|
246
|
+
// one the person just made land?) must be the engine's answer now.
|
|
247
|
+
const probe = agentId;
|
|
238
248
|
const detected = async () => {
|
|
239
|
-
const value = await ctx.request
|
|
249
|
+
const value = await detection(ctx.request, probe, ctx.detect, { fresh: true, signal, timeoutMs: STATUS_MS });
|
|
240
250
|
return value.candidates?.some(candidate => candidate.kind === 'claude-cli' && candidate.credentials === true);
|
|
241
251
|
};
|
|
242
252
|
native = ctx.nativeClaudeLogin?.();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@byokit/openclaw",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.1",
|
|
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",
|