@byokit/openclaw 0.1.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 +47 -0
- package/LICENSE +202 -0
- package/README.md +174 -0
- package/dist/approvals.d.ts +29 -0
- package/dist/approvals.js +200 -0
- package/dist/bridge.d.ts +78 -0
- package/dist/bridge.js +345 -0
- package/dist/classify.d.ts +4 -0
- package/dist/classify.js +14 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.js +74 -0
- package/dist/constants.d.ts +3 -0
- package/dist/constants.js +13 -0
- package/dist/device.d.ts +63 -0
- package/dist/device.js +192 -0
- package/dist/engine.d.ts +43 -0
- package/dist/engine.js +212 -0
- package/dist/generated/events.d.ts +61 -0
- package/dist/generated/events.js +1 -0
- package/dist/generated/methods.d.ts +2368 -0
- package/dist/generated/methods.js +1 -0
- package/dist/generated/params.d.ts +1717 -0
- package/dist/generated/params.js +5 -0
- package/dist/generated/report.json +1356 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/kit.d.ts +108 -0
- package/dist/kit.js +655 -0
- package/dist/link.d.ts +24 -0
- package/dist/link.js +273 -0
- package/dist/members.d.ts +11 -0
- package/dist/members.js +23 -0
- package/dist/migrate.d.ts +22 -0
- package/dist/migrate.js +118 -0
- package/dist/notices.d.ts +9 -0
- package/dist/notices.js +75 -0
- package/dist/routes.d.ts +9 -0
- package/dist/routes.js +15 -0
- package/dist/routes.json +398 -0
- package/dist/runs.d.ts +14 -0
- package/dist/runs.js +110 -0
- package/dist/signin.d.ts +24 -0
- package/dist/signin.js +251 -0
- package/dist/testing/contract.d.ts +12 -0
- package/dist/testing/contract.js +250 -0
- package/dist/testing/fake-gateway.d.ts +19 -0
- package/dist/testing/fake-gateway.js +378 -0
- package/dist/testing/index.d.ts +3 -0
- package/dist/testing/index.js +4 -0
- package/dist/testing/model-stub.d.ts +29 -0
- package/dist/testing/model-stub.js +182 -0
- package/dist/transport.d.ts +22 -0
- package/dist/transport.js +94 -0
- package/dist/types.d.ts +125 -0
- package/dist/types.js +2 -0
- package/dist/words.d.ts +12 -0
- package/dist/words.js +26 -0
- package/dist/words.json +20 -0
- package/engine/package-lock.json +4680 -0
- package/engine/package.json +1 -0
- package/package.json +68 -0
- package/plugin/index.js +183 -0
- package/plugin/package.json +1 -0
- package/policy/policy.mjs +197 -0
package/dist/bridge.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Approval, Decision, RunRef, ToolHost, ToolSpec } from './types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Asks must expire before the plugin's 195 s gate timeout, or the run gets a generic block while the approval
|
|
4
|
+
* lingers (N3). The kit passes its `approvalTimeoutMs` through; anything above is clamped to this.
|
|
5
|
+
*/
|
|
6
|
+
export declare const MAX_APPROVAL_TIMEOUT_MS = 190000;
|
|
7
|
+
export declare const DEFAULT_SOCKET_NAME = "bridge.sock";
|
|
8
|
+
export declare const DEFAULT_PARAM_PREFIX = "__byokit";
|
|
9
|
+
/** Resolve the bridge naming options, validating explicit values; defaults keep prior behavior. */
|
|
10
|
+
export declare function resolveBridge(o?: {
|
|
11
|
+
socketName?: string;
|
|
12
|
+
paramPrefix?: string;
|
|
13
|
+
}): {
|
|
14
|
+
socketName: string;
|
|
15
|
+
paramPrefix: string;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Write the bridge plugin's generated files into the engine's plugin dir: the manifest OpenClaw discovers
|
|
19
|
+
* ownership from, and the tool table the shipped `plugin/index.js` registers at load. Deterministic: rewriting
|
|
20
|
+
* with the same tools leaves bytes identical.
|
|
21
|
+
*/
|
|
22
|
+
export declare function writePlugin(dir: string, o: {
|
|
23
|
+
id: string;
|
|
24
|
+
tools: ToolSpec[];
|
|
25
|
+
paramPrefix: string;
|
|
26
|
+
gateBuiltins: boolean;
|
|
27
|
+
}): void;
|
|
28
|
+
export declare class Bridge {
|
|
29
|
+
private readonly path;
|
|
30
|
+
private readonly host?;
|
|
31
|
+
private readonly tools;
|
|
32
|
+
private readonly permitted;
|
|
33
|
+
private readonly approvalTimeoutMs;
|
|
34
|
+
private readonly onAsk;
|
|
35
|
+
private readonly onAskGone;
|
|
36
|
+
private server?;
|
|
37
|
+
private readonly runs;
|
|
38
|
+
private readonly permits;
|
|
39
|
+
private readonly tickets;
|
|
40
|
+
private armed;
|
|
41
|
+
private readonly parked;
|
|
42
|
+
private stopped;
|
|
43
|
+
constructor(o: {
|
|
44
|
+
path: string;
|
|
45
|
+
host?: ToolHost;
|
|
46
|
+
tools: ReadonlySet<string>;
|
|
47
|
+
permitted: (tool: string) => boolean;
|
|
48
|
+
approvalTimeoutMs: number;
|
|
49
|
+
onAsk(a: Approval): void;
|
|
50
|
+
onAskGone(id: string): void;
|
|
51
|
+
});
|
|
52
|
+
start(): Promise<void>;
|
|
53
|
+
stop(): void;
|
|
54
|
+
register(run: RunRef): void;
|
|
55
|
+
unregister(sessionKey: string): void;
|
|
56
|
+
allowOnce(rule: {
|
|
57
|
+
keyPrefix: string;
|
|
58
|
+
tool: string;
|
|
59
|
+
input?: (i: Record<string, unknown>) => boolean;
|
|
60
|
+
}, ms: number): void;
|
|
61
|
+
disallowOnce(): void;
|
|
62
|
+
/** Resolve a parked `{ ask }` gate. Returns false when the id is not a parked ask. */
|
|
63
|
+
resolveAsk(id: string, d: Decision): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Admit one call: permitted tools get a permit, the rest get a single-use ticket (N1: never both, so no
|
|
66
|
+
* orphan ticket outlives the approval that created it). Builtins run inside the engine and never call back,
|
|
67
|
+
* so they get neither.
|
|
68
|
+
*/
|
|
69
|
+
private admit;
|
|
70
|
+
private mint;
|
|
71
|
+
private reply;
|
|
72
|
+
private deny;
|
|
73
|
+
private serve;
|
|
74
|
+
private handle;
|
|
75
|
+
private gate;
|
|
76
|
+
private gateRun;
|
|
77
|
+
private call;
|
|
78
|
+
}
|
package/dist/bridge.js
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
// The fail-closed tool bridge: plugin hook -> unix socket -> app gate, with parked asks and one-use permits (5.9, O5).
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { createServer } from 'node:net';
|
|
5
|
+
import { rm } from 'node:fs/promises';
|
|
6
|
+
import { words } from "./words.js";
|
|
7
|
+
/** Newline-framed JSON, one request per connection; anything larger is not a gate request. */
|
|
8
|
+
const FRAME_CAP = 1_000_000;
|
|
9
|
+
/**
|
|
10
|
+
* Asks must expire before the plugin's 195 s gate timeout, or the run gets a generic block while the approval
|
|
11
|
+
* lingers (N3). The kit passes its `approvalTimeoutMs` through; anything above is clamped to this.
|
|
12
|
+
*/
|
|
13
|
+
export const MAX_APPROVAL_TIMEOUT_MS = 190_000;
|
|
14
|
+
const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
15
|
+
const memberOfKey = (key) => {
|
|
16
|
+
const m = /^agent:([a-z][a-z0-9-]{0,31}):/.exec(key);
|
|
17
|
+
return m?.[1];
|
|
18
|
+
};
|
|
19
|
+
export const DEFAULT_SOCKET_NAME = 'bridge.sock';
|
|
20
|
+
export const DEFAULT_PARAM_PREFIX = '__byokit';
|
|
21
|
+
/** Resolve the bridge naming options, validating explicit values; defaults keep prior behavior. */
|
|
22
|
+
export function resolveBridge(o) {
|
|
23
|
+
const socketName = o?.socketName ?? DEFAULT_SOCKET_NAME;
|
|
24
|
+
const paramPrefix = o?.paramPrefix ?? DEFAULT_PARAM_PREFIX;
|
|
25
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*\.sock$/.test(socketName))
|
|
26
|
+
throw new Error(`invalid bridge socketName: ${socketName}`);
|
|
27
|
+
if (!/^__[A-Za-z0-9]+$/.test(paramPrefix))
|
|
28
|
+
throw new Error(`invalid bridge paramPrefix: ${paramPrefix}`);
|
|
29
|
+
return { socketName, paramPrefix };
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Write the bridge plugin's generated files into the engine's plugin dir: the manifest OpenClaw discovers
|
|
33
|
+
* ownership from, and the tool table the shipped `plugin/index.js` registers at load. Deterministic: rewriting
|
|
34
|
+
* with the same tools leaves bytes identical.
|
|
35
|
+
*/
|
|
36
|
+
export function writePlugin(dir, o) {
|
|
37
|
+
resolveBridge({ paramPrefix: o.paramPrefix });
|
|
38
|
+
const runParam = `${o.paramPrefix}_run`;
|
|
39
|
+
const permitParam = `${o.paramPrefix}_permit`;
|
|
40
|
+
const manifest = {
|
|
41
|
+
id: o.id,
|
|
42
|
+
name: 'BYOKit bridge',
|
|
43
|
+
activation: { onStartup: true },
|
|
44
|
+
contracts: { tools: o.tools.map((t) => t.name) },
|
|
45
|
+
configSchema: { type: 'object', additionalProperties: false },
|
|
46
|
+
};
|
|
47
|
+
const table = {
|
|
48
|
+
id: o.id,
|
|
49
|
+
runParam,
|
|
50
|
+
permitParam,
|
|
51
|
+
// true: engine builtins (web_fetch, memory, ...) are gated too, not only the app's tools.
|
|
52
|
+
gateBuiltins: o.gateBuiltins,
|
|
53
|
+
tools: o.tools.map((t) => ({ name: t.name, description: t.description, parameters: t.parameters })),
|
|
54
|
+
};
|
|
55
|
+
for (const [file, data] of [['openclaw.plugin.json', manifest], ['tools.json', table]]) {
|
|
56
|
+
const text = JSON.stringify(data, null, 2) + '\n';
|
|
57
|
+
const path = `${dir}/${file}`;
|
|
58
|
+
if (!existsSync(path) || readFileSync(path, 'utf8') !== text)
|
|
59
|
+
writeFileSync(path, text, { mode: 0o600 });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export class Bridge {
|
|
63
|
+
path;
|
|
64
|
+
host;
|
|
65
|
+
tools;
|
|
66
|
+
permitted;
|
|
67
|
+
approvalTimeoutMs;
|
|
68
|
+
onAsk;
|
|
69
|
+
onAskGone;
|
|
70
|
+
server;
|
|
71
|
+
runs = new Map();
|
|
72
|
+
permits = new Map();
|
|
73
|
+
tickets = [];
|
|
74
|
+
armed;
|
|
75
|
+
parked = new Map();
|
|
76
|
+
stopped = false;
|
|
77
|
+
constructor(o) {
|
|
78
|
+
this.path = o.path;
|
|
79
|
+
this.host = o.host;
|
|
80
|
+
this.tools = o.tools;
|
|
81
|
+
this.permitted = o.permitted;
|
|
82
|
+
this.approvalTimeoutMs = Math.min(o.approvalTimeoutMs, MAX_APPROVAL_TIMEOUT_MS);
|
|
83
|
+
this.onAsk = o.onAsk;
|
|
84
|
+
this.onAskGone = o.onAskGone;
|
|
85
|
+
}
|
|
86
|
+
start() {
|
|
87
|
+
if (this.server)
|
|
88
|
+
return Promise.resolve();
|
|
89
|
+
this.stopped = false;
|
|
90
|
+
// A stale socket file from a crashed run is not a listener; a live one survives unlinking.
|
|
91
|
+
rmSync(this.path, { force: true });
|
|
92
|
+
this.server = createServer((socket) => this.serve(socket));
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
this.server.once('error', reject);
|
|
95
|
+
this.server.listen(this.path, () => {
|
|
96
|
+
this.server.removeListener('error', reject);
|
|
97
|
+
resolve();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
stop() {
|
|
102
|
+
this.stopped = true;
|
|
103
|
+
for (const [id, p] of this.parked) {
|
|
104
|
+
clearTimeout(p.timer);
|
|
105
|
+
this.reply(p.socket, { allow: false, reason: words('approval.expired') });
|
|
106
|
+
// Parked gates end here; without this their approvals outlive the connection (N9).
|
|
107
|
+
this.onAskGone(id);
|
|
108
|
+
}
|
|
109
|
+
this.parked.clear();
|
|
110
|
+
this.server?.close();
|
|
111
|
+
this.server = undefined;
|
|
112
|
+
void rm(this.path, { force: true }).catch(() => { });
|
|
113
|
+
}
|
|
114
|
+
register(run) {
|
|
115
|
+
this.runs.set(run.sessionKey, run);
|
|
116
|
+
}
|
|
117
|
+
unregister(sessionKey) {
|
|
118
|
+
this.runs.delete(sessionKey);
|
|
119
|
+
for (const [permit, p] of this.permits)
|
|
120
|
+
if (p.key === sessionKey)
|
|
121
|
+
this.permits.delete(permit);
|
|
122
|
+
for (let i = this.tickets.length - 1; i >= 0; i--)
|
|
123
|
+
if (this.tickets[i].key === sessionKey)
|
|
124
|
+
this.tickets.splice(i, 1);
|
|
125
|
+
}
|
|
126
|
+
allowOnce(rule, ms) {
|
|
127
|
+
this.armed = { ...rule, until: Date.now() + ms };
|
|
128
|
+
}
|
|
129
|
+
disallowOnce() {
|
|
130
|
+
this.armed = undefined;
|
|
131
|
+
}
|
|
132
|
+
/** Resolve a parked `{ ask }` gate. Returns false when the id is not a parked ask. */
|
|
133
|
+
resolveAsk(id, d) {
|
|
134
|
+
const p = this.parked.get(id);
|
|
135
|
+
if (!p)
|
|
136
|
+
return false;
|
|
137
|
+
this.parked.delete(id);
|
|
138
|
+
clearTimeout(p.timer);
|
|
139
|
+
if (d.allow)
|
|
140
|
+
this.admit(p.socket, p.run.sessionKey, p.tool, p.input);
|
|
141
|
+
else
|
|
142
|
+
this.reply(p.socket, { allow: false, reason: d.reason ?? words('approval.expired') });
|
|
143
|
+
this.onAskGone(id);
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Admit one call: permitted tools get a permit, the rest get a single-use ticket (N1: never both, so no
|
|
148
|
+
* orphan ticket outlives the approval that created it). Builtins run inside the engine and never call back,
|
|
149
|
+
* so they get neither.
|
|
150
|
+
*/
|
|
151
|
+
admit(socket, key, tool, input) {
|
|
152
|
+
if (!this.tools.has(tool)) {
|
|
153
|
+
this.reply(socket, { allow: true });
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (this.permitted(tool)) {
|
|
157
|
+
this.reply(socket, { allow: true, permit: this.mint(key, tool, input) });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
this.tickets.push({ key, tool, input: JSON.stringify(input) });
|
|
161
|
+
while (this.tickets.length > 1024)
|
|
162
|
+
this.tickets.shift();
|
|
163
|
+
this.reply(socket, { allow: true });
|
|
164
|
+
}
|
|
165
|
+
mint(key, tool, input) {
|
|
166
|
+
const permit = randomBytes(16).toString('hex');
|
|
167
|
+
this.permits.set(permit, { permit, key, tool, input: JSON.stringify(input) });
|
|
168
|
+
// Single-use entries are consumed on the next call; evict the oldest if the plugin never calls back.
|
|
169
|
+
while (this.permits.size > 1024)
|
|
170
|
+
this.permits.delete(this.permits.keys().next().value);
|
|
171
|
+
return permit;
|
|
172
|
+
}
|
|
173
|
+
reply(socket, message) {
|
|
174
|
+
try {
|
|
175
|
+
socket.write(JSON.stringify(message) + '\n');
|
|
176
|
+
}
|
|
177
|
+
catch { /* the plugin is gone; the deny stands */ }
|
|
178
|
+
socket.end();
|
|
179
|
+
}
|
|
180
|
+
deny(socket, reason) {
|
|
181
|
+
this.reply(socket, { allow: false, reason });
|
|
182
|
+
}
|
|
183
|
+
serve(socket) {
|
|
184
|
+
let buffer = '';
|
|
185
|
+
socket.on('data', (chunk) => {
|
|
186
|
+
buffer += String(chunk);
|
|
187
|
+
if (buffer.length > FRAME_CAP)
|
|
188
|
+
return this.deny(socket, 'request too large');
|
|
189
|
+
const end = buffer.indexOf('\n');
|
|
190
|
+
if (end < 0)
|
|
191
|
+
return;
|
|
192
|
+
const line = buffer.slice(0, end);
|
|
193
|
+
buffer = buffer.slice(end + 1);
|
|
194
|
+
this.handle(socket, line);
|
|
195
|
+
});
|
|
196
|
+
socket.on('error', () => { });
|
|
197
|
+
}
|
|
198
|
+
handle(socket, line) {
|
|
199
|
+
let message;
|
|
200
|
+
try {
|
|
201
|
+
message = JSON.parse(line);
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return this.deny(socket, 'not a gate request');
|
|
205
|
+
}
|
|
206
|
+
if (!isRecord(message) || typeof message.kind !== 'string')
|
|
207
|
+
return this.deny(socket, 'not a gate request');
|
|
208
|
+
if (message.kind === 'gate')
|
|
209
|
+
return void this.gate(socket, message);
|
|
210
|
+
if (message.kind === 'call')
|
|
211
|
+
return void this.call(socket, message);
|
|
212
|
+
return this.deny(socket, 'not a gate request');
|
|
213
|
+
}
|
|
214
|
+
gate(socket, message) {
|
|
215
|
+
const key = message.key;
|
|
216
|
+
const tool = message.tool;
|
|
217
|
+
const input = message.input;
|
|
218
|
+
if (typeof key !== 'string' || typeof tool !== 'string' || !isRecord(input)) {
|
|
219
|
+
return this.deny(socket, 'not a gate request');
|
|
220
|
+
}
|
|
221
|
+
const run = this.runs.get(key);
|
|
222
|
+
if (run)
|
|
223
|
+
return void this.gateRun(socket, run, tool, input);
|
|
224
|
+
const armed = this.armed;
|
|
225
|
+
if (armed && Date.now() < armed.until && key.startsWith(armed.keyPrefix) && tool === armed.tool &&
|
|
226
|
+
(!armed.input || armed.input(input))) {
|
|
227
|
+
this.armed = undefined;
|
|
228
|
+
// '' when unattributable: never a member id (B2).
|
|
229
|
+
const member = memberOfKey(key) ?? '';
|
|
230
|
+
const runRef = { sessionKey: key, member };
|
|
231
|
+
return void this.gateRun(socket, runRef, tool, input);
|
|
232
|
+
}
|
|
233
|
+
return this.deny(socket, 'unknown run');
|
|
234
|
+
}
|
|
235
|
+
async gateRun(socket, run, tool, input) {
|
|
236
|
+
if (!this.host)
|
|
237
|
+
return this.deny(socket, "can't check this action right now");
|
|
238
|
+
let result;
|
|
239
|
+
try {
|
|
240
|
+
result = await this.host.gate(run, tool, input, { builtin: !this.tools.has(tool) });
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
return this.deny(socket, "can't check this action right now");
|
|
244
|
+
}
|
|
245
|
+
if (this.stopped)
|
|
246
|
+
return;
|
|
247
|
+
if ('ask' in result) {
|
|
248
|
+
const now = Date.now();
|
|
249
|
+
const approval = {
|
|
250
|
+
id: randomBytes(12).toString('base64url'),
|
|
251
|
+
source: 'gate',
|
|
252
|
+
member: run.member,
|
|
253
|
+
sessionKey: run.sessionKey,
|
|
254
|
+
tool,
|
|
255
|
+
summary: result.ask.summary,
|
|
256
|
+
input,
|
|
257
|
+
at: now,
|
|
258
|
+
expires: now + this.approvalTimeoutMs,
|
|
259
|
+
};
|
|
260
|
+
const parked = {
|
|
261
|
+
socket,
|
|
262
|
+
timer: setTimeout(() => {
|
|
263
|
+
if (this.parked.delete(approval.id)) {
|
|
264
|
+
this.reply(socket, { allow: false, reason: words('approval.expired') });
|
|
265
|
+
this.onAskGone(approval.id);
|
|
266
|
+
}
|
|
267
|
+
}, this.approvalTimeoutMs),
|
|
268
|
+
approval,
|
|
269
|
+
run,
|
|
270
|
+
tool,
|
|
271
|
+
input,
|
|
272
|
+
};
|
|
273
|
+
parked.timer.unref?.();
|
|
274
|
+
this.parked.set(approval.id, parked);
|
|
275
|
+
// A dead caller (run abort, hook timeout) must not leave an approval nothing will use (N2).
|
|
276
|
+
socket.once('close', () => {
|
|
277
|
+
if (this.parked.delete(approval.id)) {
|
|
278
|
+
clearTimeout(parked.timer);
|
|
279
|
+
this.onAskGone(approval.id);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
this.onAsk(approval);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (result.allow) {
|
|
286
|
+
this.admit(socket, run.sessionKey, tool, input);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
this.deny(socket, result.reason);
|
|
290
|
+
}
|
|
291
|
+
async call(socket, message) {
|
|
292
|
+
const key = message.key;
|
|
293
|
+
const tool = message.tool;
|
|
294
|
+
const input = message.input;
|
|
295
|
+
if (typeof key !== 'string' || typeof tool !== 'string' || !isRecord(input)) {
|
|
296
|
+
return this.reply(socket, { ok: false, reason: 'not a call request' });
|
|
297
|
+
}
|
|
298
|
+
// Only the app's own tools run through host.call; a builtin never calls back.
|
|
299
|
+
if (!this.tools.has(tool))
|
|
300
|
+
return this.reply(socket, { ok: false, reason: 'this call was not allowed' });
|
|
301
|
+
const wanted = JSON.stringify(input);
|
|
302
|
+
const permit = message.permit;
|
|
303
|
+
if (typeof permit === 'string') {
|
|
304
|
+
const p = this.permits.get(permit);
|
|
305
|
+
if (!p || p.key !== key || p.tool !== tool || p.input !== wanted) {
|
|
306
|
+
return this.reply(socket, { ok: false, reason: 'this call was not allowed' });
|
|
307
|
+
}
|
|
308
|
+
this.permits.delete(permit);
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
// Permitted tools travel by permit only. A ticket is itself proof of a gate allow for this exact
|
|
312
|
+
// key + tool + input, so no run registration is needed (N10: allowOnce admits unregistered keys).
|
|
313
|
+
if (this.permitted(tool))
|
|
314
|
+
return this.reply(socket, { ok: false, reason: 'this call was not allowed' });
|
|
315
|
+
const ticket = this.tickets.findIndex((t) => t.key === key && t.tool === tool && t.input === wanted);
|
|
316
|
+
if (ticket < 0)
|
|
317
|
+
return this.reply(socket, { ok: false, reason: 'this call was not allowed' });
|
|
318
|
+
this.tickets.splice(ticket, 1);
|
|
319
|
+
}
|
|
320
|
+
if (!this.host)
|
|
321
|
+
return this.reply(socket, { ok: false, reason: "can't run this action right now" });
|
|
322
|
+
const run = this.runs.get(key) ?? { sessionKey: key, member: memberOfKey(key) ?? '' };
|
|
323
|
+
// The plugin destroys its socket when the run aborts (run abort, hook timeout); without this the host's
|
|
324
|
+
// sandboxed command keeps running after the tool is gone.
|
|
325
|
+
const controller = new AbortController();
|
|
326
|
+
const onSocketGone = () => controller.abort();
|
|
327
|
+
if (socket.destroyed)
|
|
328
|
+
controller.abort();
|
|
329
|
+
else {
|
|
330
|
+
socket.once('close', onSocketGone);
|
|
331
|
+
socket.once('error', onSocketGone);
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
const text = await this.host.call(run, tool, input, controller.signal);
|
|
335
|
+
this.reply(socket, { ok: true, text });
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
this.reply(socket, { ok: false, reason: error instanceof Error ? error.message.slice(0, 200) : 'the call failed' });
|
|
339
|
+
}
|
|
340
|
+
finally {
|
|
341
|
+
socket.removeListener('close', onSocketGone);
|
|
342
|
+
socket.removeListener('error', onSocketGone);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
package/dist/classify.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Classify an engine error message into a person-visible kind, with `until` carried when present (5.8, O8).
|
|
2
|
+
// The regexes are Crewhouse `classifyText`; the kinds are the kit's: rate_limit|overloaded → resting,
|
|
3
|
+
// signed_out → signed-out, not_included → plan, network → network, anything unrecognised → other.
|
|
4
|
+
export function classify(message) {
|
|
5
|
+
const retry = /try again in ~?(\d+)\s*(min|h)/i.exec(message);
|
|
6
|
+
const until = retry ? Date.now() + Number(retry[1]) * (retry[2].toLowerCase() === 'h' ? 3_600_000 : 60_000) : undefined;
|
|
7
|
+
const kind = /your plan doesn't include/i.test(message) ? 'plan'
|
|
8
|
+
: /usage limit|rate.?limit|quota|too many requests|\b429\b/i.test(message) ? 'resting'
|
|
9
|
+
: /overloaded|high demand|\b50[234]\b|unavailable/i.test(message) ? 'resting'
|
|
10
|
+
: /unauthori[sz]ed|\b40[13]\b|sign in again|expired|invalid.*token|authentication/i.test(message) ? 'signed-out'
|
|
11
|
+
: /fetch failed|network|ENOTFOUND|EAI_AGAIN|ECONN|socket hang up/i.test(message) ? 'network'
|
|
12
|
+
: 'other';
|
|
13
|
+
return until === undefined ? { kind } : { kind, until };
|
|
14
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { KitOptions } from './kit.ts';
|
|
2
|
+
import type { Member } from './types.ts';
|
|
3
|
+
export declare function reconcileConfig(saved: object | undefined, o: {
|
|
4
|
+
root: string;
|
|
5
|
+
stateDir: string;
|
|
6
|
+
port: number;
|
|
7
|
+
pluginId: string;
|
|
8
|
+
pluginDir: string;
|
|
9
|
+
policyPath: string;
|
|
10
|
+
app?: object;
|
|
11
|
+
installPolicy?: KitOptions['installPolicy'];
|
|
12
|
+
}): object;
|
|
13
|
+
export declare function memoryLimited(config: object, member: Member): boolean;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { dirname, join } from 'node:path';
|
|
2
|
+
const object = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
3
|
+
const merge = (a, b) => {
|
|
4
|
+
for (const [k, v] of Object.entries(b))
|
|
5
|
+
a[k] = object(v) ? merge(object(a[k]) ? a[k] : {}, v) : Array.isArray(v) ? structuredClone(v) : v;
|
|
6
|
+
return a;
|
|
7
|
+
};
|
|
8
|
+
const safeMemory = (memory) => {
|
|
9
|
+
if (!object(memory) || !object(memory.search))
|
|
10
|
+
return;
|
|
11
|
+
const s = memory.search;
|
|
12
|
+
const local = ['none', 'local', 'ollama', 'lmstudio', 'github-copilot'].includes(s.provider);
|
|
13
|
+
let remote = true;
|
|
14
|
+
if (s.provider === 'ollama' || s.provider === 'lmstudio') {
|
|
15
|
+
try {
|
|
16
|
+
const url = new URL(s.remote?.baseUrl);
|
|
17
|
+
remote = ['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname) && !s.remote?.apiKey && !url.username && !url.password;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
remote = false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (!local || !remote)
|
|
24
|
+
s.provider = 'none';
|
|
25
|
+
s.fallback = 'none';
|
|
26
|
+
};
|
|
27
|
+
export function reconcileConfig(saved, o) {
|
|
28
|
+
const c = merge(merge({}, object(saved) ? saved : {}), object(o.app) ? o.app : {});
|
|
29
|
+
merge(c, {
|
|
30
|
+
gateway: { mode: 'local', bind: 'loopback', port: o.port, auth: { mode: 'token' }, controlUi: { enabled: false }, tailscale: { mode: 'off' } },
|
|
31
|
+
discovery: { mdns: { mode: 'off' } }, env: { shellEnv: { enabled: false } },
|
|
32
|
+
update: { checkOnStart: false, auto: { enabled: false } }, telemetry: { enabled: false },
|
|
33
|
+
models: { catalogRefresh: { enabled: false } }, logging: { file: join(o.stateDir, 'logs', 'openclaw-events.log') },
|
|
34
|
+
});
|
|
35
|
+
c.channels ??= {};
|
|
36
|
+
c.agents ??= {};
|
|
37
|
+
c.agents.defaults ??= {};
|
|
38
|
+
c.agents.defaults.models ??= {};
|
|
39
|
+
c.agents.defaults.models['openai/*'] = { ...c.agents.defaults.models['openai/*'], agentRuntime: { id: 'openclaw' } };
|
|
40
|
+
const allow = c.agents.defaults.modelPolicy?.allow;
|
|
41
|
+
if (Array.isArray(allow) && allow.length && !allow.includes('openai/*'))
|
|
42
|
+
allow.push('openai/*');
|
|
43
|
+
c.memory ??= {};
|
|
44
|
+
c.memory.search ??= { provider: 'none' };
|
|
45
|
+
safeMemory(c.memory);
|
|
46
|
+
for (const entry of Object.values(c.agents.entries ?? {}))
|
|
47
|
+
safeMemory(entry.memory);
|
|
48
|
+
// The engine rejects a patched multi-agent roster without explicit ownership (O11); it normalizes the
|
|
49
|
+
// same value on boot, so the kit writes what the engine would. A lone main (or empty) roster stays sole.
|
|
50
|
+
const roster = Object.keys(c.agents.entries ?? {});
|
|
51
|
+
if (roster.length > 1 || (roster.length === 1 && roster[0] !== 'main'))
|
|
52
|
+
c.agents.ownership = 'explicit';
|
|
53
|
+
c.plugins ??= {};
|
|
54
|
+
c.plugins.load ??= {};
|
|
55
|
+
c.plugins.load.paths = [...new Set([...(c.plugins.load.paths ?? []).filter((p) => !p.includes('byokit-openclaw-bridge')), o.pluginDir])];
|
|
56
|
+
c.plugins.allow = [...new Set([...(c.plugins.allow ?? []), o.pluginId])];
|
|
57
|
+
c.plugins.entries ??= {};
|
|
58
|
+
c.plugins.entries[o.pluginId] = merge(c.plugins.entries[o.pluginId] ?? {}, { hooks: { timeouts: { before_tool_call: 200_000 } } });
|
|
59
|
+
c.security ??= {};
|
|
60
|
+
c.security.installPolicy = { enabled: true, exec: {
|
|
61
|
+
source: 'exec', command: process.execPath, args: [o.policyPath],
|
|
62
|
+
trustedDirs: [dirname(process.execPath), dirname(o.policyPath)], timeoutMs: 10_000,
|
|
63
|
+
passEnv: ['OPENCLAW_STATE_DIR'], env: {
|
|
64
|
+
BYOKIT_TRUSTED_SKILLS: o.installPolicy?.trustedSkills ?? '',
|
|
65
|
+
BYOKIT_OWN_ROOTS: JSON.stringify(o.installPolicy?.ownRoots ?? []),
|
|
66
|
+
},
|
|
67
|
+
} };
|
|
68
|
+
return c;
|
|
69
|
+
}
|
|
70
|
+
export function memoryLimited(config, member) {
|
|
71
|
+
const c = config;
|
|
72
|
+
const entry = c.agents?.entries?.[member];
|
|
73
|
+
return (entry?.memory ?? c.memory)?.search?.provider === 'none';
|
|
74
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare const ENGINE_VERSION = "2026.8.1";
|
|
2
|
+
export declare const PROTOCOL_VERSION = 4;
|
|
3
|
+
export declare const OPERATOR_SCOPES: readonly ["operator.read", "operator.write", "operator.admin", "operator.approvals", "operator.questions", "operator.pairing", "operator.talk"];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// The pins this kit is built against (D4, D6). Data, not behavior: an installed engine on another version is
|
|
2
|
+
// `needs-update`, never silently run.
|
|
3
|
+
export const ENGINE_VERSION = '2026.8.1';
|
|
4
|
+
export const PROTOCOL_VERSION = 4;
|
|
5
|
+
export const OPERATOR_SCOPES = [
|
|
6
|
+
'operator.read',
|
|
7
|
+
'operator.write',
|
|
8
|
+
'operator.admin',
|
|
9
|
+
'operator.approvals',
|
|
10
|
+
'operator.questions',
|
|
11
|
+
'operator.pairing',
|
|
12
|
+
'operator.talk',
|
|
13
|
+
];
|
package/dist/device.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { DeviceLink } from '@byokit/link';
|
|
2
|
+
import { type AccountView } from './words.ts';
|
|
3
|
+
import type { Approval, Decision, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, KitState, Route, RunEnd, RunEvent, SignInView } from './types.ts';
|
|
4
|
+
export type { GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult } from './types.ts';
|
|
5
|
+
export type { Approval, Decision, KitState, Route, RunEnd, RunEvent, SignInView } from './types.ts';
|
|
6
|
+
/** One `oc.events` frame: a member's Gateway event, typed by name, or an approval add/resolve (7.2). */
|
|
7
|
+
export type OpenClawLinkEvent = {
|
|
8
|
+
[E in GatewayEventName]: {
|
|
9
|
+
event: E;
|
|
10
|
+
payload: GatewayEventPayload<E>;
|
|
11
|
+
};
|
|
12
|
+
}[GatewayEventName] | {
|
|
13
|
+
event: 'approval';
|
|
14
|
+
change: 'added' | 'resolved';
|
|
15
|
+
approval: Approval;
|
|
16
|
+
};
|
|
17
|
+
/** One `oc.sessions` row: `sessions.list` filtered to the member's `agent:<member>:` keys. */
|
|
18
|
+
export type SessionRow = {
|
|
19
|
+
sessionKey: string;
|
|
20
|
+
[k: string]: unknown;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* The host ended a stream with its own words (e.g. a refusal). Portable twin of link's PublicLinkError:
|
|
24
|
+
* same shape ({ code: 'failed', sealed: true }) so a screen can show .message the same way. A device-side
|
|
25
|
+
* import of link's class would drag node:* into this portable entry (test/portable.test.ts).
|
|
26
|
+
*/
|
|
27
|
+
export declare class LinkRefused extends Error {
|
|
28
|
+
readonly code = "failed";
|
|
29
|
+
readonly sealed = true;
|
|
30
|
+
constructor(message: string);
|
|
31
|
+
}
|
|
32
|
+
export declare function openclawDevice(link: DeviceLink): {
|
|
33
|
+
state(): Promise<{
|
|
34
|
+
state: KitState;
|
|
35
|
+
words: string;
|
|
36
|
+
}>;
|
|
37
|
+
routes(): Promise<Route[]>;
|
|
38
|
+
signIn: {
|
|
39
|
+
start(p: string, via: 'browser' | 'code'): Promise<SignInView>;
|
|
40
|
+
view(p: string): Promise<AccountView>;
|
|
41
|
+
paste(p: string, t: string): Promise<void>;
|
|
42
|
+
cancel(p: string): Promise<void>;
|
|
43
|
+
};
|
|
44
|
+
signOut(p: string): Promise<void>;
|
|
45
|
+
sessions(): Promise<SessionRow[]>;
|
|
46
|
+
run(message: string, o?: {
|
|
47
|
+
sessionKey?: string;
|
|
48
|
+
model?: string;
|
|
49
|
+
}): AsyncIterable<RunEvent | {
|
|
50
|
+
type: 'end';
|
|
51
|
+
end: RunEnd;
|
|
52
|
+
}>;
|
|
53
|
+
steer(k: string, t: string): Promise<void>;
|
|
54
|
+
abort(k: string): Promise<void>;
|
|
55
|
+
approvals(): Promise<Approval[]>;
|
|
56
|
+
decide(id: string, d: Decision): Promise<void>;
|
|
57
|
+
events(): AsyncIterable<OpenClawLinkEvent>;
|
|
58
|
+
registerNotices(seed: Uint8Array): Promise<void>;
|
|
59
|
+
openNotice(data: Record<string, unknown>, seed: Uint8Array): Approval | null;
|
|
60
|
+
call<M extends GatewayMethod>(method: M, params: GatewayParams<M>): Promise<GatewayResult<M>>;
|
|
61
|
+
};
|
|
62
|
+
export { openNotice } from './notices.ts';
|
|
63
|
+
export { stateWords, toAccountView, words, type AccountView, type WordKey } from './words.ts';
|