agent-device-lab 0.0.0-stage → 0.3.1

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.
Files changed (54) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +10 -0
  3. package/README.md +150 -2
  4. package/assets/dashboard/app.css +238 -0
  5. package/assets/dashboard/app.js +976 -0
  6. package/assets/dashboard/index.html +120 -0
  7. package/bin/agentlab.js +8 -0
  8. package/dist/cli/ci.js +443 -0
  9. package/dist/cli/flow.js +127 -0
  10. package/dist/cli/main.js +855 -0
  11. package/dist/cli/signals.js +38 -0
  12. package/dist/core/action-log.js +125 -0
  13. package/dist/core/auth.js +175 -0
  14. package/dist/core/bundle.js +432 -0
  15. package/dist/core/checks.js +137 -0
  16. package/dist/core/ci.js +389 -0
  17. package/dist/core/commands.js +366 -0
  18. package/dist/core/control.js +217 -0
  19. package/dist/core/detectors.js +392 -0
  20. package/dist/core/devices.js +83 -0
  21. package/dist/core/doctor.js +203 -0
  22. package/dist/core/explore-safety.js +110 -0
  23. package/dist/core/extract.js +1445 -0
  24. package/dist/core/feed.js +493 -0
  25. package/dist/core/findings.js +179 -0
  26. package/dist/core/format.js +311 -0
  27. package/dist/core/init.js +607 -0
  28. package/dist/core/lab.js +1979 -0
  29. package/dist/core/migrate.js +35 -0
  30. package/dist/core/observation.js +161 -0
  31. package/dist/core/ownership.js +142 -0
  32. package/dist/core/process-group.js +206 -0
  33. package/dist/core/process-identity.js +56 -0
  34. package/dist/core/profile.js +816 -0
  35. package/dist/core/project-runner.js +359 -0
  36. package/dist/core/replay.js +303 -0
  37. package/dist/core/reproduction.js +36 -0
  38. package/dist/core/scan-policy.js +156 -0
  39. package/dist/core/scan-report.js +145 -0
  40. package/dist/core/scan.js +455 -0
  41. package/dist/core/schema.js +39 -0
  42. package/dist/core/services.js +134 -0
  43. package/dist/core/steps.js +80 -0
  44. package/dist/core/sweep.js +138 -0
  45. package/dist/core/trace-sanitize.js +641 -0
  46. package/dist/core/url-redact.js +25 -0
  47. package/dist/core/versions.js +29 -0
  48. package/dist/core/zip.js +264 -0
  49. package/dist/daemon/main.js +160 -0
  50. package/dist/daemon/state.js +93 -0
  51. package/dist/dashboard/open.js +25 -0
  52. package/dist/dashboard/server.js +366 -0
  53. package/dist/mcp/server.js +89 -0
  54. package/package.json +63 -4
@@ -0,0 +1,264 @@
1
+ import { deflateRawSync, inflateRawSync } from 'node:zlib';
2
+ export class ZipError extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = 'ZipError';
6
+ }
7
+ }
8
+ export const ZIP_MAX_ENTRIES = 20_000;
9
+ export const ZIP_MAX_TOTAL_BYTES = 1024 ** 3;
10
+ const SIG_LOCAL = 0x04034b50;
11
+ const SIG_CENTRAL = 0x02014b50;
12
+ const SIG_EOCD = 0x06054b50;
13
+ const SIG_ZIP64_LOCATOR = 0x07064b50;
14
+ const SIG_DESCRIPTOR = 0x08074b50;
15
+ let crcTable;
16
+ function table() {
17
+ if (crcTable)
18
+ return crcTable;
19
+ const t = new Uint32Array(256);
20
+ for (let n = 0; n < 256; n++) {
21
+ let c = n;
22
+ for (let k = 0; k < 8; k++)
23
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
24
+ t[n] = c >>> 0;
25
+ }
26
+ crcTable = t;
27
+ return t;
28
+ }
29
+ export function crc32(data) {
30
+ const t = table();
31
+ let c = 0xffffffff;
32
+ for (let i = 0; i < data.length; i++)
33
+ c = t[(c ^ data[i]) & 0xff] ^ (c >>> 8);
34
+ return (c ^ 0xffffffff) >>> 0;
35
+ }
36
+ /** Throws for names that could escape an extraction directory or that are ambiguous. */
37
+ export function checkZipName(name) {
38
+ if (name.length === 0)
39
+ throw new ZipError('zip entry with an empty name');
40
+ if (name.includes('\0'))
41
+ throw new ZipError('zip entry name contains a NUL byte');
42
+ if (name.includes('\\'))
43
+ throw new ZipError('zip entry name contains a backslash');
44
+ if (name.startsWith('/') || /^[A-Za-z]:/.test(name))
45
+ throw new ZipError('zip entry name is an absolute path');
46
+ if (name.split('/').includes('..'))
47
+ throw new ZipError('zip entry name contains a ".." segment');
48
+ }
49
+ export function readZip(buf) {
50
+ if (buf.length < 22)
51
+ throw new ZipError('not a zip archive (too short)');
52
+ // End of central directory: the last record whose comment length reaches exactly to the end of the file.
53
+ let eocd = -1;
54
+ for (let i = buf.length - 22; i >= Math.max(0, buf.length - 22 - 0xffff); i--) {
55
+ if (buf.readUInt32LE(i) === SIG_EOCD && i + 22 + buf.readUInt16LE(i + 20) === buf.length) {
56
+ eocd = i;
57
+ break;
58
+ }
59
+ }
60
+ if (eocd < 0)
61
+ throw new ZipError('not a zip archive (no end of central directory)');
62
+ if (eocd >= 20 && buf.readUInt32LE(eocd - 20) === SIG_ZIP64_LOCATOR)
63
+ throw new ZipError('zip64 archives are not supported');
64
+ const diskNo = buf.readUInt16LE(eocd + 4);
65
+ const cdDisk = buf.readUInt16LE(eocd + 6);
66
+ const countHere = buf.readUInt16LE(eocd + 8);
67
+ const count = buf.readUInt16LE(eocd + 10);
68
+ const cdSize = buf.readUInt32LE(eocd + 12);
69
+ const cdOffset = buf.readUInt32LE(eocd + 16);
70
+ if (diskNo !== 0 || cdDisk !== 0 || countHere !== count)
71
+ throw new ZipError('multi-disk zip archives are not supported');
72
+ if (count === 0xffff || cdSize === 0xffffffff || cdOffset === 0xffffffff)
73
+ throw new ZipError('zip64 archives are not supported');
74
+ if (count > ZIP_MAX_ENTRIES)
75
+ throw new ZipError(`zip has more than ${ZIP_MAX_ENTRIES} entries`);
76
+ if (cdOffset + cdSize > eocd)
77
+ throw new ZipError('central directory is out of bounds');
78
+ const records = [];
79
+ const seen = new Set();
80
+ let p = cdOffset;
81
+ for (let n = 0; n < count; n++) {
82
+ if (p + 46 > cdOffset + cdSize || buf.readUInt32LE(p) !== SIG_CENTRAL)
83
+ throw new ZipError('bad central directory record');
84
+ const flags = buf.readUInt16LE(p + 8);
85
+ const method = buf.readUInt16LE(p + 10);
86
+ const crc = buf.readUInt32LE(p + 16);
87
+ const csize = buf.readUInt32LE(p + 20);
88
+ const usize = buf.readUInt32LE(p + 24);
89
+ const nameLen = buf.readUInt16LE(p + 28);
90
+ const extraLen = buf.readUInt16LE(p + 30);
91
+ const commentLen = buf.readUInt16LE(p + 32);
92
+ const disk = buf.readUInt16LE(p + 34);
93
+ const offset = buf.readUInt32LE(p + 42);
94
+ const end = p + 46 + nameLen + extraLen + commentLen;
95
+ if (end > cdOffset + cdSize)
96
+ throw new ZipError('central directory record is out of bounds');
97
+ if (flags & 0x1 || flags & 0x40 || flags & 0x2000)
98
+ throw new ZipError('encrypted zip entries are not supported');
99
+ if (method !== 0 && method !== 8)
100
+ throw new ZipError(`unsupported zip compression method ${method}`);
101
+ if (csize === 0xffffffff || usize === 0xffffffff || offset === 0xffffffff || disk !== 0)
102
+ throw new ZipError('zip64 archives are not supported');
103
+ // Extra fields: a zip64 field means the sizes above are not the real ones.
104
+ let x = p + 46 + nameLen;
105
+ const xEnd = x + extraLen;
106
+ while (x + 4 <= xEnd) {
107
+ const id = buf.readUInt16LE(x);
108
+ const len = buf.readUInt16LE(x + 2);
109
+ if (id === 0x0001 || id === 0x9901)
110
+ throw new ZipError('zip64 or encrypted extra fields are not supported');
111
+ x += 4 + len;
112
+ }
113
+ const nameBytes = buf.subarray(p + 46, p + 46 + nameLen);
114
+ let name = nameBytes.toString('utf8');
115
+ const dir = name.endsWith('/');
116
+ if (dir)
117
+ name = name.slice(0, -1);
118
+ checkZipName(name);
119
+ if (seen.has(name))
120
+ throw new ZipError('duplicate zip entry name');
121
+ seen.add(name);
122
+ if (method === 0 && csize !== usize)
123
+ throw new ZipError('stored zip entry has mismatched sizes');
124
+ records.push({ name, method, flags, crc, csize, usize, offset, nameBytes: Buffer.from(nameBytes), dir });
125
+ p = end;
126
+ }
127
+ if (p !== cdOffset + cdSize)
128
+ throw new ZipError('central directory size mismatch');
129
+ const extents = [];
130
+ const entries = [];
131
+ let total = 0;
132
+ for (const r of records) {
133
+ if (r.offset + 30 > cdOffset || buf.readUInt32LE(r.offset) !== SIG_LOCAL)
134
+ throw new ZipError('bad local header');
135
+ const lflags = buf.readUInt16LE(r.offset + 6);
136
+ const lmethod = buf.readUInt16LE(r.offset + 8);
137
+ const lcrc = buf.readUInt32LE(r.offset + 14);
138
+ const lcsize = buf.readUInt32LE(r.offset + 18);
139
+ const lusize = buf.readUInt32LE(r.offset + 22);
140
+ const lnameLen = buf.readUInt16LE(r.offset + 26);
141
+ const lextraLen = buf.readUInt16LE(r.offset + 28);
142
+ if (lmethod !== r.method)
143
+ throw new ZipError('local header disagrees with the central directory (method)');
144
+ if (lflags & 0x1)
145
+ throw new ZipError('encrypted zip entries are not supported');
146
+ const nameStart = r.offset + 30;
147
+ if (nameStart + lnameLen > cdOffset || !buf.subarray(nameStart, nameStart + lnameLen).equals(r.nameBytes)) {
148
+ throw new ZipError('local header disagrees with the central directory (name)');
149
+ }
150
+ const dataStart = nameStart + lnameLen + lextraLen;
151
+ const dataEnd = dataStart + r.csize;
152
+ if (dataEnd > cdOffset)
153
+ throw new ZipError('zip entry data is out of bounds');
154
+ let entryEnd = dataEnd;
155
+ if (lflags & 0x8) {
156
+ // Sizes and CRC follow the data; they must agree with the central directory.
157
+ let d = dataEnd;
158
+ if (d + 4 <= cdOffset && buf.readUInt32LE(d) === SIG_DESCRIPTOR)
159
+ d += 4;
160
+ if (d + 12 > cdOffset)
161
+ throw new ZipError('missing data descriptor');
162
+ if (buf.readUInt32LE(d) !== r.crc || buf.readUInt32LE(d + 4) !== r.csize || buf.readUInt32LE(d + 8) !== r.usize) {
163
+ throw new ZipError('data descriptor disagrees with the central directory');
164
+ }
165
+ entryEnd = d + 12;
166
+ }
167
+ else if (lcrc !== r.crc || lcsize !== r.csize || lusize !== r.usize) {
168
+ throw new ZipError('local header disagrees with the central directory (sizes)');
169
+ }
170
+ extents.push([r.offset, entryEnd]);
171
+ if (r.dir) {
172
+ if (r.usize !== 0)
173
+ throw new ZipError('zip directory entry has content');
174
+ continue;
175
+ }
176
+ total += r.usize;
177
+ if (total > ZIP_MAX_TOTAL_BYTES)
178
+ throw new ZipError('zip expands to more than 1 GiB');
179
+ const raw = buf.subarray(dataStart, dataEnd);
180
+ let data;
181
+ if (r.method === 0)
182
+ data = Buffer.from(raw);
183
+ else {
184
+ try {
185
+ data = inflateRawSync(raw, { maxOutputLength: Math.max(r.usize, 1) });
186
+ }
187
+ catch {
188
+ throw new ZipError('zip entry cannot be inflated (corrupt or larger than declared)');
189
+ }
190
+ }
191
+ if (data.length !== r.usize)
192
+ throw new ZipError('zip entry size does not match its header');
193
+ if (crc32(data) !== r.crc)
194
+ throw new ZipError('zip entry fails its CRC check');
195
+ entries.push({ name: r.name, data });
196
+ }
197
+ extents.sort((a, b) => a[0] - b[0]);
198
+ for (let i = 1; i < extents.length; i++) {
199
+ if (extents[i][0] < extents[i - 1][1])
200
+ throw new ZipError('zip entries overlap');
201
+ }
202
+ return entries;
203
+ }
204
+ export function writeZip(entries) {
205
+ if (entries.length > 0xfffe)
206
+ throw new ZipError('too many zip entries to write');
207
+ const seen = new Set();
208
+ const parts = [];
209
+ const central = [];
210
+ let offset = 0;
211
+ for (const e of entries) {
212
+ checkZipName(e.name);
213
+ if (seen.has(e.name))
214
+ throw new ZipError('duplicate zip entry name');
215
+ seen.add(e.name);
216
+ const name = Buffer.from(e.name, 'utf8');
217
+ const compressed = deflateRawSync(e.data, { level: 6 });
218
+ const crc = crc32(e.data);
219
+ if (e.data.length >= 0xffffffff || compressed.length >= 0xffffffff || offset >= 0xffffffff)
220
+ throw new ZipError('zip entry too large to write');
221
+ const local = Buffer.alloc(30);
222
+ local.writeUInt32LE(SIG_LOCAL, 0);
223
+ local.writeUInt16LE(20, 4); // version needed
224
+ local.writeUInt16LE(0x0800, 6); // UTF-8 names
225
+ local.writeUInt16LE(8, 8); // deflate
226
+ local.writeUInt16LE(0, 10); // time
227
+ local.writeUInt16LE(0x0021, 12); // date: 1980-01-01, fixed so output is reproducible
228
+ local.writeUInt32LE(crc, 14);
229
+ local.writeUInt32LE(compressed.length, 18);
230
+ local.writeUInt32LE(e.data.length, 22);
231
+ local.writeUInt16LE(name.length, 26);
232
+ local.writeUInt16LE(0, 28);
233
+ parts.push(local, name, compressed);
234
+ const cd = Buffer.alloc(46);
235
+ cd.writeUInt32LE(SIG_CENTRAL, 0);
236
+ cd.writeUInt16LE((3 << 8) | 20, 4); // made by: unix, 2.0
237
+ cd.writeUInt16LE(20, 6);
238
+ cd.writeUInt16LE(0x0800, 8);
239
+ cd.writeUInt16LE(8, 10);
240
+ cd.writeUInt16LE(0, 12);
241
+ cd.writeUInt16LE(0x0021, 14);
242
+ cd.writeUInt32LE(crc, 16);
243
+ cd.writeUInt32LE(compressed.length, 20);
244
+ cd.writeUInt32LE(e.data.length, 24);
245
+ cd.writeUInt16LE(name.length, 28);
246
+ cd.writeUInt16LE(0, 30);
247
+ cd.writeUInt16LE(0, 32);
248
+ cd.writeUInt16LE(0, 34);
249
+ cd.writeUInt16LE(0, 36);
250
+ cd.writeUInt32LE(((0o100644 << 16) >>> 0), 38);
251
+ cd.writeUInt32LE(offset, 42);
252
+ central.push(cd, name);
253
+ offset += local.length + name.length + compressed.length;
254
+ }
255
+ const cdBuf = Buffer.concat(central);
256
+ const eocd = Buffer.alloc(22);
257
+ eocd.writeUInt32LE(SIG_EOCD, 0);
258
+ eocd.writeUInt16LE(entries.length, 8);
259
+ eocd.writeUInt16LE(entries.length, 10);
260
+ eocd.writeUInt32LE(cdBuf.length, 12);
261
+ eocd.writeUInt32LE(offset, 16);
262
+ return Buffer.concat([...parts, cdBuf, eocd]);
263
+ }
264
+ //# sourceMappingURL=zip.js.map
@@ -0,0 +1,160 @@
1
+ // Long-lived session process. `agentlab start` spawns it detached; later CLI invocations talk to it
2
+ // over a unix socket. It owns the browser, the session and any server it started, and writes an
3
+ // ordered action log to stdout (redirected to .agentlab/daemon.log). Unless started with --no-ui it
4
+ // also serves the live dashboard on 127.0.0.1.
5
+ import { createServer } from 'node:http';
6
+ import { chmodSync, mkdirSync, rmSync } from 'node:fs';
7
+ import { SessionHost, dispatch } from '../core/commands.js';
8
+ import { SessionFeed } from '../core/feed.js';
9
+ import { formatAction, formatClose, formatControlChange, formatHuman, formatService } from '../core/format.js';
10
+ import { identify } from '../core/process-identity.js';
11
+ import { Dashboard } from '../dashboard/server.js';
12
+ import { clearState, paths, writeState } from './state.js';
13
+ const dir = process.argv[2];
14
+ const ui = !process.argv.includes('--no-ui');
15
+ if (!dir) {
16
+ console.error('usage: daemon <stateDir> [--no-ui]');
17
+ process.exit(2);
18
+ }
19
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
20
+ const { socket } = paths(dir);
21
+ const ts = () => new Date().toISOString().slice(11, 23);
22
+ const logBlock = (head, body) => {
23
+ process.stdout.write(`[${ts()}] ${head}\n`);
24
+ if (body)
25
+ process.stdout.write(body.split('\n').map((l) => ` ${l}`).join('\n') + '\n');
26
+ };
27
+ /** The service output echoed into daemon.log stops here (env AGENTLAB_DAEMON_ECHO_MAX_BYTES); the service's own log file is capped separately. */
28
+ const echoMax = Number(process.env.AGENTLAB_DAEMON_ECHO_MAX_BYTES) > 0 ? Number(process.env.AGENTLAB_DAEMON_ECHO_MAX_BYTES) : 5 * 1024 * 1024;
29
+ let echoed = 0;
30
+ function echoServerLine(line) {
31
+ if (echoed > echoMax)
32
+ return;
33
+ const text = `[${ts()}] [web] ${line}\n`;
34
+ echoed += Buffer.byteLength(text);
35
+ if (echoed > echoMax)
36
+ process.stdout.write(`[${ts()}] [web] … server output in this log stopped at ${echoMax} bytes; the full output is in the run's service log files\n`);
37
+ else
38
+ process.stdout.write(text);
39
+ }
40
+ const state = { pid: process.pid, daemon: identify(process.pid), socket, startedAt: new Date().toISOString() };
41
+ const feed = new SessionFeed();
42
+ function onEvent(e) {
43
+ feed.apply(e);
44
+ switch (e.kind) {
45
+ case 'server-log':
46
+ echoServerLine(e.line);
47
+ break;
48
+ case 'start': {
49
+ const { session, server, observation } = e.result;
50
+ state.sessionId = session.id;
51
+ state.server = { url: server.url, owned: server.owned, pid: server.pid, identity: host.lab.serverIdentity() };
52
+ state.services = host.lab.serviceRecords();
53
+ writeState(dir, state);
54
+ logBlock(`start ${session.id} ${session.device.id} ${session.browser.headed ? 'headed' : 'headless'}; server ${server.owned ? `started pid ${server.pid} (owned)` : 'reused (not owned)'}; route ${observation.route}`);
55
+ if (e.result.services.length > 1)
56
+ logBlock('services', e.result.services.map(formatService).join('\n'));
57
+ break;
58
+ }
59
+ case 'observe':
60
+ logBlock(`observe gen ${e.observation.gen} route ${e.observation.route} controls ${e.observation.controls.length} layout flags ${e.observation.layout.length}`);
61
+ break;
62
+ case 'act': {
63
+ const [head, ...rest] = formatAction(e.result).split('\n');
64
+ // Keep the log readable: a navigation embeds a full observation, which the log does not need.
65
+ logBlock(head ?? '', rest.filter((l) => /^(note|changed|console):/.test(l)).join('\n'));
66
+ break;
67
+ }
68
+ case 'control':
69
+ logBlock(`control: ${formatControlChange(e.change)}`);
70
+ break;
71
+ case 'human':
72
+ logBlock(`person ${formatHuman(e.action)}`);
73
+ break;
74
+ case 'refused':
75
+ logBlock(`${e.command} refused: ${e.error.code}`);
76
+ break;
77
+ case 'closed':
78
+ logBlock(formatClose(e.result));
79
+ shutdown(0);
80
+ break;
81
+ }
82
+ }
83
+ const host = new SessionHost({ stateDir: dir, onEvent, evidenceFrames: ui });
84
+ const dashboard = ui ? await Dashboard.listen({
85
+ feed, source: () => host.lab,
86
+ control: (op, by) => host.supervise(op, by),
87
+ input: (i) => host.lab.humanInput(i),
88
+ }) : undefined;
89
+ if (dashboard) {
90
+ // Agents get the view-only URL; the person's control URL stays in the owner-only daemon.json (`agentlab ui`).
91
+ host.dashboardUrl = () => dashboard.viewUrl;
92
+ state.dashboard = { url: dashboard.url };
93
+ }
94
+ let queue = Promise.resolve();
95
+ let shuttingDown = false;
96
+ const server = createServer((req, res) => {
97
+ if (req.method !== 'POST' || req.url !== '/rpc') {
98
+ res.writeHead(404).end();
99
+ return;
100
+ }
101
+ let body = '';
102
+ req.setEncoding('utf8');
103
+ req.on('data', (c) => { body += c; });
104
+ req.on('end', () => {
105
+ // Commands run strictly one at a time against the single page.
106
+ queue = queue.then(async () => {
107
+ let response;
108
+ let command = '?';
109
+ let args;
110
+ try {
111
+ ({ command, args } = JSON.parse(body));
112
+ }
113
+ catch { /* dispatch reports the unknown command */ }
114
+ const out = await dispatch(host, 'cli', command, args);
115
+ if (out.ok) {
116
+ response = { ok: true, result: out.output.result, text: out.output.text };
117
+ }
118
+ else {
119
+ response = { ok: false, error: out.error };
120
+ logBlock(`${command} failed: ${out.error.code}: ${out.error.message}`);
121
+ }
122
+ res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(response));
123
+ if (command === 'start' && !response.ok)
124
+ shutdown(1);
125
+ });
126
+ });
127
+ });
128
+ function shutdown(code) {
129
+ if (shuttingDown)
130
+ return;
131
+ shuttingDown = true;
132
+ // Give in-flight responses (and the dashboard's final status) a moment to flush first.
133
+ setTimeout(async () => {
134
+ server.close();
135
+ clearState(dir);
136
+ await dashboard?.close().catch(() => undefined);
137
+ process.exit(code);
138
+ }, 100);
139
+ }
140
+ for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
141
+ process.on(sig, () => {
142
+ logBlock(`received ${sig}`);
143
+ void host.lab.close(`daemon received ${sig}`).then(() => shutdown(0));
144
+ });
145
+ }
146
+ process.on('uncaughtException', (err) => {
147
+ logBlock(`uncaught error: ${err.stack ?? err.message}`);
148
+ void host.lab.close('daemon crashed').finally(() => shutdown(1));
149
+ });
150
+ rmSync(socket, { force: true });
151
+ // Only the owner may connect: bind under a restrictive umask (the fallback path lives in the shared temp
152
+ // directory), then set the mode explicitly.
153
+ const previousUmask = process.umask(0o177);
154
+ server.listen(socket, () => {
155
+ chmodSync(socket, 0o600);
156
+ writeState(dir, state);
157
+ logBlock(`daemon ${process.pid} listening on ${socket}${dashboard ? `; dashboard on ${dashboard.origin}` : ''}`);
158
+ });
159
+ process.umask(previousUmask);
160
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1,93 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { request } from 'node:http';
4
+ import { homedir, tmpdir } from 'node:os';
5
+ import { basename, join, resolve } from 'node:path';
6
+ import { checkIdentity, identify } from '../core/process-identity.js';
7
+ /**
8
+ * Where runs, logs and the daemon record live. AGENTLAB_HOME wins. A directory that was initialised
9
+ * (it has agentlab.json or .agentlab/) keeps them in ./.agentlab; anywhere else they go under the
10
+ * user's state directory, so running agentlab never litters an unrelated directory.
11
+ */
12
+ export function stateDir(cwd = process.cwd()) {
13
+ if (process.env.AGENTLAB_HOME)
14
+ return resolve(process.env.AGENTLAB_HOME);
15
+ const local = join(cwd, '.agentlab');
16
+ if (existsSync(local) || existsSync(join(cwd, 'agentlab.json')))
17
+ return local;
18
+ return join(userStateRoot(), 'projects', `${basename(cwd).replace(/[^\w.-]/g, '_') || 'root'}-${createHash('sha1').update(cwd).digest('hex').slice(0, 10)}`);
19
+ }
20
+ /** $XDG_STATE_HOME/agentlab, else ~/.local/state/agentlab. `rm -rf` of it removes everything the lab kept outside projects. */
21
+ export function userStateRoot() {
22
+ return join(process.env.XDG_STATE_HOME || join(homedir(), '.local', 'state'), 'agentlab');
23
+ }
24
+ export const paths = (dir) => {
25
+ const preferred = join(dir, 'daemon.sock');
26
+ // Unix socket paths are limited to ~107 bytes; fall back to a hashed name in the temp dir.
27
+ const socket = preferred.length < 100 ? preferred : join(tmpdir(), `agentlab-${createHash('sha1').update(dir).digest('hex').slice(0, 12)}.sock`);
28
+ return { socket, state: join(dir, 'daemon.json'), log: join(dir, 'daemon.log') };
29
+ };
30
+ export function readState(dir) {
31
+ try {
32
+ return JSON.parse(readFileSync(paths(dir).state, 'utf8'));
33
+ }
34
+ catch {
35
+ return undefined;
36
+ }
37
+ }
38
+ export function writeState(dir, state) {
39
+ const file = paths(dir).state;
40
+ writeFileSync(file, JSON.stringify(state, null, 2), { mode: 0o600 });
41
+ chmodSync(file, 0o600);
42
+ }
43
+ export function clearState(dir) {
44
+ const p = paths(dir);
45
+ rmSync(p.state, { force: true });
46
+ if (existsSync(p.socket))
47
+ rmSync(p.socket, { force: true });
48
+ }
49
+ const dashboardFile = (dir) => join(dir, 'mcp-dashboard.json');
50
+ export function writeDashboardRecord(dir, url) {
51
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
52
+ const record = { url, pid: process.pid, identity: identify(process.pid) };
53
+ writeFileSync(dashboardFile(dir), JSON.stringify(record, null, 2), { mode: 0o600 });
54
+ chmodSync(dashboardFile(dir), 0o600);
55
+ }
56
+ export function clearDashboardRecord(dir) {
57
+ rmSync(dashboardFile(dir), { force: true });
58
+ }
59
+ /** The live MCP server's dashboard record, if its process is still the one that wrote it. */
60
+ export function readDashboardRecord(dir) {
61
+ try {
62
+ const r = JSON.parse(readFileSync(dashboardFile(dir), 'utf8'));
63
+ return checkIdentity(r.identity) === 'same' ? r : undefined;
64
+ }
65
+ catch {
66
+ return undefined;
67
+ }
68
+ }
69
+ /** Is the recorded daemon the process currently holding that pid? Records without identity are unverifiable. */
70
+ export function daemonCheck(state) {
71
+ return checkIdentity(state.daemon);
72
+ }
73
+ export function rpc(socket, command, args = {}) {
74
+ return new Promise((resolvePromise, reject) => {
75
+ const body = JSON.stringify({ command, args });
76
+ const req = request({ socketPath: socket, path: '/rpc', method: 'POST', headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(body) } }, (res) => {
77
+ let data = '';
78
+ res.setEncoding('utf8');
79
+ res.on('data', (chunk) => { data += chunk; });
80
+ res.on('end', () => {
81
+ try {
82
+ resolvePromise(JSON.parse(data));
83
+ }
84
+ catch (err) {
85
+ reject(err);
86
+ }
87
+ });
88
+ });
89
+ req.on('error', reject);
90
+ req.end(body);
91
+ });
92
+ }
93
+ //# sourceMappingURL=state.js.map
@@ -0,0 +1,25 @@
1
+ import { spawn } from 'node:child_process';
2
+ /** Whether a desktop browser can be opened from this process. */
3
+ export function hasDisplay() {
4
+ if (process.platform === 'darwin' || process.platform === 'win32')
5
+ return true;
6
+ return !!(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
7
+ }
8
+ /** Open a URL in the user's browser without waiting for it. Returns false when there is no display. */
9
+ export function openInBrowser(url) {
10
+ if (!hasDisplay())
11
+ return false;
12
+ const [cmd, args] = process.platform === 'darwin' ? ['open', [url]]
13
+ : process.platform === 'win32' ? ['cmd', ['/c', 'start', '', url]]
14
+ : ['xdg-open', [url]];
15
+ try {
16
+ const child = spawn(cmd, args, { detached: true, stdio: 'ignore' });
17
+ child.on('error', () => undefined);
18
+ child.unref();
19
+ return true;
20
+ }
21
+ catch {
22
+ return false;
23
+ }
24
+ }
25
+ //# sourceMappingURL=open.js.map