@byokit/openclaw 0.14.0 → 0.14.4

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/dist/kit.js CHANGED
@@ -1,31 +1,33 @@
1
+ import { openCodeWorkspaces } from '@byokit/accounts/opencode-go';
1
2
  import { readFileSync } from 'node:fs';
2
3
  import { join } from 'node:path';
3
4
  import { setTimeout as delay } from 'node:timers/promises';
4
5
  import { isDeepStrictEqual } from 'node:util';
5
6
  import { fileURLToPath } from 'node:url';
6
- import { PROTOCOL_VERSION, LIVE_CATALOGS } from "./constants.js";
7
- import { atomic } from "./engine-patches.js";
8
- import { Approvals } from "./approvals.js";
9
- import { Bridge } from "./bridge.js";
10
- import { Engine, refusedOnly } from "./engine.js";
11
- import { AuthStoreSealSizeError } from "./auth-store.js";
12
- import { gatewayTransport } from "./transport.js";
13
- import { createMembers } from "./members.js";
14
- import { createAccounts, memberOfAccount } from "./accounts.js";
15
- import { createLocks } from "./locks.js";
16
- import { keyAgentId, KEY_PREFIX, MEMBER_ID } from "./members.js";
17
- import { confirmRetainedLogin as confirmLogin, migrateRetainedLogin as migrateLogin } from "./migrate.js";
18
- import { createRuns, signedInProviders } from "./runs.js";
19
- import { authStatus, createAuthStatus } from "./auth-status.js";
20
- import { outputSchema } from "./output.js";
21
- import { createKeys } from "./keys.js";
22
- import { routes as routeTable } from "./routes.js";
23
- import { providerOf } from "./pick.js";
24
- import { signIn as startSignIn, signOut as engineSignOut } from "./signin.js";
25
- import { reconcileConfig, browserToolPolicySafe, browserProfileAcknowledged, browserSessionMayRun, memoryLimited as configMemoryLimited, configKeyPath, readConfigKey, writeConfigKey } from "./config.js";
26
- import { createBrowserHost } from "./browser/host.js";
27
- import { fileSignInStore } from "./browser/store.js";
28
- import { launchBroker } from "./browser/broker.js";
7
+ import { PROTOCOL_VERSION, LIVE_CATALOGS } from './constants.js';
8
+ import { atomic } from './engine-patches.js';
9
+ import { Approvals } from './approvals.js';
10
+ import { Bridge } from './bridge.js';
11
+ import { Engine, canonicalStateDir, refusedOnly } from './engine.js';
12
+ import { AuthStoreSealSizeError } from './auth-store.js';
13
+ import { gatewayTransport } from './transport.js';
14
+ import { createMembers } from './members.js';
15
+ import { createAccounts, memberOfAccount } from './accounts.js';
16
+ import { createLocks } from './locks.js';
17
+ import { keyAgentId, KEY_PREFIX, MEMBER_ID } from './members.js';
18
+ import { confirmRetainedLogin as confirmLogin, migrateRetainedLogin as migrateLogin } from './migrate.js';
19
+ import { createRuns, signedInProviders } from './runs.js';
20
+ import { authStatus, createAuthStatus } from './auth-status.js';
21
+ import { outputSchema } from './output.js';
22
+ import { createKeys } from './keys.js';
23
+ import { routes as routeTable } from './routes.js';
24
+ import { installedPlugins, pinnedSpec, recordPlugin, PluginInstallError } from './plugins.js';
25
+ import { providerOf } from './pick.js';
26
+ import { signIn as startSignIn, signOut as engineSignOut } from './signin.js';
27
+ import { reconcileConfig, browserToolPolicySafe, browserProfileAcknowledged, browserSessionMayRun, memoryLimited as configMemoryLimited, configKeyPath, readConfigKey, writeConfigKey } from './config.js';
28
+ import { createBrowserHost } from './browser/host.js';
29
+ import { fileSignInStore } from './browser/store.js';
30
+ import { launchBroker } from './browser/broker.js';
29
31
  /** The engine's learning-mode config key (5.6 "Engine learning state"). */
30
32
  const LEARNING_KEY = 'skills.workshop.autonomous.mode';
31
33
  const RESTART_MS = 60_000;
@@ -436,6 +438,7 @@ export class OpenClawKit {
436
438
  members;
437
439
  locks = createLocks();
438
440
  routeMap = new Map();
441
+ installs = Promise.resolve();
439
442
  accountsCtl;
440
443
  // One bridge and one approval surface per kit: listeners and run registrations survive reconnects (B3).
441
444
  // Only the socket itself is per-connection. Approvals always reaches the live transport through this.request().
@@ -651,7 +654,7 @@ export class OpenClawKit {
651
654
  || (o.browser && ['browser', 'request_sign_in', 'exec', 'process', 'code_execution', 'terminal', 'read', 'write', 'edit', 'apply_patch', 'gateway'].includes(t.name)))
652
655
  throw new Error(`invalid tool name: ${t.name}`);
653
656
  }
654
- this.o = o;
657
+ this.o = { ...o, stateDir: canonicalStateDir(o.stateDir) };
655
658
  this.engine = new Engine({ ...o, pluginId: o.plugin?.id ?? 'byokit', tools: [...o.tools ?? [], ...(o.browser ? [{
656
659
  name: 'request_sign_in', description: 'Ask the person to sign in to the current browser site.',
657
660
  parameters: { type: 'object', additionalProperties: false, properties: {
@@ -668,10 +671,12 @@ export class OpenClawKit {
668
671
  dropLane: (member, provider) => this.dropKeyLane(member, provider),
669
672
  backfillLane: (member, model) => this.backfillKeyLane(member, model),
670
673
  restarting: () => this.engineRestarting(),
674
+ routes: () => this.routes(),
671
675
  request: (method, params, options) => this.request()(method, params, options),
672
676
  ensure: (member) => this.ensureMember(member),
673
677
  authConfig: () => this.authSnapshot(),
674
- restarted: (authBefore, signal) => this.restarted(authBefore, signal) });
678
+ restarted: (authBefore, signal) => this.restarted(authBefore, signal),
679
+ setProviderHeaders: (provider, headers) => this.patchConfig({ models: { providers: { [provider]: { headers: headers ?? null } } } }) });
675
680
  const slot = {};
676
681
  this.approvalsCtl = new Approvals({
677
682
  request: (method, params, co) => this.request()(method, params, co),
@@ -960,10 +965,69 @@ export class OpenClawKit {
960
965
  onDisconnect: (fn) => { this.signInDisconnects.add(fn); return () => { this.signInDisconnects.delete(fn); }; },
961
966
  stopSignal: this.engine.stopSignal,
962
967
  authConfig: () => this.authSnapshot(),
963
- restarted: (authBefore, signal) => this.restarted(authBefore, signal) };
968
+ restarted: (authBefore, signal) => this.restarted(authBefore, signal),
969
+ setProviderHeaders: (provider, headers) => this.patchConfig({ models: { providers: { [provider]: { headers: headers ?? null } } } }),
970
+ listWorkspaces: (key, signal) => this.o.listWorkspaces
971
+ ? this.o.listWorkspaces(key, signal)
972
+ : openCodeWorkspaces(key, (input, init) => fetch(input, { ...init, signal: init?.signal ? AbortSignal.any([init.signal, signal]) : signal })) };
964
973
  }
965
974
  routes() {
966
- return routeTable();
975
+ return routeTable({ plugins: installedPlugins(this.engine.root) });
976
+ }
977
+ /** B7 (5.7): install one external provider plugin a `needs_plugin` route names into this kit's own engine, at its
978
+ * pin; resolves once the restarted gateway lists it enabled at that version. Discovery never installs. Serialized:
979
+ * concurrent installs would race the allow-list update and double-spawn the installer into one engine tree. */
980
+ installPlugin(pluginId) {
981
+ const run = this.installs.then(() => this.runInstall(pluginId));
982
+ this.installs = run.catch(() => { });
983
+ return run;
984
+ }
985
+ async runInstall(pluginId) {
986
+ const pin = pinnedSpec(pluginId);
987
+ const version = pin.spec.slice(pin.spec.lastIndexOf('@') + 1);
988
+ const loaded = async () => {
989
+ const { plugins } = await this.request()('plugins.list', {});
990
+ return plugins.some((p) => p.id === pluginId && p.enabled && p.version === version);
991
+ };
992
+ if (!(await loaded())) {
993
+ // The installer enables a plugin only when the (restrictive once non-empty) allow list already names it.
994
+ const allow = (this.getConfigKey('plugins.allow') ?? []);
995
+ const added = !allow.includes(pluginId);
996
+ if (added)
997
+ this.setConfigKey('plugins.allow', [...allow, pluginId]);
998
+ let code;
999
+ let output;
1000
+ try {
1001
+ ({ code, output } = await this.engine.installPlugin(pin.spec));
1002
+ }
1003
+ catch (error) {
1004
+ if (added)
1005
+ this.setConfigKey('plugins.allow', allow);
1006
+ throw error;
1007
+ }
1008
+ if (code !== 0) {
1009
+ if (added)
1010
+ this.setConfigKey('plugins.allow', allow);
1011
+ if (output.includes('pinned plugin integrity mismatch'))
1012
+ throw new PluginInstallError('integrity', `${pin.spec} does not match its pinned integrity; nothing was installed.`);
1013
+ if (output.includes('not a pinned plugin version'))
1014
+ throw new PluginInstallError('version', `The registry does not serve ${pin.spec} as pinned; nothing was installed.`);
1015
+ const reason = output.trim().split('\n').filter((line) => line.trim()).slice(-3).join(' ');
1016
+ throw new PluginInstallError('install', `${pin.spec} did not install: ${reason || `the installer exited ${code}`}`);
1017
+ }
1018
+ }
1019
+ // The running gateway hot-reloads the installer's config write without loading the plugin (verified live on
1020
+ // 2026.8.35); a safe restart (after in-flight requests) loads it, and the kit reconnects. Reads fail while it is down.
1021
+ if (!(await loaded()))
1022
+ await this.request()('gateway.restart.request', { reason: `byokit installed plugin ${pluginId}` });
1023
+ const deadline = Date.now() + RESTART_MS;
1024
+ while (!(await loaded().catch(() => false))) {
1025
+ if (Date.now() >= deadline)
1026
+ throw new PluginInstallError('install', `${pin.spec} installed, but the engine did not load it.`);
1027
+ await delay(500);
1028
+ }
1029
+ // Readiness (routes(), the keys gates) tracks this record, so it names the plugin only once the engine lists it loaded.
1030
+ recordPlugin(this.engine.root, pluginId);
967
1031
  }
968
1032
  async providerStatus(member) {
969
1033
  const { agentId } = await this.ensureMember(member);
@@ -1068,7 +1132,10 @@ export class OpenClawKit {
1068
1132
  return Promise.reject(new Error('gateway not ready'));
1069
1133
  return this.accountsCtl.setDefaults(member, d);
1070
1134
  }
1071
- /** Save an explicitly supplied API key (billed per use) in this member's separate key agent. */
1135
+ /** Save an explicitly supplied API key (billed per use) in this member's separate key agent.
1136
+ * `orgId` names the workspace a Go plan login acts for; it rides every probe and run as `x-opencode-org-id`,
1137
+ * and replacing the key rewrites it. Without it a Go key's workspaces are listed: one is used, several resolve
1138
+ * `needs_workspace` with the rows for the app's pick (add again with `orgId`). */
1072
1139
  addKey(agentId, o) {
1073
1140
  return this.keys.add(agentId, o);
1074
1141
  }
@@ -1095,7 +1162,7 @@ export class OpenClawKit {
1095
1162
  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
1163
  }
1097
1164
  async backfillKeyLane(member, model) {
1098
- const found = model === undefined ? [] : routeTable().filter((r) => r.keyEntry && r.readiness === 'ready' && r.provider === providerOf(model));
1165
+ const found = model === undefined ? [] : this.routes().filter((r) => r.keyEntry && r.readiness === 'ready' && r.provider === providerOf(model));
1099
1166
  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
1167
  }
1101
1168
  async prepareKeyOwner(agentId) {
@@ -1137,7 +1204,7 @@ export class OpenClawKit {
1137
1204
  return confirmLogin({ ...this.signInCtx(), seal: this.o.authSeal, log: this.o.log }, member, source);
1138
1205
  }
1139
1206
  // runs (5.8): the facade owns no run state; every call delegates to the O8 module
1140
- // with this kit's live transport, member table and bridge (firstmate clearance, O9).
1207
+ // with this kit's live transport, member table and bridge (spec item O9).
1141
1208
  runs() {
1142
1209
  return createRuns({
1143
1210
  request: (method, params, o) => this.request()(method, params, o),
@@ -1230,7 +1297,8 @@ export class OpenClawKit {
1230
1297
  const safe = reconcileConfig(response.config, { root: this.engine.root, stateDir: this.o.stateDir,
1231
1298
  port: Number(readFileSync(join(this.engine.root, 'port'), 'utf8')), pluginId: this.o.plugin?.id ?? 'byokit',
1232
1299
  pluginDir: join(this.engine.root, 'plugin'), policyPath: fileURLToPath(new URL('../policy/policy.mjs', import.meta.url)), app: patch,
1233
- offered: this.o.offered, installPolicy: this.o.installPolicy, ...(this.o.browser ? { browser: this.browserConfig() } : {}) });
1300
+ offered: this.o.offered, installPolicy: this.o.installPolicy, plugins: installedPlugins(this.engine.root),
1301
+ ...(this.o.browser ? { browser: this.browserConfig() } : {}) });
1234
1302
  if (this.o.browser && !browserToolPolicySafe(safe, this.toolNames()))
1235
1303
  throw new Error('browser tool policy refused');
1236
1304
  if (this.o.browser) {
package/dist/link.js CHANGED
@@ -7,11 +7,11 @@ import { createServer } from 'node:http';
7
7
  import { WebSocketServer } from 'ws';
8
8
  import { PublicLinkError, } from '@byokit/pair';
9
9
  import { reach } from '@byokit/discover';
10
- import { ENGINE_VERSION } from "./constants.js";
11
- import { b64urlDecode, b64urlEncode, sealNotice } from "./notices.js";
12
- import { routeFor } from "./routes.js";
13
- import { outputSchema } from "./output.js";
14
- import { stateWords, words } from "./words.js";
10
+ import { ENGINE_VERSION } from './constants.js';
11
+ import { b64urlDecode, b64urlEncode, sealNotice } from './notices.js';
12
+ import { routeFor } from './routes.js';
13
+ import { outputSchema } from './output.js';
14
+ import { stateWords, words } from './words.js';
15
15
  const VIEW_OPS = new Set(['oc.state', 'oc.routes', 'oc.signin.view', 'oc.sessions', 'oc.approvals', 'oc.events',
16
16
  'oc.browser.state', 'oc.browser.signins', 'oc.browser.thumb']);
17
17
  // This kit's own version, read once from its package.json (beside src/ and dist/ alike).
package/dist/members.d.ts CHANGED
@@ -6,6 +6,9 @@ export declare const BASE_AGENT = "byokit-base";
6
6
  export declare const keyAgentId: (member: Member) => string;
7
7
  /** Resolves once agents.list shows `id`, which agents.create answers before. */
8
8
  export declare function untilListed(request: GatewayTransport['request'], id: string): Promise<void>;
9
+ /** Resolves once agents.list no longer shows `id`, which agents.delete answers before: the engine's roster
10
+ * hot-reload applies a beat after the delete answer, the same lag as after agents.create. */
11
+ export declare function untilUnlisted(request: GatewayTransport['request'], id: string): Promise<void>;
9
12
  export declare function createMembers(ctx: {
10
13
  request: GatewayTransport['request'];
11
14
  root: string;
package/dist/members.js CHANGED
@@ -23,6 +23,16 @@ export async function untilListed(request, id) {
23
23
  await new Promise((resolve) => setTimeout(resolve, VISIBLE_POLL_MS));
24
24
  }
25
25
  }
26
+ /** Resolves once agents.list no longer shows `id`, which agents.delete answers before: the engine's roster
27
+ * hot-reload applies a beat after the delete answer, the same lag as after agents.create. */
28
+ export async function untilUnlisted(request, id) {
29
+ const deadline = Date.now() + VISIBLE_TIMEOUT_MS;
30
+ while (await listed(request, id)) {
31
+ if (Date.now() >= deadline)
32
+ throw new Error(`agent ${id} is still listed by agents.list after agents.delete`);
33
+ await new Promise((resolve) => setTimeout(resolve, VISIBLE_POLL_MS));
34
+ }
35
+ }
26
36
  export function createMembers(ctx) {
27
37
  const cache = new Map();
28
38
  return { ensure(member) {
package/dist/migrate.js CHANGED
@@ -2,9 +2,9 @@
2
2
  // only when the Gateway itself reports every provider signed in.
3
3
  import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
4
4
  import { join } from 'node:path';
5
- import { retireArchive } from "./auth-store.js";
6
- import { MEMBER_ID } from "./members.js";
7
- import { providers } from "./signin.js";
5
+ import { retireArchive } from './auth-store.js';
6
+ import { MEMBER_ID } from './members.js';
7
+ import { providers } from './signin.js';
8
8
  const MOVED = '.moved-to-engine';
9
9
  const MARKER = '.canonicalized';
10
10
  const TRY_MS = 1_500;
package/dist/pick.js CHANGED
@@ -1,4 +1,4 @@
1
- import { words } from "./words.js";
1
+ import { words } from './words.js';
2
2
  const DAY_MS = 24 * 60 * 60 * 1000;
3
3
  const finite = (n) => typeof n === 'number' && Number.isFinite(n);
4
4
  /** The lowercased provider of a well-formed `provider/model` reference; undefined when the reference is malformed. */
@@ -0,0 +1,17 @@
1
+ export type PluginInstallFailure = 'unknown_plugin' | 'version' | 'integrity' | 'install';
2
+ export declare class PluginInstallError extends Error {
3
+ readonly code: PluginInstallFailure;
4
+ constructor(code: PluginInstallFailure, message: string);
5
+ }
6
+ type Pin = {
7
+ spec: string;
8
+ integrity: string;
9
+ };
10
+ /** engine/plugins.json: `{ "<plugin id>": { spec: '<package>@<engine version>', integrity } }`. */
11
+ export declare const pluginPins: () => Record<string, Pin>;
12
+ /** The exact spec to install for a plugin a route needs: its `install.npmSpec` at the engine pin, and pinned so. */
13
+ export declare function pinnedSpec(pluginId: string): Pin;
14
+ /** The external plugins this kit installed into its own engine. */
15
+ export declare function installedPlugins(root: string): string[];
16
+ export declare function recordPlugin(root: string, pluginId: string): void;
17
+ export {};
@@ -0,0 +1,40 @@
1
+ // External provider plugins (5.7, B7): the pins the kit installs at, and the ids it installed. Installing is the
2
+ // kit's explicit call; discovery only reads the record.
3
+ import { existsSync, readFileSync } from 'node:fs';
4
+ import { dirname, join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { atomic } from './engine-patches.js';
7
+ import { ENGINE_VERSION } from './constants.js';
8
+ import { routes } from './routes.js';
9
+ export class PluginInstallError extends Error {
10
+ code;
11
+ constructor(code, message) {
12
+ super(message);
13
+ this.name = 'PluginInstallError';
14
+ this.code = code;
15
+ }
16
+ }
17
+ const kitDir = dirname(dirname(fileURLToPath(import.meta.url)));
18
+ /** engine/plugins.json: `{ "<plugin id>": { spec: '<package>@<engine version>', integrity } }`. */
19
+ export const pluginPins = () => JSON.parse(readFileSync(join(kitDir, 'engine', 'plugins.json'), 'utf8'));
20
+ /** The exact spec to install for a plugin a route needs: its `install.npmSpec` at the engine pin, and pinned so. */
21
+ export function pinnedSpec(pluginId) {
22
+ const npmSpec = routes().find(route => route.needs?.plugin === pluginId && route.install?.npmSpec)?.install?.npmSpec;
23
+ if (!npmSpec)
24
+ throw new PluginInstallError('unknown_plugin', `No route needs a plugin named ${pluginId}.`);
25
+ const pin = pluginPins()[pluginId];
26
+ if (pin?.spec !== `${npmSpec}@${ENGINE_VERSION}`)
27
+ throw new PluginInstallError('version', `${pluginId} has no pin for engine ${ENGINE_VERSION}.`);
28
+ return pin;
29
+ }
30
+ const recordOf = (root) => join(root, 'installed-plugins.json');
31
+ /** The external plugins this kit installed into its own engine. */
32
+ export function installedPlugins(root) {
33
+ const path = recordOf(root);
34
+ return existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : [];
35
+ }
36
+ export function recordPlugin(root, pluginId) {
37
+ const installed = installedPlugins(root);
38
+ if (!installed.includes(pluginId))
39
+ atomic(recordOf(root), JSON.stringify([...installed, pluginId]) + '\n');
40
+ }