@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/device.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// The device side (7.2): portable — browsers, React Native, Node; no node:* or Node-only imports may reach here
|
|
2
|
+
// (test/portable.test.ts guards it). Only type imports from @byokit/link; the seal crypto bundles cleanly.
|
|
3
|
+
import { boxKeyPairFromSeed } from '@byokit/seal';
|
|
4
|
+
import { toAccountView } from "./words.js";
|
|
5
|
+
import { b64urlEncode, openNotice } from "./notices.js";
|
|
6
|
+
/**
|
|
7
|
+
* The host ended a stream with its own words (e.g. a refusal). Portable twin of link's PublicLinkError:
|
|
8
|
+
* same shape ({ code: 'failed', sealed: true }) so a screen can show .message the same way. A device-side
|
|
9
|
+
* import of link's class would drag node:* into this portable entry (test/portable.test.ts).
|
|
10
|
+
*/
|
|
11
|
+
export class LinkRefused extends Error {
|
|
12
|
+
code = 'failed';
|
|
13
|
+
sealed = true;
|
|
14
|
+
constructor(message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'LinkRefused';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
20
|
+
/**
|
|
21
|
+
* Newline-delimited JSON frames off a link stream, in order. A host end with an error becomes a
|
|
22
|
+
* LinkRefused with the host's words (a refusal arrives after the stream opens, so it cannot ride
|
|
23
|
+
* the open itself).
|
|
24
|
+
*
|
|
25
|
+
* Plain manual iterables, deliberately not async generators: on this runtime a generator parked at
|
|
26
|
+
* an await never settles a return() call, which would wedge teardown of an infinite stream. A plain
|
|
27
|
+
* return() method always runs, ends the stream and wakes any parked next().
|
|
28
|
+
*/
|
|
29
|
+
function liveStream(open) {
|
|
30
|
+
let s;
|
|
31
|
+
let opening;
|
|
32
|
+
let failed;
|
|
33
|
+
let hasFailed = false;
|
|
34
|
+
let ended = false;
|
|
35
|
+
let endError;
|
|
36
|
+
let closed = false;
|
|
37
|
+
let text = '';
|
|
38
|
+
const pending = [];
|
|
39
|
+
const decoder = new TextDecoder();
|
|
40
|
+
const waiters = new Set();
|
|
41
|
+
const wake = () => {
|
|
42
|
+
for (const waiter of waiters)
|
|
43
|
+
waiter();
|
|
44
|
+
waiters.clear();
|
|
45
|
+
};
|
|
46
|
+
const ensure = () => {
|
|
47
|
+
opening ??= open().then((stream) => {
|
|
48
|
+
if (closed) {
|
|
49
|
+
stream.end();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
s = stream;
|
|
53
|
+
stream.onData = (chunk) => {
|
|
54
|
+
text += decoder.decode(chunk, { stream: true });
|
|
55
|
+
let at = text.indexOf('\n');
|
|
56
|
+
while (at >= 0) {
|
|
57
|
+
pending.push(text.slice(0, at));
|
|
58
|
+
text = text.slice(at + 1);
|
|
59
|
+
at = text.indexOf('\n');
|
|
60
|
+
}
|
|
61
|
+
wake();
|
|
62
|
+
};
|
|
63
|
+
stream.onEnd = (error) => {
|
|
64
|
+
ended = true;
|
|
65
|
+
endError = error;
|
|
66
|
+
wake();
|
|
67
|
+
};
|
|
68
|
+
}, (error) => {
|
|
69
|
+
failed = error;
|
|
70
|
+
hasFailed = true;
|
|
71
|
+
wake();
|
|
72
|
+
});
|
|
73
|
+
return opening;
|
|
74
|
+
};
|
|
75
|
+
return {
|
|
76
|
+
[Symbol.asyncIterator]() {
|
|
77
|
+
return {
|
|
78
|
+
async next() {
|
|
79
|
+
for (;;) {
|
|
80
|
+
if (closed)
|
|
81
|
+
return { value: undefined, done: true };
|
|
82
|
+
while (pending.length) {
|
|
83
|
+
const line = pending.shift();
|
|
84
|
+
if (line)
|
|
85
|
+
return { value: JSON.parse(line), done: false };
|
|
86
|
+
}
|
|
87
|
+
if (hasFailed)
|
|
88
|
+
throw failed;
|
|
89
|
+
if (ended) {
|
|
90
|
+
if (endError !== undefined)
|
|
91
|
+
throw new LinkRefused(endError);
|
|
92
|
+
return { value: undefined, done: true };
|
|
93
|
+
}
|
|
94
|
+
// Opening phase races the open against abandonment; once open, park only on the
|
|
95
|
+
// waiters (re-racing a settled open would spin microtasks and starve the loop).
|
|
96
|
+
if (s === undefined) {
|
|
97
|
+
await Promise.race([ensure(), new Promise((resolve) => {
|
|
98
|
+
waiters.add(resolve);
|
|
99
|
+
})]);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
await new Promise((resolve) => {
|
|
103
|
+
waiters.add(resolve);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
async return() {
|
|
109
|
+
closed = true;
|
|
110
|
+
s?.end();
|
|
111
|
+
wake();
|
|
112
|
+
return { value: undefined, done: true };
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
export function openclawDevice(link) {
|
|
119
|
+
return {
|
|
120
|
+
state: () => link.request('oc.state'),
|
|
121
|
+
routes: () => link.request('oc.routes'),
|
|
122
|
+
signIn: {
|
|
123
|
+
start: (p, via) => link.request('oc.signin.start', { provider: p, via }),
|
|
124
|
+
view: async (p) => {
|
|
125
|
+
const { ready, view } = (await link.request('oc.signin.view', { provider: p }));
|
|
126
|
+
return toAccountView(view, ready);
|
|
127
|
+
},
|
|
128
|
+
paste: async (p, t) => {
|
|
129
|
+
await link.request('oc.signin.paste', { provider: p, text: t });
|
|
130
|
+
},
|
|
131
|
+
cancel: async (p) => {
|
|
132
|
+
await link.request('oc.signin.cancel', { provider: p });
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
signOut: async (p) => {
|
|
136
|
+
await link.request('oc.signout', { provider: p });
|
|
137
|
+
},
|
|
138
|
+
sessions: () => link.request('oc.sessions'),
|
|
139
|
+
run: (message, o) => {
|
|
140
|
+
const inner = liveStream(() => link.stream('oc.run', { message, ...(o?.sessionKey ? { sessionKey: o.sessionKey } : {}),
|
|
141
|
+
...(o?.model !== undefined ? { model: o.model } : {}) }));
|
|
142
|
+
let finished = false;
|
|
143
|
+
return {
|
|
144
|
+
[Symbol.asyncIterator]() {
|
|
145
|
+
const it = inner[Symbol.asyncIterator]();
|
|
146
|
+
return {
|
|
147
|
+
next: async () => {
|
|
148
|
+
if (finished)
|
|
149
|
+
return { value: undefined, done: true };
|
|
150
|
+
const frame = await it.next();
|
|
151
|
+
if (frame.done)
|
|
152
|
+
return { value: undefined, done: true };
|
|
153
|
+
if (isRecord(frame.value) && frame.value.type === 'end') {
|
|
154
|
+
finished = true;
|
|
155
|
+
await it.return?.();
|
|
156
|
+
return { value: frame.value, done: false };
|
|
157
|
+
}
|
|
158
|
+
return { value: frame.value, done: false };
|
|
159
|
+
},
|
|
160
|
+
return: async () => {
|
|
161
|
+
finished = true;
|
|
162
|
+
await it.return?.();
|
|
163
|
+
return { value: undefined, done: true };
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
},
|
|
169
|
+
steer: async (k, t) => {
|
|
170
|
+
await link.request('oc.steer', { sessionKey: k, text: t });
|
|
171
|
+
},
|
|
172
|
+
abort: async (k) => {
|
|
173
|
+
await link.request('oc.abort', { sessionKey: k });
|
|
174
|
+
},
|
|
175
|
+
approvals: () => link.request('oc.approvals'),
|
|
176
|
+
decide: async (id, d) => {
|
|
177
|
+
await link.request('oc.decide', { id, ...d });
|
|
178
|
+
},
|
|
179
|
+
events: () => liveStream(() => link.stream('oc.events', {})),
|
|
180
|
+
registerNotices: async (seed) => {
|
|
181
|
+
if (!ArrayBuffer.isView(seed) || !(seed instanceof Uint8Array) || seed.length !== 32)
|
|
182
|
+
throw new Error('registerNotices needs a 32-byte seed');
|
|
183
|
+
await link.request('oc.notices.register', { boxPublicKey: b64urlEncode(boxKeyPairFromSeed(seed).publicKey) });
|
|
184
|
+
},
|
|
185
|
+
openNotice,
|
|
186
|
+
call: (method, params) => link.request('oc.call', { method, params }),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
// Portable, re-exported by ./device (7.3).
|
|
190
|
+
export { openNotice } from "./notices.js";
|
|
191
|
+
// The kit's sentences, so a phone or browser shows the same words the computer does (5.14).
|
|
192
|
+
export { stateWords, toAccountView, words } from "./words.js";
|
package/dist/engine.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { KitOptions } from './kit.ts';
|
|
2
|
+
import type { KitState, ToolSpec } from './types.ts';
|
|
3
|
+
export type EngineOptions = Pick<KitOptions, 'stateDir' | 'engineDir' | 'npmPath' | 'enginePath' | 'config' | 'installPolicy' | 'log' | 'bridge'> & {
|
|
4
|
+
pluginId: string;
|
|
5
|
+
tools: ToolSpec[];
|
|
6
|
+
gateBuiltins?: boolean;
|
|
7
|
+
spawnEngine: boolean;
|
|
8
|
+
onState(s: KitState): void;
|
|
9
|
+
onExit(code: number | null): void;
|
|
10
|
+
};
|
|
11
|
+
export declare class Engine {
|
|
12
|
+
readonly root: string;
|
|
13
|
+
readonly bridgeSock: string;
|
|
14
|
+
private readonly dir;
|
|
15
|
+
private child?;
|
|
16
|
+
private stopping;
|
|
17
|
+
private repaired;
|
|
18
|
+
private prepared?;
|
|
19
|
+
private port;
|
|
20
|
+
private token;
|
|
21
|
+
private readonly o;
|
|
22
|
+
private readonly paramPrefix;
|
|
23
|
+
constructor(o: EngineOptions);
|
|
24
|
+
private state;
|
|
25
|
+
private get entry();
|
|
26
|
+
private freePort;
|
|
27
|
+
prepare(): Promise<void>;
|
|
28
|
+
private prepareOnce;
|
|
29
|
+
doctorContext(): {
|
|
30
|
+
entry: string;
|
|
31
|
+
env: Record<string, string>;
|
|
32
|
+
};
|
|
33
|
+
doctor(timeoutMs: number): {
|
|
34
|
+
status: number | null;
|
|
35
|
+
};
|
|
36
|
+
private stalePid;
|
|
37
|
+
start(): Promise<{
|
|
38
|
+
port: number;
|
|
39
|
+
token: string;
|
|
40
|
+
identityPath: string;
|
|
41
|
+
}>;
|
|
42
|
+
stop(): Promise<void>;
|
|
43
|
+
}
|
package/dist/engine.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
import { randomBytes, generateKeyPairSync } from 'node:crypto';
|
|
3
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, writeFileSync, copyFileSync, rmSync } from 'node:fs';
|
|
4
|
+
import { createServer } from 'node:net';
|
|
5
|
+
import { join, dirname } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
8
|
+
import { ENGINE_VERSION } from "./constants.js";
|
|
9
|
+
import { reconcileConfig } from "./config.js";
|
|
10
|
+
import { writePlugin, resolveBridge } from "./bridge.js";
|
|
11
|
+
const kitDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
12
|
+
const putOnce = (path, data) => { if (!existsSync(path))
|
|
13
|
+
writeFileSync(path, data, { mode: 0o600, flag: 'wx' }); };
|
|
14
|
+
const putChanged = (path, data) => {
|
|
15
|
+
if (!existsSync(path) || readFileSync(path, 'utf8') !== data)
|
|
16
|
+
writeFileSync(path, data, { mode: 0o600 });
|
|
17
|
+
};
|
|
18
|
+
export class Engine {
|
|
19
|
+
root;
|
|
20
|
+
bridgeSock;
|
|
21
|
+
dir;
|
|
22
|
+
child;
|
|
23
|
+
stopping = false;
|
|
24
|
+
repaired = false;
|
|
25
|
+
prepared;
|
|
26
|
+
port = 0;
|
|
27
|
+
token = '';
|
|
28
|
+
o;
|
|
29
|
+
paramPrefix;
|
|
30
|
+
constructor(o) {
|
|
31
|
+
this.o = o;
|
|
32
|
+
const bridge = resolveBridge(o.bridge);
|
|
33
|
+
this.root = join(o.stateDir, 'openclaw');
|
|
34
|
+
this.dir = o.engineDir ?? join(this.root, 'engine');
|
|
35
|
+
this.bridgeSock = join(this.root, bridge.socketName);
|
|
36
|
+
this.paramPrefix = bridge.paramPrefix;
|
|
37
|
+
}
|
|
38
|
+
state(phase, why, retryAt) { this.o.onState({ phase, ...(why ? { why } : {}), ...(retryAt ? { retryAt } : {}) }); }
|
|
39
|
+
get entry() { return join(this.dir, 'node_modules', 'openclaw', 'openclaw.mjs'); }
|
|
40
|
+
async freePort() {
|
|
41
|
+
const server = createServer();
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
server.once('error', reject);
|
|
44
|
+
server.listen(0, '127.0.0.1', () => {
|
|
45
|
+
const a = server.address();
|
|
46
|
+
const port = a && typeof a !== 'string' ? a.port : 0;
|
|
47
|
+
server.close(() => resolve(port));
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
prepare() {
|
|
52
|
+
if (this.prepared)
|
|
53
|
+
return this.prepared;
|
|
54
|
+
const pending = this.prepareOnce();
|
|
55
|
+
this.prepared = pending;
|
|
56
|
+
void pending.finally(() => { if (this.prepared === pending)
|
|
57
|
+
this.prepared = undefined; }).catch(() => { });
|
|
58
|
+
return pending;
|
|
59
|
+
}
|
|
60
|
+
async prepareOnce() {
|
|
61
|
+
for (const d of [this.root, join(this.root, 'home'), join(this.root, 'state'), join(this.root, 'tmp'), join(this.root, 'install-home'), join(this.root, 'npm-cache'), join(this.o.stateDir, 'logs'), this.dir])
|
|
62
|
+
mkdirSync(d, { recursive: true, mode: 0o700 });
|
|
63
|
+
if (this.o.spawnEngine) {
|
|
64
|
+
const versionPath = join(this.dir, 'node_modules', 'openclaw', 'package.json');
|
|
65
|
+
const version = existsSync(versionPath) ? JSON.parse(readFileSync(versionPath, 'utf8')).version : undefined;
|
|
66
|
+
if (!existsSync(this.entry) || version !== ENGINE_VERSION) {
|
|
67
|
+
this.state('installing');
|
|
68
|
+
rmSync(join(this.dir, 'node_modules'), { recursive: true, force: true });
|
|
69
|
+
for (const f of ['package.json', 'package-lock.json'])
|
|
70
|
+
copyFileSync(join(kitDir, 'engine', f), join(this.dir, f));
|
|
71
|
+
const result = spawnSync(this.o.npmPath ?? 'npm', ['ci', '--ignore-scripts', '--no-audit', '--no-fund', '--prefix', this.dir], {
|
|
72
|
+
env: { PATH: process.env.PATH ?? '/usr/bin:/bin', HOME: join(this.root, 'install-home'), npm_config_cache: join(this.root, 'npm-cache'), OPENCLAW_DISABLE_BUNDLED_PLUGIN_POSTINSTALL: '1' },
|
|
73
|
+
timeout: 300_000, stdio: 'pipe', encoding: 'utf8', maxBuffer: 1024 * 1024,
|
|
74
|
+
});
|
|
75
|
+
if (result.status !== 0) {
|
|
76
|
+
this.state('failed', 'install');
|
|
77
|
+
throw new Error(`engine install: ${result.error ?? result.stderr?.slice(-500)}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (!existsSync(this.entry) || JSON.parse(readFileSync(versionPath, 'utf8')).version !== ENGINE_VERSION) {
|
|
81
|
+
this.state('needs-update', 'version');
|
|
82
|
+
throw new Error('engine version mismatch');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const tokenFile = join(this.root, 'token');
|
|
86
|
+
putOnce(tokenFile, randomBytes(32).toString('hex'));
|
|
87
|
+
this.token = readFileSync(tokenFile, 'utf8').trim();
|
|
88
|
+
const portFile = join(this.root, 'port');
|
|
89
|
+
putOnce(portFile, String(await this.freePort()));
|
|
90
|
+
this.port = Number(readFileSync(portFile, 'utf8'));
|
|
91
|
+
if (!Number.isInteger(this.port) || this.port < 1 || this.port > 65535 || this.port === 18789) {
|
|
92
|
+
this.state('failed', 'port');
|
|
93
|
+
throw new Error('invalid engine port');
|
|
94
|
+
}
|
|
95
|
+
const pluginDir = join(this.root, 'plugin');
|
|
96
|
+
mkdirSync(pluginDir, { recursive: true, mode: 0o700 });
|
|
97
|
+
// The shipped plugin follows the kit on every prepare too: a state dir from an older kit must not keep its gate.
|
|
98
|
+
for (const f of ['package.json', 'index.js']) {
|
|
99
|
+
const source = join(kitDir, 'plugin', f);
|
|
100
|
+
putChanged(join(pluginDir, f), readFileSync(source, 'utf8'));
|
|
101
|
+
}
|
|
102
|
+
// The bridge manifest and tool table follow the app's tools on every prepare (O5).
|
|
103
|
+
writePlugin(pluginDir, { id: this.o.pluginId, tools: this.o.tools, paramPrefix: this.paramPrefix,
|
|
104
|
+
gateBuiltins: this.o.gateBuiltins !== false });
|
|
105
|
+
const path = join(this.root, 'openclaw.json');
|
|
106
|
+
const saved = existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : undefined;
|
|
107
|
+
const config = reconcileConfig(saved, { root: this.root, stateDir: this.o.stateDir, port: this.port, pluginId: this.o.pluginId,
|
|
108
|
+
pluginDir, policyPath: join(kitDir, 'policy', 'policy.mjs'), app: this.o.config, installPolicy: this.o.installPolicy });
|
|
109
|
+
putChanged(path, JSON.stringify(config, null, 2) + '\n');
|
|
110
|
+
}
|
|
111
|
+
doctorContext() {
|
|
112
|
+
const home = join(this.root, 'home');
|
|
113
|
+
return { entry: this.entry, env: {
|
|
114
|
+
PATH: ['/usr/bin', '/bin', ...this.o.enginePath ?? []].join(':'), LANG: 'C.UTF-8', HOME: home, OPENCLAW_HOME: home,
|
|
115
|
+
OPENCLAW_STATE_DIR: join(this.root, 'state'), OPENCLAW_CONFIG_PATH: join(this.root, 'openclaw.json'),
|
|
116
|
+
XDG_CONFIG_HOME: join(home, '.config'), XDG_CACHE_HOME: join(home, '.cache'), XDG_DATA_HOME: join(home, '.local/share'),
|
|
117
|
+
XDG_STATE_HOME: join(home, '.local/state'), CODEX_HOME: join(home, '.codex'), CLAUDE_CONFIG_DIR: join(home, '.claude'),
|
|
118
|
+
TMPDIR: join(this.root, 'tmp'), OPENCLAW_NO_RESPAWN: '1', OPENCLAW_SKIP_CHANNELS: '1', OPENCLAW_DISABLE_BONJOUR: '1',
|
|
119
|
+
OPENCLAW_EXEC_SHELL_SNAPSHOT: '0', OPENCLAW_LOAD_SHELL_ENV: '0', OPENCLAW_GATEWAY_TOKEN: this.token,
|
|
120
|
+
BYOKIT_BRIDGE_SOCK: this.bridgeSock,
|
|
121
|
+
} };
|
|
122
|
+
}
|
|
123
|
+
doctor(timeoutMs) {
|
|
124
|
+
const { entry, env } = this.doctorContext();
|
|
125
|
+
const result = spawnSync(process.execPath, [entry, 'doctor', '--fix', '--yes', '--non-interactive'], { cwd: env.HOME, env, timeout: timeoutMs, stdio: 'pipe' });
|
|
126
|
+
return { status: result.status };
|
|
127
|
+
}
|
|
128
|
+
stalePid() {
|
|
129
|
+
const path = join(this.root, 'gateway.pid');
|
|
130
|
+
if (!existsSync(path))
|
|
131
|
+
return;
|
|
132
|
+
const pid = Number(readFileSync(path, 'utf8'));
|
|
133
|
+
if (!Number.isSafeInteger(pid) || pid < 1)
|
|
134
|
+
return;
|
|
135
|
+
try {
|
|
136
|
+
const cmd = readFileSync(`/proc/${pid}/cmdline`, 'utf8');
|
|
137
|
+
if (cmd.includes(this.entry) && cmd.includes('gateway'))
|
|
138
|
+
process.kill(-pid, 'SIGTERM');
|
|
139
|
+
}
|
|
140
|
+
catch { /* pid is gone or not ours */ }
|
|
141
|
+
}
|
|
142
|
+
async start() {
|
|
143
|
+
await this.prepare();
|
|
144
|
+
const identityPath = join(this.root, 'device.json');
|
|
145
|
+
if (!existsSync(identityPath)) {
|
|
146
|
+
const { privateKey, publicKey } = generateKeyPairSync('ed25519');
|
|
147
|
+
putOnce(identityPath, JSON.stringify({ privateKey: privateKey.export({ format: 'pem', type: 'pkcs8' }), publicKey: publicKey.export({ format: 'pem', type: 'spki' }) }));
|
|
148
|
+
}
|
|
149
|
+
if (!this.o.spawnEngine)
|
|
150
|
+
return { port: this.port, token: this.token, identityPath };
|
|
151
|
+
if (this.child && this.child.exitCode === null)
|
|
152
|
+
return { port: this.port, token: this.token, identityPath };
|
|
153
|
+
this.stopping = false;
|
|
154
|
+
this.stalePid();
|
|
155
|
+
this.state('starting');
|
|
156
|
+
const { entry, env } = this.doctorContext();
|
|
157
|
+
const fd = openSync(join(this.o.stateDir, 'logs', 'openclaw.log'), 'a', 0o600);
|
|
158
|
+
try {
|
|
159
|
+
this.child = spawn(process.execPath, [entry, 'gateway', '--port', String(this.port)], { cwd: env.HOME, env, detached: true, stdio: ['ignore', fd, fd] });
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
closeSync(fd);
|
|
163
|
+
}
|
|
164
|
+
const child = this.child;
|
|
165
|
+
if (!child.pid) {
|
|
166
|
+
this.state('failed', 'exited');
|
|
167
|
+
throw new Error('engine spawn failed');
|
|
168
|
+
}
|
|
169
|
+
writeFileSync(join(this.root, 'gateway.pid'), String(child.pid), { mode: 0o600 });
|
|
170
|
+
child.once('exit', (code) => {
|
|
171
|
+
if (this.child !== child || this.stopping)
|
|
172
|
+
return;
|
|
173
|
+
this.child = undefined;
|
|
174
|
+
this.o.onExit(code);
|
|
175
|
+
if (code === 78 && !this.repaired) {
|
|
176
|
+
this.repaired = true;
|
|
177
|
+
this.state('repairing');
|
|
178
|
+
const result = this.doctor(60_000);
|
|
179
|
+
if (result.status === 0) {
|
|
180
|
+
void this.start().catch(() => this.state('failed', 'exited'));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
this.state('failed', 'exited');
|
|
185
|
+
});
|
|
186
|
+
return { port: this.port, token: this.token, identityPath };
|
|
187
|
+
}
|
|
188
|
+
async stop() {
|
|
189
|
+
this.stopping = true;
|
|
190
|
+
const child = this.child;
|
|
191
|
+
this.child = undefined;
|
|
192
|
+
if (child?.pid && child.exitCode === null) {
|
|
193
|
+
try {
|
|
194
|
+
process.kill(-child.pid, 'SIGTERM');
|
|
195
|
+
}
|
|
196
|
+
catch { /* already gone */ }
|
|
197
|
+
for (let i = 0; i < 15 && child.exitCode === null; i++)
|
|
198
|
+
await delay(200);
|
|
199
|
+
if (child.exitCode === null) {
|
|
200
|
+
try {
|
|
201
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
202
|
+
}
|
|
203
|
+
catch { /* already gone */ }
|
|
204
|
+
}
|
|
205
|
+
for (let i = 0; i < 15 && child.exitCode === null; i++)
|
|
206
|
+
await delay(200);
|
|
207
|
+
}
|
|
208
|
+
rmSync(join(this.root, 'gateway.pid'), { force: true });
|
|
209
|
+
rmSync(this.bridgeSock, { force: true });
|
|
210
|
+
this.state('stopped');
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { AgentEvent, DevicePairSetupCompletedEvent, DevicePairSetupDeliveryUncertainEvent, NodeInvokeInputEvent, PortalChangedEvent, ProgressCardChangedEvent, QuestionRequestedEvent, SessionOperationEvent, SessionSharingEvent, SessionSharingEvidenceEvent, SessionSuggestionEvent, SessionTypingEvent, ShutdownEvent, TerminalDataEvent, TerminalExitEvent, TickEvent, UsersPrefsChangedEvent } from '@openclaw/gateway-protocol';
|
|
2
|
+
import type { ChatEvent, QuestionResolvedEvent, SessionApprovalEvent, TaskSuggestionEvent } from './params.ts';
|
|
3
|
+
export interface GatewayEvents {
|
|
4
|
+
'agent': AgentEvent;
|
|
5
|
+
'chat': ChatEvent;
|
|
6
|
+
'chat.metadata.changed': unknown;
|
|
7
|
+
'connect.challenge': unknown;
|
|
8
|
+
'controlUi.sessionPullRequests.changed': unknown;
|
|
9
|
+
'cron': unknown;
|
|
10
|
+
'device.pair.changed': unknown;
|
|
11
|
+
'device.pair.requested': unknown;
|
|
12
|
+
'device.pair.resolved': unknown;
|
|
13
|
+
'device.pair.setup.completed': DevicePairSetupCompletedEvent;
|
|
14
|
+
'device.pair.setup.deliveryUncertain': DevicePairSetupDeliveryUncertainEvent;
|
|
15
|
+
'exec.approval.requested': unknown;
|
|
16
|
+
'exec.approval.resolved': unknown;
|
|
17
|
+
'health': unknown;
|
|
18
|
+
'heartbeat': unknown;
|
|
19
|
+
'node.invoke.cancel': unknown;
|
|
20
|
+
'node.invoke.input': NodeInvokeInputEvent;
|
|
21
|
+
'node.invoke.request': unknown;
|
|
22
|
+
'node.pair.requested': unknown;
|
|
23
|
+
'node.pair.resolved': unknown;
|
|
24
|
+
'node.presence': unknown;
|
|
25
|
+
'node.runnerInventory.changed': unknown;
|
|
26
|
+
'openclaw.approval.requested': unknown;
|
|
27
|
+
'openclaw.approval.resolved': unknown;
|
|
28
|
+
'plugin.approval.requested': unknown;
|
|
29
|
+
'plugin.approval.resolved': unknown;
|
|
30
|
+
'portal.changed': PortalChangedEvent;
|
|
31
|
+
'presence': unknown;
|
|
32
|
+
'progressCard.changed': ProgressCardChangedEvent;
|
|
33
|
+
'question.requested': QuestionRequestedEvent;
|
|
34
|
+
'question.resolved': QuestionResolvedEvent;
|
|
35
|
+
'session.approval': SessionApprovalEvent;
|
|
36
|
+
'session.message': unknown;
|
|
37
|
+
'session.observer': unknown;
|
|
38
|
+
'session.operation': SessionOperationEvent;
|
|
39
|
+
'session.sharing': SessionSharingEvent;
|
|
40
|
+
'session.sharing.evidence': SessionSharingEvidenceEvent;
|
|
41
|
+
'session.suggestion': SessionSuggestionEvent;
|
|
42
|
+
'session.tool': unknown;
|
|
43
|
+
'session.typing': SessionTypingEvent;
|
|
44
|
+
'sessions.changed': unknown;
|
|
45
|
+
'shutdown': ShutdownEvent;
|
|
46
|
+
'skills.changed': unknown;
|
|
47
|
+
'talk.event': unknown;
|
|
48
|
+
'talk.mode': unknown;
|
|
49
|
+
'task': unknown;
|
|
50
|
+
'task.suggestion': TaskSuggestionEvent;
|
|
51
|
+
'terminal.data': TerminalDataEvent;
|
|
52
|
+
'terminal.exit': TerminalExitEvent;
|
|
53
|
+
'tick': TickEvent;
|
|
54
|
+
'ui.command': unknown;
|
|
55
|
+
'update.available': unknown;
|
|
56
|
+
'users.prefs.changed': UsersPrefsChangedEvent;
|
|
57
|
+
'voicewake.changed': unknown;
|
|
58
|
+
'voicewake.routing.changed': unknown;
|
|
59
|
+
}
|
|
60
|
+
export type GatewayEventName = keyof GatewayEvents;
|
|
61
|
+
export type GatewayEventPayload<E extends GatewayEventName> = GatewayEvents[E];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|