@byokit/openclaw 0.19.0 → 0.21.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 +11 -0
- package/README.md +66 -6
- package/dist/accounts.d.ts +3 -1
- package/dist/accounts.js +30 -9
- package/dist/approvals.js +5 -2
- package/dist/bridge.js +8 -4
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +6 -2
- package/dist/delegations.d.ts +10 -2
- package/dist/delegations.js +83 -18
- package/dist/helpers.d.ts +28 -0
- package/dist/helpers.js +72 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/keys.d.ts +3 -1
- package/dist/keys.js +49 -17
- package/dist/kit.d.ts +13 -1
- package/dist/kit.js +63 -12
- package/dist/runs.d.ts +2 -0
- package/dist/runs.js +30 -11
- package/dist/signin.js +2 -3
- package/dist/testing/fake-gateway.js +47 -6
- package/dist/types.d.ts +24 -2
- package/package.json +1 -1
package/dist/helpers.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Helpers (5.8.3): a member's personas on one account the member already signed in. A helper is its own session
|
|
2
|
+
// namespace `agent:<member>:h-<helper>:` run on the agent holding that account, with its own persona as the run's system
|
|
3
|
+
// text: the pin shares no sign-in between agents (a shared agentDir is refused, authInheritance is global), so no agent
|
|
4
|
+
// is made and nothing is copied. Its record is a kit-owned file; the engine is never asked.
|
|
5
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { loadIndex } from './accounts.js';
|
|
8
|
+
import { KEY_PREFIX, MEMBER_ID } from './members.js';
|
|
9
|
+
export const HELPER_ID = /^[a-z][a-z0-9-]{0,15}$/;
|
|
10
|
+
/** A session key tail of some helper: only a run naming that helper speaks there. */
|
|
11
|
+
export const HELPER_TAIL = /^h-[a-z][a-z0-9-]{0,15}:/;
|
|
12
|
+
/** The helper an engine key `agent:<agent>:h-<helper>:<rest>` belongs to, from its tail; undefined for any other key. */
|
|
13
|
+
export const helperOf = (key) => /^agent:[^:]+:h-([a-z][a-z0-9-]{0,15}):/.exec(key)?.[1];
|
|
14
|
+
const PERSONA_MAX = 8192;
|
|
15
|
+
/** A refused helper request, before any run: `code` says which rule refused it. */
|
|
16
|
+
export class HelperError extends Error {
|
|
17
|
+
code;
|
|
18
|
+
constructor(code, message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = 'HelperError';
|
|
21
|
+
this.code = code;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function createHelpers(ctx) {
|
|
25
|
+
const valid = (member, helper) => typeof member === 'string' && MEMBER_ID.test(member)
|
|
26
|
+
&& !member.startsWith(KEY_PREFIX) && typeof helper === 'string' && HELPER_ID.test(helper);
|
|
27
|
+
const path = (member, helper) => join(ctx.root, 'helpers', member, `${helper}.json`);
|
|
28
|
+
const read = (member, helper) => {
|
|
29
|
+
try {
|
|
30
|
+
const saved = JSON.parse(readFileSync(path(member, helper), 'utf8'));
|
|
31
|
+
return saved?.v === 1 && typeof saved.account === 'string' && typeof saved.agent === 'string'
|
|
32
|
+
&& typeof saved.persona === 'string' ? saved : undefined;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const view = (member, helper, saved) => ({ member, helper, account: saved.account, agentId: saved.agent, sessionPrefix: `agent:${member}:h-${helper}:` });
|
|
39
|
+
return {
|
|
40
|
+
async ensure(member, helper, o) {
|
|
41
|
+
const persona = o?.persona;
|
|
42
|
+
// The persona is the helper's whole system text: bounded, plain text (newlines and tabs only).
|
|
43
|
+
if (!valid(member, helper) || typeof persona !== 'string' || !persona.trim() || persona.length > PERSONA_MAX
|
|
44
|
+
|| /[\u0000-\u0008\u000b-\u001f\u007f]/.test(persona))
|
|
45
|
+
throw new HelperError('invalid', `invalid helper: ${String(member)}/${String(helper)}`);
|
|
46
|
+
if (!(await ctx.list(member)).some((row) => row.id === o.account))
|
|
47
|
+
throw new HelperError('unknown_account', `unknown account: ${String(o.account)}`);
|
|
48
|
+
const agent = loadIndex(ctx.root, member).file.accounts[o.account]?.agent;
|
|
49
|
+
if (!agent)
|
|
50
|
+
throw new HelperError('unknown_account', `unknown account: ${o.account}`);
|
|
51
|
+
const before = read(member, helper);
|
|
52
|
+
if (before && (before.account !== o.account || before.agent !== agent))
|
|
53
|
+
throw new HelperError('bound', `helper ${helper} uses another account`);
|
|
54
|
+
const saved = { v: 1, account: o.account, agent, persona };
|
|
55
|
+
const file = path(member, helper);
|
|
56
|
+
mkdirSync(join(file, '..'), { recursive: true, mode: 0o700 });
|
|
57
|
+
writeFileSync(`${file}.tmp`, JSON.stringify(saved), { mode: 0o600 });
|
|
58
|
+
renameSync(`${file}.tmp`, file);
|
|
59
|
+
return view(member, helper, saved);
|
|
60
|
+
},
|
|
61
|
+
// A run's helper, fail closed: ensured by this kit, on an account the member's index still names on the same agent.
|
|
62
|
+
// A removed account leaves the helper unusable; it never moves to another sign-in.
|
|
63
|
+
resolve(member, helper) {
|
|
64
|
+
const saved = valid(member, helper) ? read(member, helper) : undefined;
|
|
65
|
+
const entry = saved && loadIndex(ctx.root, member).file.accounts[saved.account];
|
|
66
|
+
if (!saved || entry?.agent !== saved.agent)
|
|
67
|
+
throw new HelperError('unknown_helper', `unknown helper: ${String(helper)}`);
|
|
68
|
+
return { account: saved.account, agentId: saved.agent, persona: saved.persona,
|
|
69
|
+
binding: saved.agent === member ? { agentId: member } : { agentId: saved.agent, id: saved.account } };
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { ENGINE_VERSION, OPERATOR_SCOPES, PROTOCOL_VERSION } from './constants.ts';
|
|
2
|
-
export type { Account, AccountId, AccountPick, AccountRef, Approval, CallOptions, ChannelChat, ChannelMessage, Considered, Decision, Defaults, ModelInfo, MoveResult, PickWhy, Room, RoomSpan, RunSelection, SignInState, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, GateResult, Hello, KitState, KitEventName, KitEventPayload, BrowserPing, LearningCapture, LearningMode, Member, PlanWindow, Route, RunEnd, RunEvent, RunFrame, RunRef, DelegatedRun, DelegatedEvent, RunSpec, OutputSchema, SchemaOutput, RunUsage, ThinkingAsked, ThinkingReport, ThinkingUsed, SignInOptions, SignInView, TelegramChannel, ToolHost, ToolSpec, } from './types.ts';
|
|
2
|
+
export type { Account, AccountId, AccountPick, AccountRef, Approval, CallOptions, ChannelChat, ChannelMessage, Considered, Decision, Defaults, ModelInfo, MoveResult, PickWhy, Room, RoomSpan, RunSelection, SignInState, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, GateResult, Hello, KitState, KitEventName, KitEventPayload, BrowserPing, LearningCapture, LearningMode, Member, PlanWindow, Route, RunEnd, RunEvent, RunFrame, RunRef, Helper, DelegatedRun, DelegatedEvent, RunSpec, OutputSchema, SchemaOutput, RunUsage, ThinkingAsked, ThinkingReport, ThinkingUsed, SignInOptions, SignInView, TelegramChannel, ToolHost, ToolSpec, } from './types.ts';
|
|
3
3
|
export type { GatewayMethods } from './generated/methods.ts';
|
|
4
4
|
export type { JsonValue } from './output.ts';
|
|
5
5
|
export type { RouteFacts, RouteView } from './routes.ts';
|
|
@@ -7,6 +7,7 @@ export { OpenClawKit, type KitOptions, type RetainedLogin } from './kit.ts';
|
|
|
7
7
|
export type { AddKeyHandle, AddKeyResult, KeyFailureReason } from './keys.ts';
|
|
8
8
|
export { stateWords, toAccountView, words, type AccountView, type WordKey } from './words.ts';
|
|
9
9
|
export { chooseAccount, consider, resolveSelection, type RoomOf } from './pick.ts';
|
|
10
|
+
export { HelperError, type HelperErrorCode } from './helpers.ts';
|
|
10
11
|
export { AuthStoreUnreadableError, AuthStoreSealSizeError } from './auth-store.ts';
|
|
11
12
|
export { EngineAlreadyRunningError } from './engine-status.ts';
|
|
12
13
|
export { PluginInstallError, type PluginInstallFailure } from './plugins.ts';
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ export { ENGINE_VERSION, OPERATOR_SCOPES, PROTOCOL_VERSION } from './constants.j
|
|
|
4
4
|
export { OpenClawKit } from './kit.js';
|
|
5
5
|
export { stateWords, toAccountView, words } from './words.js';
|
|
6
6
|
export { chooseAccount, consider, resolveSelection } from './pick.js';
|
|
7
|
+
export { HelperError } from './helpers.js';
|
|
7
8
|
export { AuthStoreUnreadableError, AuthStoreSealSizeError } from './auth-store.js';
|
|
8
9
|
export { EngineAlreadyRunningError } from './engine-status.js';
|
|
9
10
|
export { PluginInstallError } from './plugins.js';
|
package/dist/keys.d.ts
CHANGED
|
@@ -37,9 +37,11 @@ export declare function createKeys(ctx: ActivationCtx & {
|
|
|
37
37
|
indexLane(member: Member, route: RouteView): Promise<void>;
|
|
38
38
|
dropLane(member: Member, provider?: string): Promise<void>;
|
|
39
39
|
backfillLane(member: Member, model: string | undefined): Promise<void>;
|
|
40
|
-
/** The provider config headers a
|
|
40
|
+
/** The provider config headers a Go key's calls and probes must carry (`null` clears them),
|
|
41
41
|
* written through the kit's own config patch. The Go workspace header is the one user. */
|
|
42
42
|
setProviderHeaders(provider: string, headers: Record<string, string> | null): Promise<void>;
|
|
43
|
+
/** Adds catalog rows to the engine's `models.providers[provider].models`, serialized with catalog sync. */
|
|
44
|
+
registerModels(provider: string, rows: Record<string, unknown>[]): Promise<void>;
|
|
43
45
|
}): {
|
|
44
46
|
models: (member: Member, o: {
|
|
45
47
|
authChoice: string;
|
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 { modelsOf } from './accounts.js';
|
|
8
|
-
import { LIVE_CATALOGS } from './constants.js';
|
|
7
|
+
import { kitId, modelsOf } from './accounts.js';
|
|
8
|
+
import { LIVE_CATALOGS, REGISTERED_CATALOGS } 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();
|
|
@@ -47,7 +47,7 @@ export function createKeys(ctx) {
|
|
|
47
47
|
};
|
|
48
48
|
const registered = async (provider, model) => {
|
|
49
49
|
const { config } = await ctx.request('config.get');
|
|
50
|
-
return Boolean(config?.models?.providers?.[provider]?.models?.some((m) =>
|
|
50
|
+
return Boolean(config?.models?.providers?.[provider]?.models?.some((m) => kitId(provider, m.id) === model));
|
|
51
51
|
};
|
|
52
52
|
const errorReason = (error, sent) => {
|
|
53
53
|
if (transient(error))
|
|
@@ -58,19 +58,46 @@ 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 engine writes a Google preview id without its `-preview` suffix, so the kit names the id it keeps.
|
|
62
|
+
const canonical = (id) => id.startsWith('google/') && id.endsWith('-preview')
|
|
63
|
+
? id.slice(0, -'-preview'.length) : id;
|
|
64
|
+
const canonicalModel = (model) => {
|
|
65
|
+
const at = model.indexOf('/');
|
|
66
|
+
return at < 0 ? model : `${model.slice(0, at)}/${canonical(model.slice(at + 1))}`;
|
|
67
|
+
};
|
|
68
|
+
// The live catalog's rows, as the engine registers them (provider-local ids, deduped by the id it keeps).
|
|
69
|
+
const catalog = async (member, provider) => {
|
|
70
|
+
await ctx.ensure(member);
|
|
71
|
+
const answer = await ctx.request('byokit.catalog', { provider });
|
|
72
|
+
if (!answer.ok) {
|
|
73
|
+
if (answer.reason !== 'no-key')
|
|
74
|
+
throw new Error('Model catalog is unavailable.');
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
const rows = (answer.models ?? []).map((m) => ({ ...m, id: canonical(String(m.id)) }));
|
|
78
|
+
return [...new Map(rows.map((m) => [String(m.id), m])).values()];
|
|
79
|
+
};
|
|
80
|
+
const listed = (provider, rows) => modelsOf(provider, { models: rows.map((m) => ({ ...m, provider })) });
|
|
61
81
|
const models = async (member, o) => {
|
|
62
82
|
if (!check(member))
|
|
63
83
|
throw new Error('Choose your own member.');
|
|
64
84
|
const route = ctx.routes().find((r) => r.choice === o.authChoice && r.keyEntry && r.readiness === 'ready');
|
|
65
85
|
if (!route || !LIVE_CATALOGS.includes(route.provider))
|
|
66
86
|
return [];
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
87
|
+
try {
|
|
88
|
+
return listed(route.provider, await catalog(member, route.provider));
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
// The engine answers `failed` when no agent holds a key: a lane without the provider's profile lists nothing,
|
|
92
|
+
// a saved one is a real failure.
|
|
93
|
+
const held = await ctx.request('models.authStatus', { agentId: keyAgentId(member) }).then((answer) => {
|
|
94
|
+
const rows = answer.providers ?? [];
|
|
95
|
+
return rows.some((row) => (typeof row === 'string' ? row : row?.provider) === route.provider);
|
|
96
|
+
}, () => undefined);
|
|
97
|
+
if (held !== false)
|
|
98
|
+
throw error;
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
74
101
|
};
|
|
75
102
|
return {
|
|
76
103
|
models,
|
|
@@ -87,13 +114,14 @@ export function createKeys(ctx) {
|
|
|
87
114
|
const route = ctx.routes().find((r) => r.choice === o.authChoice && r.keyEntry && r.readiness === 'ready');
|
|
88
115
|
if (!route || (o.model !== undefined && !LIVE_CATALOGS.includes(route.provider)))
|
|
89
116
|
return failure('not_included', 'model');
|
|
90
|
-
|
|
117
|
+
const model = o.model === undefined ? undefined : canonicalModel(o.model);
|
|
118
|
+
// A workspace names a Go login's own workspace only; another route takes no header, and a
|
|
91
119
|
// refusal here leaves the member's lanes untouched (it lands before any prepare or activation).
|
|
92
120
|
let orgId = typeof o.orgId === 'string' ? o.orgId.trim() : '';
|
|
93
121
|
if (orgId) {
|
|
94
122
|
if (orgId.length > 256)
|
|
95
123
|
return failure('invalid', 'auth');
|
|
96
|
-
if (
|
|
124
|
+
if (route.provider !== 'opencode-go')
|
|
97
125
|
return failure('not_included', 'model');
|
|
98
126
|
}
|
|
99
127
|
else if (route.provider === 'opencode-go') {
|
|
@@ -157,7 +185,7 @@ export function createKeys(ctx) {
|
|
|
157
185
|
await ctx.setProviderHeaders(route.provider, { 'x-opencode-org-id': orgId });
|
|
158
186
|
wroteHeader = true;
|
|
159
187
|
}
|
|
160
|
-
else if (
|
|
188
|
+
else if (route.provider === 'opencode-go') {
|
|
161
189
|
await ctx.setProviderHeaders(route.provider, null);
|
|
162
190
|
wroteHeader = true;
|
|
163
191
|
}
|
|
@@ -172,7 +200,8 @@ export function createKeys(ctx) {
|
|
|
172
200
|
return abandon(probeResult(result.status));
|
|
173
201
|
if (result.late)
|
|
174
202
|
return abandon(failure('retry', 'busy'));
|
|
175
|
-
|
|
203
|
+
const rows = model === undefined ? [] : await catalog(member, route.provider);
|
|
204
|
+
if (model !== undefined && !listed(route.provider, rows).some((m) => m.id === model && m.available))
|
|
176
205
|
return abandon(failure('not_included', 'model'));
|
|
177
206
|
// Prepare before publishing the ready marker: failed preparation remains unusable even if clear fails.
|
|
178
207
|
try {
|
|
@@ -183,9 +212,12 @@ export function createKeys(ctx) {
|
|
|
183
212
|
}
|
|
184
213
|
if (ac.signal.aborted)
|
|
185
214
|
return abandon(cancelled());
|
|
186
|
-
|
|
215
|
+
// A live catalog the kit does not register whole (OpenRouter) registers the chosen model alone.
|
|
216
|
+
if (model !== undefined && !REGISTERED_CATALOGS.includes(route.provider))
|
|
217
|
+
await ctx.registerModels(route.provider, rows.filter((m) => kitId(route.provider, String(m.id)) === model)).catch(() => { });
|
|
218
|
+
if (model !== undefined && LIVE_CATALOGS.includes(route.provider) && !(await registered(route.provider, model)))
|
|
187
219
|
return abandon(failure('retry', 'model'));
|
|
188
|
-
if (!(await control(member, 'seal', undefined,
|
|
220
|
+
if (!(await control(member, 'seal', undefined, model)).ok)
|
|
189
221
|
return abandon(failure('retry', 'engine'));
|
|
190
222
|
await ctx.indexLane(member, route);
|
|
191
223
|
return { status: 'ok' };
|
|
@@ -241,7 +273,7 @@ export function createKeys(ctx) {
|
|
|
241
273
|
if (cleared?.ok !== true && cleared?.absent !== true)
|
|
242
274
|
refused();
|
|
243
275
|
// A workspace header the removed key named must not outlive it; the next add names its own (or none).
|
|
244
|
-
if (o.provider
|
|
276
|
+
if (o.provider === 'opencode-go')
|
|
245
277
|
await ctx.setProviderHeaders(o.provider, null).catch(() => { throw new Error('The key could not be removed.'); });
|
|
246
278
|
await ctx.dropLane(member, o.provider);
|
|
247
279
|
});
|
package/dist/kit.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { SealingAdapter } from '@byokit/secrets';
|
|
|
2
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
|
-
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, ChannelChat, TelegramChannel, ToolHost, ToolSpec } from './types.ts';
|
|
5
|
+
import type { Account, AccountId, AccountPick, Approval, CallOptions, Decision, Defaults, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, Hello, Helper, KitState, KitEventName, KitEventPayload, LearningCapture, LearningMode, Member, ModelInfo, MoveResult, Room, RunEnd, RunEvent, RunRef, RunSelection, RunSpec, OutputSchema, SchemaOutput, SignInOptions, SignInView, ChannelChat, TelegramChannel, ToolHost, ToolSpec } from './types.ts';
|
|
6
6
|
export type KitOptions = {
|
|
7
7
|
stateDir: string;
|
|
8
8
|
authSeal?: SealingAdapter;
|
|
@@ -68,6 +68,7 @@ export declare class OpenClawKit {
|
|
|
68
68
|
attach(member: Member, runId: string, fromSeq: number, signal: AbortSignal): AsyncIterable<import("./types.ts").RunFrame>;
|
|
69
69
|
private installs;
|
|
70
70
|
private accountsCtl?;
|
|
71
|
+
private helpersCtl?;
|
|
71
72
|
private readonly bridge;
|
|
72
73
|
private readonly delegations;
|
|
73
74
|
private readonly keys;
|
|
@@ -129,6 +130,16 @@ export declare class OpenClawKit {
|
|
|
129
130
|
agentId: string;
|
|
130
131
|
workspace: string;
|
|
131
132
|
}>;
|
|
133
|
+
/** One of the member's helpers (5.8.3): its own persona and sessions on the member's `account`, no new sign-in. */
|
|
134
|
+
ensureHelper(member: Member, helper: string, o: {
|
|
135
|
+
account: AccountId;
|
|
136
|
+
persona: string;
|
|
137
|
+
}): Promise<Helper>;
|
|
138
|
+
/** Adopt a legacy conversation of the member into one of its helpers (5.8.3, S4): its whole transcript forks to
|
|
139
|
+
* `agent:<member>:h-<helper>:<tail>` on the helper's account agent (as `move` does), then the old session goes.
|
|
140
|
+
* One still in flight (a live run, a parked approval or an admitted delegated child under it) is refused `busy`
|
|
141
|
+
* and keeps its exact old key until it settles; nothing is replayed and no approval changes owner. */
|
|
142
|
+
adoptHelperSession(ref: RunRef, helper: string): Promise<MoveResult>;
|
|
132
143
|
private signInCtx;
|
|
133
144
|
routes(): RouteView[];
|
|
134
145
|
/** Start Composio's hosted MCP OAuth flow through the pinned engine; return the URL for the app to open. */
|
|
@@ -208,6 +219,7 @@ export declare class OpenClawKit {
|
|
|
208
219
|
private backfillKeyLane;
|
|
209
220
|
private prepareKeyOwner;
|
|
210
221
|
private syncCatalogs;
|
|
222
|
+
private registerModels;
|
|
211
223
|
migrateRetainedLogin(member: Member, source: RetainedLogin): Promise<'staged' | 'nothing' | 'failed'>;
|
|
212
224
|
confirmRetainedLogin(member: Member, source: RetainedLogin): Promise<boolean>;
|
|
213
225
|
private runs;
|
package/dist/kit.js
CHANGED
|
@@ -5,7 +5,7 @@ import { join } from 'node:path';
|
|
|
5
5
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
6
6
|
import { isDeepStrictEqual } from 'node:util';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
-
import { PROTOCOL_VERSION,
|
|
8
|
+
import { PROTOCOL_VERSION, REGISTERED_CATALOGS } from './constants.js';
|
|
9
9
|
import { atomic } from './engine-patches.js';
|
|
10
10
|
import { Approvals } from './approvals.js';
|
|
11
11
|
import { Bridge } from './bridge.js';
|
|
@@ -14,6 +14,7 @@ import { Engine, canonicalStateDir, refusedOnly } from './engine.js';
|
|
|
14
14
|
import { AuthStoreSealSizeError } from './auth-store.js';
|
|
15
15
|
import { gatewayTransport } from './transport.js';
|
|
16
16
|
import { createMembers } from './members.js';
|
|
17
|
+
import { createHelpers, HELPER_TAIL, HelperError } from './helpers.js';
|
|
17
18
|
import { createAccounts, memberOfAccount } from './accounts.js';
|
|
18
19
|
import { createLocks } from './locks.js';
|
|
19
20
|
import { keyAgentId, KEY_PREFIX, MEMBER_ID } from './members.js';
|
|
@@ -21,6 +22,7 @@ import { confirmRetainedLogin as confirmLogin, migrateRetainedLogin as migrateLo
|
|
|
21
22
|
import { createRuns, RunReplay, signedInProviders } from './runs.js';
|
|
22
23
|
import { authStatus, createAuthStatus } from './auth-status.js';
|
|
23
24
|
import { outputSchema } from './output.js';
|
|
25
|
+
import { words } from './words.js';
|
|
24
26
|
import { createKeys } from './keys.js';
|
|
25
27
|
import { routes as routeTable } from './routes.js';
|
|
26
28
|
import { installedPlugins, pinnedSpec, recordPlugin, PluginInstallError } from './plugins.js';
|
|
@@ -448,6 +450,7 @@ export class OpenClawKit {
|
|
|
448
450
|
}
|
|
449
451
|
installs = Promise.resolve();
|
|
450
452
|
accountsCtl;
|
|
453
|
+
helpersCtl;
|
|
451
454
|
// One bridge and one approval surface per kit: listeners and run registrations survive reconnects (B3).
|
|
452
455
|
// Only the socket itself is per-connection. Approvals always reaches the live transport through this.request().
|
|
453
456
|
bridge;
|
|
@@ -697,7 +700,12 @@ export class OpenClawKit {
|
|
|
697
700
|
ensure: (member) => this.ensureMember(member),
|
|
698
701
|
authConfig: () => this.authSnapshot(),
|
|
699
702
|
restarted: (authBefore, signal) => this.restarted(authBefore, signal),
|
|
700
|
-
setProviderHeaders: (provider, headers) => this.patchConfig({ models: { providers: { [provider]: { headers: headers ?? null } } } })
|
|
703
|
+
setProviderHeaders: (provider, headers) => this.patchConfig({ models: { providers: { [provider]: { headers: headers ?? null } } } }),
|
|
704
|
+
registerModels: (provider, rows) => {
|
|
705
|
+
const done = this.catalogs.then(() => this.registerModels(provider, rows));
|
|
706
|
+
this.catalogs = done.catch(() => { });
|
|
707
|
+
return done;
|
|
708
|
+
} });
|
|
701
709
|
const slot = {};
|
|
702
710
|
this.approvalsCtl = new Approvals({
|
|
703
711
|
request: (method, params, co) => this.request()(method, params, co),
|
|
@@ -724,6 +732,16 @@ export class OpenClawKit {
|
|
|
724
732
|
} : o.host;
|
|
725
733
|
this.delegations = new Delegations({ file: join(this.engine.root, 'delegated-runs.json'), tools: new Set(this.toolNames()),
|
|
726
734
|
memberOf: agent => this.memberOf(agent), request: (method, params, options) => this.request()(method, params, options),
|
|
735
|
+
helper: (member, helper) => {
|
|
736
|
+
if (!this.helpersCtl)
|
|
737
|
+
throw new Error('gateway not ready');
|
|
738
|
+
return this.helpersCtl.resolve(member, helper);
|
|
739
|
+
},
|
|
740
|
+
rest: async (member, account, until) => {
|
|
741
|
+
if (!this.accountsCtl)
|
|
742
|
+
throw new Error('gateway not ready');
|
|
743
|
+
await this.accountsCtl.rest(member, account, until);
|
|
744
|
+
},
|
|
727
745
|
emit: payload => { for (const fn of this.listeners)
|
|
728
746
|
fn({ event: 'byokit.delegated', payload }); },
|
|
729
747
|
revoke: (key, runId) => this.bridge.revoke(key, runId) });
|
|
@@ -833,6 +851,7 @@ export class OpenClawKit {
|
|
|
833
851
|
ensure: (member) => this.ensureMember(member), locks: this.locks,
|
|
834
852
|
signIn: (agentId, o, on, fresh) => startSignIn({ ...this.signInCtx(), ensure: async () => ({ agentId }), fresh }, agentId, o, on),
|
|
835
853
|
routes: this.routeMap, log: this.o.log, reseal: () => this.engine.authStore.reseal() });
|
|
854
|
+
this.helpersCtl = createHelpers({ root: this.engine.root, list: (member) => this.accounts(member) });
|
|
836
855
|
// The fail-closed tool bridge feeds the kit's single Approval surface (5.9, O5).
|
|
837
856
|
await Promise.race([this.bridge.start(), exit]);
|
|
838
857
|
this.off.push(transport.onEvent((e) => this.approvalsCtl.handleEvent(e)));
|
|
@@ -1068,6 +1087,31 @@ export class OpenClawKit {
|
|
|
1068
1087
|
await attaching;
|
|
1069
1088
|
return result;
|
|
1070
1089
|
}
|
|
1090
|
+
/** One of the member's helpers (5.8.3): its own persona and sessions on the member's `account`, no new sign-in. */
|
|
1091
|
+
ensureHelper(member, helper, o) {
|
|
1092
|
+
if (!this.helpersCtl)
|
|
1093
|
+
return Promise.reject(new Error('gateway not ready'));
|
|
1094
|
+
return this.helpersCtl.ensure(member, helper, o);
|
|
1095
|
+
}
|
|
1096
|
+
/** Adopt a legacy conversation of the member into one of its helpers (5.8.3, S4): its whole transcript forks to
|
|
1097
|
+
* `agent:<member>:h-<helper>:<tail>` on the helper's account agent (as `move` does), then the old session goes.
|
|
1098
|
+
* One still in flight (a live run, a parked approval or an admitted delegated child under it) is refused `busy`
|
|
1099
|
+
* and keeps its exact old key until it settles; nothing is replayed and no approval changes owner. */
|
|
1100
|
+
async adoptHelperSession(ref, helper) {
|
|
1101
|
+
if (!this.accountsCtl || !this.helpersCtl)
|
|
1102
|
+
throw new Error('gateway not ready');
|
|
1103
|
+
const tailOf = (key) => key.slice(key.indexOf(':', 'agent:'.length) + 1);
|
|
1104
|
+
const tail = tailOf(String(ref.sessionKey));
|
|
1105
|
+
if (!String(ref.sessionKey).startsWith(`agent:${ref.member}:`) || HELPER_TAIL.test(tail))
|
|
1106
|
+
throw new HelperError('session', `refused: "${String(ref.sessionKey)}" is not a legacy session of member "${String(ref.member)}"`);
|
|
1107
|
+
const { account } = this.helpersCtl.resolve(ref.member, helper);
|
|
1108
|
+
const owns = (key) => key !== undefined && tailOf(key) === tail && this.memberOf(key) === ref.member;
|
|
1109
|
+
if (this.approvals(ref.member).some((a) => owns(a.sessionKey))
|
|
1110
|
+
|| this.delegatedRuns().some((r) => owns(r.parent.sessionKey) || owns(r.root.sessionKey)))
|
|
1111
|
+
return { ok: false, code: 'busy', message: words('move.busy') };
|
|
1112
|
+
const moved = await this.accountsCtl.move(ref.member, ref.sessionKey, account, `h-${helper}:${tail}`);
|
|
1113
|
+
return moved.ok ? { ok: true, session: `agent:${ref.member}:h-${helper}:${tail}` } : moved;
|
|
1114
|
+
}
|
|
1071
1115
|
// sign-in (5.7): every body here is the module's, with this kit's transport, members and ports.
|
|
1072
1116
|
signInCtx() {
|
|
1073
1117
|
return { request: (method, params, o) => this.request()(method, params, o),
|
|
@@ -1226,6 +1270,8 @@ export class OpenClawKit {
|
|
|
1226
1270
|
move(ref, to) {
|
|
1227
1271
|
if (!this.accountsCtl)
|
|
1228
1272
|
return Promise.reject(new Error('gateway not ready'));
|
|
1273
|
+
if (HELPER_TAIL.test(String(ref.sessionKey).replace(/^agent:[^:]+:/, '')))
|
|
1274
|
+
return Promise.reject(new HelperError('session', 'refused: a helper\'s session stays on its account'));
|
|
1229
1275
|
return this.accountsCtl.move(ref.member, ref.sessionKey, to);
|
|
1230
1276
|
}
|
|
1231
1277
|
/** Sign in one more account: on the member agent while it holds none of the provider, else on a new sealed agent. */
|
|
@@ -1300,7 +1346,7 @@ export class OpenClawKit {
|
|
|
1300
1346
|
// Live provider catalogs (5.6): registers each live model with its api from the provider's catalog data, so
|
|
1301
1347
|
// embedded runs resolve it. Serialized and in the background; a failure is logged and keeps the registered list.
|
|
1302
1348
|
syncCatalogs() {
|
|
1303
|
-
for (const provider of
|
|
1349
|
+
for (const provider of REGISTERED_CATALOGS) {
|
|
1304
1350
|
if (this.o.config?.models?.providers?.[provider]?.models !== undefined)
|
|
1305
1351
|
continue;
|
|
1306
1352
|
this.catalogs = this.catalogs.then(async () => {
|
|
@@ -1310,18 +1356,22 @@ export class OpenClawKit {
|
|
|
1310
1356
|
this.o.log?.(`catalog sync ${provider}: ${answer.reason ?? 'failed'}`);
|
|
1311
1357
|
return;
|
|
1312
1358
|
}
|
|
1313
|
-
|
|
1314
|
-
const registered = config?.models?.providers?.[provider]?.models ?? [];
|
|
1315
|
-
const byId = new Map(registered.map((m) => [m.id, m]));
|
|
1316
|
-
for (const m of answer.models ?? [])
|
|
1317
|
-
byId.set(m.id, m);
|
|
1318
|
-
const models = [...byId.values()];
|
|
1319
|
-
if (!isDeepStrictEqual(models, registered))
|
|
1320
|
-
await this.patchConfig({ models: { providers: { [provider]: { models } } } });
|
|
1359
|
+
await this.registerModels(provider, answer.models ?? []);
|
|
1321
1360
|
}).catch(() => { if (this.current.phase === 'ready')
|
|
1322
1361
|
this.o.log?.(`catalog sync ${provider}: failed`); });
|
|
1323
1362
|
}
|
|
1324
1363
|
}
|
|
1364
|
+
// Merges catalog rows into the registered list by id; an unchanged list is never rewritten.
|
|
1365
|
+
async registerModels(provider, rows) {
|
|
1366
|
+
const { config } = await this.request()('config.get');
|
|
1367
|
+
const registered = config?.models?.providers?.[provider]?.models ?? [];
|
|
1368
|
+
const byId = new Map(registered.map((m) => [m.id, m]));
|
|
1369
|
+
for (const m of rows)
|
|
1370
|
+
byId.set(m.id, m);
|
|
1371
|
+
const models = [...byId.values()];
|
|
1372
|
+
if (!isDeepStrictEqual(models, registered))
|
|
1373
|
+
await this.patchConfig({ models: { providers: { [provider]: { models } } } });
|
|
1374
|
+
}
|
|
1325
1375
|
migrateRetainedLogin(member, source) {
|
|
1326
1376
|
return migrateLogin({ root: this.engine.root, prepare: () => this.prepare(), doctor: () => this.engine.doctor(120_000),
|
|
1327
1377
|
seal: this.o.authSeal, log: this.o.log, withStore: (task) => this.engine.withAuthStore(task) }, member, source);
|
|
@@ -1347,6 +1397,7 @@ export class OpenClawKit {
|
|
|
1347
1397
|
bridge: { register: (run, tools, runId) => this.bridge.register(run, tools && this.o.browser ? [...tools, 'request_sign_in'] : tools, runId) },
|
|
1348
1398
|
tools: new Set(this.toolNames()), delegations: this.delegations,
|
|
1349
1399
|
...(this.accountsCtl ? { accounts: this.accountsCtl } : {}),
|
|
1400
|
+
...(this.helpersCtl ? { helpers: this.helpersCtl } : {}),
|
|
1350
1401
|
locks: this.locks,
|
|
1351
1402
|
routes: this.routeMap,
|
|
1352
1403
|
});
|
|
@@ -1356,7 +1407,7 @@ export class OpenClawKit {
|
|
|
1356
1407
|
return (this.o.tools ?? []).map((t) => t.name);
|
|
1357
1408
|
}
|
|
1358
1409
|
async run(spec, on) {
|
|
1359
|
-
if (spec.delegates && this.o.gateBuiltins === false)
|
|
1410
|
+
if ((spec.delegates || spec.helperDelegates) && this.o.gateBuiltins === false)
|
|
1360
1411
|
throw new Error('delegation requires builtin gating');
|
|
1361
1412
|
if (this.o.browser) {
|
|
1362
1413
|
if (!await this.checkBrowserTools())
|
package/dist/runs.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Bridge } from './bridge.ts';
|
|
2
|
+
import { type createHelpers } from './helpers.ts';
|
|
2
3
|
import type { Delegations } from './delegations.ts';
|
|
3
4
|
import type { createAccounts } from './accounts.ts';
|
|
4
5
|
import type { createLocks } from './locks.ts';
|
|
@@ -40,6 +41,7 @@ export declare function createRuns(ctx: {
|
|
|
40
41
|
}>;
|
|
41
42
|
bridge: Pick<Bridge, 'register'>;
|
|
42
43
|
delegations?: Delegations;
|
|
44
|
+
helpers?: Pick<ReturnType<typeof createHelpers>, 'resolve'>;
|
|
43
45
|
tools: ReadonlySet<string>;
|
|
44
46
|
accounts?: Pick<ReturnType<typeof createAccounts>, 'bind' | 'keyRefusal' | 'pick' | 'rest'>;
|
|
45
47
|
locks?: ReturnType<typeof createLocks>;
|
package/dist/runs.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// Runs: one Gateway run per spec, streamed through typed events, ending in a classified RunEnd (5.8, O8).
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
4
|
+
import { HELPER_TAIL, HelperError } from './helpers.js';
|
|
4
5
|
import { authStatus } from './auth-status.js';
|
|
5
6
|
import { outputSchema } from './output.js';
|
|
6
7
|
import { words } from './words.js';
|
|
7
8
|
import { classify } from './classify.js';
|
|
8
9
|
import { providerOf } from './pick.js';
|
|
10
|
+
import { engineId } from './accounts.js';
|
|
9
11
|
import { catalogModelOf, clampThinking, levelsOf } from './thinking.js';
|
|
10
12
|
// The gateway gives up on a silent run after an hour; we wait ten minutes longer for the reply frame itself.
|
|
11
13
|
const WAIT_MS = 3_600_000;
|
|
@@ -163,26 +165,41 @@ export function createRuns(ctx) {
|
|
|
163
165
|
// of one of the member's account agents is the member's too; binding below checks the index names it.
|
|
164
166
|
if (!spec.sessionKey.startsWith(`agent:${spec.member}:`) && !(ctx.accounts && !keyAgent && spec.sessionKey.startsWith(`agent:${spec.member}--`)))
|
|
165
167
|
throw new Error(`refused: "${spec.sessionKey}" is not a session of member "${spec.member}"`);
|
|
166
|
-
|
|
168
|
+
const tail = spec.sessionKey.slice(spec.sessionKey.indexOf(':', 'agent:'.length) + 1);
|
|
169
|
+
// A helper (5.8.3) speaks only under `agent:<member>:h-<helper>:`, with its persona as the whole system text, and
|
|
170
|
+
// only a run naming that helper speaks there. Refused before any request.
|
|
171
|
+
if (spec.helper !== undefined && (!ctx.helpers || keyAgent || spec.system !== undefined))
|
|
172
|
+
throw new HelperError('invalid', `refused: helper ${String(spec.helper)} runs on its own account with its own persona`);
|
|
173
|
+
const helper = spec.helper === undefined ? undefined : ctx.helpers.resolve(spec.member, spec.helper);
|
|
174
|
+
if (helper ? !spec.sessionKey.startsWith(`agent:${spec.member}:h-${spec.helper}:`) : HELPER_TAIL.test(tail))
|
|
175
|
+
throw new HelperError('session', `refused: "${spec.sessionKey}" is not a session of ${helper ? `helper "${spec.helper}"` : 'this run'}`);
|
|
176
|
+
if (spec.delegates || spec.helperDelegates) {
|
|
167
177
|
if (spec.register === false || !ctx.delegations)
|
|
168
178
|
throw new Error('delegation requires a registered run');
|
|
169
|
-
ctx.delegations.validate(spec.delegates);
|
|
179
|
+
ctx.delegations.validate(spec.delegates ?? {}, spec.member, spec.helperDelegates);
|
|
170
180
|
}
|
|
171
181
|
// Same validation as the pin's NonEmptyString; preserve the caller's bytes (including whitespace).
|
|
172
182
|
if (spec.idempotencyKey !== undefined && (typeof spec.idempotencyKey !== 'string' || !spec.idempotencyKey.length))
|
|
173
183
|
throw new Error('refused: idempotencyKey must be a non-empty string');
|
|
174
184
|
const idempotencyKey = spec.idempotencyKey ?? randomUUID();
|
|
175
185
|
const output = preparedOutput ?? (spec.schema === undefined ? undefined : outputSchema(spec.schema));
|
|
176
|
-
const system = [spec.system, output?.prompt].filter(Boolean).join('\n\n');
|
|
177
|
-
|
|
186
|
+
const system = [helper?.persona ?? spec.system, output?.prompt].filter(Boolean).join('\n\n');
|
|
187
|
+
let catalogs;
|
|
188
|
+
const accountOf = async (model) => {
|
|
189
|
+
const picked = account(model);
|
|
190
|
+
catalogs ??= ctx.request('config.get', {}).then((answer) => answer.config?.models?.providers);
|
|
191
|
+
const providers = await catalogs;
|
|
192
|
+
return { provider: picked.provider, model: engineId(picked.provider, model, providers?.[picked.provider]?.models) };
|
|
193
|
+
};
|
|
194
|
+
const picked = spec.model === undefined ? undefined : await accountOf(spec.model);
|
|
178
195
|
for (const need of spec.needs ?? [])
|
|
179
|
-
|
|
196
|
+
await accountOf(need);
|
|
180
197
|
for (const tool of spec.tools ?? [])
|
|
181
198
|
if (!ctx.tools.has(tool))
|
|
182
199
|
throw new Error(`refused: "${tool}" is not one of this kit's tools`);
|
|
183
|
-
const tail = spec.sessionKey.slice(spec.sessionKey.indexOf(':', 'agent:'.length) + 1);
|
|
184
200
|
// Runs step 1 (5.15): every run first finds the account its session is bound to, the API-key lane included.
|
|
185
|
-
|
|
201
|
+
// A helper is bound to the agent whose sign-in it reads, as a conversation held there is.
|
|
202
|
+
const binding = helper?.binding ?? (ctx.accounts ? await ctx.accounts.bind(spec.member, spec.sessionKey) : undefined);
|
|
186
203
|
// Step 2 on the API-key lane: its own lane binding continues; any other binding is refused.
|
|
187
204
|
if (keyAgent && ctx.accounts && binding) {
|
|
188
205
|
const message = await ctx.accounts.keyRefusal(spec.member, binding);
|
|
@@ -191,7 +208,7 @@ export function createRuns(ctx) {
|
|
|
191
208
|
}
|
|
192
209
|
// A run that waited for its agent's lock across a move (5.15) binds again under it: a changed binding means its
|
|
193
210
|
// session went to another account, so it ends before any event and the caller's retry lands there.
|
|
194
|
-
const moved = async () => !!binding && (await ctx.accounts.bind(spec.member, spec.sessionKey))?.agentId !== binding.agentId;
|
|
211
|
+
const moved = async () => !helper && !!binding && (await ctx.accounts.bind(spec.member, spec.sessionKey))?.agentId !== binding.agentId;
|
|
195
212
|
const movedEnd = { ok: false, kind: 'other', message: words('run.moved') };
|
|
196
213
|
// Step 3: with `account` and `needs` both absent, on a key no account agent holds (or an API-key run, which binds only
|
|
197
214
|
// to its lane), exactly the run on the agent it always was (a bound key under its shared lock).
|
|
@@ -253,7 +270,7 @@ export function createRuns(ctx) {
|
|
|
253
270
|
if (!ready)
|
|
254
271
|
return { ok: false, kind: 'signed-out', message: `${pick.account.name} is not signed in` };
|
|
255
272
|
return await start(spec, on, { agentId: agent, sessionKey, idempotencyKey, system,
|
|
256
|
-
picked: pick.model ?
|
|
273
|
+
picked: pick.model ? await accountOf(pick.model) : undefined, output, chosen: pick }); // '' = no override
|
|
257
274
|
}
|
|
258
275
|
finally {
|
|
259
276
|
if (ctx.routes.get(spec.sessionKey) === route)
|
|
@@ -325,8 +342,10 @@ export function createRuns(ctx) {
|
|
|
325
342
|
}
|
|
326
343
|
});
|
|
327
344
|
try {
|
|
328
|
-
|
|
329
|
-
|
|
345
|
+
const ref = { sessionKey, member: spec.member, ...(spec.helper !== undefined ? { helper: spec.helper } : {}) };
|
|
346
|
+
releaseDelegation = spec.delegates || spec.helperDelegates
|
|
347
|
+
? ctx.delegations.register(ref, idempotencyKey, spec.tools, spec.delegates ?? {}, spec.sessionKey, spec.helperDelegates) : undefined;
|
|
348
|
+
release = spec.register !== false ? ctx.bridge.register(ref, spec.tools, idempotencyKey) : undefined;
|
|
330
349
|
if (picked && !chosen) {
|
|
331
350
|
// The picked account must be the one called: check it is signed in for this member before the run, since
|
|
332
351
|
// the engine would otherwise fail the run only after admitting it. An explicit provider/model is strict on
|
package/dist/signin.js
CHANGED
|
@@ -4,7 +4,6 @@ import { randomUUID } from 'node:crypto';
|
|
|
4
4
|
import { createServer } from 'node:http';
|
|
5
5
|
import { activate, LATE_RESTART } from './activation.js';
|
|
6
6
|
import { authStatus } from './auth-status.js';
|
|
7
|
-
import { LIVE_CATALOGS } from './constants.js';
|
|
8
7
|
import { routes } from './routes.js';
|
|
9
8
|
import { signedInProviders } from './runs.js';
|
|
10
9
|
import { words } from './words.js';
|
|
@@ -301,13 +300,13 @@ export function signIn(ctx, member, o, on) {
|
|
|
301
300
|
if (!apiKey)
|
|
302
301
|
return { state: 'failed', via, why: signal.aborted ? 'declined' : 'expired' };
|
|
303
302
|
secret = apiKey.trim() || undefined;
|
|
304
|
-
// A
|
|
303
|
+
// A Go key can act for several workspaces, and the Go gateway refuses every call until one is
|
|
305
304
|
// named (403 Workspace selection is required): the engine carries the workspace only from its provider
|
|
306
305
|
// config headers, so resolve it here the way the kit's key lane does, and write the header before the
|
|
307
306
|
// activation so the activation probe itself carries it. One workspace is used unasked; several ask here,
|
|
308
307
|
// answered through the same paste (an id or an exact name); a key the console refuses (a workspace-bound
|
|
309
308
|
// service key) needs none; a listing that cannot be read fails the sign-in before anything changes.
|
|
310
|
-
if (secret && keyRoute
|
|
309
|
+
if (secret && keyRoute?.provider === 'opencode-go' && ctx.setProviderHeaders && ctx.listWorkspaces) {
|
|
311
310
|
let workspaces;
|
|
312
311
|
try {
|
|
313
312
|
workspaces = await ctx.listWorkspaces(secret, signal);
|