@platless/axom-cli 0.1.0 → 0.3.13
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/README.md +1 -1
- package/dist/authScopes.js +19 -0
- package/dist/axom-patch/cli/README.md +110 -0
- package/dist/axom-patch/cli/pi-steering.json +37 -0
- package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +34 -0
- package/dist/axom-patch/cli/probes/pi-steering.mjs +80 -0
- package/dist/axom-patch/cli/probes/qoder-steering.mjs +89 -0
- package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +49 -0
- package/dist/axom-patch/cli/qoder-background-terminal.md +81 -0
- package/dist/axom-patch/cli/qoder-foreground-output.md +41 -0
- package/dist/axom-patch/cli/qoder-steering.json +52 -0
- package/dist/axom-patch/cli/qoder-steering.md +30 -0
- package/dist/axom-patch/cli/qoder-subagent-origin.json +73 -0
- package/dist/axom-patch/cli/qoder-subagent-subwork.md +73 -0
- package/dist/axom-patch/cli/runtime.json +37 -0
- package/dist/axom-patch/cli/steering/claude-steering-js.patch +89 -0
- package/dist/axom-patch/cli/steering/claude-steering-types.patch +28 -0
- package/dist/axom-patch/cli/steering/codex-steering.patch +57 -0
- package/dist/axom-patch/cli/steering/pi-steering.js +79 -0
- package/dist/cli.js +9 -1
- package/dist/commands/auth.js +4 -13
- package/dist/commands/execute.js +4 -4
- package/dist/commands/record.js +6 -6
- package/dist/commands/struct.js +4 -4
- package/dist/commands/workstation.js +2 -2
- package/dist/credentials.js +2 -2
- package/dist/install/claude/commands.js +13 -13
- package/dist/mcp.js +32 -14
- package/dist/workstation/acp/acpAgents.js +447 -166
- package/dist/workstation/acp/acpClient.js +232 -18
- package/dist/workstation/acp/acpSession.js +502 -74
- package/dist/workstation/acp/activityBusy.js +43 -0
- package/dist/workstation/acp/activityReporting.js +54 -0
- package/dist/workstation/acp/adoption.js +107 -49
- package/dist/workstation/acp/authUrlSniffer.js +36 -0
- package/dist/workstation/acp/contract.js +11 -6
- package/dist/workstation/acp/delegationMcp.js +24 -0
- package/dist/workstation/acp/draftInterceptor.js +9 -0
- package/dist/workstation/acp/managedPatches.js +228 -0
- package/dist/workstation/acp/patchAssets.js +63 -0
- package/dist/workstation/acp/permission.js +300 -34
- package/dist/workstation/acp/qoderStartup.js +45 -0
- package/dist/workstation/acp/registry/catalog.js +263 -4
- package/dist/workstation/acp/registry/detect.js +55 -0
- package/dist/workstation/acp/sessionScope.js +19 -0
- package/dist/workstation/acp/sessionUpdateMapper.js +39 -21
- package/dist/workstation/acp/terminalLogin.js +197 -0
- package/dist/workstation/acp/test/fakeAgent.js +185 -9
- package/dist/workstation/acp/validate.js +33 -5
- package/dist/workstation/agentSessions.js +532 -44
- package/dist/workstation/channels/agent.js +7 -1
- package/dist/workstation/channels/computer.js +152 -4
- package/dist/workstation/channels/management.js +3 -3
- package/dist/workstation/config.js +3 -1
- package/dist/workstation/connection/encryptedChannel.js +88 -21
- package/dist/workstation/daemon.js +25 -3
- package/dist/workstation/desktop/binding.js +36 -0
- package/dist/workstation/desktop/bridge.js +212 -0
- package/dist/workstation/desktop/flow-control.js +52 -0
- package/dist/workstation/desktop/focused-text.js +130 -0
- package/dist/workstation/desktop/protocol.js +35 -0
- package/dist/workstation/fileOps.js +277 -0
- package/dist/workstation/filePaths.js +174 -0
- package/dist/workstation/fileWorker.js +81 -0
- package/dist/workstation/fileWorkerClient.js +121 -0
- package/dist/workstation/oauth/deviceAuthorization.js +21 -12
- package/dist/workstation/protocol/agent.js +154 -5
- package/dist/workstation/protocol/common.js +6 -0
- package/dist/workstation/protocol/computer.js +92 -3
- package/dist/workstation/qoderAdapter/backgroundProcess.js +106 -0
- package/dist/workstation/qoderAdapter/backgroundTerminal.js +262 -0
- package/dist/workstation/qoderAdapter/completedContinuation.js +139 -0
- package/dist/workstation/qoderAdapter/contract.js +78 -0
- package/dist/workstation/qoderAdapter/foregroundOutput.js +30 -0
- package/dist/workstation/qoderAdapter/history.js +87 -0
- package/dist/workstation/qoderAdapter/identityIndex.js +191 -0
- package/dist/workstation/qoderAdapter/inputQueue.js +42 -0
- package/dist/workstation/qoderAdapter/main.js +93 -0
- package/dist/workstation/qoderAdapter/mapper.js +117 -0
- package/dist/workstation/qoderAdapter/observerPlugin.js +245 -0
- package/dist/workstation/qoderAdapter/observerReceiver.js +187 -0
- package/dist/workstation/qoderAdapter/permissions.js +61 -0
- package/dist/workstation/qoderAdapter/profile.js +93 -0
- package/dist/workstation/qoderAdapter/sdkDriver.js +49 -0
- package/dist/workstation/qoderAdapter/session.js +319 -0
- package/dist/workstation/qoderAdapter/subagentMeta.js +28 -0
- package/dist/workstation/qoderAdapter/subwork.js +77 -0
- package/dist/workstation/qoderAdapter/taskControl.js +38 -0
- package/dist/workstation/qoderAdapter/taskEvent.js +26 -0
- package/dist/workstation/qoderAdapter/taskLedger.js +163 -0
- package/dist/workstation/qoderAdapter/types.js +1 -0
- package/dist/workstation/qoderAdapter/usage.js +37 -0
- package/dist/workstation/register.js +4 -0
- package/dist/workstation/tools/browser.js +394 -0
- package/dist/workstation/tools/client.js +269 -0
- package/dist/workstation/tools/core.js +50 -0
- package/dist/workstation/tools/files.js +525 -0
- package/dist/workstation/tools/keeperProcess.js +114 -0
- package/dist/workstation/tools/terminal.js +214 -0
- package/dist/workstation/tools/validation.js +50 -0
- package/dist/workstation/update/coordinator.js +29 -0
- package/dist/workstation/update/npmInstaller.js +2 -0
- package/dist/workstation/update/ptyPreflight.js +24 -0
- package/dist/workstation/update/taskRegistry.js +44 -20
- package/dist/workstation/update/watchdog.js +66 -0
- package/package.json +14 -4
- package/skills/axom/SKILL.md +55 -17
- package/skills/axom-definition/SKILL.md +32 -32
- package/skills/axom-platform/SKILL.md +39 -39
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { resolveBrowserBinding, browserCapability } from '../desktop/binding.js';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { promises as fs } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import net from 'node:net';
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { object, TOOL_REQUEST_BYTES, TOOL_RESULT_BYTES, validateRequest } from './validation.js';
|
|
10
|
+
/** Unix socket paths must remain below macOS/Linux's ~104-byte sockaddr_un limit. */
|
|
11
|
+
export function keeperPaths(options, temporaryDirectory = os.tmpdir(), platform = process.platform, kind = 'terminal') {
|
|
12
|
+
const identity = createHash('sha256').update(JSON.stringify([path.resolve(options.configDir), options.workstationId, options.server, ...(kind === 'browser' ? ['browser'] : [])])).digest('hex').slice(0, 32);
|
|
13
|
+
const namespace = `axom-${kind === 'browser' ? 'browser' : 'tools'}-${process.getuid?.() ?? 'user'}`;
|
|
14
|
+
// macOS /var/folders/.../T often exhausts sockaddr_un before adding a private namespace. Honor
|
|
15
|
+
// TMPDIR where it fits on Linux; the short system temporary root is checked for private ownership.
|
|
16
|
+
let dir = path.join(platform === 'darwin' ? '/tmp' : temporaryDirectory, namespace, identity);
|
|
17
|
+
if (Buffer.byteLength(path.join(dir, 'keeper.sock')) > 100)
|
|
18
|
+
dir = path.join('/tmp', namespace, identity);
|
|
19
|
+
// Browser ports are disjoint from terminal startup and the normal Linux ephemeral range.
|
|
20
|
+
return { dir, socket: path.join(dir, 'keeper.sock'), lock: path.join(dir, 'startup.lock'), pidFile: path.join(dir, 'keeper.pid.json'), lockPort: kind === 'browser' ? 10000 + Number.parseInt(identity.slice(0, 8), 16) % 8000 : 20000 + Number.parseInt(identity.slice(0, 8), 16) % 40000, unit: `axom-${kind === 'browser' ? 'browser' : 'tools'}-keeper-${identity}` };
|
|
21
|
+
}
|
|
22
|
+
export async function privateDirectory(dir) {
|
|
23
|
+
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
24
|
+
const stat = await fs.lstat(dir);
|
|
25
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || (process.getuid && stat.uid !== process.getuid()))
|
|
26
|
+
throw new Error('Unsafe keeper state directory');
|
|
27
|
+
await fs.chmod(dir, 0o700);
|
|
28
|
+
}
|
|
29
|
+
export function keeperRpc(socketPath, method, params, timeoutMs = 7000) {
|
|
30
|
+
const id = randomUUID();
|
|
31
|
+
const request = JSON.stringify({ id, method, params }) + '\n';
|
|
32
|
+
if (Buffer.byteLength(request) > TOOL_REQUEST_BYTES)
|
|
33
|
+
return Promise.reject(new Error('Tool request too large'));
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const socket = net.createConnection(socketPath);
|
|
36
|
+
let buffered = '';
|
|
37
|
+
let done = false;
|
|
38
|
+
const finish = (error, result) => {
|
|
39
|
+
if (done)
|
|
40
|
+
return;
|
|
41
|
+
done = true;
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
socket.destroy();
|
|
44
|
+
if (error)
|
|
45
|
+
reject(error);
|
|
46
|
+
else
|
|
47
|
+
resolve(result);
|
|
48
|
+
};
|
|
49
|
+
const timer = setTimeout(() => finish(new Error('Workstation tool request timed out; mutation outcome may be unknown')), timeoutMs);
|
|
50
|
+
socket.setEncoding('utf8');
|
|
51
|
+
socket.on('connect', () => socket.end(request));
|
|
52
|
+
socket.on('data', chunk => {
|
|
53
|
+
buffered += chunk;
|
|
54
|
+
if (Buffer.byteLength(buffered) > TOOL_RESULT_BYTES) {
|
|
55
|
+
finish(new Error('Keeper response too large'));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const newline = buffered.indexOf('\n');
|
|
59
|
+
if (newline < 0)
|
|
60
|
+
return;
|
|
61
|
+
try {
|
|
62
|
+
const reply = JSON.parse(buffered.slice(0, newline));
|
|
63
|
+
if (!object(reply) || reply.id !== id || typeof reply.ok !== 'boolean' || !object(reply.result) || !(reply.error === null || typeof reply.error === 'string') || (reply.ok ? reply.error !== null : !reply.error))
|
|
64
|
+
throw new Error('Invalid keeper response');
|
|
65
|
+
finish(reply.ok ? undefined : new Error(reply.error), reply.result);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
finish(error instanceof Error ? error : new Error('Invalid keeper response'));
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
socket.on('error', error => finish(error));
|
|
72
|
+
socket.on('close', () => finish(new Error('Workstation tools keeper disconnected')));
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
76
|
+
function pidAlive(pid) {
|
|
77
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
78
|
+
return false;
|
|
79
|
+
try {
|
|
80
|
+
process.kill(pid, 0);
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
return error.code === 'EPERM';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
export class ToolsClient {
|
|
88
|
+
options;
|
|
89
|
+
kind;
|
|
90
|
+
paths;
|
|
91
|
+
starting;
|
|
92
|
+
browser;
|
|
93
|
+
constructor(options, kind = 'terminal') {
|
|
94
|
+
this.options = options;
|
|
95
|
+
this.kind = kind;
|
|
96
|
+
this.paths = keeperPaths(options, undefined, undefined, kind);
|
|
97
|
+
}
|
|
98
|
+
async probe(timeoutMs = 250) {
|
|
99
|
+
try {
|
|
100
|
+
const reply = await keeperRpc(this.paths.socket, 'keeper.ping', {}, timeoutMs);
|
|
101
|
+
if (reply.identity !== this.paths.unit || !Number.isSafeInteger(reply.pid))
|
|
102
|
+
return null;
|
|
103
|
+
return { pid: reply.pid };
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async request(method, params) {
|
|
110
|
+
validateRequest(method, params);
|
|
111
|
+
const binding = resolveBrowserBinding(this.options.browserBinding);
|
|
112
|
+
if (!binding && method === 'tools.info') {
|
|
113
|
+
// Desktop sharing is optional. Checking terminal/files must not start a
|
|
114
|
+
// separate browser or require its dependencies when no VNC was configured.
|
|
115
|
+
const metadata = { browser: false, browserMode: 'shared', browserBinding: browserCapability(undefined) };
|
|
116
|
+
return this.kind === 'browser' ? metadata : { ...await this.localRequest(method, params), ...metadata };
|
|
117
|
+
}
|
|
118
|
+
if (binding && (method.startsWith('browser.') || method === 'tools.info')) {
|
|
119
|
+
const metadata = { browser: true, browserMode: 'shared', browserBinding: browserCapability(binding) };
|
|
120
|
+
if (method === 'browser.list')
|
|
121
|
+
return { ...metadata, sessions: [] };
|
|
122
|
+
if (method === 'tools.info')
|
|
123
|
+
return this.kind === 'browser' ? metadata : { ...await this.localRequest(method, params), ...metadata };
|
|
124
|
+
throw new Error('Shared browser uses the live desktop viewer; legacy browser session operations are unavailable');
|
|
125
|
+
}
|
|
126
|
+
if (this.kind === 'terminal' && (method.startsWith('browser.') || method === 'tools.info')) {
|
|
127
|
+
this.browser ??= new ToolsClient(this.options, 'browser');
|
|
128
|
+
if (method.startsWith('browser.'))
|
|
129
|
+
return this.browser.request(method, params);
|
|
130
|
+
const [terminal, browser] = await Promise.all([this.localRequest(method, params), this.browser.request(method, params).catch(error => ({ browser: false, browserError: error.message }))]);
|
|
131
|
+
const result = { ...terminal, ...browser, browserMode: 'legacy', browserBinding: browserCapability(undefined) };
|
|
132
|
+
if (result.browser)
|
|
133
|
+
delete result.browserError;
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
return this.localRequest(method, params);
|
|
137
|
+
}
|
|
138
|
+
async localRequest(method, params) {
|
|
139
|
+
const deadline = Date.now() + 7500;
|
|
140
|
+
await this.ensure(deadline);
|
|
141
|
+
// Never automatically retry a request: terminal input and file mutations may have executed
|
|
142
|
+
// even if their response was lost. Chunk retry is a caller decision with exact offset/data.
|
|
143
|
+
return keeperRpc(this.paths.socket, method, params, Math.max(1, deadline - Date.now()));
|
|
144
|
+
}
|
|
145
|
+
ensure(deadline) {
|
|
146
|
+
if (!this.starting) {
|
|
147
|
+
this.starting = this.start(deadline).finally(() => { this.starting = undefined; });
|
|
148
|
+
}
|
|
149
|
+
return this.starting;
|
|
150
|
+
}
|
|
151
|
+
async start(deadline) {
|
|
152
|
+
await privateDirectory(path.dirname(this.paths.dir));
|
|
153
|
+
await privateDirectory(this.paths.dir);
|
|
154
|
+
if (await this.probe())
|
|
155
|
+
return;
|
|
156
|
+
while (Date.now() < deadline - 500) {
|
|
157
|
+
if (await this.probe())
|
|
158
|
+
return;
|
|
159
|
+
let lock;
|
|
160
|
+
try {
|
|
161
|
+
lock = await this.acquireStartupLock();
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
if (error.code !== 'EADDRINUSE')
|
|
165
|
+
throw error;
|
|
166
|
+
await delay(50);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
if (await this.probe())
|
|
171
|
+
return;
|
|
172
|
+
// A probe timeout is not process death. A busy emulator can delay ping; unlinking its live
|
|
173
|
+
// socket would orphan every PTY. The startup PID manifest is written before initialization.
|
|
174
|
+
while (await this.liveManifest()) {
|
|
175
|
+
if (await this.probe())
|
|
176
|
+
return;
|
|
177
|
+
if (Date.now() >= deadline - 300)
|
|
178
|
+
throw new Error('Tools keeper is alive but busy or unavailable; retry later');
|
|
179
|
+
await delay(50);
|
|
180
|
+
}
|
|
181
|
+
await fs.unlink(this.paths.socket).catch(error => { if (error.code !== 'ENOENT')
|
|
182
|
+
throw error; });
|
|
183
|
+
const launchConfig = Buffer.from(JSON.stringify({ socket: this.paths.socket, identity: this.paths.unit, home: this.options.home, roots: this.options.roots ?? [], kind: this.kind, profileRoot: path.join(path.resolve(this.options.configDir), 'browser-profiles', this.paths.unit), executablePath: this.options.browserExecutable ?? process.env.AXOM_BROWSER_EXECUTABLE, proxy: this.options.browserProxy ?? process.env.AXOM_BROWSER_PROXY })).toString('base64');
|
|
184
|
+
const script = fileURLToPath(new URL('./keeperProcess.js', import.meta.url));
|
|
185
|
+
if (script.endsWith('/src/workstation/tools/keeperProcess.js'))
|
|
186
|
+
throw new Error('Tools keeper requires built CLI distribution');
|
|
187
|
+
let isolated = false;
|
|
188
|
+
if (process.platform === 'linux' && this.options.useSystemd !== false && await this.systemdAvailable()) {
|
|
189
|
+
// setsid/detached cannot escape systemd's KillMode=control-group. A separate user service
|
|
190
|
+
// gives keeper/PTYS their own cgroup and allows the transport daemon to restart safely.
|
|
191
|
+
isolated = await this.systemdLaunch(script, launchConfig, deadline);
|
|
192
|
+
if (!isolated) {
|
|
193
|
+
// An existing user unit may still be starting. Never spawn a same-cgroup fallback
|
|
194
|
+
// on a systemd host, where a daemon restart would silently kill every live terminal.
|
|
195
|
+
while (Date.now() < deadline - 100) {
|
|
196
|
+
if (await this.probe())
|
|
197
|
+
return;
|
|
198
|
+
await delay(50);
|
|
199
|
+
}
|
|
200
|
+
throw new Error('Cannot start isolated tools keeper user service');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (!isolated) {
|
|
204
|
+
const child = spawn(process.execPath, [script, launchConfig], { detached: true, stdio: 'ignore' });
|
|
205
|
+
child.on('error', () => { });
|
|
206
|
+
child.unref();
|
|
207
|
+
}
|
|
208
|
+
while (Date.now() < deadline - 100) {
|
|
209
|
+
if (await this.probe())
|
|
210
|
+
return;
|
|
211
|
+
await delay(50);
|
|
212
|
+
}
|
|
213
|
+
throw new Error('Workstation tools keeper failed to start');
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
await new Promise(resolve => lock.close(() => resolve()));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
throw new Error('Workstation tools keeper startup is busy or timed out');
|
|
220
|
+
}
|
|
221
|
+
/** A kernel bind is an atomic cross-process lock with automatic release on process death.
|
|
222
|
+
* It carries no requests, and needs no stale-file unlink or ownership-blind finally cleanup.
|
|
223
|
+
* Rare identity-port collisions only serialize startup; all data still uses the private socket. */
|
|
224
|
+
acquireStartupLock() {
|
|
225
|
+
return new Promise((resolve, reject) => {
|
|
226
|
+
const lock = net.createServer(socket => socket.destroy());
|
|
227
|
+
lock.once('error', reject);
|
|
228
|
+
lock.listen({ host: '127.0.0.1', port: this.paths.lockPort, exclusive: true }, () => resolve(lock));
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
async liveManifest() {
|
|
232
|
+
try {
|
|
233
|
+
const stat = await fs.lstat(this.paths.pidFile);
|
|
234
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 4096)
|
|
235
|
+
throw new Error('Invalid tools keeper PID manifest');
|
|
236
|
+
const value = JSON.parse(await fs.readFile(this.paths.pidFile, 'utf8'));
|
|
237
|
+
if (value.identity !== this.paths.unit)
|
|
238
|
+
throw new Error('Tools keeper identity mismatch');
|
|
239
|
+
return pidAlive(value.pid);
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
if (error.code === 'ENOENT')
|
|
243
|
+
return false;
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
async systemdAvailable() {
|
|
248
|
+
return new Promise(resolve => {
|
|
249
|
+
const child = spawn('systemctl', ['--user', 'show-environment'], { stdio: 'ignore' });
|
|
250
|
+
let done = false;
|
|
251
|
+
const finish = (ok) => { if (done)
|
|
252
|
+
return; done = true; clearTimeout(timer); resolve(ok); };
|
|
253
|
+
const timer = setTimeout(() => { child.kill(); finish(false); }, 500);
|
|
254
|
+
child.on('error', () => finish(false));
|
|
255
|
+
child.on('exit', code => finish(code === 0));
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
async systemdLaunch(script, config, deadline) {
|
|
259
|
+
return new Promise(resolve => {
|
|
260
|
+
const child = spawn('systemd-run', ['--user', '--quiet', '--collect', `--unit=${this.paths.unit}`, '--property=Type=exec', '--property=Restart=on-failure', '--property=RestartSec=1', '--', process.execPath, script, config], { stdio: 'ignore' });
|
|
261
|
+
let done = false;
|
|
262
|
+
const finish = (ok) => { if (done)
|
|
263
|
+
return; done = true; clearTimeout(timer); resolve(ok); };
|
|
264
|
+
const timer = setTimeout(() => { child.kill(); finish(false); }, Math.max(1, Math.min(2000, deadline - Date.now() - 500)));
|
|
265
|
+
child.on('error', () => finish(false));
|
|
266
|
+
child.on('exit', code => finish(code === 0));
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import { StructuredFiles } from './files.js';
|
|
3
|
+
import { PersistentTerminals } from './terminal.js';
|
|
4
|
+
import { keys, validateRequest, TOOL_RESULT_BYTES } from './validation.js';
|
|
5
|
+
/** All mutations are serialized across clients; shell processes continue independently. */
|
|
6
|
+
export class ToolsCore {
|
|
7
|
+
files;
|
|
8
|
+
terminals;
|
|
9
|
+
queue = Promise.resolve();
|
|
10
|
+
disposed = false;
|
|
11
|
+
timer;
|
|
12
|
+
constructor(files, terminals, ttl) {
|
|
13
|
+
this.files = files;
|
|
14
|
+
this.terminals = terminals;
|
|
15
|
+
this.timer = setInterval(() => { this.queue = this.queue.then(() => this.files.expire()).catch(() => { }); }, Math.min(ttl, 60_000));
|
|
16
|
+
this.timer.unref();
|
|
17
|
+
}
|
|
18
|
+
static async create(options = {}) {
|
|
19
|
+
const files = await StructuredFiles.create(options.home ?? os.homedir(), options.roots ?? [], options.uploadTtlMs, options.uploadStateDir);
|
|
20
|
+
return new ToolsCore(files, new PersistentTerminals(files, options), options.uploadTtlMs ?? 15 * 60_000);
|
|
21
|
+
}
|
|
22
|
+
dispatch(method, params = {}) {
|
|
23
|
+
const task = this.queue.then(async () => {
|
|
24
|
+
if (this.disposed)
|
|
25
|
+
throw new Error('Workstation tools keeper stopped');
|
|
26
|
+
validateRequest(method, params);
|
|
27
|
+
let result;
|
|
28
|
+
if (method === 'tools.info') {
|
|
29
|
+
keys(params, []);
|
|
30
|
+
result = { version: 1, terminal: this.terminals.available, browser: false, files: true, home: this.files.home, roots: this.files.roots, downloads: this.files.downloads, ...(this.terminals.error ? { terminalError: this.terminals.error } : {}), browserError: 'Browser tools are not enabled in this release' };
|
|
31
|
+
}
|
|
32
|
+
else if (method.startsWith('terminal.'))
|
|
33
|
+
result = await this.terminals.dispatch(method, params);
|
|
34
|
+
else
|
|
35
|
+
result = await this.files.dispatch(method, params);
|
|
36
|
+
if (Buffer.byteLength(JSON.stringify(result)) > TOOL_RESULT_BYTES - 1024)
|
|
37
|
+
throw new Error('Tool result too large');
|
|
38
|
+
return result;
|
|
39
|
+
});
|
|
40
|
+
this.queue = task.catch(() => { });
|
|
41
|
+
return task;
|
|
42
|
+
}
|
|
43
|
+
async dispose() {
|
|
44
|
+
this.disposed = true;
|
|
45
|
+
clearInterval(this.timer);
|
|
46
|
+
await this.queue;
|
|
47
|
+
await this.terminals.dispose();
|
|
48
|
+
await this.files.dispose();
|
|
49
|
+
}
|
|
50
|
+
}
|