@byokit/openclaw 0.3.5 → 0.4.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.
@@ -7,6 +7,7 @@ import { readFileSync } from 'node:fs';
7
7
  import { dirname, join } from 'node:path';
8
8
  import { ENGINE_VERSION, PROTOCOL_VERSION } from "../constants.js";
9
9
  import { toolCalls } from "./model-stub.js";
10
+ import { routes } from "../routes.js";
10
11
  // The device-code script from Crewhouse's openclaw-wizard.test.ts, ported: the step, then progress, then done.
11
12
  const DEVICE_STEP = {
12
13
  id: 'step-device', type: 'note', executor: 'client',
@@ -98,6 +99,7 @@ export function fakeGateway(script) {
98
99
  const failures = new Map();
99
100
  const handlers = new Map();
100
101
  const agents = new Map();
102
+ const keys = new Map();
101
103
  const wizards = new Map();
102
104
  const runs = new Map();
103
105
  const configBox = { hash: 'hash-1', config: {} };
@@ -220,8 +222,37 @@ export function fakeGateway(script) {
220
222
  const agent = agents.get(String(p?.agentId ?? 'main'));
221
223
  if (agent)
222
224
  agent.providers = agent.providers.filter((provider) => provider !== p.provider);
225
+ if (keys.get(String(p.agentId))?.provider === p.provider)
226
+ keys.delete(String(p.agentId));
223
227
  return {};
224
228
  },
229
+ 'byokit.keys': (p) => {
230
+ const id = `byokit-key-${p.member}`;
231
+ if (p.action === 'prepare') {
232
+ keys.delete(id);
233
+ ensureAgent(id).providers = [];
234
+ return { ok: true };
235
+ }
236
+ const key = keys.get(id);
237
+ if (!key)
238
+ return { ok: false };
239
+ if (p.action === 'seal') {
240
+ key.sealed = true;
241
+ return { ok: true };
242
+ }
243
+ return { ok: key.sealed, ...(key.sealed ? { model: key.model } : {}) };
244
+ },
245
+ 'openclaw.setup.activate': (p) => {
246
+ if (p.kind !== 'api-key')
247
+ throw new Error('only the API-key setup path is simulated');
248
+ const route = routes().find((r) => r.choice === p.authChoice && r.keyEntry);
249
+ if (!route)
250
+ return { ok: false, status: 'unavailable' };
251
+ const model = `${route.provider}/fake-key-model`;
252
+ keys.set(p.agentId, { provider: route.provider, key: p.apiKey, model, sealed: false });
253
+ signInAgent(p.agentId, route.provider);
254
+ return { ok: true, status: 'ok', modelRef: model };
255
+ },
225
256
  'openclaw.setup.auth.start': (p) => {
226
257
  const sessionId = String(p.sessionId ?? `fake-${randomUUID()}`);
227
258
  wizards.set(sessionId, { agentId: String(p.agentId ?? 'main'), answered: false, pulls: 0 });
@@ -338,7 +369,8 @@ export function fakeGateway(script) {
338
369
  for (const [method, handler] of Object.entries(script ?? {}))
339
370
  handlers.set(method, handler);
340
371
  const dispatch = async (method, params, bridgeSock) => {
341
- calls.push({ method, params });
372
+ calls.push({ method, params: method === 'openclaw.setup.activate' && isPlainObject(params)
373
+ ? { ...params, ...(params.apiKey === undefined ? {} : { apiKey: '[redacted]' }) } : params });
342
374
  const queue = failures.get(method);
343
375
  if (queue?.length)
344
376
  throw new Error(queue.shift());
package/dist/types.d.ts CHANGED
@@ -34,6 +34,8 @@ export interface RunSpec extends RunRef {
34
34
  }[];
35
35
  thinking?: 'off' | 'low' | 'medium' | 'high';
36
36
  model?: string;
37
+ /** Explicit paid key choice. Uses a separate member-owned agent and separate session history. */
38
+ auth?: 'apiKey';
37
39
  tools?: string[];
38
40
  register?: boolean;
39
41
  }
@@ -139,6 +141,14 @@ export type Route = {
139
141
  offer: boolean;
140
142
  reason: string;
141
143
  source: string;
144
+ revision: string;
145
+ checked: string;
146
+ label: string;
147
+ keyEntry: boolean;
148
+ keyErrors: {
149
+ invalid: 'key.invalid';
150
+ not_included: 'key.notIncluded';
151
+ } | null;
142
152
  };
143
153
  export interface GatewayTransport {
144
154
  start(): Promise<Hello>;
package/dist/words.json CHANGED
@@ -19,5 +19,12 @@
19
19
  "approval.ask": "{helper} wants to {summary}. Allow it?",
20
20
  "approval.expired": "Nobody answered in time, so this wasn't allowed.",
21
21
  "approval.notice": "Something is waiting for your yes.",
22
- "link.notAllowed": "This device can't do that. Ask the person at the computer."
22
+ "link.notAllowed": "This device can't do that. Ask the person at the computer.",
23
+ "key.label": "API key (billed per use)",
24
+ "key.entry": "Paste your API key. You pay the provider for each use.",
25
+ "key.checking": "Checking your key…",
26
+ "key.ok": "Your key is ready. Use it only when you choose this option.",
27
+ "key.invalid": "This key could not be saved. Check it and try again.",
28
+ "key.notIncluded": "This option is not available here.",
29
+ "key.missing": "Add an API key to use this option."
23
30
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byokit/openclaw",
3
- "version": "0.3.5",
3
+ "version": "0.4.0",
4
4
  "description": "The OpenClaw runtime kit: the pinned engine's full operator surface with plain-words helpers for members, sign-in, runs and approvals.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -52,9 +52,9 @@
52
52
  "prepack": "tsc -b && node ../../scripts/fix-words-dts.cjs"
53
53
  },
54
54
  "dependencies": {
55
- "@byokit/link": "0.6.0",
56
- "@byokit/relay": "0.4.2",
57
- "@byokit/reach": "0.4.0",
55
+ "@byokit/link": "0.7.0",
56
+ "@byokit/relay": "0.5.0",
57
+ "@byokit/reach": "0.6.0",
58
58
  "@byokit/seal": "0.2.0",
59
59
  "@byokit/secrets": "0.5.0",
60
60
  "@openclaw/gateway-client": "2026.8.1",
@@ -62,7 +62,7 @@
62
62
  "ws": "8.21.3"
63
63
  },
64
64
  "devDependencies": {
65
- "@byokit/ui-core": "0.4.0"
65
+ "@byokit/ui-core": "0.6.0"
66
66
  },
67
67
  "publishConfig": {
68
68
  "access": "public"
package/plugin/index.js CHANGED
@@ -3,6 +3,7 @@
3
3
  // schemas and descriptions stay app data. No imports beyond node builtins: nothing here needs resolving in-gateway.
4
4
  import { connect } from 'node:net';
5
5
  import { readFileSync } from 'node:fs';
6
+ import { registerKeys } from './keys.js';
6
7
 
7
8
  /** TypeBox's Kind is a global symbol, so these schemas are valid TypeBox without importing typebox. */
8
9
  const Kind = Symbol.for('TypeBox.Kind');
@@ -114,6 +115,7 @@ export default {
114
115
  jsonSchema: { type: 'object', additionalProperties: false, properties: {} },
115
116
  },
116
117
  register(api) {
118
+ if (api.registerGatewayMethod) registerKeys(api);
117
119
  for (const spec of table.tools) {
118
120
  api.registerTool({
119
121
  name: spec.name,
package/plugin/keys.js ADDED
@@ -0,0 +1,101 @@
1
+ // Agent-local key isolation. The engine owns the SQLite transaction and publishes the changed runtime snapshot.
2
+ // No key value crosses this method. Only the typed setup.activate request ever carries a secret.
3
+ export function registerKeys(api) {
4
+ api.registerGatewayMethod('byokit.keys', async ({ params, respond }) => {
5
+ const member = params?.member;
6
+ if (typeof member !== 'string' || !/^[a-z][a-z0-9-]{0,31}$/.test(member) || member.startsWith('byokit-key-')
7
+ || !['prepare', 'seal', 'ready'].includes(params.action)) {
8
+ respond(true, { ok: false });
9
+ return;
10
+ }
11
+ const diagnostics = params.action === 'ready' && params.diagnostics === true ? { stage: 'imports' } : undefined;
12
+ const reply = (value) => respond(true, { ...value, ...(diagnostics ? { diagnostics } : {}) });
13
+ try {
14
+ const { readFile, writeFile, rm } = await import('node:fs/promises');
15
+ const { join } = await import('node:path');
16
+ const { resolveAgentDir } = await import('openclaw/plugin-sdk/agent-runtime');
17
+ const { updateAuthProfileStoreWithLock } = await import('openclaw/plugin-sdk/provider-auth');
18
+ // Read the current config: activation changes the roster and selection during the same gateway lifetime.
19
+ if (diagnostics) diagnostics.stage = 'config';
20
+ const config = JSON.parse(await readFile(process.env.OPENCLAW_CONFIG_PATH, 'utf8'));
21
+ const keyId = `byokit-key-${member}`;
22
+ if (!config.agents?.entries?.[member] || !config.agents?.entries?.[keyId]) {
23
+ reply({ ok: false });
24
+ return;
25
+ }
26
+ const keyDir = resolveAgentDir(config, keyId);
27
+ const marker = join(keyDir, 'byokit-key-ready.json');
28
+ if (params.action === 'ready') {
29
+ if (diagnostics) diagnostics.stage = 'marker';
30
+ const selected = JSON.parse(await readFile(marker, 'utf8'));
31
+ let ready = false;
32
+ if (diagnostics) diagnostics.stage = 'store';
33
+ await updateAuthProfileStoreWithLock({ agentDir: keyDir, updater(store) {
34
+ if (diagnostics) {
35
+ diagnostics.profiles = Object.entries(store.profiles).map(([id, p]) => ({ id, apiKey: p.type === 'api_key', local: p.copyToAgents === false }));
36
+ diagnostics.order = store.order;
37
+ diagnostics.selected = selected.profileId;
38
+ diagnostics.stage = 'checked';
39
+ }
40
+ const profile = store.profiles[selected.profileId];
41
+ ready = profile?.type === 'api_key' && profile.copyToAgents === false
42
+ && store.order?.[profile.provider]?.length === 1 && store.order[profile.provider][0] === selected.profileId;
43
+ return false;
44
+ } });
45
+ reply({ ok: ready, ...(ready ? { model: selected.model } : {}) });
46
+ return;
47
+ }
48
+ await rm(marker, { force: true });
49
+ if (params.action === 'prepare') {
50
+ // Only local profiles are loaded by this transaction: no shared-store read-through or CLI sync.
51
+ const normal = await updateAuthProfileStoreWithLock({
52
+ agentDir: resolveAgentDir(config, member),
53
+ saveOptions: { filterExternalAuthProfiles: false, syncExternalCli: false },
54
+ updater(store) {
55
+ if (Object.values(store.profiles).some((p) => p.type === 'api_key'))
56
+ throw new Error('The normal agent already has a key.');
57
+ const providers = new Set([params.provider, ...Object.values(store.profiles).map((p) => p.provider)].filter((p) => typeof p === 'string'));
58
+ store.order ??= {};
59
+ for (const provider of providers) store.order[provider] = Object.entries(store.profiles)
60
+ .filter(([, p]) => p.provider === provider && p.type === 'oauth').map(([id]) => id);
61
+ return true;
62
+ },
63
+ });
64
+ if (!normal) { respond(true, { ok: false }); return; }
65
+ // A replacement starts empty. During activation this agent is unavailable to kit.run.
66
+ const cleared = await updateAuthProfileStoreWithLock({ agentDir: keyDir,
67
+ saveOptions: { filterExternalAuthProfiles: false, syncExternalCli: false },
68
+ updater(store) { store.profiles = {}; store.order = {}; return true; } });
69
+ respond(true, { ok: Boolean(cleared) });
70
+ return;
71
+ }
72
+ const ref = config.agents.entries[keyId].model;
73
+ const primary = typeof ref === 'string' ? ref : ref?.primary;
74
+ const at = typeof primary === 'string' ? primary.lastIndexOf('@') : -1;
75
+ if (at < 1) { respond(true, { ok: false }); return; }
76
+ const profileId = primary.slice(at + 1);
77
+ const model = primary.slice(0, at);
78
+ let found = false;
79
+ const sealed = await updateAuthProfileStoreWithLock({ agentDir: keyDir,
80
+ saveOptions: { filterExternalAuthProfiles: false, syncExternalCli: false },
81
+ updater(store) {
82
+ const profile = store.profiles[profileId];
83
+ if (profile?.type !== 'api_key') return false;
84
+ found = true;
85
+ profile.copyToAgents = false;
86
+ store.profiles = { [profileId]: profile };
87
+ // A stored agent-local order wins over global config and contains exactly this key, never a sign-in.
88
+ store.order = { [profile.provider]: [profileId] };
89
+ store.lastGood = {};
90
+ return true;
91
+ },
92
+ });
93
+ if (!sealed || !found) { respond(true, { ok: false }); return; }
94
+ await writeFile(marker, JSON.stringify({ profileId, model }), { mode: 0o600 });
95
+ respond(true, { ok: true });
96
+ } catch {
97
+ // Neither engine errors nor store contents (which contain secrets) ever leave this method.
98
+ reply({ ok: false });
99
+ }
100
+ }, { scope: 'operator.admin' });
101
+ }