@moxt-ai/mobius 0.0.3 → 0.0.5

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 (88) hide show
  1. package/dist/acp/session-manager.d.ts.map +1 -1
  2. package/dist/acp/session-manager.js +15 -29
  3. package/dist/acp/session-manager.js.map +1 -1
  4. package/dist/agent-catalog/agent-diagnostics.d.ts +3 -0
  5. package/dist/agent-catalog/agent-diagnostics.d.ts.map +1 -0
  6. package/dist/agent-catalog/agent-diagnostics.js +62 -0
  7. package/dist/agent-catalog/agent-diagnostics.js.map +1 -0
  8. package/dist/agent-catalog/discover-local-agents.d.ts +77 -0
  9. package/dist/agent-catalog/discover-local-agents.d.ts.map +1 -0
  10. package/dist/agent-catalog/discover-local-agents.js +478 -0
  11. package/dist/agent-catalog/discover-local-agents.js.map +1 -0
  12. package/dist/agent-catalog/local-agent.d.ts +38 -0
  13. package/dist/agent-catalog/local-agent.d.ts.map +1 -0
  14. package/dist/agent-catalog/local-agent.js +93 -0
  15. package/dist/agent-catalog/local-agent.js.map +1 -0
  16. package/dist/agent-catalog/publish-agent-catalog.d.ts +3 -0
  17. package/dist/agent-catalog/publish-agent-catalog.d.ts.map +1 -0
  18. package/dist/agent-catalog/publish-agent-catalog.js +58 -0
  19. package/dist/agent-catalog/publish-agent-catalog.js.map +1 -0
  20. package/dist/agent-installation/agent-installation-manager.d.ts +60 -0
  21. package/dist/agent-installation/agent-installation-manager.d.ts.map +1 -0
  22. package/dist/agent-installation/agent-installation-manager.js +242 -0
  23. package/dist/agent-installation/agent-installation-manager.js.map +1 -0
  24. package/dist/agent-installation/automatic-adapter-provisioner.d.ts +18 -0
  25. package/dist/agent-installation/automatic-adapter-provisioner.d.ts.map +1 -0
  26. package/dist/agent-installation/automatic-adapter-provisioner.js +116 -0
  27. package/dist/agent-installation/automatic-adapter-provisioner.js.map +1 -0
  28. package/dist/agent-installation/fixed-agent-install-recipes.d.ts +20 -0
  29. package/dist/agent-installation/fixed-agent-install-recipes.d.ts.map +1 -0
  30. package/dist/agent-installation/fixed-agent-install-recipes.js +163 -0
  31. package/dist/agent-installation/fixed-agent-install-recipes.js.map +1 -0
  32. package/dist/agent-installation/fixed-installation-executor.d.ts +20 -0
  33. package/dist/agent-installation/fixed-installation-executor.d.ts.map +1 -0
  34. package/dist/agent-installation/fixed-installation-executor.js +213 -0
  35. package/dist/agent-installation/fixed-installation-executor.js.map +1 -0
  36. package/dist/agent-installation/installation-grants.d.ts +2 -0
  37. package/dist/agent-installation/installation-grants.d.ts.map +1 -0
  38. package/dist/agent-installation/installation-grants.js +52 -0
  39. package/dist/agent-installation/installation-grants.js.map +1 -0
  40. package/dist/agent-installation/local-installation-authorizer.d.ts +7 -0
  41. package/dist/agent-installation/local-installation-authorizer.d.ts.map +1 -0
  42. package/dist/agent-installation/local-installation-authorizer.js +32 -0
  43. package/dist/agent-installation/local-installation-authorizer.js.map +1 -0
  44. package/dist/agent-installation/managed-adapter-installer.d.ts +21 -0
  45. package/dist/agent-installation/managed-adapter-installer.d.ts.map +1 -0
  46. package/dist/agent-installation/managed-adapter-installer.js +161 -0
  47. package/dist/agent-installation/managed-adapter-installer.js.map +1 -0
  48. package/dist/agent-installation/verified-download.d.ts +24 -0
  49. package/dist/agent-installation/verified-download.d.ts.map +1 -0
  50. package/dist/agent-installation/verified-download.js +158 -0
  51. package/dist/agent-installation/verified-download.js.map +1 -0
  52. package/dist/audit/audit-store.d.ts +1 -0
  53. package/dist/audit/audit-store.d.ts.map +1 -1
  54. package/dist/audit/audit-store.js +12 -4
  55. package/dist/audit/audit-store.js.map +1 -1
  56. package/dist/cli.js +4284 -1178
  57. package/dist/cli.js.map +4 -4
  58. package/dist/config.d.ts +6 -10
  59. package/dist/config.d.ts.map +1 -1
  60. package/dist/config.js +10 -53
  61. package/dist/config.js.map +1 -1
  62. package/dist/index.js +3476 -458
  63. package/dist/index.js.map +4 -4
  64. package/dist/pairing/pair-local-machine.d.ts +2 -2
  65. package/dist/pairing/pair-local-machine.d.ts.map +1 -1
  66. package/dist/pairing/pair-local-machine.js +41 -20
  67. package/dist/pairing/pair-local-machine.js.map +1 -1
  68. package/dist/process/executable.d.ts +40 -0
  69. package/dist/process/executable.d.ts.map +1 -0
  70. package/dist/process/executable.js +313 -0
  71. package/dist/process/executable.js.map +1 -0
  72. package/dist/relay-connection/liveness.d.ts +14 -0
  73. package/dist/relay-connection/liveness.d.ts.map +1 -0
  74. package/dist/relay-connection/liveness.js +102 -0
  75. package/dist/relay-connection/liveness.js.map +1 -0
  76. package/dist/relay-connection/timer.d.ts +8 -0
  77. package/dist/relay-connection/timer.d.ts.map +1 -0
  78. package/dist/relay-connection/timer.js +8 -0
  79. package/dist/relay-connection/timer.js.map +1 -0
  80. package/dist/run-daemon.d.ts +3 -2
  81. package/dist/run-daemon.d.ts.map +1 -1
  82. package/dist/run-daemon.js +272 -22
  83. package/dist/run-daemon.js.map +1 -1
  84. package/dist/storage/state-directory.d.ts +2 -0
  85. package/dist/storage/state-directory.d.ts.map +1 -0
  86. package/dist/storage/state-directory.js +10 -0
  87. package/dist/storage/state-directory.js.map +1 -0
  88. package/package.json +3 -5
package/dist/index.js CHANGED
@@ -23,65 +23,466 @@ var createAcpClient = (handler) => client({ name: "mobius" }).onRequest(
23
23
 
24
24
  // src/config.ts
25
25
  import { fileURLToPath } from "node:url";
26
- var repositoryRoot = fileURLToPath(new URL("../../../", import.meta.url));
27
- function adapterEntrypoint(packageName) {
28
- return fileURLToPath(new URL("./index.js", import.meta.resolve(packageName)));
26
+
27
+ // src/agent-catalog/local-agent.ts
28
+ import { realpathSync, statSync } from "node:fs";
29
+
30
+ // src/process/executable.ts
31
+ import { spawn } from "node:child_process";
32
+ import { constants } from "node:fs";
33
+ import { access, realpath, stat } from "node:fs/promises";
34
+ import { isAbsolute, join, relative, resolve } from "node:path";
35
+ var DEFAULT_OUTPUT_LIMIT_BYTES = 16 * 1024;
36
+ var PROCESS_GRACEFUL_STOP_TIMEOUT_MILLISECONDS = 1e3;
37
+ var PROCESS_FORCED_STOP_TIMEOUT_MILLISECONDS = 5e3;
38
+ var ExecutableIdentity = class {
39
+ changedAtMilliseconds;
40
+ device;
41
+ inode;
42
+ realPath;
43
+ requestedPath;
44
+ size;
45
+ constructor(requestedPath, realPath_, device, inode, size, changedAtMilliseconds) {
46
+ this.changedAtMilliseconds = changedAtMilliseconds;
47
+ this.device = device;
48
+ this.inode = inode;
49
+ this.realPath = realPath_;
50
+ this.requestedPath = requestedPath;
51
+ this.size = size;
52
+ }
53
+ matches = (candidate) => this.requestedPath === candidate.requestedPath && this.realPath === candidate.realPath && this.device === candidate.device && this.inode === candidate.inode && this.size === candidate.size && this.changedAtMilliseconds === candidate.changedAtMilliseconds;
54
+ };
55
+ var ResolvedExecutable = class {
56
+ identity;
57
+ path;
58
+ constructor(identity) {
59
+ this.identity = identity;
60
+ this.path = identity.realPath;
61
+ }
62
+ };
63
+ var ProcessExecutionError = class extends Error {
64
+ code;
65
+ constructor(code, message, options = {}) {
66
+ super(message, options);
67
+ this.code = code;
68
+ this.name = "ProcessExecutionError";
69
+ }
70
+ };
71
+ var ProcessResult = class {
72
+ stderr;
73
+ stdout;
74
+ constructor(stdout, stderr) {
75
+ this.stderr = stderr;
76
+ this.stdout = stdout;
77
+ }
78
+ };
79
+ function isInside(root, candidate) {
80
+ const difference = relative(root, candidate);
81
+ return difference.length === 0 || !difference.startsWith("..") && !isAbsolute(difference);
82
+ }
83
+ function executableExtensions(executableName, environment, platform) {
84
+ if (platform !== "win32") {
85
+ return [""];
86
+ }
87
+ if (/\.[a-zA-Z0-9]+$/.test(executableName)) {
88
+ return [""];
89
+ }
90
+ const configured = environment["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD";
91
+ const extensions = configured.split(";").map((extension) => extension.trim()).filter((extension) => extension.length > 0).map(
92
+ (extension) => extension.startsWith(".") ? extension : `.${extension}`
93
+ );
94
+ return extensions.length > 0 ? extensions : [".EXE"];
95
+ }
96
+ async function resolveExecutablePath(requestedPath, platform = process.platform) {
97
+ const absoluteRequestedPath = resolve(requestedPath);
98
+ try {
99
+ const resolvedPath = await realpath(absoluteRequestedPath);
100
+ const metadata = await stat(resolvedPath);
101
+ if (!metadata.isFile()) {
102
+ return null;
103
+ }
104
+ if (platform !== "win32") {
105
+ await access(resolvedPath, constants.X_OK);
106
+ }
107
+ return new ResolvedExecutable(
108
+ new ExecutableIdentity(
109
+ absoluteRequestedPath,
110
+ resolvedPath,
111
+ metadata.dev,
112
+ metadata.ino,
113
+ metadata.size,
114
+ metadata.ctimeMs
115
+ )
116
+ );
117
+ } catch {
118
+ return null;
119
+ }
120
+ }
121
+ async function findExecutableOnPath(executableName, environment, approvedRoot, platform = process.platform) {
122
+ const encodedPath = environment["PATH"] ?? "";
123
+ const separator = platform === "win32" ? ";" : ":";
124
+ const approvedRealPath = await realpath(resolve(approvedRoot));
125
+ const extensions = executableExtensions(
126
+ executableName,
127
+ environment,
128
+ platform
129
+ );
130
+ for (const entry of encodedPath.split(separator)) {
131
+ if (entry.length === 0) {
132
+ continue;
133
+ }
134
+ for (const extension of extensions) {
135
+ const requestedPath = resolve(
136
+ join(entry, `${executableName}${extension}`)
137
+ );
138
+ const executable = await resolveExecutablePath(requestedPath, platform);
139
+ if (executable !== null && !isInside(approvedRealPath, requestedPath) && !isInside(approvedRealPath, executable.path)) {
140
+ return executable;
141
+ }
142
+ }
143
+ }
144
+ return null;
145
+ }
146
+ async function revalidateExecutable(identity, platform = process.platform) {
147
+ const current = await resolveExecutablePath(identity.requestedPath, platform);
148
+ return current !== null && identity.matches(current.identity);
149
+ }
150
+ function appendBounded(chunks, chunk, currentBytes, limitBytes) {
151
+ const nextBytes = currentBytes + chunk.byteLength;
152
+ if (nextBytes > limitBytes) {
153
+ throw new ProcessExecutionError(
154
+ "process_output_limit",
155
+ "The executable produced too much output"
156
+ );
157
+ }
158
+ chunks.push(chunk);
159
+ return nextBytes;
160
+ }
161
+ function terminateProcess(child) {
162
+ if (child.exitCode !== null || child.signalCode !== null || child.pid === void 0) {
163
+ return;
164
+ }
165
+ if (process.platform !== "win32") {
166
+ try {
167
+ process.kill(-child.pid, "SIGTERM");
168
+ return;
169
+ } catch {
170
+ }
171
+ }
172
+ child.kill("SIGTERM");
173
+ }
174
+ function forceTerminateProcess(child) {
175
+ if (child.exitCode !== null || child.signalCode !== null || child.pid === void 0) {
176
+ return;
177
+ }
178
+ if (process.platform !== "win32") {
179
+ try {
180
+ process.kill(-child.pid, "SIGKILL");
181
+ return;
182
+ } catch {
183
+ }
184
+ }
185
+ child.kill("SIGKILL");
186
+ }
187
+ async function stopProcessTree(child) {
188
+ if (child.exitCode !== null || child.signalCode !== null) {
189
+ return;
190
+ }
191
+ terminateProcess(child);
192
+ const exitedAfterTermination = await waitForProcessExit(
193
+ child,
194
+ PROCESS_GRACEFUL_STOP_TIMEOUT_MILLISECONDS
195
+ );
196
+ if (exitedAfterTermination) {
197
+ return;
198
+ }
199
+ forceTerminateProcess(child);
200
+ const exitedAfterForce = await waitForProcessExit(
201
+ child,
202
+ PROCESS_FORCED_STOP_TIMEOUT_MILLISECONDS
203
+ );
204
+ if (!exitedAfterForce) {
205
+ throw new ProcessExecutionError(
206
+ "process_termination_timeout",
207
+ "The executable process could not be stopped"
208
+ );
209
+ }
210
+ }
211
+ function waitForProcessExit(child, timeoutMilliseconds) {
212
+ if (child.exitCode !== null || child.signalCode !== null) {
213
+ return Promise.resolve(true);
214
+ }
215
+ return new Promise((resolveExit) => {
216
+ let settled = false;
217
+ const finish = (exited) => {
218
+ if (settled) {
219
+ return;
220
+ }
221
+ settled = true;
222
+ clearTimeout(timeout);
223
+ child.removeListener("exit", handleExit);
224
+ resolveExit(exited);
225
+ };
226
+ const handleExit = () => finish(true);
227
+ const timeout = setTimeout(() => finish(false), timeoutMilliseconds);
228
+ child.once("exit", handleExit);
229
+ if (child.exitCode !== null || child.signalCode !== null) {
230
+ finish(true);
231
+ }
232
+ });
233
+ }
234
+ async function runBoundedProcess(options) {
235
+ options.signal.throwIfAborted();
236
+ const outputLimitBytes = options.outputLimitBytes ?? DEFAULT_OUTPUT_LIMIT_BYTES;
237
+ const child = spawn(options.command, [...options.arguments], {
238
+ cwd: options.cwd,
239
+ detached: process.platform !== "win32",
240
+ env: { ...options.environment },
241
+ shell: false,
242
+ stdio: ["ignore", "pipe", "pipe"]
243
+ });
244
+ const stdout = [];
245
+ const stderr = [];
246
+ let stdoutBytes = 0;
247
+ let stderrBytes = 0;
248
+ let interruptionFailure = null;
249
+ let interruptionCleanupFailure = null;
250
+ let stopping = false;
251
+ const interrupted = Promise.withResolvers();
252
+ const interrupt = (failure) => {
253
+ if (stopping) {
254
+ return;
255
+ }
256
+ stopping = true;
257
+ interruptionFailure = failure;
258
+ const stop = async () => {
259
+ try {
260
+ await stopProcessTree(child);
261
+ } catch (error61) {
262
+ interruptionCleanupFailure = error61 instanceof ProcessExecutionError ? error61 : new ProcessExecutionError(
263
+ "process_termination_failed",
264
+ "The executable process could not be stopped",
265
+ { cause: error61 }
266
+ );
267
+ } finally {
268
+ interrupted.resolve();
269
+ }
270
+ };
271
+ void stop();
272
+ };
273
+ const collectStdout = (chunk) => {
274
+ if (interruptionFailure !== null) {
275
+ return;
276
+ }
277
+ try {
278
+ stdoutBytes = appendBounded(stdout, chunk, stdoutBytes, outputLimitBytes);
279
+ } catch (error61) {
280
+ const failure = error61 instanceof ProcessExecutionError ? error61 : new ProcessExecutionError(
281
+ "process_output_limit",
282
+ "The executable produced too much output",
283
+ { cause: error61 }
284
+ );
285
+ interrupt(failure);
286
+ }
287
+ };
288
+ const collectStderr = (chunk) => {
289
+ if (interruptionFailure !== null) {
290
+ return;
291
+ }
292
+ try {
293
+ stderrBytes = appendBounded(stderr, chunk, stderrBytes, outputLimitBytes);
294
+ } catch (error61) {
295
+ const failure = error61 instanceof ProcessExecutionError ? error61 : new ProcessExecutionError(
296
+ "process_output_limit",
297
+ "The executable produced too much output",
298
+ { cause: error61 }
299
+ );
300
+ interrupt(failure);
301
+ }
302
+ };
303
+ child.stdout?.on("data", collectStdout);
304
+ child.stderr?.on("data", collectStderr);
305
+ const timeout = setTimeout(() => {
306
+ interrupt(
307
+ new ProcessExecutionError(
308
+ "process_timeout",
309
+ "The executable probe timed out"
310
+ )
311
+ );
312
+ }, options.timeoutMilliseconds);
313
+ const abort = () => interrupt(
314
+ new ProcessExecutionError(
315
+ "process_cancelled",
316
+ "The executable probe was cancelled",
317
+ { cause: options.signal.reason }
318
+ )
319
+ );
320
+ options.signal.addEventListener("abort", abort, { once: true });
321
+ if (options.signal.aborted) {
322
+ abort();
323
+ }
324
+ try {
325
+ const exit = new Promise((resolveExit, rejectExit) => {
326
+ child.once("error", rejectExit);
327
+ child.once("exit", (code) => resolveExit(code ?? 1));
328
+ });
329
+ const exitCode = await Promise.race([
330
+ exit,
331
+ interrupted.promise.then(() => -1)
332
+ ]);
333
+ if (interruptionCleanupFailure !== null) {
334
+ throw interruptionCleanupFailure;
335
+ }
336
+ if (interruptionFailure !== null) {
337
+ throw interruptionFailure;
338
+ }
339
+ if (exitCode !== 0) {
340
+ throw new ProcessExecutionError(
341
+ "process_exit_failed",
342
+ "The executable probe failed"
343
+ );
344
+ }
345
+ return new ProcessResult(
346
+ Buffer.concat(stdout).toString("utf8"),
347
+ Buffer.concat(stderr).toString("utf8")
348
+ );
349
+ } catch (error61) {
350
+ if (error61 instanceof ProcessExecutionError) {
351
+ throw error61;
352
+ }
353
+ throw new ProcessExecutionError(
354
+ "process_spawn_failed",
355
+ "The executable could not be started",
356
+ { cause: error61 }
357
+ );
358
+ } finally {
359
+ clearTimeout(timeout);
360
+ options.signal.removeEventListener("abort", abort);
361
+ child.stdout?.removeListener("data", collectStdout);
362
+ child.stderr?.removeListener("data", collectStderr);
363
+ if (stopping) {
364
+ await interrupted.promise;
365
+ } else {
366
+ await stopProcessTree(child);
367
+ }
368
+ }
369
+ }
370
+
371
+ // src/agent-catalog/local-agent.ts
372
+ function readTrustedIdentity(path) {
373
+ const resolvedPath = realpathSync(path);
374
+ const metadata = statSync(resolvedPath);
375
+ return new ExecutableIdentity(
376
+ resolvedPath,
377
+ resolvedPath,
378
+ metadata.dev,
379
+ metadata.ino,
380
+ metadata.size,
381
+ metadata.ctimeMs
382
+ );
29
383
  }
30
384
  var LocalAgentDefinition = class {
385
+ adapterIdentity;
386
+ adapterPath;
387
+ adapterVersion;
388
+ agentIdentity;
389
+ agentVersion;
31
390
  arguments;
32
391
  command;
392
+ environment;
33
393
  id;
34
394
  label;
35
- constructor(id, label, command, arguments_) {
395
+ constructor(id, label, command, arguments_, environment = {}, adapterVersion = "", agentVersion = "", adapterIdentity = readTrustedIdentity(command), agentIdentity = adapterIdentity) {
396
+ this.adapterIdentity = adapterIdentity;
397
+ this.adapterPath = adapterIdentity.realPath;
398
+ this.adapterVersion = adapterVersion;
399
+ this.agentIdentity = agentIdentity;
400
+ this.agentVersion = agentVersion;
36
401
  this.id = id;
37
402
  this.label = label;
38
- this.command = command;
39
- this.arguments = arguments_;
403
+ this.command = adapterIdentity.realPath;
404
+ this.arguments = Object.freeze([...arguments_]);
405
+ this.environment = Object.freeze({ ...environment });
40
406
  }
41
407
  };
42
- function loadLocalAgents() {
43
- const overridePath = process.env["MOBIUS_ACP_AGENT_PATH"]?.trim();
44
- if (overridePath !== void 0 && overridePath.length > 0) {
45
- return [
46
- new LocalAgentDefinition("codex", "Codex", process.execPath, [
47
- overridePath,
48
- "--agent-name",
49
- "Codex"
50
- ]),
51
- new LocalAgentDefinition("claude", "Claude Code", process.execPath, [
52
- overridePath,
53
- "--agent-name",
54
- "Claude Code"
55
- ])
56
- ];
408
+ var InstallableAgentDefinition = class {
409
+ id;
410
+ label;
411
+ requiresLocalApproval;
412
+ constructor(id, label, requiresLocalApproval = true) {
413
+ this.id = id;
414
+ this.label = label;
415
+ this.requiresLocalApproval = requiresLocalApproval;
57
416
  }
58
- return [
59
- new LocalAgentDefinition("codex", "Codex", process.execPath, [
60
- adapterEntrypoint("@agentclientprotocol/codex-acp")
61
- ]),
62
- new LocalAgentDefinition("claude", "Claude Code", process.execPath, [
63
- adapterEntrypoint("@agentclientprotocol/claude-agent-acp")
64
- ])
65
- ];
417
+ };
418
+ var LocalAgentCatalogSnapshot = class {
419
+ agents;
420
+ installableAgents;
421
+ constructor(agents, installableAgents) {
422
+ this.agents = Object.freeze([...agents]);
423
+ this.installableAgents = Object.freeze([...installableAgents]);
424
+ }
425
+ };
426
+ var LocalAgentCatalogError = class extends Error {
427
+ code = "agent_not_ready";
428
+ constructor() {
429
+ super("The requested local Agent is not ready");
430
+ this.name = "LocalAgentCatalogError";
431
+ }
432
+ };
433
+ var LocalAgentCatalog = class {
434
+ #snapshot;
435
+ constructor(agents, installableAgents) {
436
+ this.#snapshot = this.validate(agents, installableAgents);
437
+ }
438
+ find = (agentId) => {
439
+ for (const agent of this.#snapshot.agents) {
440
+ if (agent.id === agentId) {
441
+ return agent;
442
+ }
443
+ }
444
+ throw new LocalAgentCatalogError();
445
+ };
446
+ read = () => this.#snapshot;
447
+ transition = (agents, installableAgents) => {
448
+ const next = this.validate(agents, installableAgents);
449
+ this.#snapshot = next;
450
+ return next;
451
+ };
452
+ validate = (agents, installableAgents) => {
453
+ const identifiers = /* @__PURE__ */ new Set();
454
+ for (const agent of agents) {
455
+ if (identifiers.has(agent.id)) {
456
+ throw new Error("The local Agent catalog contains duplicate entries");
457
+ }
458
+ identifiers.add(agent.id);
459
+ }
460
+ for (const agent of installableAgents) {
461
+ if (identifiers.has(agent.id)) {
462
+ throw new Error("A local Agent cannot be ready and installable");
463
+ }
464
+ identifiers.add(agent.id);
465
+ }
466
+ return new LocalAgentCatalogSnapshot(agents, installableAgents);
467
+ };
468
+ };
469
+
470
+ // src/config.ts
471
+ function loadApprovedRoot() {
472
+ return process.env["MOBIUS_APPROVED_ROOT"] ?? process.cwd();
66
473
  }
67
- function createDaemonConfig(connection) {
474
+ function createDaemonConfig(connection, agents = [], installableAgents = []) {
68
475
  return {
69
- agents: loadLocalAgents(),
476
+ agents,
70
477
  approvedRoot: connection.approvedRoot,
71
478
  auditDatabasePath: connection.auditDatabasePath,
72
479
  bindingId: "workspace",
480
+ catalog: new LocalAgentCatalog(agents, installableAgents),
73
481
  channelId: connection.channelId,
482
+ installableAgents,
74
483
  relayUrl: connection.relayUrl
75
484
  };
76
485
  }
77
- function findLocalAgent(agents, agentId) {
78
- for (const agent of agents) {
79
- if (agent.id === agentId) {
80
- return agent;
81
- }
82
- }
83
- throw new Error("The requested local agent is not approved");
84
- }
85
486
  function loadDaemonConfig() {
86
487
  const relayUrl = new URL(
87
488
  // biome-ignore lint/complexity/useLiteralKeys: Environment variables use an index signature.
@@ -95,22 +496,26 @@ function loadDaemonConfig() {
95
496
  if (relayUrl.protocol !== "ws:" && relayUrl.protocol !== "wss:") {
96
497
  throw new Error("MOBIUS_RELAY_URL must use ws or wss");
97
498
  }
98
- return createDaemonConfig({
99
- // biome-ignore lint/complexity/useLiteralKeys: Environment variables use an index signature.
100
- approvedRoot: process.env["MOBIUS_APPROVED_ROOT"] ?? repositoryRoot,
101
- auditDatabasePath: (
499
+ return createDaemonConfig(
500
+ {
501
+ approvedRoot: loadApprovedRoot(),
502
+ auditDatabasePath: (
503
+ // biome-ignore lint/complexity/useLiteralKeys: Environment variables use an index signature.
504
+ process.env["MOBIUS_AUDIT_DATABASE"] ?? fileURLToPath(
505
+ new URL("../../../.mobius/daemon.sqlite", import.meta.url)
506
+ )
507
+ ),
102
508
  // biome-ignore lint/complexity/useLiteralKeys: Environment variables use an index signature.
103
- process.env["MOBIUS_AUDIT_DATABASE"] ?? fileURLToPath(new URL("../../../.mobius/daemon.sqlite", import.meta.url))
104
- ),
105
- // biome-ignore lint/complexity/useLiteralKeys: Environment variables use an index signature.
106
- channelId: process.env["MOBIUS_CHANNEL_ID"] ?? "local",
107
- relayUrl
108
- });
509
+ channelId: process.env["MOBIUS_CHANNEL_ID"] ?? "local",
510
+ relayUrl
511
+ },
512
+ [],
513
+ []
514
+ );
109
515
  }
110
516
 
111
517
  // src/acp/session-manager.ts
112
- import { spawn } from "node:child_process";
113
- import { once } from "node:events";
518
+ import { spawn as spawn2 } from "node:child_process";
114
519
  import {
115
520
  methods as methods2,
116
521
  ndJsonStream,
@@ -2077,8 +2482,8 @@ function emoji() {
2077
2482
  }
2078
2483
  var ipv4 = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/;
2079
2484
  var ipv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$/;
2080
- var mac = (delimiter) => {
2081
- const escapedDelim = escapeRegex(delimiter ?? ":");
2485
+ var mac = (delimiter2) => {
2486
+ const escapedDelim = escapeRegex(delimiter2 ?? ":");
2082
2487
  return new RegExp(`^(?:[0-9A-F]{2}${escapedDelim}){5}[0-9A-F]{2}$|^(?:[0-9a-f]{2}${escapedDelim}){5}[0-9a-f]{2}$`);
2083
2488
  };
2084
2489
  var cidrv4 = /^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/([0-9]|[1-2][0-9]|3[0-2])$/;
@@ -19004,6 +19409,14 @@ var SESSION_ELICITATION_RESPOND_KIND = "session.elicitation.respond";
19004
19409
  var SESSION_ELICITATION_RESOLVED_KIND = "session.elicitation.resolved";
19005
19410
  var SESSION_OPERATION_FAILED_KIND = "session.operation.failed";
19006
19411
  var AGENT_CONTENT_KIND = "agent.content";
19412
+ var AGENT_INSTALL_REQUEST_KIND = "agent.install.request";
19413
+ var AGENT_INSTALL_CANCEL_KIND = "agent.install.cancel";
19414
+ var AGENT_INSTALL_AWAITING_APPROVAL_KIND = "agent.install.awaiting_approval";
19415
+ var AGENT_INSTALL_STARTED_KIND = "agent.install.started";
19416
+ var AGENT_INSTALL_PROGRESS_KIND = "agent.install.progress";
19417
+ var AGENT_INSTALL_COMPLETED_KIND = "agent.install.completed";
19418
+ var AGENT_INSTALL_FAILED_KIND = "agent.install.failed";
19419
+ var AGENT_INSTALL_CANCELLED_KIND = "agent.install.cancelled";
19007
19420
  var IDENTIFIER_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/;
19008
19421
  var DIRECTORY_IDENTIFIER_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,2047}$/;
19009
19422
  var IDENTIFIER = external_exports.string().min(1).max(128).regex(IDENTIFIER_PATTERN);
@@ -19277,6 +19690,55 @@ var agentContentEventSchema = external_exports.strictObject({
19277
19690
  protocolVersion: PROTOCOL_VERSION,
19278
19691
  sessionId: IDENTIFIER
19279
19692
  }).readonly();
19693
+ var agentInstallIdentityFields = {
19694
+ agentId: IDENTIFIER,
19695
+ protocolVersion: PROTOCOL_VERSION,
19696
+ requestId: IDENTIFIER
19697
+ };
19698
+ var agentInstallRequestCommandSchema = external_exports.strictObject({
19699
+ ...agentInstallIdentityFields,
19700
+ kind: external_exports.literal(AGENT_INSTALL_REQUEST_KIND)
19701
+ }).readonly();
19702
+ var agentInstallCancelCommandSchema = external_exports.strictObject({
19703
+ kind: external_exports.literal(AGENT_INSTALL_CANCEL_KIND),
19704
+ protocolVersion: PROTOCOL_VERSION,
19705
+ requestId: IDENTIFIER
19706
+ }).readonly();
19707
+ var agentInstallAwaitingApprovalEventSchema = external_exports.strictObject({
19708
+ ...agentInstallIdentityFields,
19709
+ kind: external_exports.literal(AGENT_INSTALL_AWAITING_APPROVAL_KIND)
19710
+ }).readonly();
19711
+ var agentInstallStartedEventSchema = external_exports.strictObject({
19712
+ ...agentInstallIdentityFields,
19713
+ kind: external_exports.literal(AGENT_INSTALL_STARTED_KIND)
19714
+ }).readonly();
19715
+ var installationStageSchema = external_exports.enum([
19716
+ "downloading",
19717
+ "verifying",
19718
+ "installing_agent",
19719
+ "installing_adapter",
19720
+ "checking_acp"
19721
+ ]);
19722
+ var agentInstallProgressEventSchema = external_exports.strictObject({
19723
+ ...agentInstallIdentityFields,
19724
+ kind: external_exports.literal(AGENT_INSTALL_PROGRESS_KIND),
19725
+ stage: installationStageSchema
19726
+ }).readonly();
19727
+ var agentInstallCompletedEventSchema = external_exports.strictObject({
19728
+ ...agentInstallIdentityFields,
19729
+ authenticationMayBeRequired: external_exports.boolean(),
19730
+ kind: external_exports.literal(AGENT_INSTALL_COMPLETED_KIND)
19731
+ }).readonly();
19732
+ var agentInstallFailedEventSchema = external_exports.strictObject({
19733
+ ...agentInstallIdentityFields,
19734
+ code: IDENTIFIER,
19735
+ kind: external_exports.literal(AGENT_INSTALL_FAILED_KIND),
19736
+ message: external_exports.string().min(1).max(500)
19737
+ }).readonly();
19738
+ var agentInstallCancelledEventSchema = external_exports.strictObject({
19739
+ ...agentInstallIdentityFields,
19740
+ kind: external_exports.literal(AGENT_INSTALL_CANCELLED_KIND)
19741
+ }).readonly();
19280
19742
  function parseJson(encoded) {
19281
19743
  try {
19282
19744
  return JSON.parse(encoded);
@@ -19325,10 +19787,21 @@ var decodeSessionElicitationRespondCommand = (encoded) => decode3(encoded, sessi
19325
19787
  var encodeSessionElicitationResolvedEvent = (message) => encode3(message, sessionElicitationResolvedEventSchema);
19326
19788
  var encodeSessionOperationFailedEvent = (message) => encode3(message, sessionOperationFailedEventSchema);
19327
19789
  var encodeAgentContentEvent = (message) => encode3(message, agentContentEventSchema);
19790
+ var decodeAgentInstallRequestCommand = (encoded) => decode3(encoded, agentInstallRequestCommandSchema);
19791
+ var decodeAgentInstallCancelCommand = (encoded) => decode3(encoded, agentInstallCancelCommandSchema);
19792
+ var encodeAgentInstallAwaitingApprovalEvent = (message) => encode3(message, agentInstallAwaitingApprovalEventSchema);
19793
+ var encodeAgentInstallStartedEvent = (message) => encode3(message, agentInstallStartedEventSchema);
19794
+ var encodeAgentInstallProgressEvent = (message) => encode3(message, agentInstallProgressEventSchema);
19795
+ var encodeAgentInstallCompletedEvent = (message) => encode3(message, agentInstallCompletedEventSchema);
19796
+ var encodeAgentInstallFailedEvent = (message) => encode3(message, agentInstallFailedEventSchema);
19797
+ var encodeAgentInstallCancelledEvent = (message) => encode3(message, agentInstallCancelledEventSchema);
19328
19798
 
19329
19799
  // ../protocol/src/messages.ts
19330
19800
  var SESSION_PROMPT_KIND = "session.prompt";
19331
19801
  var SESSION_CANCEL_KIND = "session.cancel";
19802
+ var RUNTIME_LIVENESS_READY_KIND = "runtime.liveness.ready";
19803
+ var RUNTIME_HEARTBEAT_KIND = "runtime.heartbeat";
19804
+ var RUNTIME_HEARTBEAT_ACK_KIND = "runtime.heartbeat.ack";
19332
19805
  var SESSION_STARTED_KIND = "session.started";
19333
19806
  var AGENT_MESSAGE_DELTA_KIND = "agent.message.delta";
19334
19807
  var AGENT_ACTIVITY_KIND = "agent.activity";
@@ -19551,6 +20024,38 @@ function decodeSessionCancelCommand(encoded) {
19551
20024
  sessionId: readIdentifier(value, "sessionId")
19552
20025
  };
19553
20026
  }
20027
+ function decodeRuntimeLivenessReadyEvent(encoded) {
20028
+ const value = parseJson2(encoded);
20029
+ assertExactKeys(value, ["kind", "protocolVersion"]);
20030
+ assertProtocolVersion(value);
20031
+ assertKind(value, RUNTIME_LIVENESS_READY_KIND);
20032
+ return {
20033
+ kind: RUNTIME_LIVENESS_READY_KIND,
20034
+ protocolVersion: MOBIUS_PROTOCOL_VERSION
20035
+ };
20036
+ }
20037
+ function decodeRuntimeHeartbeatEvent(encoded) {
20038
+ const value = parseJson2(encoded);
20039
+ assertExactKeys(value, ["heartbeatId", "kind", "protocolVersion"]);
20040
+ assertProtocolVersion(value);
20041
+ assertKind(value, RUNTIME_HEARTBEAT_KIND);
20042
+ return {
20043
+ heartbeatId: readIdentifier(value, "heartbeatId"),
20044
+ kind: RUNTIME_HEARTBEAT_KIND,
20045
+ protocolVersion: MOBIUS_PROTOCOL_VERSION
20046
+ };
20047
+ }
20048
+ function decodeRuntimeHeartbeatAckEvent(encoded) {
20049
+ const value = parseJson2(encoded);
20050
+ assertExactKeys(value, ["heartbeatId", "kind", "protocolVersion"]);
20051
+ assertProtocolVersion(value);
20052
+ assertKind(value, RUNTIME_HEARTBEAT_ACK_KIND);
20053
+ return {
20054
+ heartbeatId: readIdentifier(value, "heartbeatId"),
20055
+ kind: RUNTIME_HEARTBEAT_ACK_KIND,
20056
+ protocolVersion: MOBIUS_PROTOCOL_VERSION
20057
+ };
20058
+ }
19554
20059
  function decodeSessionStartedEvent(encoded) {
19555
20060
  const value = parseJson2(encoded);
19556
20061
  assertExactKeys(value, [
@@ -19668,6 +20173,9 @@ function decodeSessionFailedEvent(encoded) {
19668
20173
  sessionId: readIdentifier(value, "sessionId")
19669
20174
  };
19670
20175
  }
20176
+ function encodeRuntimeHeartbeatEvent(message) {
20177
+ return JSON.stringify(decodeRuntimeHeartbeatEvent(JSON.stringify(message)));
20178
+ }
19671
20179
  function encodeSessionStartedEvent(message) {
19672
20180
  return JSON.stringify(decodeSessionStartedEvent(JSON.stringify(message)));
19673
20181
  }
@@ -20360,7 +20868,7 @@ function createWritableAcpStream(output2) {
20360
20868
  if (outputFailure !== void 0) {
20361
20869
  return Promise.reject(outputFailure);
20362
20870
  }
20363
- return new Promise((resolve2, reject) => {
20871
+ return new Promise((resolve6, reject) => {
20364
20872
  pendingWriteFailures.add(reject);
20365
20873
  output2.write(chunk, (error61) => {
20366
20874
  pendingWriteFailures.delete(reject);
@@ -20373,34 +20881,12 @@ function createWritableAcpStream(output2) {
20373
20881
  reject(outputFailure);
20374
20882
  return;
20375
20883
  }
20376
- resolve2();
20884
+ resolve6();
20377
20885
  });
20378
20886
  });
20379
20887
  }
20380
20888
  });
20381
20889
  }
20382
- async function stopAgentProcess(child) {
20383
- child.stdin?.end();
20384
- if (child.exitCode !== null) {
20385
- return;
20386
- }
20387
- child.kill("SIGTERM");
20388
- try {
20389
- await once(child, "exit", {
20390
- signal: AbortSignal.timeout(PROCESS_STOP_TIMEOUT_MILLISECONDS)
20391
- });
20392
- } catch {
20393
- if (child.exitCode === null) {
20394
- child.kill("SIGKILL");
20395
- try {
20396
- await once(child, "exit", {
20397
- signal: AbortSignal.timeout(PROCESS_STOP_TIMEOUT_MILLISECONDS)
20398
- });
20399
- } catch {
20400
- }
20401
- }
20402
- }
20403
- }
20404
20890
  function truncate2(value, maximumLength) {
20405
20891
  return value.length <= maximumLength ? value : `${value.slice(0, Math.max(0, maximumLength - 1))}\u2026`;
20406
20892
  }
@@ -20923,8 +21409,11 @@ var AcpProcessSession = class _AcpProcessSession {
20923
21409
  this.#getObserver = getObserver;
20924
21410
  this.#mobiusSessionId = mobiusSessionId;
20925
21411
  this.#store = store;
20926
- this.#child = spawn(agent.command, [...agent.arguments], {
21412
+ this.#child = spawn2(agent.adapterPath, [...agent.arguments], {
20927
21413
  cwd,
21414
+ detached: process.platform !== "win32",
21415
+ env: { ...process.env, ...agent.environment },
21416
+ shell: false,
20928
21417
  stdio: ["pipe", "pipe", "pipe"]
20929
21418
  });
20930
21419
  const stdin = this.#child.stdin;
@@ -20938,9 +21427,9 @@ var AcpProcessSession = class _AcpProcessSession {
20938
21427
  stderr.on("data", (chunk) => {
20939
21428
  process.stderr.write(`[${agent.id}-acp] ${chunk}`);
20940
21429
  });
20941
- this.#processExit = new Promise((resolve2) => {
20942
- this.#child.once("error", resolve2);
20943
- this.#child.once("exit", resolve2);
21430
+ this.#processExit = new Promise((resolve6) => {
21431
+ this.#child.once("error", resolve6);
21432
+ this.#child.once("exit", resolve6);
20944
21433
  });
20945
21434
  const stream = ndJsonStream(
20946
21435
  createWritableAcpStream(stdin),
@@ -20950,6 +21439,18 @@ var AcpProcessSession = class _AcpProcessSession {
20950
21439
  }
20951
21440
  static start = async (options, getObserver, store) => {
20952
21441
  options.signal.throwIfAborted();
21442
+ const [adapterUnchanged, agentUnchanged] = await Promise.all([
21443
+ revalidateExecutable(options.agent.adapterIdentity),
21444
+ revalidateExecutable(options.agent.agentIdentity)
21445
+ ]);
21446
+ options.signal.throwIfAborted();
21447
+ if (!adapterUnchanged || !agentUnchanged) {
21448
+ throw new AcpExecutionError(
21449
+ "The local Agent runtime changed; restart Mobius before starting a new conversation",
21450
+ {},
21451
+ "agent_runtime_changed"
21452
+ );
21453
+ }
20953
21454
  const runtime = new _AcpProcessSession(
20954
21455
  options.agent,
20955
21456
  options.cwd,
@@ -20991,7 +21492,7 @@ var AcpProcessSession = class _AcpProcessSession {
20991
21492
  plan: {},
20992
21493
  session: { configOptions: { boolean: {} } }
20993
21494
  },
20994
- clientInfo: { name: "mobius", version: "0.0.2" },
21495
+ clientInfo: { name: "mobius", version: "0.0.3" },
20995
21496
  protocolVersion: PROTOCOL_VERSION2
20996
21497
  },
20997
21498
  { cancellationSignal: signal }
@@ -21629,7 +22130,7 @@ var AcpProcessSession = class _AcpProcessSession {
21629
22130
  };
21630
22131
  closeProcess = async () => {
21631
22132
  this.#connection.close();
21632
- await stopAgentProcess(this.#child);
22133
+ await stopProcessTree(this.#child);
21633
22134
  };
21634
22135
  race = async (operation, exitMessage, signal) => {
21635
22136
  signal.throwIfAborted();
@@ -21951,9 +22452,16 @@ var AuditStore = class {
21951
22452
  recorded_at TEXT NOT NULL,
21952
22453
  event TEXT NOT NULL,
21953
22454
  command_id TEXT NOT NULL,
21954
- session_id TEXT NOT NULL
22455
+ session_id TEXT NOT NULL,
22456
+ details_json TEXT NOT NULL DEFAULT '{}'
21955
22457
  ) STRICT
21956
22458
  `);
22459
+ const columns = this.database.prepare("PRAGMA table_info(audit_events)").all();
22460
+ if (!columns.some((column) => Reflect.get(column, "name") === "details_json")) {
22461
+ this.database.exec(
22462
+ "ALTER TABLE audit_events ADD COLUMN details_json TEXT NOT NULL DEFAULT '{}'"
22463
+ );
22464
+ }
21957
22465
  }
21958
22466
  record(record2) {
21959
22467
  this.database.prepare(
@@ -21962,14 +22470,16 @@ var AuditStore = class {
21962
22470
  recorded_at,
21963
22471
  event,
21964
22472
  command_id,
21965
- session_id
21966
- ) VALUES (?, ?, ?, ?)
22473
+ session_id,
22474
+ details_json
22475
+ ) VALUES (?, ?, ?, ?, ?)
21967
22476
  `
21968
22477
  ).run(
21969
22478
  (/* @__PURE__ */ new Date()).toISOString(),
21970
22479
  record2.event,
21971
22480
  record2.commandId,
21972
- record2.sessionId
22481
+ record2.sessionId,
22482
+ JSON.stringify(record2.details ?? {})
21973
22483
  );
21974
22484
  }
21975
22485
  close() {
@@ -21978,7 +22488,7 @@ var AuditStore = class {
21978
22488
  };
21979
22489
 
21980
22490
  // src/authorization/authorize-binding.ts
21981
- import { realpath, stat } from "node:fs/promises";
22491
+ import { realpath as realpath2, stat as stat2 } from "node:fs/promises";
21982
22492
  var LocalAuthorizationError = class extends Error {
21983
22493
  code = "local_authorization_failed";
21984
22494
  constructor(message, options = {}) {
@@ -21991,8 +22501,8 @@ async function authorizeBinding(requestedBindingId, approvedBindingId, approvedR
21991
22501
  throw new LocalAuthorizationError("The runtime binding is not authorized");
21992
22502
  }
21993
22503
  try {
21994
- const resolvedRoot = await realpath(approvedRoot);
21995
- const metadata = await stat(resolvedRoot);
22504
+ const resolvedRoot = await realpath2(approvedRoot);
22505
+ const metadata = await stat2(resolvedRoot);
21996
22506
  if (!metadata.isDirectory()) {
21997
22507
  throw new LocalAuthorizationError("The approved root is not a directory");
21998
22508
  }
@@ -22010,324 +22520,1155 @@ async function authorizeBinding(requestedBindingId, approvedBindingId, approvedR
22010
22520
  // src/pairing/pair-local-machine.ts
22011
22521
  import { randomUUID } from "node:crypto";
22012
22522
  import {
22013
- mkdir,
22014
- open as open2,
22015
- realpath as realpath2,
22016
- rename,
22017
- stat as stat2,
22523
+ mkdir as mkdir2,
22524
+ open as open4,
22525
+ realpath as realpath5,
22526
+ rename as rename2,
22527
+ stat as stat4,
22018
22528
  writeFile
22019
22529
  } from "node:fs/promises";
22020
- import { homedir, hostname as hostname3 } from "node:os";
22021
- import { basename, join, resolve } from "node:path";
22530
+ import { hostname as hostname3 } from "node:os";
22531
+ import { basename, join as join6, resolve as resolve4 } from "node:path";
22022
22532
  import { fetch } from "undici";
22023
22533
 
22024
- // src/network/environment-proxy.ts
22025
- import { EnvHttpProxyAgent } from "undici";
22026
- var environmentProxyDispatcher = new EnvHttpProxyAgent();
22534
+ // src/agent-catalog/discover-local-agents.ts
22535
+ import { spawn as spawn3 } from "node:child_process";
22536
+ import { realpath as realpath4 } from "node:fs/promises";
22537
+ import { isAbsolute as isAbsolute2, join as join5, relative as relative3, resolve as resolve3 } from "node:path";
22538
+ import {
22539
+ methods as methods3,
22540
+ ndJsonStream as ndJsonStream2,
22541
+ PROTOCOL_VERSION as PROTOCOL_VERSION3
22542
+ } from "@agentclientprotocol/sdk";
22027
22543
 
22028
- // src/pairing/pair-local-machine.ts
22029
- var PAIRING_TIMEOUT_MILLISECONDS = 2e4;
22030
- var PAIRING_CODE_PATTERN = /^[A-HJ-NP-Z2-9]{4}(?:-[A-HJ-NP-Z2-9]{4}){4}$/;
22031
- var IDENTIFIER_PATTERN3 = /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/;
22032
- var TOKEN_PATTERN = /^[a-zA-Z0-9_-]{32,256}$/;
22033
- var MAX_CONNECTION_FILE_LENGTH = 16 * 1024;
22034
- var CLAIM_RESPONSE_KEYS = ["channelId", "daemonToken"];
22035
- var SAVED_CONNECTION_KEYS = [
22036
- "approvedRoot",
22037
- "channelId",
22038
- "daemonToken",
22039
- "relayOrigin"
22040
- ];
22041
- var PairingClientError = class extends Error {
22042
- code;
22043
- constructor(code, message, options = {}) {
22044
- super(message, options);
22045
- this.code = code;
22046
- this.name = "PairingClientError";
22047
- }
22048
- };
22544
+ // src/agent-installation/installation-grants.ts
22545
+ import { open as open2 } from "node:fs/promises";
22546
+ import { join as join2 } from "node:path";
22547
+ var MAX_GRANT_FILE_LENGTH = 16 * 1024;
22548
+ var AGENT_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/;
22049
22549
  function isRecord4(value) {
22050
22550
  return typeof value === "object" && value !== null && !Array.isArray(value);
22051
22551
  }
22052
- function hasExactKeys(value, expectedKeys) {
22053
- const keys = Object.keys(value);
22054
- return keys.length === expectedKeys.length && expectedKeys.every((key) => Object.hasOwn(value, key));
22055
- }
22056
- function readSingleOption(arguments_, option) {
22057
- const values = [];
22058
- for (let index = 0; index < arguments_.length; index += 1) {
22059
- if (arguments_[index] !== option) {
22060
- continue;
22552
+ async function readGrantedAgents(stateDirectory) {
22553
+ const path = join2(stateDirectory, "installation-grants.json");
22554
+ let handle;
22555
+ try {
22556
+ handle = await open2(path, "r");
22557
+ } catch (error61) {
22558
+ if (error61 instanceof Error && "code" in error61 && error61.code === "ENOENT") {
22559
+ return /* @__PURE__ */ new Set();
22061
22560
  }
22062
- const value = arguments_[index + 1];
22063
- if (value === void 0 || value.startsWith("--")) {
22064
- throw new PairingClientError(
22065
- "invalid_arguments",
22066
- `${option} requires a value`
22067
- );
22561
+ throw error61;
22562
+ }
22563
+ try {
22564
+ const metadata = await handle.stat();
22565
+ if (!metadata.isFile() || metadata.size > MAX_GRANT_FILE_LENGTH || typeof process.getuid === "function" && metadata.uid !== process.getuid() || process.platform !== "win32" && (metadata.mode & 63) !== 0) {
22566
+ throw new Error("The local installation grant file is not private");
22068
22567
  }
22069
- values.push(value);
22070
- index += 1;
22568
+ const encoded = await handle.readFile({ encoding: "utf8" });
22569
+ const value = JSON.parse(encoded);
22570
+ if (!isRecord4(value) || Object.keys(value).length !== 2 || value["version"] !== 1 || !Array.isArray(value["agents"])) {
22571
+ throw new Error("The local installation grant file is invalid");
22572
+ }
22573
+ const agents = /* @__PURE__ */ new Set();
22574
+ for (const candidate of value["agents"]) {
22575
+ if (typeof candidate !== "string" || !AGENT_ID_PATTERN.test(candidate) || agents.has(candidate)) {
22576
+ throw new Error("The local installation grant file is invalid");
22577
+ }
22578
+ agents.add(candidate);
22579
+ }
22580
+ return agents;
22581
+ } finally {
22582
+ await handle.close();
22071
22583
  }
22072
- if (values.length !== 1) {
22073
- throw new PairingClientError(
22074
- "invalid_arguments",
22075
- `${option} must be provided exactly once`
22076
- );
22584
+ }
22585
+
22586
+ // src/agent-installation/managed-adapter-installer.ts
22587
+ import {
22588
+ mkdir,
22589
+ mkdtemp,
22590
+ open as open3,
22591
+ realpath as realpath3,
22592
+ rename,
22593
+ rm,
22594
+ stat as stat3
22595
+ } from "node:fs/promises";
22596
+ import { delimiter, dirname as dirname3, join as join3, relative as relative2 } from "node:path";
22597
+ var ADAPTER_INSTALL_TIMEOUT_MILLISECONDS = 2 * 60 * 1e3;
22598
+ var PACKAGE_METADATA_LIMIT_BYTES = 64 * 1024;
22599
+ var ManagedAdapterInstallError = class extends Error {
22600
+ code;
22601
+ constructor(code, message, options = {}) {
22602
+ super(message, options);
22603
+ this.code = code;
22604
+ this.name = "ManagedAdapterInstallError";
22077
22605
  }
22078
- return values[0] ?? "";
22606
+ };
22607
+ function isRecord5(value) {
22608
+ return typeof value === "object" && value !== null && !Array.isArray(value);
22079
22609
  }
22080
- function assertKnownArguments(arguments_) {
22081
- for (let index = 0; index < arguments_.length; index += 2) {
22082
- const option = arguments_[index];
22083
- if (option !== "--url" && option !== "--code" && option !== "--root") {
22084
- throw new PairingClientError(
22085
- "invalid_arguments",
22086
- `Unknown pairing option: ${option ?? "missing option"}`
22610
+ function packagePath(root, packageName) {
22611
+ return join3(root, "node_modules", ...packageName.split("/"));
22612
+ }
22613
+ async function readPackageJson(path) {
22614
+ const handle = await open3(path, "r");
22615
+ try {
22616
+ const metadata = await handle.stat();
22617
+ if (!metadata.isFile() || metadata.size > PACKAGE_METADATA_LIMIT_BYTES) {
22618
+ throw new ManagedAdapterInstallError(
22619
+ "adapter_verification_failed",
22620
+ "The managed adapter package metadata is invalid"
22087
22621
  );
22088
22622
  }
22623
+ const value = JSON.parse(
22624
+ await handle.readFile({ encoding: "utf8" })
22625
+ );
22626
+ if (!isRecord5(value)) {
22627
+ throw new ManagedAdapterInstallError(
22628
+ "adapter_verification_failed",
22629
+ "The managed adapter package metadata is invalid"
22630
+ );
22631
+ }
22632
+ return value;
22633
+ } finally {
22634
+ await handle.close();
22089
22635
  }
22090
22636
  }
22091
- function parseRelayOrigin(value) {
22092
- let url2;
22093
- try {
22094
- url2 = new URL(value);
22095
- } catch (error61) {
22096
- throw new PairingClientError(
22097
- "invalid_relay_url",
22098
- "The relay URL is invalid",
22099
- { cause: error61 }
22637
+ function readBinPath(packageMetadata, executableName) {
22638
+ const bin = packageMetadata["bin"];
22639
+ if (typeof bin === "string") {
22640
+ return bin;
22641
+ }
22642
+ if (isRecord5(bin) && typeof bin[executableName] === "string") {
22643
+ return bin[executableName];
22644
+ }
22645
+ throw new ManagedAdapterInstallError(
22646
+ "adapter_verification_failed",
22647
+ "The managed adapter executable declaration is invalid"
22648
+ );
22649
+ }
22650
+ async function validateManagedAdapter(root, specification) {
22651
+ const rootPath = await realpath3(root);
22652
+ const packageRoot = await realpath3(
22653
+ packagePath(rootPath, specification.adapterPackage)
22654
+ );
22655
+ const packageDifference = relative2(rootPath, packageRoot);
22656
+ if (packageDifference.startsWith("..") || packageDifference.length === 0) {
22657
+ throw new ManagedAdapterInstallError(
22658
+ "adapter_verification_failed",
22659
+ "The managed adapter package escaped its private cache"
22100
22660
  );
22101
22661
  }
22102
- if (url2.protocol !== "https:" && url2.protocol !== "http:") {
22103
- throw new PairingClientError(
22104
- "invalid_relay_url",
22105
- "The relay URL must use http or https"
22662
+ const metadata = await readPackageJson(join3(packageRoot, "package.json"));
22663
+ if (metadata["name"] !== specification.adapterPackage || metadata["version"] !== specification.adapterVersion) {
22664
+ throw new ManagedAdapterInstallError(
22665
+ "adapter_verification_failed",
22666
+ "The managed adapter package identity is invalid"
22106
22667
  );
22107
22668
  }
22108
- if (url2.username.length > 0 || url2.password.length > 0) {
22109
- throw new PairingClientError(
22110
- "invalid_relay_url",
22111
- "The relay URL cannot contain credentials"
22669
+ const declaredBin = readBinPath(metadata, specification.adapterExecutable);
22670
+ const executable = await resolveExecutablePath(
22671
+ join3(packageRoot, declaredBin)
22672
+ );
22673
+ if (executable === null) {
22674
+ throw new ManagedAdapterInstallError(
22675
+ "adapter_verification_failed",
22676
+ "The managed adapter executable is invalid"
22112
22677
  );
22113
22678
  }
22114
- if (url2.protocol === "http:" && url2.hostname !== "127.0.0.1" && url2.hostname !== "localhost") {
22115
- throw new PairingClientError(
22116
- "invalid_relay_url",
22117
- "A remote relay must use https"
22679
+ const executableDifference = relative2(rootPath, executable.path);
22680
+ if (executableDifference.startsWith("..") || executableDifference.length === 0) {
22681
+ throw new ManagedAdapterInstallError(
22682
+ "adapter_verification_failed",
22683
+ "The managed adapter executable escaped its private cache"
22118
22684
  );
22119
22685
  }
22120
- url2.hash = "";
22121
- url2.pathname = "/";
22122
- url2.search = "";
22123
- return url2;
22686
+ return executable;
22124
22687
  }
22125
- function parsePairingCode(value) {
22126
- const code = value.trim().toUpperCase();
22127
- if (!PAIRING_CODE_PATTERN.test(code)) {
22128
- throw new PairingClientError(
22129
- "invalid_pairing_code",
22130
- "The pairing code must match the code shown in the browser"
22688
+ var ManagedAdapterInstaller = class {
22689
+ #approvedRoot;
22690
+ #environment;
22691
+ #stateDirectory;
22692
+ constructor(options) {
22693
+ this.#approvedRoot = options.approvedRoot;
22694
+ this.#environment = options.environment;
22695
+ this.#stateDirectory = options.stateDirectory;
22696
+ }
22697
+ install = async (specification, signal) => {
22698
+ signal.throwIfAborted();
22699
+ const agentCache = join3(this.#stateDirectory, "adapters", specification.id);
22700
+ const published = join3(agentCache, specification.adapterVersion);
22701
+ try {
22702
+ const metadata = await stat3(published);
22703
+ if (metadata.isDirectory()) {
22704
+ return await validateManagedAdapter(published, specification);
22705
+ }
22706
+ } catch (error61) {
22707
+ if (!(error61 instanceof Error) || !("code" in error61) || error61.code !== "ENOENT") {
22708
+ throw error61;
22709
+ }
22710
+ }
22711
+ await mkdir(agentCache, { mode: 448, recursive: true });
22712
+ const staging = await mkdtemp(
22713
+ join3(agentCache, `.${specification.adapterVersion}.staging-`)
22131
22714
  );
22715
+ try {
22716
+ const npm = await findExecutableOnPath(
22717
+ process.platform === "win32" ? "npm.cmd" : "npm",
22718
+ this.#environment,
22719
+ this.#approvedRoot
22720
+ );
22721
+ if (npm === null) {
22722
+ throw new ManagedAdapterInstallError(
22723
+ "package_manager_not_found",
22724
+ "A supported npm executable is required to install the ACP adapter"
22725
+ );
22726
+ }
22727
+ await runBoundedProcess({
22728
+ arguments: [
22729
+ "install",
22730
+ "--prefix",
22731
+ staging,
22732
+ "--omit=optional",
22733
+ "--ignore-scripts",
22734
+ "--no-audit",
22735
+ "--no-fund",
22736
+ "--package-lock=false",
22737
+ "--save=false",
22738
+ "--registry=https://registry.npmjs.org/",
22739
+ `${specification.adapterPackage}@${specification.adapterVersion}`
22740
+ ],
22741
+ command: npm.path,
22742
+ cwd: staging,
22743
+ environment: {
22744
+ HOME: join3(staging, "home"),
22745
+ NPM_CONFIG_AUDIT: "false",
22746
+ NPM_CONFIG_FUND: "false",
22747
+ NPM_CONFIG_IGNORE_SCRIPTS: "true",
22748
+ NPM_CONFIG_OMIT: "optional",
22749
+ NPM_CONFIG_REGISTRY: "https://registry.npmjs.org/",
22750
+ PATH: `${dirname3(process.execPath)}${delimiter}/usr/bin${delimiter}/bin`
22751
+ },
22752
+ outputLimitBytes: 64 * 1024,
22753
+ signal,
22754
+ timeoutMilliseconds: ADAPTER_INSTALL_TIMEOUT_MILLISECONDS
22755
+ });
22756
+ await validateManagedAdapter(staging, specification);
22757
+ try {
22758
+ await rename(staging, published);
22759
+ } catch (error61) {
22760
+ try {
22761
+ return await validateManagedAdapter(published, specification);
22762
+ } catch {
22763
+ throw new ManagedAdapterInstallError(
22764
+ "adapter_publish_failed",
22765
+ "The managed adapter could not be published",
22766
+ { cause: error61 }
22767
+ );
22768
+ }
22769
+ }
22770
+ return await validateManagedAdapter(published, specification);
22771
+ } catch (error61) {
22772
+ if (signal.aborted) {
22773
+ signal.throwIfAborted();
22774
+ }
22775
+ if (error61 instanceof ManagedAdapterInstallError) {
22776
+ throw error61;
22777
+ }
22778
+ throw new ManagedAdapterInstallError(
22779
+ "adapter_install_failed",
22780
+ "The pinned ACP adapter could not be installed",
22781
+ { cause: error61 }
22782
+ );
22783
+ } finally {
22784
+ await rm(staging, { force: true, recursive: true });
22785
+ }
22786
+ };
22787
+ };
22788
+
22789
+ // src/storage/state-directory.ts
22790
+ import { homedir } from "node:os";
22791
+ import { join as join4, resolve as resolve2 } from "node:path";
22792
+ function readStateDirectory(environment = process.env) {
22793
+ const configured = environment["MOBIUS_STATE_DIRECTORY"]?.trim();
22794
+ if (configured !== void 0 && configured.length > 0) {
22795
+ return resolve2(configured);
22132
22796
  }
22133
- return code;
22797
+ return join4(homedir(), ".mobius");
22134
22798
  }
22135
- function parsePairCommand(arguments_) {
22136
- assertKnownArguments(arguments_);
22137
- return {
22138
- code: parsePairingCode(readSingleOption(arguments_, "--code")),
22139
- relayOrigin: parseRelayOrigin(readSingleOption(arguments_, "--url")),
22140
- root: readSingleOption(arguments_, "--root")
22141
- };
22799
+
22800
+ // src/agent-catalog/discover-local-agents.ts
22801
+ var VERSION_PROBE_TIMEOUT_MILLISECONDS = 3e3;
22802
+ var ACP_PROBE_TIMEOUT_MILLISECONDS = 5e3;
22803
+ var ACP_OUTPUT_LIMIT_BYTES = 1024 * 1024;
22804
+ var LocalAgentSpecification = class {
22805
+ recoveryCommand = () => `npm install -g --omit=optional ${this.adapterPackage}@${this.adapterVersion}`;
22806
+ supportsInstallation = (platform, architecture) => (platform === "darwin" || platform === "linux") && (architecture === "arm64" || architecture === "x64");
22807
+ };
22808
+ var CodexAgentSpecification = class extends LocalAgentSpecification {
22809
+ adapterEnvironmentVariable = "CODEX_PATH";
22810
+ adapterExecutable = "codex-acp";
22811
+ adapterOverrideVariable = "MOBIUS_CODEX_ACP_PATH";
22812
+ adapterPackage = "@agentclientprotocol/codex-acp";
22813
+ adapterVersion = "1.8.0";
22814
+ agentExecutable = "codex";
22815
+ agentOverrideVariable = "MOBIUS_CODEX_PATH";
22816
+ id = "codex";
22817
+ label = "Codex";
22818
+ managedAdapterExecutablePath = [
22819
+ "adapters",
22820
+ "codex",
22821
+ "1.8.0",
22822
+ "node_modules",
22823
+ "@agentclientprotocol",
22824
+ "codex-acp",
22825
+ "dist",
22826
+ "index.js"
22827
+ ];
22828
+ managedAgentExecutablePath = ["agents", "codex", "bin", "codex"];
22829
+ };
22830
+ var ClaudeCodeAgentSpecification = class extends LocalAgentSpecification {
22831
+ adapterEnvironmentVariable = "CLAUDE_CODE_EXECUTABLE";
22832
+ adapterExecutable = "claude-agent-acp";
22833
+ adapterOverrideVariable = "MOBIUS_CLAUDE_ACP_PATH";
22834
+ adapterPackage = "@agentclientprotocol/claude-agent-acp";
22835
+ adapterVersion = "0.73.0";
22836
+ agentExecutable = "claude";
22837
+ agentOverrideVariable = "MOBIUS_CLAUDE_PATH";
22838
+ id = "claude";
22839
+ label = "Claude Code";
22840
+ managedAdapterExecutablePath = [
22841
+ "adapters",
22842
+ "claude",
22843
+ "0.73.0",
22844
+ "node_modules",
22845
+ "@agentclientprotocol",
22846
+ "claude-agent-acp",
22847
+ "dist",
22848
+ "index.js"
22849
+ ];
22850
+ managedAgentExecutablePath = [
22851
+ "agents",
22852
+ "claude",
22853
+ "home",
22854
+ ".local",
22855
+ "bin",
22856
+ "claude"
22857
+ ];
22858
+ };
22859
+ function supportedLocalAgentSpecifications() {
22860
+ return Object.freeze([
22861
+ new CodexAgentSpecification(),
22862
+ new ClaudeCodeAgentSpecification()
22863
+ ]);
22142
22864
  }
22143
- function parseChannelArgument(arguments_) {
22144
- if (arguments_.length !== 2 || arguments_[0] !== "--channel") {
22145
- throw new PairingClientError(
22146
- "invalid_arguments",
22147
- "Usage: mobius start --channel <channel-id>"
22148
- );
22865
+ var AgentDiscovery = class {
22866
+ specification;
22867
+ constructor(specification) {
22868
+ this.specification = specification;
22149
22869
  }
22150
- const channelId = arguments_[1] ?? "";
22151
- if (!IDENTIFIER_PATTERN3.test(channelId)) {
22152
- throw new PairingClientError(
22153
- "invalid_channel_id",
22154
- "The saved channel ID is invalid"
22155
- );
22870
+ };
22871
+ var AvailableAgent = class extends AgentDiscovery {
22872
+ agent;
22873
+ status = "ready";
22874
+ constructor(specification, agent) {
22875
+ super(specification);
22876
+ this.agent = agent;
22156
22877
  }
22157
- return channelId;
22158
- }
22159
- async function resolveApprovedRoot(path) {
22160
- const requestedRoot = resolve(path);
22161
- try {
22162
- const approvedRoot = await realpath2(requestedRoot);
22163
- const metadata = await stat2(approvedRoot);
22164
- if (!metadata.isDirectory()) {
22165
- throw new PairingClientError(
22166
- "invalid_workspace",
22167
- "The approved workspace root is not a directory"
22168
- );
22169
- }
22170
- return approvedRoot;
22171
- } catch (error61) {
22172
- if (error61 instanceof PairingClientError) {
22173
- throw error61;
22174
- }
22175
- throw new PairingClientError(
22176
- "invalid_workspace",
22177
- "The approved workspace root cannot be resolved",
22178
- { cause: error61 }
22179
- );
22878
+ };
22879
+ var UnavailableAgent = class extends AgentDiscovery {
22880
+ };
22881
+ var MissingAdapter = class extends UnavailableAgent {
22882
+ code = "adapter_not_found";
22883
+ message = "ACP adapter not found";
22884
+ status = "missing_adapter";
22885
+ constructor(specification) {
22886
+ super(specification);
22180
22887
  }
22181
- }
22182
- async function readJsonResponse(response) {
22183
- if (response.headers.get("cf-mitigated") === "challenge") {
22184
- throw new PairingClientError(
22185
- "relay_challenge",
22186
- "Cloudflare is protecting this temporary preview. Claim the deployment or use an authenticated deployment, then pair again"
22888
+ };
22889
+ var MissingAgent = class extends UnavailableAgent {
22890
+ code = "agent_not_found";
22891
+ message = "Agent executable not found";
22892
+ status = "missing_agent";
22893
+ constructor(specification) {
22894
+ super(specification);
22895
+ }
22896
+ };
22897
+ var ProbeFailed = class extends UnavailableAgent {
22898
+ code;
22899
+ message;
22900
+ status = "probe_failed";
22901
+ constructor(specification, code, message) {
22902
+ super(specification);
22903
+ this.code = code;
22904
+ this.message = message;
22905
+ }
22906
+ };
22907
+ var IncompatibleAdapter = class extends UnavailableAgent {
22908
+ code = "acp_incompatible";
22909
+ message = "ACP adapter is incompatible";
22910
+ status = "incompatible_adapter";
22911
+ constructor(specification) {
22912
+ super(specification);
22913
+ }
22914
+ };
22915
+ var LocalAgentInventory = class {
22916
+ available;
22917
+ discoveries;
22918
+ installable;
22919
+ unavailable;
22920
+ constructor(discoveries, grantedAgents = /* @__PURE__ */ new Set(), platform = process.platform, architecture = process.arch) {
22921
+ const available = [];
22922
+ const unavailable = [];
22923
+ for (const discovery of discoveries) {
22924
+ if (discovery instanceof AvailableAgent) {
22925
+ available.push(discovery);
22926
+ } else if (discovery instanceof UnavailableAgent) {
22927
+ unavailable.push(discovery);
22928
+ } else {
22929
+ throw new Error("Unsupported Agent discovery outcome");
22930
+ }
22931
+ }
22932
+ this.available = Object.freeze(available);
22933
+ this.discoveries = Object.freeze([...discoveries]);
22934
+ this.installable = Object.freeze(
22935
+ unavailable.filter(
22936
+ (discovery) => discovery.specification.supportsInstallation(platform, architecture)
22937
+ ).map(
22938
+ (discovery) => new InstallableAgentDefinition(
22939
+ discovery.specification.id,
22940
+ discovery.specification.label,
22941
+ !grantedAgents.has(discovery.specification.id)
22942
+ )
22943
+ )
22187
22944
  );
22945
+ this.unavailable = Object.freeze(unavailable);
22188
22946
  }
22189
- const encoded = await response.text();
22947
+ readyAgents = () => this.available.map((discovery) => discovery.agent);
22948
+ };
22949
+ var AgentProbeError = class extends Error {
22950
+ code;
22951
+ constructor(code, message, options = {}) {
22952
+ super(message, options);
22953
+ this.code = code;
22954
+ this.name = "AgentProbeError";
22955
+ }
22956
+ };
22957
+ var DiscoveryAcpHandler = class {
22958
+ onElicitation = (_request, _signal) => Promise.reject(new Error("ACP probe does not accept elicitations"));
22959
+ onElicitationComplete = (_notification) => Promise.resolve();
22960
+ onPermission = (_request, _signal) => Promise.reject(new Error("ACP probe does not grant permissions"));
22961
+ onUpdate = (_notification) => Promise.resolve();
22962
+ };
22963
+ async function authorizeManagedExecutable(managed, options) {
22964
+ const approvedRoot = await realpath4(resolve3(options.approvedRoot));
22965
+ const requestedDifference = relative3(
22966
+ approvedRoot,
22967
+ managed.identity.requestedPath
22968
+ );
22969
+ const resolvedDifference = relative3(approvedRoot, managed.path);
22970
+ const requestedInside = requestedDifference.length === 0 || !requestedDifference.startsWith("..") && !isAbsolute2(requestedDifference);
22971
+ const resolvedInside = resolvedDifference.length === 0 || !resolvedDifference.startsWith("..") && !isAbsolute2(resolvedDifference);
22972
+ return requestedInside || resolvedInside ? null : managed;
22973
+ }
22974
+ async function resolveManagedExecutable(managedExecutablePath, options) {
22975
+ const managed = await resolveExecutablePath(
22976
+ join5(readStateDirectory(options.environment), ...managedExecutablePath),
22977
+ options.platform
22978
+ );
22979
+ return managed === null ? null : await authorizeManagedExecutable(managed, options);
22980
+ }
22981
+ async function resolveManagedAdapterExecutable(specification, options) {
22982
+ let managed;
22190
22983
  try {
22191
- return JSON.parse(encoded);
22192
- } catch (error61) {
22193
- throw new PairingClientError(
22194
- "invalid_relay_response",
22195
- "The relay returned an invalid response",
22196
- { cause: error61 }
22984
+ managed = await validateManagedAdapter(
22985
+ join5(
22986
+ readStateDirectory(options.environment),
22987
+ "adapters",
22988
+ specification.id,
22989
+ specification.adapterVersion
22990
+ ),
22991
+ specification
22197
22992
  );
22993
+ } catch {
22994
+ return null;
22198
22995
  }
22996
+ return await authorizeManagedExecutable(managed, options);
22199
22997
  }
22200
- function readRelayError(value) {
22201
- if (isRecord4(value)) {
22202
- const error61 = value["error"];
22203
- if (isRecord4(error61)) {
22204
- const code = error61["code"];
22205
- const message = error61["message"];
22206
- if (typeof code === "string" && typeof message === "string") {
22207
- return new PairingClientError(code, message);
22208
- }
22998
+ async function resolveSpecificationExecutables(executableName, overrideVariable, resolveManaged, options) {
22999
+ const override = options.environment[overrideVariable];
23000
+ if (override !== void 0) {
23001
+ if (override.length === 0 || !isAbsolute2(override)) {
23002
+ return [];
22209
23003
  }
23004
+ const executable = await resolveExecutablePath(override, options.platform);
23005
+ return executable === null ? [] : [executable];
22210
23006
  }
22211
- return new PairingClientError(
22212
- "pairing_failed",
22213
- "The relay could not pair this machine"
23007
+ const external = await findExecutableOnPath(
23008
+ executableName,
23009
+ options.environment,
23010
+ options.approvedRoot,
23011
+ options.platform
23012
+ );
23013
+ const managed = await resolveManaged();
23014
+ if (external === null) {
23015
+ return managed === null ? [] : [managed];
23016
+ }
23017
+ if (managed === null || managed.path === external.path) {
23018
+ return [external];
23019
+ }
23020
+ return [external, managed];
23021
+ }
23022
+ async function resolveAgentExecutables(specification, options) {
23023
+ return await resolveSpecificationExecutables(
23024
+ specification.agentExecutable,
23025
+ specification.agentOverrideVariable,
23026
+ async () => await resolveManagedExecutable(
23027
+ specification.managedAgentExecutablePath,
23028
+ options
23029
+ ),
23030
+ options
22214
23031
  );
22215
23032
  }
22216
- function decodeClaimResponse(value) {
22217
- if (!isRecord4(value) || !hasExactKeys(value, CLAIM_RESPONSE_KEYS)) {
22218
- throw new PairingClientError(
22219
- "invalid_relay_response",
22220
- "The relay returned an invalid pairing response"
23033
+ async function resolveAdapterExecutables(specification, options) {
23034
+ return await resolveSpecificationExecutables(
23035
+ specification.adapterExecutable,
23036
+ specification.adapterOverrideVariable,
23037
+ async () => await resolveManagedAdapterExecutable(specification, options),
23038
+ options
23039
+ );
23040
+ }
23041
+ async function resolveAgentExecutable(specification, options) {
23042
+ const executables = await resolveAgentExecutables(specification, options);
23043
+ return executables[0] ?? null;
23044
+ }
23045
+ async function resolveAdapterExecutable(specification, options) {
23046
+ const executables = await resolveAdapterExecutables(specification, options);
23047
+ return executables[0] ?? null;
23048
+ }
23049
+ function normalizedVersion(output2) {
23050
+ return output2.trim().split(/\r?\n/, 1)[0]?.trim().slice(0, 128) ?? "";
23051
+ }
23052
+ async function probeVersion(executable, environment, approvedRoot, signal) {
23053
+ const result = await runBoundedProcess({
23054
+ arguments: ["--version"],
23055
+ command: executable.path,
23056
+ cwd: approvedRoot,
23057
+ environment,
23058
+ signal,
23059
+ timeoutMilliseconds: VERSION_PROBE_TIMEOUT_MILLISECONDS
23060
+ });
23061
+ const version2 = normalizedVersion(result.stdout || result.stderr);
23062
+ if (version2.length === 0) {
23063
+ throw new ProcessExecutionError(
23064
+ "process_exit_failed",
23065
+ "The executable did not report a version"
22221
23066
  );
22222
23067
  }
22223
- const channelId = value["channelId"];
22224
- const daemonToken = value["daemonToken"];
22225
- if (typeof channelId !== "string" || !IDENTIFIER_PATTERN3.test(channelId) || typeof daemonToken !== "string" || !TOKEN_PATTERN.test(daemonToken)) {
22226
- throw new PairingClientError(
22227
- "invalid_relay_response",
22228
- "The relay returned an invalid pairing response"
22229
- );
23068
+ return version2;
23069
+ }
23070
+ var VerifiedAgentExecutable = class {
23071
+ executable;
23072
+ version;
23073
+ constructor(executable, version2) {
23074
+ this.executable = executable;
23075
+ this.version = version2;
22230
23076
  }
22231
- return { channelId, daemonToken };
23077
+ };
23078
+ async function findVerifiedAgentExecutable(specification, options) {
23079
+ const executables = await resolveAgentExecutables(specification, options);
23080
+ for (const executable of executables) {
23081
+ try {
23082
+ const version2 = await probeVersion(
23083
+ executable,
23084
+ options.environment,
23085
+ options.approvedRoot,
23086
+ options.signal
23087
+ );
23088
+ return new VerifiedAgentExecutable(executable, version2);
23089
+ } catch {
23090
+ if (options.signal.aborted) {
23091
+ options.signal.throwIfAborted();
23092
+ }
23093
+ }
23094
+ }
23095
+ return null;
22232
23096
  }
22233
- async function claimPairing(options, approvedRoot) {
22234
- const agents = loadLocalAgents().map((agent) => ({
22235
- id: agent.id,
22236
- label: agent.label
22237
- }));
22238
- const endpoint = new URL("/v1/pairings/claim", options.relayOrigin);
22239
- let response;
22240
- try {
22241
- response = await fetch(endpoint, {
22242
- body: JSON.stringify({
22243
- agents,
22244
- machineName: hostname3(),
22245
- pairingCode: options.code,
22246
- workspaceName: basename(approvedRoot) || "Local workspace"
22247
- }),
22248
- dispatcher: environmentProxyDispatcher,
22249
- headers: { "content-type": "application/json" },
22250
- method: "POST",
22251
- signal: AbortSignal.timeout(PAIRING_TIMEOUT_MILLISECONDS)
22252
- });
23097
+ function readableAcpStream(input2) {
23098
+ let byteLength = 0;
23099
+ let open6 = true;
23100
+ return new ReadableStream({
23101
+ start(controller) {
23102
+ input2.on("data", (chunk) => {
23103
+ if (!open6) {
23104
+ return;
23105
+ }
23106
+ if (!(chunk instanceof Uint8Array)) {
23107
+ open6 = false;
23108
+ controller.error(new Error("ACP adapter produced invalid output"));
23109
+ return;
23110
+ }
23111
+ byteLength += chunk.byteLength;
23112
+ if (byteLength > ACP_OUTPUT_LIMIT_BYTES) {
23113
+ open6 = false;
23114
+ controller.error(new Error("ACP adapter output limit exceeded"));
23115
+ return;
23116
+ }
23117
+ controller.enqueue(new Uint8Array(chunk));
23118
+ });
23119
+ input2.once("end", () => {
23120
+ if (open6) {
23121
+ open6 = false;
23122
+ controller.close();
23123
+ }
23124
+ });
23125
+ input2.once("error", (error61) => {
23126
+ if (open6) {
23127
+ open6 = false;
23128
+ controller.error(error61);
23129
+ }
23130
+ });
23131
+ },
23132
+ cancel() {
23133
+ open6 = false;
23134
+ }
23135
+ });
23136
+ }
23137
+ function writableAcpStream(output2) {
23138
+ return new WritableStream({
23139
+ write(chunk) {
23140
+ return new Promise((resolveWrite, rejectWrite) => {
23141
+ output2.write(chunk, (error61) => {
23142
+ if (error61) {
23143
+ rejectWrite(error61);
23144
+ return;
23145
+ }
23146
+ resolveWrite();
23147
+ });
23148
+ });
23149
+ }
23150
+ });
23151
+ }
23152
+ async function probeAcp(agent, environment, approvedRoot, signal) {
23153
+ signal.throwIfAborted();
23154
+ const controller = new AbortController();
23155
+ let timedOut = false;
23156
+ const timeout = setTimeout(() => {
23157
+ timedOut = true;
23158
+ controller.abort(new Error("ACP probe timed out"));
23159
+ }, ACP_PROBE_TIMEOUT_MILLISECONDS);
23160
+ const abort = () => controller.abort(signal.reason);
23161
+ signal.addEventListener("abort", abort, { once: true });
23162
+ if (signal.aborted) {
23163
+ abort();
23164
+ }
23165
+ const child = spawn3(agent.adapterPath, [...agent.arguments], {
23166
+ cwd: approvedRoot,
23167
+ detached: process.platform !== "win32",
23168
+ env: { ...environment, ...agent.environment },
23169
+ shell: false,
23170
+ stdio: ["pipe", "pipe", "pipe"]
23171
+ });
23172
+ const stdin = child.stdin;
23173
+ const stdout = child.stdout;
23174
+ const stderr = child.stderr;
23175
+ if (stdin === null || stdout === null || stderr === null) {
23176
+ await stopProcessTree(child);
23177
+ throw new AgentProbeError(
23178
+ "acp_incompatible",
23179
+ "ACP adapter streams are unavailable"
23180
+ );
23181
+ }
23182
+ let stderrBytes = 0;
23183
+ const consumeStderr = (chunk) => {
23184
+ stderrBytes += chunk.byteLength;
23185
+ if (stderrBytes > ACP_OUTPUT_LIMIT_BYTES) {
23186
+ controller.abort(new Error("ACP adapter output limit exceeded"));
23187
+ }
23188
+ };
23189
+ stderr.on("data", consumeStderr);
23190
+ const connection = createAcpClient(new DiscoveryAcpHandler()).connect(
23191
+ ndJsonStream2(writableAcpStream(stdin), readableAcpStream(stdout))
23192
+ );
23193
+ const processExit = new Promise((_resolve, reject) => {
23194
+ child.once("error", (error61) => reject(error61));
23195
+ child.once(
23196
+ "exit",
23197
+ () => reject(new Error("ACP adapter exited during initialization"))
23198
+ );
23199
+ });
23200
+ try {
23201
+ const initialized = await Promise.race([
23202
+ connection.agent.request(
23203
+ methods3.agent.initialize,
23204
+ {
23205
+ clientCapabilities: {},
23206
+ clientInfo: { name: "mobius", version: "0.0.3" },
23207
+ protocolVersion: PROTOCOL_VERSION3
23208
+ },
23209
+ { cancellationSignal: controller.signal }
23210
+ ),
23211
+ processExit
23212
+ ]);
23213
+ if (initialized.protocolVersion !== PROTOCOL_VERSION3) {
23214
+ throw new AgentProbeError(
23215
+ "acp_incompatible",
23216
+ "ACP adapter selected an unsupported protocol version"
23217
+ );
23218
+ }
22253
23219
  } catch (error61) {
22254
- throw new PairingClientError(
22255
- "relay_unavailable",
22256
- "The pairing relay could not be reached",
23220
+ if (signal.aborted) {
23221
+ signal.throwIfAborted();
23222
+ }
23223
+ if (timedOut) {
23224
+ throw new AgentProbeError(
23225
+ "adapter_probe_timeout",
23226
+ "ACP adapter probe timed out",
23227
+ { cause: error61 }
23228
+ );
23229
+ }
23230
+ if (error61 instanceof AgentProbeError) {
23231
+ throw error61;
23232
+ }
23233
+ throw new AgentProbeError(
23234
+ "acp_incompatible",
23235
+ "ACP adapter initialization failed",
22257
23236
  { cause: error61 }
22258
23237
  );
23238
+ } finally {
23239
+ clearTimeout(timeout);
23240
+ signal.removeEventListener("abort", abort);
23241
+ connection.close();
23242
+ stdin.end();
23243
+ stderr.removeListener("data", consumeStderr);
23244
+ await stopProcessTree(child);
23245
+ }
23246
+ }
23247
+ function probeFailure(specification, executableKind, error61) {
23248
+ if (error61 instanceof AgentProbeError) {
23249
+ return error61.code === "acp_incompatible" ? new IncompatibleAdapter(specification) : new ProbeFailed(specification, error61.code, error61.message);
23250
+ }
23251
+ if (error61 instanceof ProcessExecutionError) {
23252
+ const code = error61.code === "process_timeout" ? `${executableKind}_probe_timeout` : `${executableKind}_probe_failed`;
23253
+ return new ProbeFailed(
23254
+ specification,
23255
+ code,
23256
+ `${executableKind === "agent" ? "Agent" : "Adapter"} version probe failed`
23257
+ );
22259
23258
  }
22260
- const value = await readJsonResponse(response);
22261
- if (!response.ok) {
22262
- throw readRelayError(value);
22263
- }
22264
- return decodeClaimResponse(value);
23259
+ return new ProbeFailed(
23260
+ specification,
23261
+ `${executableKind}_probe_failed`,
23262
+ `${executableKind === "agent" ? "Agent" : "Adapter"} probe failed`
23263
+ );
22265
23264
  }
22266
- function readStateDirectory() {
22267
- const configured = process.env["MOBIUS_STATE_DIRECTORY"]?.trim();
22268
- if (configured !== void 0 && configured.length > 0) {
22269
- return resolve(configured);
23265
+ async function discoverSpecification(specification, options) {
23266
+ options.signal.throwIfAborted();
23267
+ const [adapters, agents] = await Promise.all([
23268
+ resolveAdapterExecutables(specification, options),
23269
+ resolveAgentExecutables(specification, options)
23270
+ ]);
23271
+ if (adapters.length === 0) {
23272
+ return new MissingAdapter(specification);
23273
+ }
23274
+ if (agents.length === 0) {
23275
+ return new MissingAgent(specification);
23276
+ }
23277
+ let lastFailure = null;
23278
+ for (const agent of agents) {
23279
+ let agentVersion = "";
23280
+ try {
23281
+ agentVersion = await probeVersion(
23282
+ agent,
23283
+ options.environment,
23284
+ options.approvedRoot,
23285
+ options.signal
23286
+ );
23287
+ } catch (error61) {
23288
+ if (options.signal.aborted) {
23289
+ options.signal.throwIfAborted();
23290
+ }
23291
+ lastFailure = probeFailure(specification, "agent", error61);
23292
+ continue;
23293
+ }
23294
+ for (const adapter of adapters) {
23295
+ let adapterVersion = "";
23296
+ try {
23297
+ adapterVersion = await probeVersion(
23298
+ adapter,
23299
+ {
23300
+ ...options.environment,
23301
+ [specification.adapterEnvironmentVariable]: agent.path
23302
+ },
23303
+ options.approvedRoot,
23304
+ options.signal
23305
+ );
23306
+ } catch (error61) {
23307
+ if (options.signal.aborted) {
23308
+ options.signal.throwIfAborted();
23309
+ }
23310
+ lastFailure = probeFailure(specification, "adapter", error61);
23311
+ continue;
23312
+ }
23313
+ const definition = new LocalAgentDefinition(
23314
+ specification.id,
23315
+ specification.label,
23316
+ adapter.path,
23317
+ [],
23318
+ { [specification.adapterEnvironmentVariable]: agent.path },
23319
+ adapterVersion,
23320
+ agentVersion,
23321
+ adapter.identity,
23322
+ agent.identity
23323
+ );
23324
+ try {
23325
+ await probeAcp(
23326
+ definition,
23327
+ options.environment,
23328
+ options.approvedRoot,
23329
+ options.signal
23330
+ );
23331
+ return new AvailableAgent(specification, definition);
23332
+ } catch (error61) {
23333
+ if (options.signal.aborted) {
23334
+ options.signal.throwIfAborted();
23335
+ }
23336
+ lastFailure = probeFailure(specification, "adapter", error61);
23337
+ }
23338
+ }
22270
23339
  }
22271
- return join(homedir(), ".mobius");
23340
+ return lastFailure ?? new IncompatibleAdapter(specification);
22272
23341
  }
22273
- async function persistConnection(channelId, relayOrigin, daemonToken, approvedRoot) {
22274
- const connectionsDirectory = join(readStateDirectory(), "connections");
22275
- await mkdir(connectionsDirectory, { mode: 448, recursive: true });
22276
- const connectionPath = join(connectionsDirectory, `${channelId}.json`);
22277
- const temporaryPath = join(
22278
- connectionsDirectory,
22279
- `.${channelId}.${randomUUID()}.tmp`
23342
+ async function discoverLocalAgentInventory(options) {
23343
+ const specifications = options.specifications ?? supportedLocalAgentSpecifications();
23344
+ const discoveries = await Promise.all(
23345
+ specifications.map(
23346
+ async (specification) => await discoverSpecification(specification, options)
23347
+ )
22280
23348
  );
22281
- await writeFile(
22282
- temporaryPath,
22283
- `${JSON.stringify(
22284
- {
22285
- approvedRoot,
22286
- channelId,
22287
- daemonToken,
22288
- relayOrigin: relayOrigin.toString()
22289
- },
22290
- null,
22291
- 2
22292
- )}
22293
- `,
22294
- { flag: "wx", mode: 384 }
23349
+ let grantedAgents = /* @__PURE__ */ new Set();
23350
+ try {
23351
+ grantedAgents = await readGrantedAgents(
23352
+ readStateDirectory(options.environment)
23353
+ );
23354
+ } catch {
23355
+ }
23356
+ return new LocalAgentInventory(
23357
+ discoveries,
23358
+ grantedAgents,
23359
+ options.platform,
23360
+ options.architecture
22295
23361
  );
22296
- await rename(temporaryPath, connectionPath);
22297
- return connectionPath;
22298
23362
  }
22299
- function decodeSavedConnection(value, expectedChannelId) {
22300
- if (!isRecord4(value) || !hasExactKeys(value, SAVED_CONNECTION_KEYS)) {
23363
+
23364
+ // src/network/environment-proxy.ts
23365
+ import { EnvHttpProxyAgent } from "undici";
23366
+ var environmentProxyDispatcher = new EnvHttpProxyAgent();
23367
+
23368
+ // src/pairing/pair-local-machine.ts
23369
+ var PAIRING_TIMEOUT_MILLISECONDS = 2e4;
23370
+ var PAIRING_CODE_PATTERN = /^[A-HJ-NP-Z2-9]{4}(?:-[A-HJ-NP-Z2-9]{4}){4}$/;
23371
+ var IDENTIFIER_PATTERN3 = /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/;
23372
+ var TOKEN_PATTERN = /^[a-zA-Z0-9_-]{32,256}$/;
23373
+ var MAX_CONNECTION_FILE_LENGTH = 16 * 1024;
23374
+ var CLAIM_RESPONSE_KEYS = ["channelId", "daemonToken"];
23375
+ var SAVED_CONNECTION_KEYS = [
23376
+ "approvedRoot",
23377
+ "channelId",
23378
+ "daemonToken",
23379
+ "relayOrigin"
23380
+ ];
23381
+ var PairingClientError = class extends Error {
23382
+ code;
23383
+ constructor(code, message, options = {}) {
23384
+ super(message, options);
23385
+ this.code = code;
23386
+ this.name = "PairingClientError";
23387
+ }
23388
+ };
23389
+ function isRecord6(value) {
23390
+ return typeof value === "object" && value !== null && !Array.isArray(value);
23391
+ }
23392
+ function hasExactKeys(value, expectedKeys) {
23393
+ const keys = Object.keys(value);
23394
+ return keys.length === expectedKeys.length && expectedKeys.every((key) => Object.hasOwn(value, key));
23395
+ }
23396
+ function readSingleOption(arguments_, option) {
23397
+ const values = [];
23398
+ for (let index = 0; index < arguments_.length; index += 1) {
23399
+ if (arguments_[index] !== option) {
23400
+ continue;
23401
+ }
23402
+ const value = arguments_[index + 1];
23403
+ if (value === void 0 || value.startsWith("--")) {
23404
+ throw new PairingClientError(
23405
+ "invalid_arguments",
23406
+ `${option} requires a value`
23407
+ );
23408
+ }
23409
+ values.push(value);
23410
+ index += 1;
23411
+ }
23412
+ if (values.length !== 1) {
22301
23413
  throw new PairingClientError(
22302
- "invalid_connection",
22303
- "The saved connection is invalid"
23414
+ "invalid_arguments",
23415
+ `${option} must be provided exactly once`
22304
23416
  );
22305
23417
  }
22306
- const approvedRoot = value["approvedRoot"];
22307
- const channelId = value["channelId"];
22308
- const daemonToken = value["daemonToken"];
22309
- const relayOrigin = value["relayOrigin"];
22310
- if (typeof approvedRoot !== "string" || typeof channelId !== "string" || channelId !== expectedChannelId || !IDENTIFIER_PATTERN3.test(channelId) || typeof daemonToken !== "string" || !TOKEN_PATTERN.test(daemonToken) || typeof relayOrigin !== "string") {
23418
+ return values[0] ?? "";
23419
+ }
23420
+ function assertKnownArguments(arguments_) {
23421
+ for (let index = 0; index < arguments_.length; index += 2) {
23422
+ const option = arguments_[index];
23423
+ if (option !== "--url" && option !== "--code" && option !== "--root") {
23424
+ throw new PairingClientError(
23425
+ "invalid_arguments",
23426
+ `Unknown pairing option: ${option ?? "missing option"}`
23427
+ );
23428
+ }
23429
+ }
23430
+ }
23431
+ function parseRelayOrigin(value) {
23432
+ let url2;
23433
+ try {
23434
+ url2 = new URL(value);
23435
+ } catch (error61) {
22311
23436
  throw new PairingClientError(
22312
- "invalid_connection",
22313
- "The saved connection is invalid"
23437
+ "invalid_relay_url",
23438
+ "The relay URL is invalid",
23439
+ { cause: error61 }
23440
+ );
23441
+ }
23442
+ if (url2.protocol !== "https:" && url2.protocol !== "http:") {
23443
+ throw new PairingClientError(
23444
+ "invalid_relay_url",
23445
+ "The relay URL must use http or https"
23446
+ );
23447
+ }
23448
+ if (url2.username.length > 0 || url2.password.length > 0) {
23449
+ throw new PairingClientError(
23450
+ "invalid_relay_url",
23451
+ "The relay URL cannot contain credentials"
23452
+ );
23453
+ }
23454
+ if (url2.protocol === "http:" && url2.hostname !== "127.0.0.1" && url2.hostname !== "localhost") {
23455
+ throw new PairingClientError(
23456
+ "invalid_relay_url",
23457
+ "A remote relay must use https"
23458
+ );
23459
+ }
23460
+ url2.hash = "";
23461
+ url2.pathname = "/";
23462
+ url2.search = "";
23463
+ return url2;
23464
+ }
23465
+ function parsePairingCode(value) {
23466
+ const code = value.trim().toUpperCase();
23467
+ if (!PAIRING_CODE_PATTERN.test(code)) {
23468
+ throw new PairingClientError(
23469
+ "invalid_pairing_code",
23470
+ "The pairing code must match the code shown in the browser"
22314
23471
  );
22315
23472
  }
23473
+ return code;
23474
+ }
23475
+ function parsePairCommand(arguments_) {
23476
+ assertKnownArguments(arguments_);
22316
23477
  return {
22317
- approvedRoot,
22318
- channelId,
22319
- daemonToken,
22320
- relayOrigin: parseRelayOrigin(relayOrigin)
23478
+ code: parsePairingCode(readSingleOption(arguments_, "--code")),
23479
+ relayOrigin: parseRelayOrigin(readSingleOption(arguments_, "--url")),
23480
+ root: readSingleOption(arguments_, "--root")
22321
23481
  };
22322
23482
  }
22323
- async function readSavedConnection(channelId) {
22324
- const connectionPath = join(
22325
- readStateDirectory(),
22326
- "connections",
22327
- `${channelId}.json`
22328
- );
23483
+ function parseChannelArgument(arguments_) {
23484
+ if (arguments_.length !== 2 || arguments_[0] !== "--channel") {
23485
+ throw new PairingClientError(
23486
+ "invalid_arguments",
23487
+ "Usage: mobius start --channel <channel-id>"
23488
+ );
23489
+ }
23490
+ const channelId = arguments_[1] ?? "";
23491
+ if (!IDENTIFIER_PATTERN3.test(channelId)) {
23492
+ throw new PairingClientError(
23493
+ "invalid_channel_id",
23494
+ "The saved channel ID is invalid"
23495
+ );
23496
+ }
23497
+ return channelId;
23498
+ }
23499
+ async function resolveApprovedRoot(path) {
23500
+ const requestedRoot = resolve4(path);
23501
+ try {
23502
+ const approvedRoot = await realpath5(requestedRoot);
23503
+ const metadata = await stat4(approvedRoot);
23504
+ if (!metadata.isDirectory()) {
23505
+ throw new PairingClientError(
23506
+ "invalid_workspace",
23507
+ "The approved workspace root is not a directory"
23508
+ );
23509
+ }
23510
+ return approvedRoot;
23511
+ } catch (error61) {
23512
+ if (error61 instanceof PairingClientError) {
23513
+ throw error61;
23514
+ }
23515
+ throw new PairingClientError(
23516
+ "invalid_workspace",
23517
+ "The approved workspace root cannot be resolved",
23518
+ { cause: error61 }
23519
+ );
23520
+ }
23521
+ }
23522
+ async function readJsonResponse(response) {
23523
+ if (response.headers.get("cf-mitigated") === "challenge") {
23524
+ throw new PairingClientError(
23525
+ "relay_challenge",
23526
+ "Cloudflare is protecting this temporary preview. Claim the deployment or use an authenticated deployment, then pair again"
23527
+ );
23528
+ }
23529
+ const encoded = await response.text();
23530
+ try {
23531
+ return JSON.parse(encoded);
23532
+ } catch (error61) {
23533
+ throw new PairingClientError(
23534
+ "invalid_relay_response",
23535
+ "The relay returned an invalid response",
23536
+ { cause: error61 }
23537
+ );
23538
+ }
23539
+ }
23540
+ function readRelayError(value) {
23541
+ if (isRecord6(value)) {
23542
+ const error61 = value["error"];
23543
+ if (isRecord6(error61)) {
23544
+ const code = error61["code"];
23545
+ const message = error61["message"];
23546
+ if (typeof code === "string" && typeof message === "string") {
23547
+ return new PairingClientError(code, message);
23548
+ }
23549
+ }
23550
+ }
23551
+ return new PairingClientError(
23552
+ "pairing_failed",
23553
+ "The relay could not pair this machine"
23554
+ );
23555
+ }
23556
+ function decodeClaimResponse(value) {
23557
+ if (!isRecord6(value) || !hasExactKeys(value, CLAIM_RESPONSE_KEYS)) {
23558
+ throw new PairingClientError(
23559
+ "invalid_relay_response",
23560
+ "The relay returned an invalid pairing response"
23561
+ );
23562
+ }
23563
+ const channelId = value["channelId"];
23564
+ const daemonToken = value["daemonToken"];
23565
+ if (typeof channelId !== "string" || !IDENTIFIER_PATTERN3.test(channelId) || typeof daemonToken !== "string" || !TOKEN_PATTERN.test(daemonToken)) {
23566
+ throw new PairingClientError(
23567
+ "invalid_relay_response",
23568
+ "The relay returned an invalid pairing response"
23569
+ );
23570
+ }
23571
+ return { channelId, daemonToken };
23572
+ }
23573
+ async function claimPairing(options, approvedRoot, localAgents, installableAgents, signal) {
23574
+ const agents = localAgents.map((agent) => ({
23575
+ id: agent.id,
23576
+ label: agent.label
23577
+ }));
23578
+ const endpoint = new URL("/v1/pairings/claim", options.relayOrigin);
23579
+ let response;
23580
+ try {
23581
+ response = await fetch(endpoint, {
23582
+ body: JSON.stringify({
23583
+ agents,
23584
+ installableAgents: installableAgents.map((agent) => ({
23585
+ id: agent.id,
23586
+ label: agent.label,
23587
+ requiresLocalApproval: agent.requiresLocalApproval
23588
+ })),
23589
+ machineName: hostname3(),
23590
+ pairingCode: options.code,
23591
+ workspaceName: basename(approvedRoot) || "Local workspace"
23592
+ }),
23593
+ dispatcher: environmentProxyDispatcher,
23594
+ headers: { "content-type": "application/json" },
23595
+ method: "POST",
23596
+ signal: AbortSignal.any([
23597
+ signal,
23598
+ AbortSignal.timeout(PAIRING_TIMEOUT_MILLISECONDS)
23599
+ ])
23600
+ });
23601
+ } catch (error61) {
23602
+ throw new PairingClientError(
23603
+ "relay_unavailable",
23604
+ "The pairing relay could not be reached",
23605
+ { cause: error61 }
23606
+ );
23607
+ }
23608
+ const value = await readJsonResponse(response);
23609
+ if (!response.ok) {
23610
+ throw readRelayError(value);
23611
+ }
23612
+ return decodeClaimResponse(value);
23613
+ }
23614
+ async function persistConnection(channelId, relayOrigin, daemonToken, approvedRoot) {
23615
+ const connectionsDirectory = join6(readStateDirectory(), "connections");
23616
+ await mkdir2(connectionsDirectory, { mode: 448, recursive: true });
23617
+ const connectionPath = join6(connectionsDirectory, `${channelId}.json`);
23618
+ const temporaryPath = join6(
23619
+ connectionsDirectory,
23620
+ `.${channelId}.${randomUUID()}.tmp`
23621
+ );
23622
+ await writeFile(
23623
+ temporaryPath,
23624
+ `${JSON.stringify(
23625
+ {
23626
+ approvedRoot,
23627
+ channelId,
23628
+ daemonToken,
23629
+ relayOrigin: relayOrigin.toString()
23630
+ },
23631
+ null,
23632
+ 2
23633
+ )}
23634
+ `,
23635
+ { flag: "wx", mode: 384 }
23636
+ );
23637
+ await rename2(temporaryPath, connectionPath);
23638
+ return connectionPath;
23639
+ }
23640
+ function decodeSavedConnection(value, expectedChannelId) {
23641
+ if (!isRecord6(value) || !hasExactKeys(value, SAVED_CONNECTION_KEYS)) {
23642
+ throw new PairingClientError(
23643
+ "invalid_connection",
23644
+ "The saved connection is invalid"
23645
+ );
23646
+ }
23647
+ const approvedRoot = value["approvedRoot"];
23648
+ const channelId = value["channelId"];
23649
+ const daemonToken = value["daemonToken"];
23650
+ const relayOrigin = value["relayOrigin"];
23651
+ if (typeof approvedRoot !== "string" || typeof channelId !== "string" || channelId !== expectedChannelId || !IDENTIFIER_PATTERN3.test(channelId) || typeof daemonToken !== "string" || !TOKEN_PATTERN.test(daemonToken) || typeof relayOrigin !== "string") {
23652
+ throw new PairingClientError(
23653
+ "invalid_connection",
23654
+ "The saved connection is invalid"
23655
+ );
23656
+ }
23657
+ return {
23658
+ approvedRoot,
23659
+ channelId,
23660
+ daemonToken,
23661
+ relayOrigin: parseRelayOrigin(relayOrigin)
23662
+ };
23663
+ }
23664
+ async function readSavedConnection(channelId) {
23665
+ const connectionPath = join6(
23666
+ readStateDirectory(),
23667
+ "connections",
23668
+ `${channelId}.json`
23669
+ );
22329
23670
  let encoded = "";
22330
- const handle = await open2(connectionPath, "r");
23671
+ const handle = await open4(connectionPath, "r");
22331
23672
  try {
22332
23673
  const metadata = await handle.stat();
22333
23674
  if (!metadata.isFile() || metadata.size > MAX_CONNECTION_FILE_LENGTH) {
@@ -22336,101 +23677,1386 @@ async function readSavedConnection(channelId) {
22336
23677
  "The saved connection is invalid"
22337
23678
  );
22338
23679
  }
22339
- encoded = await handle.readFile({ encoding: "utf8" });
22340
- } finally {
22341
- await handle.close();
23680
+ encoded = await handle.readFile({ encoding: "utf8" });
23681
+ } finally {
23682
+ await handle.close();
23683
+ }
23684
+ let value;
23685
+ try {
23686
+ value = JSON.parse(encoded);
23687
+ } catch (error61) {
23688
+ throw new PairingClientError(
23689
+ "invalid_connection",
23690
+ "The saved connection is invalid",
23691
+ { cause: error61 }
23692
+ );
23693
+ }
23694
+ const connection = decodeSavedConnection(value, channelId);
23695
+ return {
23696
+ approvedRoot: await resolveApprovedRoot(connection.approvedRoot),
23697
+ channelId: connection.channelId,
23698
+ daemonToken: connection.daemonToken,
23699
+ relayOrigin: connection.relayOrigin
23700
+ };
23701
+ }
23702
+ function assertInventoryUsable(inventory) {
23703
+ if (inventory.available.length + inventory.installable.length === 0) {
23704
+ throw new PairingClientError(
23705
+ "no_supported_agent",
23706
+ "No supported Agent is ready or safely installable on this machine"
23707
+ );
23708
+ }
23709
+ }
23710
+ function buildPairedDaemon(connection, connectionPath, inventory) {
23711
+ assertInventoryUsable(inventory);
23712
+ const relayUrl = new URL(
23713
+ `/v1/channels/${encodeURIComponent(connection.channelId)}/daemon`,
23714
+ connection.relayOrigin
23715
+ );
23716
+ relayUrl.protocol = relayUrl.protocol === "https:" ? "wss:" : "ws:";
23717
+ relayUrl.searchParams.set("token", connection.daemonToken);
23718
+ const auditDatabasePath = join6(
23719
+ readStateDirectory(),
23720
+ "audit",
23721
+ `${connection.channelId}.sqlite`
23722
+ );
23723
+ return {
23724
+ channelUrl: new URL(
23725
+ `/channels/${encodeURIComponent(connection.channelId)}`,
23726
+ connection.relayOrigin
23727
+ ).toString(),
23728
+ config: createDaemonConfig(
23729
+ {
23730
+ approvedRoot: connection.approvedRoot,
23731
+ auditDatabasePath,
23732
+ channelId: connection.channelId,
23733
+ relayUrl
23734
+ },
23735
+ inventory.readyAgents(),
23736
+ inventory.installable
23737
+ ),
23738
+ connectionPath
23739
+ };
23740
+ }
23741
+ async function pairLocalMachine(arguments_, signal = new AbortController().signal) {
23742
+ const options = parsePairCommand(arguments_);
23743
+ const approvedRoot = await resolveApprovedRoot(options.root);
23744
+ const inventory = await discoverLocalAgentInventory({
23745
+ approvedRoot,
23746
+ environment: process.env,
23747
+ signal
23748
+ });
23749
+ assertInventoryUsable(inventory);
23750
+ const agents = inventory.readyAgents();
23751
+ const claim2 = await claimPairing(
23752
+ options,
23753
+ approvedRoot,
23754
+ agents,
23755
+ inventory.installable,
23756
+ signal
23757
+ );
23758
+ const connectionPath = await persistConnection(
23759
+ claim2.channelId,
23760
+ options.relayOrigin,
23761
+ claim2.daemonToken,
23762
+ approvedRoot
23763
+ );
23764
+ return buildPairedDaemon(
23765
+ {
23766
+ approvedRoot,
23767
+ channelId: claim2.channelId,
23768
+ daemonToken: claim2.daemonToken,
23769
+ relayOrigin: options.relayOrigin
23770
+ },
23771
+ connectionPath,
23772
+ inventory
23773
+ );
23774
+ }
23775
+ async function resumeLocalMachine(arguments_, signal = new AbortController().signal) {
23776
+ const channelId = parseChannelArgument(arguments_);
23777
+ const connectionPath = join6(
23778
+ readStateDirectory(),
23779
+ "connections",
23780
+ `${channelId}.json`
23781
+ );
23782
+ const connection = await readSavedConnection(channelId);
23783
+ const inventory = await discoverLocalAgentInventory({
23784
+ approvedRoot: connection.approvedRoot,
23785
+ environment: process.env,
23786
+ signal
23787
+ });
23788
+ return buildPairedDaemon(connection, connectionPath, inventory);
23789
+ }
23790
+
23791
+ // src/run-daemon.ts
23792
+ import { randomUUID as randomUUID4 } from "node:crypto";
23793
+ import { basename as basename3 } from "node:path";
23794
+ import { WebSocket as WebSocket2 } from "undici";
23795
+
23796
+ // src/agent-catalog/publish-agent-catalog.ts
23797
+ import { fetch as fetch2 } from "undici";
23798
+ var CATALOG_UPDATE_TIMEOUT_MILLISECONDS = 2e4;
23799
+ var AgentCatalogPublishError = class extends Error {
23800
+ code = "agent_catalog_update_failed";
23801
+ constructor(message, options = {}) {
23802
+ super(message, options);
23803
+ this.name = "AgentCatalogPublishError";
23804
+ }
23805
+ };
23806
+ function catalogEndpoint(relayUrl) {
23807
+ const endpoint = new URL(relayUrl);
23808
+ endpoint.protocol = endpoint.protocol === "wss:" ? "https:" : "http:";
23809
+ if (!endpoint.pathname.endsWith("/daemon")) {
23810
+ throw new AgentCatalogPublishError("The Relay URL is invalid");
23811
+ }
23812
+ endpoint.pathname = `${endpoint.pathname.slice(0, -"/daemon".length)}/catalog`;
23813
+ return endpoint;
23814
+ }
23815
+ async function publishAgentCatalog(config2, signal) {
23816
+ signal.throwIfAborted();
23817
+ const timeoutSignal = AbortSignal.timeout(
23818
+ CATALOG_UPDATE_TIMEOUT_MILLISECONDS
23819
+ );
23820
+ const requestSignal = AbortSignal.any([signal, timeoutSignal]);
23821
+ let response;
23822
+ const catalog = config2.catalog.read();
23823
+ try {
23824
+ response = await fetch2(catalogEndpoint(config2.relayUrl), {
23825
+ body: JSON.stringify({
23826
+ agents: catalog.agents.map((agent) => ({
23827
+ id: agent.id,
23828
+ label: agent.label
23829
+ })),
23830
+ installableAgents: catalog.installableAgents.map((agent) => ({
23831
+ id: agent.id,
23832
+ label: agent.label,
23833
+ requiresLocalApproval: agent.requiresLocalApproval
23834
+ }))
23835
+ }),
23836
+ dispatcher: environmentProxyDispatcher,
23837
+ headers: { "content-type": "application/json" },
23838
+ method: "PUT",
23839
+ signal: requestSignal
23840
+ });
23841
+ } catch (error61) {
23842
+ if (signal.aborted) {
23843
+ signal.throwIfAborted();
23844
+ }
23845
+ throw new AgentCatalogPublishError(
23846
+ timeoutSignal.aborted ? "The Agent catalog update timed out" : "The Agent catalog could not be updated",
23847
+ { cause: error61 }
23848
+ );
23849
+ }
23850
+ await response.body?.cancel();
23851
+ if (!response.ok) {
23852
+ throw new AgentCatalogPublishError(
23853
+ "The Relay rejected the Agent catalog update"
23854
+ );
23855
+ }
23856
+ }
23857
+
23858
+ // src/agent-installation/agent-installation-manager.ts
23859
+ var INSTALLATION_DEADLINE_MILLISECONDS = 10 * 60 * 1e3;
23860
+ var AgentInstallation = class {
23861
+ agentId;
23862
+ requestId;
23863
+ constructor(requestId, agentId) {
23864
+ this.agentId = agentId;
23865
+ this.requestId = requestId;
23866
+ }
23867
+ };
23868
+ var RequestedInstallation = class extends AgentInstallation {
23869
+ status = "requested";
23870
+ constructor(requestId, agentId) {
23871
+ super(requestId, agentId);
23872
+ }
23873
+ };
23874
+ var AwaitingLocalApproval = class extends AgentInstallation {
23875
+ status = "awaiting_local_approval";
23876
+ constructor(requestId, agentId) {
23877
+ super(requestId, agentId);
23878
+ }
23879
+ };
23880
+ var RunningInstallation = class extends AgentInstallation {
23881
+ stage;
23882
+ status = "running";
23883
+ constructor(requestId, agentId, stage) {
23884
+ super(requestId, agentId);
23885
+ this.stage = stage;
23886
+ }
23887
+ };
23888
+ var SucceededInstallation = class extends AgentInstallation {
23889
+ authenticationMayBeRequired = true;
23890
+ status = "succeeded";
23891
+ constructor(requestId, agentId) {
23892
+ super(requestId, agentId);
23893
+ }
23894
+ };
23895
+ var FailedInstallation = class extends AgentInstallation {
23896
+ code;
23897
+ message;
23898
+ status = "failed";
23899
+ constructor(requestId, agentId, code, message) {
23900
+ super(requestId, agentId);
23901
+ this.code = code;
23902
+ this.message = message;
23903
+ }
23904
+ };
23905
+ var CancelledInstallation = class extends AgentInstallation {
23906
+ status = "cancelled";
23907
+ constructor(requestId, agentId) {
23908
+ super(requestId, agentId);
23909
+ }
23910
+ };
23911
+ var InstallationManagerError = class extends Error {
23912
+ code;
23913
+ constructor(code, message, options = {}) {
23914
+ super(message, options);
23915
+ this.code = code;
23916
+ this.name = "InstallationManagerError";
23917
+ }
23918
+ };
23919
+ var NullInstallationAudit = class {
23920
+ record = () => void 0;
23921
+ };
23922
+ var InstallationJob = class {
23923
+ agentId;
23924
+ completion = Promise.withResolvers();
23925
+ controller = new AbortController();
23926
+ requestId;
23927
+ state;
23928
+ constructor(requestId, agentId) {
23929
+ this.agentId = agentId;
23930
+ this.requestId = requestId;
23931
+ this.state = new RequestedInstallation(requestId, agentId);
23932
+ }
23933
+ };
23934
+ function specificationFor(specifications, agentId) {
23935
+ for (const specification of specifications) {
23936
+ if (specification.id === agentId) {
23937
+ return specification;
23938
+ }
23939
+ }
23940
+ return null;
23941
+ }
23942
+ function executionFailure(error61) {
23943
+ if (error61 instanceof InstallationManagerError) {
23944
+ return error61;
23945
+ }
23946
+ return new InstallationManagerError(
23947
+ "installation_failed",
23948
+ "The local Agent installation failed",
23949
+ { cause: error61 }
23950
+ );
23951
+ }
23952
+ var AgentInstallationManager = class {
23953
+ #activeAgents = /* @__PURE__ */ new Map();
23954
+ #audit;
23955
+ #authorizer;
23956
+ #executor;
23957
+ #jobs = /* @__PURE__ */ new Map();
23958
+ #observer;
23959
+ #specifications;
23960
+ constructor(specifications, authorizer, executor, observer, audit = new NullInstallationAudit()) {
23961
+ this.#audit = audit;
23962
+ this.#authorizer = authorizer;
23963
+ this.#executor = executor;
23964
+ this.#observer = observer;
23965
+ this.#specifications = Object.freeze([...specifications]);
23966
+ }
23967
+ request = async (command, signal) => {
23968
+ const existing = this.#jobs.get(command.requestId);
23969
+ if (existing !== void 0) {
23970
+ if (existing.agentId !== command.agentId) {
23971
+ const conflict = new FailedInstallation(
23972
+ command.requestId,
23973
+ command.agentId,
23974
+ "installation_request_conflict",
23975
+ "The installation request identifier is already in use"
23976
+ );
23977
+ this.#audit.record(
23978
+ "agent_installation_failed_installation_request_conflict",
23979
+ command.requestId,
23980
+ command.agentId
23981
+ );
23982
+ await this.#observer.onState(conflict);
23983
+ return conflict;
23984
+ }
23985
+ await this.#observer.onState(existing.state);
23986
+ return await existing.completion.promise;
23987
+ }
23988
+ this.#audit.record(
23989
+ "agent_installation_requested",
23990
+ command.requestId,
23991
+ command.agentId
23992
+ );
23993
+ const specification = specificationFor(
23994
+ this.#specifications,
23995
+ command.agentId
23996
+ );
23997
+ if (specification === null) {
23998
+ const unknown2 = new FailedInstallation(
23999
+ command.requestId,
24000
+ command.agentId,
24001
+ "unknown_agent",
24002
+ "The requested Agent is not supported"
24003
+ );
24004
+ const job2 = new InstallationJob(command.requestId, command.agentId);
24005
+ job2.state = unknown2;
24006
+ job2.completion.resolve(unknown2);
24007
+ this.#jobs.set(command.requestId, job2);
24008
+ this.#audit.record(
24009
+ "agent_installation_failed_unknown_agent",
24010
+ command.requestId,
24011
+ command.agentId
24012
+ );
24013
+ await this.#observer.onState(unknown2);
24014
+ return unknown2;
24015
+ }
24016
+ if (this.#activeAgents.has(command.agentId)) {
24017
+ const busy = new FailedInstallation(
24018
+ command.requestId,
24019
+ command.agentId,
24020
+ "installation_in_progress",
24021
+ "An installation for this Agent is already running"
24022
+ );
24023
+ const job2 = new InstallationJob(command.requestId, command.agentId);
24024
+ job2.state = busy;
24025
+ job2.completion.resolve(busy);
24026
+ this.#jobs.set(command.requestId, job2);
24027
+ this.#audit.record(
24028
+ "agent_installation_failed_installation_in_progress",
24029
+ command.requestId,
24030
+ command.agentId
24031
+ );
24032
+ await this.#observer.onState(busy);
24033
+ return busy;
24034
+ }
24035
+ const job = new InstallationJob(command.requestId, command.agentId);
24036
+ this.#jobs.set(command.requestId, job);
24037
+ this.#activeAgents.set(command.agentId, command.requestId);
24038
+ void this.#run(job, specification, signal);
24039
+ return await job.completion.promise;
24040
+ };
24041
+ cancel = async (requestId) => {
24042
+ const job = this.#jobs.get(requestId);
24043
+ if (job === void 0) {
24044
+ throw new InstallationManagerError(
24045
+ "installation_not_found",
24046
+ "The installation request was not found"
24047
+ );
24048
+ }
24049
+ if (job.state instanceof SucceededInstallation || job.state instanceof FailedInstallation || job.state instanceof CancelledInstallation) {
24050
+ await this.#observer.onState(job.state);
24051
+ return job.state;
24052
+ }
24053
+ this.#audit.record(
24054
+ "agent_installation_cancellation_requested",
24055
+ job.requestId,
24056
+ job.agentId
24057
+ );
24058
+ job.controller.abort(new Error("Agent installation cancelled"));
24059
+ return await job.completion.promise;
24060
+ };
24061
+ close = async () => {
24062
+ const completions = [];
24063
+ for (const requestId of this.#activeAgents.values()) {
24064
+ const job = this.#jobs.get(requestId);
24065
+ if (job === void 0) {
24066
+ throw new InstallationManagerError(
24067
+ "installation_state_invalid",
24068
+ "An active Agent installation is unavailable"
24069
+ );
24070
+ }
24071
+ job.controller.abort(new Error("Mobius daemon stopped"));
24072
+ completions.push(job.completion.promise);
24073
+ }
24074
+ await Promise.all(completions);
24075
+ };
24076
+ #transition = async (job, state) => {
24077
+ job.state = state;
24078
+ await this.#observer.onState(state);
24079
+ };
24080
+ #run = async (job, specification, externalSignal) => {
24081
+ const abort = () => job.controller.abort(externalSignal.reason);
24082
+ externalSignal.addEventListener("abort", abort, { once: true });
24083
+ if (externalSignal.aborted) {
24084
+ abort();
24085
+ }
24086
+ const deadline = setTimeout(() => {
24087
+ job.controller.abort(
24088
+ new InstallationManagerError(
24089
+ "installation_timeout",
24090
+ "The local Agent installation exceeded its deadline"
24091
+ )
24092
+ );
24093
+ }, INSTALLATION_DEADLINE_MILLISECONDS);
24094
+ const signal = job.controller.signal;
24095
+ try {
24096
+ await this.#transition(
24097
+ job,
24098
+ new AwaitingLocalApproval(job.requestId, job.agentId)
24099
+ );
24100
+ this.#audit.record(
24101
+ "agent_installation_awaiting_approval",
24102
+ job.requestId,
24103
+ job.agentId
24104
+ );
24105
+ const approved = await this.#authorizer.authorize(
24106
+ specification.id,
24107
+ specification.label,
24108
+ job.requestId,
24109
+ signal
24110
+ );
24111
+ signal.throwIfAborted();
24112
+ if (!approved) {
24113
+ const denied = new FailedInstallation(
24114
+ job.requestId,
24115
+ job.agentId,
24116
+ "installation_not_authorized",
24117
+ "Installation was not approved on the local machine"
24118
+ );
24119
+ await this.#transition(job, denied);
24120
+ this.#audit.record(
24121
+ "agent_installation_denied",
24122
+ job.requestId,
24123
+ job.agentId
24124
+ );
24125
+ job.completion.resolve(denied);
24126
+ return;
24127
+ }
24128
+ this.#audit.record(
24129
+ "agent_installation_authorized",
24130
+ job.requestId,
24131
+ job.agentId
24132
+ );
24133
+ await this.#executor.execute(
24134
+ job.agentId,
24135
+ job.requestId,
24136
+ async (stage) => {
24137
+ signal.throwIfAborted();
24138
+ await this.#transition(
24139
+ job,
24140
+ new RunningInstallation(job.requestId, job.agentId, stage)
24141
+ );
24142
+ this.#audit.record(
24143
+ `agent_installation_${stage}`,
24144
+ job.requestId,
24145
+ job.agentId
24146
+ );
24147
+ },
24148
+ signal
24149
+ );
24150
+ signal.throwIfAborted();
24151
+ const succeeded = new SucceededInstallation(job.requestId, job.agentId);
24152
+ await this.#transition(job, succeeded);
24153
+ this.#audit.record(
24154
+ "agent_installation_completed",
24155
+ job.requestId,
24156
+ job.agentId
24157
+ );
24158
+ job.completion.resolve(succeeded);
24159
+ } catch (error61) {
24160
+ if (signal.aborted) {
24161
+ if (signal.reason instanceof InstallationManagerError && signal.reason.code === "installation_timeout") {
24162
+ const timedOut = new FailedInstallation(
24163
+ job.requestId,
24164
+ job.agentId,
24165
+ signal.reason.code,
24166
+ signal.reason.message
24167
+ );
24168
+ await this.#transition(job, timedOut);
24169
+ this.#audit.record(
24170
+ "agent_installation_failed_installation_timeout",
24171
+ job.requestId,
24172
+ job.agentId
24173
+ );
24174
+ job.completion.resolve(timedOut);
24175
+ return;
24176
+ }
24177
+ const cancelled = new CancelledInstallation(job.requestId, job.agentId);
24178
+ await this.#transition(job, cancelled);
24179
+ this.#audit.record(
24180
+ "agent_installation_cancelled",
24181
+ job.requestId,
24182
+ job.agentId
24183
+ );
24184
+ job.completion.resolve(cancelled);
24185
+ } else {
24186
+ const failure = executionFailure(error61);
24187
+ const failed = new FailedInstallation(
24188
+ job.requestId,
24189
+ job.agentId,
24190
+ failure.code,
24191
+ failure.message
24192
+ );
24193
+ await this.#transition(job, failed);
24194
+ this.#audit.record(
24195
+ `agent_installation_failed_${failure.code}`,
24196
+ job.requestId,
24197
+ job.agentId
24198
+ );
24199
+ job.completion.resolve(failed);
24200
+ }
24201
+ } finally {
24202
+ clearTimeout(deadline);
24203
+ externalSignal.removeEventListener("abort", abort);
24204
+ if (this.#activeAgents.get(job.agentId) === job.requestId) {
24205
+ this.#activeAgents.delete(job.agentId);
24206
+ }
24207
+ }
24208
+ };
24209
+ };
24210
+
24211
+ // src/agent-installation/automatic-adapter-provisioner.ts
24212
+ import { randomUUID as randomUUID2 } from "node:crypto";
24213
+ var AUTOMATIC_ADAPTER_FAILURE_CODES = /* @__PURE__ */ new Set([
24214
+ "adapter_not_found",
24215
+ "adapter_probe_failed",
24216
+ "adapter_probe_timeout",
24217
+ "acp_incompatible"
24218
+ ]);
24219
+ var InstalledAutomaticAdapter = class {
24220
+ executable;
24221
+ requestId;
24222
+ specification;
24223
+ constructor(specification, requestId, executable) {
24224
+ this.executable = executable;
24225
+ this.requestId = requestId;
24226
+ this.specification = specification;
24227
+ }
24228
+ };
24229
+ function shouldProvisionAdapter(unavailable, options) {
24230
+ return AUTOMATIC_ADAPTER_FAILURE_CODES.has(unavailable.code) && options.environment[unavailable.specification.adapterOverrideVariable] === void 0 && unavailable.specification.supportsInstallation(
24231
+ options.platform ?? process.platform,
24232
+ options.architecture ?? process.arch
24233
+ );
24234
+ }
24235
+ function stableFailureCode(error61) {
24236
+ return error61 instanceof ManagedAdapterInstallError ? error61.code : "adapter_install_failed";
24237
+ }
24238
+ function discoveryOptions(options, environment, signal, specifications) {
24239
+ return {
24240
+ ...options.architecture === void 0 ? {} : { architecture: options.architecture },
24241
+ approvedRoot: options.approvedRoot,
24242
+ environment,
24243
+ ...options.platform === void 0 ? {} : { platform: options.platform },
24244
+ signal,
24245
+ specifications
24246
+ };
24247
+ }
24248
+ var AutomaticAdapterProvisioner = class {
24249
+ #adapterInstaller;
24250
+ #options;
24251
+ #specifications;
24252
+ constructor(options, adapterInstaller, specifications = supportedLocalAgentSpecifications()) {
24253
+ this.#adapterInstaller = adapterInstaller;
24254
+ this.#options = options;
24255
+ this.#specifications = Object.freeze([...specifications]);
24256
+ }
24257
+ prepare = async (signal) => {
24258
+ signal.throwIfAborted();
24259
+ let inventory = await discoverLocalAgentInventory(
24260
+ discoveryOptions(
24261
+ this.#options,
24262
+ this.#options.environment,
24263
+ signal,
24264
+ this.#specifications
24265
+ )
24266
+ );
24267
+ const candidates = inventory.unavailable.filter(
24268
+ (unavailable) => shouldProvisionAdapter(unavailable, this.#options)
24269
+ );
24270
+ const attempts = [];
24271
+ for (const unavailable of candidates) {
24272
+ signal.throwIfAborted();
24273
+ const specification = unavailable.specification;
24274
+ const verifiedAgent = await findVerifiedAgentExecutable(
24275
+ specification,
24276
+ discoveryOptions(
24277
+ this.#options,
24278
+ this.#options.environment,
24279
+ signal,
24280
+ this.#specifications
24281
+ )
24282
+ );
24283
+ if (verifiedAgent === null) {
24284
+ attempts.push(null);
24285
+ continue;
24286
+ }
24287
+ const requestId = `automatic-adapter-${randomUUID2()}`;
24288
+ this.#options.audit.record(
24289
+ "agent_adapter_auto_install_started",
24290
+ requestId,
24291
+ specification.id,
24292
+ {
24293
+ agentVersion: verifiedAgent.version,
24294
+ adapterVersion: specification.adapterVersion
24295
+ }
24296
+ );
24297
+ process.stdout.write(
24298
+ `Installing the pinned ACP adapter for ${specification.label}.
24299
+ `
24300
+ );
24301
+ try {
24302
+ const executable = await this.#adapterInstaller.install(
24303
+ specification,
24304
+ signal
24305
+ );
24306
+ this.#options.audit.record(
24307
+ "agent_adapter_auto_install_completed",
24308
+ requestId,
24309
+ specification.id,
24310
+ { adapterVersion: specification.adapterVersion }
24311
+ );
24312
+ attempts.push(
24313
+ new InstalledAutomaticAdapter(specification, requestId, executable)
24314
+ );
24315
+ } catch (error61) {
24316
+ if (signal.aborted) {
24317
+ this.#options.audit.record(
24318
+ "agent_adapter_auto_install_cancelled",
24319
+ requestId,
24320
+ specification.id
24321
+ );
24322
+ signal.throwIfAborted();
24323
+ }
24324
+ this.#options.audit.record(
24325
+ "agent_adapter_auto_install_failed",
24326
+ requestId,
24327
+ specification.id,
24328
+ { code: stableFailureCode(error61) }
24329
+ );
24330
+ process.stderr.write(
24331
+ `The pinned ACP adapter for ${specification.label} could not be installed automatically; the daemon will continue.
24332
+ `
24333
+ );
24334
+ attempts.push(null);
24335
+ }
24336
+ }
24337
+ signal.throwIfAborted();
24338
+ const installed = attempts.filter(
24339
+ (attempt) => attempt !== null
24340
+ );
24341
+ if (installed.length > 0) {
24342
+ const verificationEnvironment = {
24343
+ ...this.#options.environment
24344
+ };
24345
+ for (const attempt of installed) {
24346
+ verificationEnvironment[attempt.specification.adapterOverrideVariable] = attempt.executable.path;
24347
+ }
24348
+ inventory = await discoverLocalAgentInventory(
24349
+ discoveryOptions(
24350
+ this.#options,
24351
+ verificationEnvironment,
24352
+ signal,
24353
+ this.#specifications
24354
+ )
24355
+ );
24356
+ }
24357
+ this.#options.catalog.transition(
24358
+ inventory.readyAgents(),
24359
+ inventory.installable
24360
+ );
24361
+ for (const attempt of installed) {
24362
+ const available = inventory.available.find(
24363
+ (candidate) => candidate instanceof AvailableAgent && candidate.specification.id === attempt.specification.id
24364
+ );
24365
+ if (available === void 0) {
24366
+ const unavailable = inventory.unavailable.find(
24367
+ (candidate) => candidate.specification.id === attempt.specification.id
24368
+ );
24369
+ this.#options.audit.record(
24370
+ "agent_adapter_auto_install_verification_failed",
24371
+ attempt.requestId,
24372
+ attempt.specification.id,
24373
+ { code: unavailable?.code ?? "agent_verification_failed" }
24374
+ );
24375
+ continue;
24376
+ }
24377
+ this.#options.audit.record(
24378
+ "agent_adapter_auto_install_verified",
24379
+ attempt.requestId,
24380
+ attempt.specification.id,
24381
+ {
24382
+ adapterVersion: available.agent.adapterVersion,
24383
+ agentVersion: available.agent.agentVersion
24384
+ }
24385
+ );
24386
+ process.stdout.write(
24387
+ `${attempt.specification.label} ACP integration is ready.
24388
+ `
24389
+ );
24390
+ }
24391
+ return inventory;
24392
+ };
24393
+ };
24394
+
24395
+ // src/agent-installation/fixed-installation-executor.ts
24396
+ import { realpathSync as realpathSync2, statSync as statSync2 } from "node:fs";
24397
+ import { isAbsolute as isAbsolute3, relative as relative4, resolve as resolve5 } from "node:path";
24398
+
24399
+ // src/agent-installation/fixed-agent-install-recipes.ts
24400
+ import { mkdir as mkdir3, mkdtemp as mkdtemp2, rm as rm2 } from "node:fs/promises";
24401
+ import { join as join7 } from "node:path";
24402
+
24403
+ // src/agent-installation/verified-download.ts
24404
+ import { createHash } from "node:crypto";
24405
+ import { open as open5, unlink } from "node:fs/promises";
24406
+ import { fetch as fetch3 } from "undici";
24407
+ var MAX_REDIRECTS = 4;
24408
+ var REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
24409
+ var InstallerDownloadError = class extends Error {
24410
+ code;
24411
+ constructor(code, message, options = {}) {
24412
+ super(message, options);
24413
+ this.code = code;
24414
+ this.name = "InstallerDownloadError";
24415
+ }
24416
+ };
24417
+ var VerifiedDownload = class {
24418
+ bytes;
24419
+ sha256;
24420
+ url;
24421
+ constructor(url2, bytes, sha256) {
24422
+ this.bytes = bytes;
24423
+ this.sha256 = sha256;
24424
+ this.url = url2;
24425
+ }
24426
+ };
24427
+ var UndiciInstallerDownloadTransport = class {
24428
+ get = async (url2, signal) => await fetch3(url2, {
24429
+ dispatcher: environmentProxyDispatcher,
24430
+ headers: {
24431
+ accept: "text/plain, application/octet-stream;q=0.9",
24432
+ "user-agent": "Mobius-Agent-Installer/0.0.3"
24433
+ },
24434
+ method: "GET",
24435
+ redirect: "manual",
24436
+ signal
24437
+ });
24438
+ };
24439
+ function validateUrl(url2, allowedHosts) {
24440
+ if (url2.protocol !== "https:" || url2.username.length > 0 || url2.password.length > 0 || url2.port.length > 0 || !allowedHosts.has(url2.hostname)) {
24441
+ throw new InstallerDownloadError(
24442
+ "installer_host_not_allowed",
24443
+ "The installer download target is not allowed"
24444
+ );
24445
+ }
24446
+ }
24447
+ async function fetchInstaller(options, signal, transport) {
24448
+ let url2 = new URL(options.url);
24449
+ for (let redirectCount = 0; redirectCount <= MAX_REDIRECTS; redirectCount += 1) {
24450
+ validateUrl(url2, options.allowedHosts);
24451
+ const response = await transport.get(url2, signal);
24452
+ if (!REDIRECT_STATUSES.has(response.status)) {
24453
+ return { response, url: url2 };
24454
+ }
24455
+ await response.body?.cancel();
24456
+ const location = response.headers.get("location");
24457
+ if (location === null || redirectCount === MAX_REDIRECTS) {
24458
+ throw new InstallerDownloadError(
24459
+ "installer_redirect_invalid",
24460
+ "The installer returned an invalid redirect"
24461
+ );
24462
+ }
24463
+ let redirected;
24464
+ try {
24465
+ redirected = new URL(location, url2);
24466
+ } catch (error61) {
24467
+ throw new InstallerDownloadError(
24468
+ "installer_redirect_invalid",
24469
+ "The installer returned an invalid redirect",
24470
+ { cause: error61 }
24471
+ );
24472
+ }
24473
+ try {
24474
+ validateUrl(redirected, options.allowedHosts);
24475
+ } catch (error61) {
24476
+ throw new InstallerDownloadError(
24477
+ "installer_redirect_not_allowed",
24478
+ "The installer redirect left the approved host allowlist",
24479
+ { cause: error61 }
24480
+ );
24481
+ }
24482
+ url2 = redirected;
24483
+ }
24484
+ throw new InstallerDownloadError(
24485
+ "installer_redirect_invalid",
24486
+ "The installer returned too many redirects"
24487
+ );
24488
+ }
24489
+ async function writeCompleteChunk(handle, chunk) {
24490
+ let offset = 0;
24491
+ while (offset < chunk.byteLength) {
24492
+ const written = await handle.write(
24493
+ chunk,
24494
+ offset,
24495
+ chunk.byteLength - offset
24496
+ );
24497
+ if (written.bytesWritten < 1) {
24498
+ throw new InstallerDownloadError(
24499
+ "installer_write_failed",
24500
+ "The verified installer could not be stored"
24501
+ );
24502
+ }
24503
+ offset += written.bytesWritten;
24504
+ }
24505
+ }
24506
+ async function downloadVerifiedFile(options, transport = new UndiciInstallerDownloadTransport()) {
24507
+ options.signal.throwIfAborted();
24508
+ const timeoutSignal = AbortSignal.timeout(options.timeoutMilliseconds);
24509
+ const signal = AbortSignal.any([options.signal, timeoutSignal]);
24510
+ let handle = null;
24511
+ let responseBody = null;
24512
+ try {
24513
+ const fetched = await fetchInstaller(options, signal, transport);
24514
+ responseBody = fetched.response.body;
24515
+ if (!fetched.response.ok || fetched.response.body === null) {
24516
+ throw new InstallerDownloadError(
24517
+ "installer_download_failed",
24518
+ "The approved installer could not be downloaded"
24519
+ );
24520
+ }
24521
+ const contentLength = fetched.response.headers.get("content-length");
24522
+ if (contentLength !== null) {
24523
+ const declaredLength = Number(contentLength);
24524
+ if (!Number.isSafeInteger(declaredLength) || declaredLength < 1 || declaredLength > options.maximumBytes) {
24525
+ throw new InstallerDownloadError(
24526
+ "installer_size_invalid",
24527
+ "The installer size is outside the approved limit"
24528
+ );
24529
+ }
24530
+ }
24531
+ handle = await open5(options.destination, "wx", 384);
24532
+ const digest = createHash("sha256");
24533
+ const reader = fetched.response.body.getReader();
24534
+ let bytes = 0;
24535
+ try {
24536
+ while (true) {
24537
+ signal.throwIfAborted();
24538
+ const chunk = await reader.read();
24539
+ if (chunk.done) {
24540
+ break;
24541
+ }
24542
+ bytes += chunk.value.byteLength;
24543
+ if (bytes > options.maximumBytes) {
24544
+ throw new InstallerDownloadError(
24545
+ "installer_size_invalid",
24546
+ "The installer exceeded the approved size limit"
24547
+ );
24548
+ }
24549
+ digest.update(chunk.value);
24550
+ await writeCompleteChunk(handle, chunk.value);
24551
+ }
24552
+ } finally {
24553
+ await reader.cancel().catch(() => void 0);
24554
+ reader.releaseLock();
24555
+ }
24556
+ if (bytes === 0) {
24557
+ throw new InstallerDownloadError(
24558
+ "installer_verification_failed",
24559
+ "The downloaded installer is empty"
24560
+ );
24561
+ }
24562
+ await handle.sync();
24563
+ await handle.close();
24564
+ handle = null;
24565
+ return new VerifiedDownload(fetched.url, bytes, digest.digest("hex"));
24566
+ } catch (error61) {
24567
+ if (handle !== null) {
24568
+ await handle.close().catch(() => void 0);
24569
+ }
24570
+ await unlink(options.destination).catch(() => void 0);
24571
+ if (options.signal.aborted) {
24572
+ options.signal.throwIfAborted();
24573
+ }
24574
+ if (timeoutSignal.aborted) {
24575
+ throw new InstallerDownloadError(
24576
+ "installer_download_timeout",
24577
+ "The installer download timed out",
24578
+ { cause: error61 }
24579
+ );
24580
+ }
24581
+ if (error61 instanceof InstallerDownloadError) {
24582
+ throw error61;
24583
+ }
24584
+ throw new InstallerDownloadError(
24585
+ "installer_download_failed",
24586
+ "The approved installer could not be downloaded",
24587
+ { cause: error61 }
24588
+ );
24589
+ } finally {
24590
+ await responseBody?.cancel().catch(() => void 0);
24591
+ }
24592
+ }
24593
+
24594
+ // src/agent-installation/fixed-agent-install-recipes.ts
24595
+ var INSTALLER_DOWNLOAD_LIMIT_BYTES = 1024 * 1024;
24596
+ var INSTALLER_DOWNLOAD_TIMEOUT_MILLISECONDS = 6e4;
24597
+ var AGENT_INSTALL_TIMEOUT_MILLISECONDS = 5 * 60 * 1e3;
24598
+ var MINIMAL_PATH = "/usr/bin:/bin";
24599
+ var FixedAgentInstallRecipe = class {
24600
+ assertSupportedPlatform = () => {
24601
+ if (process.platform !== "darwin" && process.platform !== "linux" || process.arch !== "arm64" && process.arch !== "x64") {
24602
+ throw new InstallationManagerError(
24603
+ "unsupported_install_platform",
24604
+ "This Agent installation recipe does not support the local platform"
24605
+ );
24606
+ }
24607
+ };
24608
+ requireInstalledExecutable = async (path) => {
24609
+ const executable = await resolveExecutablePath(path);
24610
+ if (executable === null) {
24611
+ throw new InstallationManagerError(
24612
+ "agent_verification_failed",
24613
+ "The Agent installer did not produce a valid executable"
24614
+ );
24615
+ }
24616
+ return executable;
24617
+ };
24618
+ };
24619
+ var CodexInstallRecipe = class extends FixedAgentInstallRecipe {
24620
+ id = "codex";
24621
+ label = "Codex";
24622
+ releaseChannel = "latest";
24623
+ install = async (options) => {
24624
+ this.assertSupportedPlatform();
24625
+ const root = join7(options.stateDirectory, "agents", this.id);
24626
+ const temporaryRoot = join7(options.stateDirectory, "temporary");
24627
+ await Promise.all([
24628
+ mkdir3(root, { mode: 448, recursive: true }),
24629
+ mkdir3(temporaryRoot, { mode: 448, recursive: true })
24630
+ ]);
24631
+ const temporaryDirectory = await mkdtemp2(
24632
+ join7(temporaryRoot, ".codex-install-")
24633
+ );
24634
+ const installerPath = join7(temporaryDirectory, "install.sh");
24635
+ const installDirectory = join7(root, "bin");
24636
+ const codexHome = join7(root, "home");
24637
+ try {
24638
+ await Promise.all([
24639
+ mkdir3(installDirectory, { mode: 448, recursive: true }),
24640
+ mkdir3(codexHome, { mode: 448, recursive: true })
24641
+ ]);
24642
+ await options.report("downloading");
24643
+ const download = await downloadVerifiedFile({
24644
+ allowedHosts: /* @__PURE__ */ new Set(["chatgpt.com", "releases.openai.com"]),
24645
+ destination: installerPath,
24646
+ maximumBytes: INSTALLER_DOWNLOAD_LIMIT_BYTES,
24647
+ signal: options.signal,
24648
+ timeoutMilliseconds: INSTALLER_DOWNLOAD_TIMEOUT_MILLISECONDS,
24649
+ url: new URL("https://chatgpt.com/codex/install.sh")
24650
+ });
24651
+ options.audit.record(
24652
+ "agent_installer_download_verified",
24653
+ options.requestId,
24654
+ this.id,
24655
+ {
24656
+ bytes: download.bytes.toString(),
24657
+ releaseChannel: this.releaseChannel,
24658
+ sha256: download.sha256
24659
+ }
24660
+ );
24661
+ await options.report("verifying");
24662
+ await options.report("installing_agent");
24663
+ await runBoundedProcess({
24664
+ arguments: [installerPath],
24665
+ command: "/bin/sh",
24666
+ cwd: temporaryDirectory,
24667
+ environment: {
24668
+ CODEX_HOME: codexHome,
24669
+ CODEX_INSTALL_DIR: installDirectory,
24670
+ CODEX_NON_INTERACTIVE: "1",
24671
+ CODEX_RELEASE: this.releaseChannel,
24672
+ HOME: codexHome,
24673
+ PATH: MINIMAL_PATH
24674
+ },
24675
+ outputLimitBytes: 64 * 1024,
24676
+ signal: options.signal,
24677
+ timeoutMilliseconds: AGENT_INSTALL_TIMEOUT_MILLISECONDS
24678
+ });
24679
+ return await this.requireInstalledExecutable(
24680
+ join7(installDirectory, "codex")
24681
+ );
24682
+ } catch (error61) {
24683
+ if (options.signal.aborted) {
24684
+ options.signal.throwIfAborted();
24685
+ }
24686
+ if (error61 instanceof InstallationManagerError || error61 instanceof Error && "code" in error61) {
24687
+ throw error61;
24688
+ }
24689
+ throw new InstallationManagerError(
24690
+ "agent_install_failed",
24691
+ "The fixed Codex installation recipe failed"
24692
+ );
24693
+ } finally {
24694
+ await rm2(temporaryDirectory, { force: true, recursive: true });
24695
+ }
24696
+ };
24697
+ };
24698
+ var ClaudeCodeInstallRecipe = class extends FixedAgentInstallRecipe {
24699
+ id = "claude";
24700
+ label = "Claude Code";
24701
+ releaseChannel = "stable";
24702
+ install = async (options) => {
24703
+ this.assertSupportedPlatform();
24704
+ const root = join7(options.stateDirectory, "agents", this.id);
24705
+ const managedHome = join7(root, "home");
24706
+ const temporaryRoot = join7(options.stateDirectory, "temporary");
24707
+ await Promise.all([
24708
+ mkdir3(managedHome, { mode: 448, recursive: true }),
24709
+ mkdir3(temporaryRoot, { mode: 448, recursive: true })
24710
+ ]);
24711
+ const temporaryDirectory = await mkdtemp2(
24712
+ join7(temporaryRoot, ".claude-install-")
24713
+ );
24714
+ const installerPath = join7(temporaryDirectory, "install.sh");
24715
+ try {
24716
+ await options.report("downloading");
24717
+ const download = await downloadVerifiedFile({
24718
+ allowedHosts: /* @__PURE__ */ new Set(["claude.ai"]),
24719
+ destination: installerPath,
24720
+ maximumBytes: INSTALLER_DOWNLOAD_LIMIT_BYTES,
24721
+ signal: options.signal,
24722
+ timeoutMilliseconds: INSTALLER_DOWNLOAD_TIMEOUT_MILLISECONDS,
24723
+ url: new URL("https://claude.ai/install.sh")
24724
+ });
24725
+ options.audit.record(
24726
+ "agent_installer_download_verified",
24727
+ options.requestId,
24728
+ this.id,
24729
+ {
24730
+ bytes: download.bytes.toString(),
24731
+ releaseChannel: this.releaseChannel,
24732
+ sha256: download.sha256
24733
+ }
24734
+ );
24735
+ await options.report("verifying");
24736
+ await options.report("installing_agent");
24737
+ await runBoundedProcess({
24738
+ arguments: [installerPath, "stable"],
24739
+ command: "/bin/bash",
24740
+ cwd: temporaryDirectory,
24741
+ environment: {
24742
+ HOME: managedHome,
24743
+ PATH: MINIMAL_PATH,
24744
+ SHELL: "/bin/bash"
24745
+ },
24746
+ outputLimitBytes: 64 * 1024,
24747
+ signal: options.signal,
24748
+ timeoutMilliseconds: AGENT_INSTALL_TIMEOUT_MILLISECONDS
24749
+ });
24750
+ return await this.requireInstalledExecutable(
24751
+ join7(managedHome, ".local", "bin", "claude")
24752
+ );
24753
+ } catch (error61) {
24754
+ if (options.signal.aborted) {
24755
+ options.signal.throwIfAborted();
24756
+ }
24757
+ if (error61 instanceof InstallationManagerError || error61 instanceof Error && "code" in error61) {
24758
+ throw error61;
24759
+ }
24760
+ throw new InstallationManagerError(
24761
+ "agent_install_failed",
24762
+ "The fixed Claude Code installation recipe failed"
24763
+ );
24764
+ } finally {
24765
+ await rm2(temporaryDirectory, { force: true, recursive: true });
24766
+ }
24767
+ };
24768
+ };
24769
+ function fixedAgentInstallRecipes() {
24770
+ return Object.freeze([
24771
+ new CodexInstallRecipe(),
24772
+ new ClaudeCodeInstallRecipe()
24773
+ ]);
24774
+ }
24775
+
24776
+ // src/agent-installation/fixed-installation-executor.ts
24777
+ function findSpecification(specifications, agentId) {
24778
+ for (const specification of specifications) {
24779
+ if (specification.id === agentId) {
24780
+ return specification;
24781
+ }
24782
+ }
24783
+ throw new InstallationManagerError(
24784
+ "unknown_agent",
24785
+ "The requested Agent is not supported"
24786
+ );
24787
+ }
24788
+ function findRecipe(recipes, agentId) {
24789
+ for (const recipe of recipes) {
24790
+ if (recipe.id === agentId) {
24791
+ return recipe;
24792
+ }
24793
+ }
24794
+ throw new InstallationManagerError(
24795
+ "agent_not_installable",
24796
+ "The requested Agent does not have an approved installation recipe"
24797
+ );
24798
+ }
24799
+ function isInside2(root, candidate) {
24800
+ const difference = relative4(resolve5(root), resolve5(candidate));
24801
+ return difference.length === 0 || !difference.startsWith("..") && !isAbsolute3(difference);
24802
+ }
24803
+ function resolveApprovedRoot2(path) {
24804
+ try {
24805
+ const directory = realpathSync2(resolve5(path));
24806
+ if (!statSync2(directory).isDirectory()) {
24807
+ throw new Error("Approved root is not a directory");
24808
+ }
24809
+ return directory;
24810
+ } catch (error61) {
24811
+ throw new InstallationManagerError(
24812
+ "unsafe_installation_root",
24813
+ "The approved workspace is unavailable or unsafe",
24814
+ { cause: error61 }
24815
+ );
22342
24816
  }
22343
- let value;
24817
+ }
24818
+ function resolveUserInstallationDirectory(path) {
22344
24819
  try {
22345
- value = JSON.parse(encoded);
24820
+ const directory = realpathSync2(resolve5(path));
24821
+ const metadata = statSync2(directory);
24822
+ if (!metadata.isDirectory() || typeof process.getuid === "function" && metadata.uid !== process.getuid()) {
24823
+ throw new Error("Installation directory is not user-owned");
24824
+ }
24825
+ return directory;
22346
24826
  } catch (error61) {
22347
- throw new PairingClientError(
22348
- "invalid_connection",
22349
- "The saved connection is invalid",
24827
+ throw new InstallationManagerError(
24828
+ "unsafe_installation_root",
24829
+ "The local Agent installation root is unavailable or unsafe",
22350
24830
  { cause: error61 }
22351
24831
  );
22352
24832
  }
22353
- const connection = decodeSavedConnection(value, channelId);
22354
- return {
22355
- approvedRoot: await resolveApprovedRoot(connection.approvedRoot),
22356
- channelId: connection.channelId,
22357
- daemonToken: connection.daemonToken,
22358
- relayOrigin: connection.relayOrigin
22359
- };
22360
24833
  }
22361
- function buildPairedDaemon(connection, connectionPath) {
22362
- const relayUrl = new URL(
22363
- `/v1/channels/${encodeURIComponent(connection.channelId)}/daemon`,
22364
- connection.relayOrigin
22365
- );
22366
- relayUrl.protocol = relayUrl.protocol === "https:" ? "wss:" : "ws:";
22367
- relayUrl.searchParams.set("token", connection.daemonToken);
22368
- const auditDatabasePath = join(
22369
- readStateDirectory(),
22370
- "audit",
22371
- `${connection.channelId}.sqlite`
24834
+ function translateExecutionError(error61) {
24835
+ if (error61 instanceof InstallationManagerError) {
24836
+ return error61;
24837
+ }
24838
+ if (error61 instanceof ManagedAdapterInstallError || error61 instanceof InstallerDownloadError || error61 instanceof ProcessExecutionError) {
24839
+ return new InstallationManagerError(error61.code, error61.message, {
24840
+ cause: error61
24841
+ });
24842
+ }
24843
+ return new InstallationManagerError(
24844
+ "installation_failed",
24845
+ "The fixed local Agent installation failed",
24846
+ { cause: error61 }
22372
24847
  );
22373
- return {
22374
- channelUrl: new URL(
22375
- `/channels/${encodeURIComponent(connection.channelId)}`,
22376
- connection.relayOrigin
22377
- ).toString(),
22378
- config: createDaemonConfig({
22379
- approvedRoot: connection.approvedRoot,
22380
- auditDatabasePath,
22381
- channelId: connection.channelId,
22382
- relayUrl
22383
- }),
22384
- connectionPath
22385
- };
22386
24848
  }
22387
- async function pairLocalMachine(arguments_) {
22388
- const options = parsePairCommand(arguments_);
22389
- const approvedRoot = await resolveApprovedRoot(options.root);
22390
- const claim2 = await claimPairing(options, approvedRoot);
22391
- const connectionPath = await persistConnection(
22392
- claim2.channelId,
22393
- options.relayOrigin,
22394
- claim2.daemonToken,
22395
- approvedRoot
22396
- );
22397
- return buildPairedDaemon(
22398
- {
22399
- approvedRoot,
22400
- channelId: claim2.channelId,
22401
- daemonToken: claim2.daemonToken,
22402
- relayOrigin: options.relayOrigin
22403
- },
22404
- connectionPath
22405
- );
24849
+ function requiresAgentReplacement(code) {
24850
+ return code === "agent_not_found" || code === "agent_probe_failed" || code === "agent_probe_timeout";
22406
24851
  }
22407
- async function resumeLocalMachine(arguments_) {
22408
- const channelId = parseChannelArgument(arguments_);
22409
- const connectionPath = join(
22410
- readStateDirectory(),
22411
- "connections",
22412
- `${channelId}.json`
22413
- );
22414
- return buildPairedDaemon(
22415
- await readSavedConnection(channelId),
22416
- connectionPath
22417
- );
24852
+ function requiresAdapterReplacement(code) {
24853
+ return code === "adapter_not_found" || code === "adapter_probe_failed" || code === "adapter_probe_timeout" || code === "acp_incompatible";
22418
24854
  }
24855
+ var FixedInstallationExecutor = class {
24856
+ #adapterInstaller;
24857
+ #approvedRoot;
24858
+ #audit;
24859
+ #catalog;
24860
+ #environment;
24861
+ #publishCatalog;
24862
+ #recipes;
24863
+ #specifications;
24864
+ #stateDirectory;
24865
+ constructor(options, specifications = supportedLocalAgentSpecifications(), recipes = fixedAgentInstallRecipes(), adapterInstaller) {
24866
+ const approvedRoot = resolveApprovedRoot2(options.approvedRoot);
24867
+ const stateDirectory = resolveUserInstallationDirectory(
24868
+ options.stateDirectory
24869
+ );
24870
+ if (isInside2(approvedRoot, stateDirectory)) {
24871
+ throw new InstallationManagerError(
24872
+ "unsafe_installation_root",
24873
+ "The Mobius state directory cannot be inside the approved workspace"
24874
+ );
24875
+ }
24876
+ this.#approvedRoot = approvedRoot;
24877
+ this.#audit = options.audit;
24878
+ this.#catalog = options.catalog;
24879
+ this.#environment = Object.freeze({ ...options.environment });
24880
+ this.#publishCatalog = options.publishCatalog;
24881
+ this.#recipes = Object.freeze([...recipes]);
24882
+ this.#specifications = Object.freeze([...specifications]);
24883
+ this.#stateDirectory = stateDirectory;
24884
+ this.#adapterInstaller = adapterInstaller ?? new ManagedAdapterInstaller({
24885
+ approvedRoot,
24886
+ environment: this.#environment,
24887
+ stateDirectory
24888
+ });
24889
+ }
24890
+ execute = async (agentId, requestId, report, signal) => {
24891
+ try {
24892
+ const specification = findSpecification(this.#specifications, agentId);
24893
+ const recipe = findRecipe(this.#recipes, agentId);
24894
+ this.#audit.record(
24895
+ "agent_installation_recipe_resolved",
24896
+ requestId,
24897
+ agentId,
24898
+ {
24899
+ adapterVersion: specification.adapterVersion,
24900
+ releaseChannel: recipe.releaseChannel
24901
+ }
24902
+ );
24903
+ const discoveryOptions2 = {
24904
+ approvedRoot: this.#approvedRoot,
24905
+ environment: this.#environment,
24906
+ signal
24907
+ };
24908
+ let agent = await resolveAgentExecutable(specification, discoveryOptions2);
24909
+ let installedAgent = false;
24910
+ if (agent === null) {
24911
+ agent = await recipe.install({
24912
+ audit: this.#audit,
24913
+ report,
24914
+ requestId,
24915
+ signal,
24916
+ stateDirectory: this.#stateDirectory
24917
+ });
24918
+ installedAgent = true;
24919
+ }
24920
+ let adapter = await resolveAdapterExecutable(
24921
+ specification,
24922
+ discoveryOptions2
24923
+ );
24924
+ let installedAdapter = false;
24925
+ if (adapter === null) {
24926
+ await report("installing_adapter");
24927
+ adapter = await this.#adapterInstaller.install(specification, signal);
24928
+ installedAdapter = true;
24929
+ this.#audit.record("agent_adapter_verified", requestId, agentId, {
24930
+ version: specification.adapterVersion
24931
+ });
24932
+ }
24933
+ while (true) {
24934
+ await report("checking_acp");
24935
+ const environment = { ...this.#environment };
24936
+ environment[specification.agentOverrideVariable] = agent.path;
24937
+ environment[specification.adapterOverrideVariable] = adapter.path;
24938
+ const inventory = await discoverLocalAgentInventory({
24939
+ approvedRoot: this.#approvedRoot,
24940
+ environment,
24941
+ signal,
24942
+ specifications: this.#specifications
24943
+ });
24944
+ const verified = inventory.available.find(
24945
+ (candidate) => candidate.specification.id === agentId
24946
+ );
24947
+ if (verified instanceof AvailableAgent) {
24948
+ this.#audit.record(
24949
+ "agent_installation_acp_verified",
24950
+ requestId,
24951
+ agentId,
24952
+ {
24953
+ adapterVersion: verified.agent.adapterVersion,
24954
+ agentVersion: verified.agent.agentVersion
24955
+ }
24956
+ );
24957
+ const previous = this.#catalog.read();
24958
+ this.#catalog.transition(
24959
+ inventory.readyAgents(),
24960
+ inventory.installable
24961
+ );
24962
+ try {
24963
+ await this.#publishCatalog(signal);
24964
+ } catch (error61) {
24965
+ this.#catalog.transition(
24966
+ previous.agents,
24967
+ previous.installableAgents
24968
+ );
24969
+ throw error61;
24970
+ }
24971
+ return;
24972
+ }
24973
+ const unavailable = inventory.unavailable.find(
24974
+ (candidate) => candidate.specification.id === agentId
24975
+ );
24976
+ if (unavailable === void 0) {
24977
+ throw new InstallationManagerError(
24978
+ "agent_verification_failed",
24979
+ "The complete Agent ACP path did not pass verification"
24980
+ );
24981
+ }
24982
+ if (!installedAdapter && requiresAdapterReplacement(unavailable.code)) {
24983
+ await report("installing_adapter");
24984
+ adapter = await this.#adapterInstaller.install(specification, signal);
24985
+ installedAdapter = true;
24986
+ this.#audit.record("agent_adapter_verified", requestId, agentId, {
24987
+ version: specification.adapterVersion
24988
+ });
24989
+ continue;
24990
+ }
24991
+ if (!installedAgent && (requiresAgentReplacement(unavailable.code) || unavailable.code === "acp_incompatible")) {
24992
+ agent = await recipe.install({
24993
+ audit: this.#audit,
24994
+ report,
24995
+ requestId,
24996
+ signal,
24997
+ stateDirectory: this.#stateDirectory
24998
+ });
24999
+ installedAgent = true;
25000
+ continue;
25001
+ }
25002
+ throw new InstallationManagerError(
25003
+ unavailable.code,
25004
+ unavailable.message
25005
+ );
25006
+ }
25007
+ } catch (error61) {
25008
+ if (signal.aborted) {
25009
+ signal.throwIfAborted();
25010
+ }
25011
+ throw translateExecutionError(error61);
25012
+ }
25013
+ };
25014
+ };
22419
25015
 
22420
- // src/run-daemon.ts
22421
- import { randomUUID as randomUUID2 } from "node:crypto";
22422
- import { basename as basename3 } from "node:path";
22423
- import { WebSocket as WebSocket2 } from "undici";
25016
+ // src/agent-installation/local-installation-authorizer.ts
25017
+ import { createInterface } from "node:readline/promises";
25018
+ var ConsoleInstallationAuthorizer = class {
25019
+ #stateDirectory;
25020
+ constructor(stateDirectory) {
25021
+ this.#stateDirectory = stateDirectory;
25022
+ }
25023
+ authorize = async (agentId, label, requestId, signal) => {
25024
+ signal.throwIfAborted();
25025
+ const grants = await readGrantedAgents(this.#stateDirectory);
25026
+ signal.throwIfAborted();
25027
+ if (grants.has(agentId)) {
25028
+ return true;
25029
+ }
25030
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
25031
+ return false;
25032
+ }
25033
+ const prompt = createInterface({
25034
+ input: process.stdin,
25035
+ output: process.stdout
25036
+ });
25037
+ try {
25038
+ const answer = await prompt.question(
25039
+ `
25040
+ Mobius Web requested installation of ${label} (${requestId}). Allow this fixed user-level recipe? [y/N] `,
25041
+ { signal }
25042
+ );
25043
+ const normalized = answer.trim().toLowerCase();
25044
+ return normalized === "y" || normalized === "yes";
25045
+ } finally {
25046
+ prompt.close();
25047
+ }
25048
+ };
25049
+ };
22424
25050
 
22425
25051
  // src/authorization/workspace-directory.ts
22426
- import { readdir, realpath as realpath3, stat as stat3 } from "node:fs/promises";
25052
+ import { readdir, realpath as realpath6, stat as stat5 } from "node:fs/promises";
22427
25053
  import {
22428
25054
  basename as basename2,
22429
- dirname as dirname3,
22430
- isAbsolute,
22431
- join as join2,
25055
+ dirname as dirname4,
25056
+ isAbsolute as isAbsolute4,
25057
+ join as join8,
22432
25058
  normalize,
22433
- relative
25059
+ relative as relative5
22434
25060
  } from "node:path";
22435
25061
  var ROOT_DIRECTORY_ID = "root";
22436
25062
  var DIRECTORY_ID_PREFIX = "dir-";
@@ -22469,14 +25095,14 @@ function decodeDirectoryId(value) {
22469
25095
  throw new LocalAuthorizationError("The workspace directory is invalid");
22470
25096
  }
22471
25097
  const normalized = normalize(decoded);
22472
- if (decoded.includes("\0") || isAbsolute(decoded) || normalized === ".." || normalized.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || normalized !== decoded) {
25098
+ if (decoded.includes("\0") || isAbsolute4(decoded) || normalized === ".." || normalized.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || normalized !== decoded) {
22473
25099
  throw new LocalAuthorizationError("The workspace directory is invalid");
22474
25100
  }
22475
25101
  return normalized;
22476
25102
  }
22477
25103
  function isContained(root, target) {
22478
- const remainder = relative(root, target);
22479
- return remainder.length === 0 || !isAbsolute(remainder) && remainder !== ".." && !remainder.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`);
25104
+ const remainder = relative5(root, target);
25105
+ return remainder.length === 0 || !isAbsolute4(remainder) && remainder !== ".." && !remainder.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`);
22480
25106
  }
22481
25107
  var WorkspaceDirectoryService = class {
22482
25108
  #approvedBindingId;
@@ -22494,8 +25120,8 @@ var WorkspaceDirectoryService = class {
22494
25120
  const root = await this.resolveRoot();
22495
25121
  const relativePath = decodeDirectoryId(requestedDirectoryId);
22496
25122
  try {
22497
- const target = await realpath3(join2(root, relativePath));
22498
- const metadata = await stat3(target);
25123
+ const target = await realpath6(join8(root, relativePath));
25124
+ const metadata = await stat5(target);
22499
25125
  if (!metadata.isDirectory() || !isContained(root, target)) {
22500
25126
  throw new LocalAuthorizationError(
22501
25127
  "The workspace directory is not authorized"
@@ -22523,10 +25149,10 @@ var WorkspaceDirectoryService = class {
22523
25149
  if (entries.length === 500) {
22524
25150
  break;
22525
25151
  }
22526
- const logicalChild = logicalRelativePath.length === 0 ? child.name : join2(logicalRelativePath, child.name);
25152
+ const logicalChild = logicalRelativePath.length === 0 ? child.name : join8(logicalRelativePath, child.name);
22527
25153
  try {
22528
- const resolvedChild = await realpath3(join2(target, child.name));
22529
- const metadata = await stat3(resolvedChild);
25154
+ const resolvedChild = await realpath6(join8(target, child.name));
25155
+ const metadata = await stat5(resolvedChild);
22530
25156
  if (metadata.isDirectory() && isContained(root, resolvedChild)) {
22531
25157
  entries.push({
22532
25158
  directoryId: directoryId(logicalChild),
@@ -22538,7 +25164,7 @@ var WorkspaceDirectoryService = class {
22538
25164
  }
22539
25165
  entries.sort((left, right) => left.name.localeCompare(right.name));
22540
25166
  const labelSuffix = logicalRelativePath.replaceAll("\\", "/");
22541
- const parentRelative = logicalRelativePath.length === 0 || dirname3(logicalRelativePath) === "." ? "" : dirname3(logicalRelativePath);
25167
+ const parentRelative = logicalRelativePath.length === 0 || dirname4(logicalRelativePath) === "." ? "" : dirname4(logicalRelativePath);
22542
25168
  return new WorkspaceDirectoryListing(
22543
25169
  requestedDirectoryId,
22544
25170
  labelSuffix.length === 0 ? basename2(root) : `${basename2(root)}/${labelSuffix}`,
@@ -22572,8 +25198,8 @@ var WorkspaceDirectoryService = class {
22572
25198
  };
22573
25199
  resolveRoot = async () => {
22574
25200
  try {
22575
- const root = await realpath3(this.#approvedRoot);
22576
- const metadata = await stat3(root);
25201
+ const root = await realpath6(this.#approvedRoot);
25202
+ const metadata = await stat5(root);
22577
25203
  if (!metadata.isDirectory()) {
22578
25204
  throw new LocalAuthorizationError(
22579
25205
  "The approved root is not a directory"
@@ -22607,7 +25233,7 @@ var RelayConnectionError = class extends Error {
22607
25233
  };
22608
25234
  function connectRelay(url2, signal) {
22609
25235
  signal.throwIfAborted();
22610
- return new Promise((resolve2, reject) => {
25236
+ return new Promise((resolve6, reject) => {
22611
25237
  const socket = new WebSocket(url2, {
22612
25238
  dispatcher: environmentProxyDispatcher
22613
25239
  });
@@ -22660,7 +25286,7 @@ function connectRelay(url2, signal) {
22660
25286
  }
22661
25287
  finished = true;
22662
25288
  cleanup();
22663
- resolve2(socket);
25289
+ resolve6(socket);
22664
25290
  };
22665
25291
  function onTimeout() {
22666
25292
  fail(
@@ -22680,8 +25306,179 @@ function connectRelay(url2, signal) {
22680
25306
  });
22681
25307
  }
22682
25308
 
25309
+ // src/relay-connection/liveness.ts
25310
+ import { randomUUID as randomUUID3 } from "node:crypto";
25311
+ var HEARTBEAT_ACK_TIMEOUT_MILLISECONDS = 1e4;
25312
+ var HEARTBEAT_INTERVAL_MILLISECONDS = 2e4;
25313
+ var RelayLivenessState = class {
25314
+ };
25315
+ var UnsupportedRelayLivenessState = class extends RelayLivenessState {
25316
+ };
25317
+ var AwaitingHeartbeatAckState = class extends RelayLivenessState {
25318
+ heartbeatId;
25319
+ timeout;
25320
+ constructor(heartbeatId, timeout) {
25321
+ super();
25322
+ this.heartbeatId = heartbeatId;
25323
+ this.timeout = timeout;
25324
+ }
25325
+ };
25326
+ var WaitingForHeartbeatState = class extends RelayLivenessState {
25327
+ schedule;
25328
+ constructor(schedule) {
25329
+ super();
25330
+ this.schedule = schedule;
25331
+ }
25332
+ };
25333
+ var FinishedRelayLivenessState = class extends RelayLivenessState {
25334
+ };
25335
+ var RelayConnectionLiveness = class {
25336
+ #timer;
25337
+ #transport;
25338
+ #state = new UnsupportedRelayLivenessState();
25339
+ constructor(timer, transport) {
25340
+ this.#timer = timer;
25341
+ this.#transport = transport;
25342
+ }
25343
+ acknowledge = (event) => {
25344
+ if (!(this.#state instanceof AwaitingHeartbeatAckState)) {
25345
+ throw new Error("Unexpected relay heartbeat acknowledgement");
25346
+ }
25347
+ if (event.heartbeatId !== this.#state.heartbeatId) {
25348
+ throw new Error("Relay acknowledged the wrong heartbeat");
25349
+ }
25350
+ this.#state.timeout.cancel();
25351
+ this.#state = new WaitingForHeartbeatState(
25352
+ this.#timer.schedule(
25353
+ this.#sendHeartbeat,
25354
+ HEARTBEAT_INTERVAL_MILLISECONDS
25355
+ )
25356
+ );
25357
+ };
25358
+ close = () => {
25359
+ if (this.#state instanceof AwaitingHeartbeatAckState) {
25360
+ this.#state.timeout.cancel();
25361
+ } else if (this.#state instanceof WaitingForHeartbeatState) {
25362
+ this.#state.schedule.cancel();
25363
+ } else if (!(this.#state instanceof UnsupportedRelayLivenessState) && !(this.#state instanceof FinishedRelayLivenessState)) {
25364
+ throw new Error("Unknown relay liveness state");
25365
+ }
25366
+ this.#state = new FinishedRelayLivenessState();
25367
+ };
25368
+ start = () => {
25369
+ if (this.#state instanceof UnsupportedRelayLivenessState) {
25370
+ this.#sendHeartbeat();
25371
+ return;
25372
+ }
25373
+ if (this.#state instanceof AwaitingHeartbeatAckState || this.#state instanceof WaitingForHeartbeatState) {
25374
+ return;
25375
+ }
25376
+ if (this.#state instanceof FinishedRelayLivenessState) {
25377
+ return;
25378
+ }
25379
+ throw new Error("Unknown relay liveness state");
25380
+ };
25381
+ #handleTimeout = () => {
25382
+ if (this.#state instanceof FinishedRelayLivenessState) {
25383
+ return;
25384
+ }
25385
+ if (!(this.#state instanceof AwaitingHeartbeatAckState)) {
25386
+ throw new Error("Heartbeat timeout occurred in an invalid state");
25387
+ }
25388
+ this.#state.timeout.cancel();
25389
+ this.#state = new FinishedRelayLivenessState();
25390
+ this.#transport.unresponsive();
25391
+ };
25392
+ #sendHeartbeat = () => {
25393
+ if (this.#state instanceof WaitingForHeartbeatState) {
25394
+ this.#state.schedule.cancel();
25395
+ } else if (!(this.#state instanceof UnsupportedRelayLivenessState)) {
25396
+ if (this.#state instanceof FinishedRelayLivenessState) {
25397
+ return;
25398
+ }
25399
+ throw new Error("Heartbeat was scheduled in an invalid state");
25400
+ }
25401
+ const heartbeatId = randomUUID3();
25402
+ this.#state = new AwaitingHeartbeatAckState(
25403
+ heartbeatId,
25404
+ this.#timer.schedule(
25405
+ this.#handleTimeout,
25406
+ HEARTBEAT_ACK_TIMEOUT_MILLISECONDS
25407
+ )
25408
+ );
25409
+ this.#transport.send(
25410
+ encodeRuntimeHeartbeatEvent({
25411
+ heartbeatId,
25412
+ kind: RUNTIME_HEARTBEAT_KIND,
25413
+ protocolVersion: MOBIUS_PROTOCOL_VERSION
25414
+ })
25415
+ );
25416
+ };
25417
+ };
25418
+
25419
+ // src/relay-connection/timer.ts
25420
+ var SystemDaemonTimer = class {
25421
+ schedule = (callback, milliseconds) => {
25422
+ const timeout = setTimeout(callback, milliseconds);
25423
+ return { cancel: () => clearTimeout(timeout) };
25424
+ };
25425
+ };
25426
+ var systemDaemonTimer = new SystemDaemonTimer();
25427
+
22683
25428
  // src/run-daemon.ts
22684
25429
  var RELAY_RECONNECT_DELAY_MILLISECONDS = 500;
25430
+ var RELAY_CONNECTION_REFRESH_MILLISECONDS = 20 * 60 * 1e3;
25431
+ var RelayConnectionRefreshState = class {
25432
+ };
25433
+ var ScheduledRelayConnectionRefreshState = class extends RelayConnectionRefreshState {
25434
+ };
25435
+ var DueRelayConnectionRefreshState = class extends RelayConnectionRefreshState {
25436
+ };
25437
+ var FinishedRelayConnectionRefreshState = class extends RelayConnectionRefreshState {
25438
+ };
25439
+ var RelayConnectionRefresh = class {
25440
+ #isSafe;
25441
+ #refresh;
25442
+ #scheduledTask;
25443
+ #state = new ScheduledRelayConnectionRefreshState();
25444
+ constructor(timer, isSafe, refresh) {
25445
+ this.#isSafe = isSafe;
25446
+ this.#refresh = refresh;
25447
+ this.#scheduledTask = timer.schedule(
25448
+ this.#request,
25449
+ RELAY_CONNECTION_REFRESH_MILLISECONDS
25450
+ );
25451
+ }
25452
+ afterTurnFinished = () => {
25453
+ if (this.#state instanceof DueRelayConnectionRefreshState) {
25454
+ this.#request();
25455
+ return;
25456
+ }
25457
+ if (this.#state instanceof ScheduledRelayConnectionRefreshState || this.#state instanceof FinishedRelayConnectionRefreshState) {
25458
+ return;
25459
+ }
25460
+ throw new Error("Unknown relay connection refresh state");
25461
+ };
25462
+ close = () => {
25463
+ this.#scheduledTask.cancel();
25464
+ this.#state = new FinishedRelayConnectionRefreshState();
25465
+ };
25466
+ #request = () => {
25467
+ if (this.#state instanceof FinishedRelayConnectionRefreshState) {
25468
+ return;
25469
+ }
25470
+ if (!(this.#state instanceof ScheduledRelayConnectionRefreshState) && !(this.#state instanceof DueRelayConnectionRefreshState)) {
25471
+ throw new Error("Unknown relay connection refresh state");
25472
+ }
25473
+ if (!this.#isSafe()) {
25474
+ this.#state = new DueRelayConnectionRefreshState();
25475
+ return;
25476
+ }
25477
+ this.#state = new FinishedRelayConnectionRefreshState();
25478
+ this.#scheduledTask.cancel();
25479
+ this.#refresh();
25480
+ };
25481
+ };
22685
25482
  var DaemonAcpObserver = class {
22686
25483
  #socket;
22687
25484
  constructor(socket) {
@@ -22724,6 +25521,127 @@ var DaemonAcpObserver = class {
22724
25521
  return Promise.resolve();
22725
25522
  };
22726
25523
  };
25524
+ var AuditInstallationEvents = class {
25525
+ #audit;
25526
+ constructor(audit) {
25527
+ this.#audit = audit;
25528
+ }
25529
+ record = (event, requestId, agentId, details = {}) => {
25530
+ this.#audit.record({
25531
+ commandId: requestId,
25532
+ details,
25533
+ event,
25534
+ sessionId: `agent-installation:${agentId}`
25535
+ });
25536
+ };
25537
+ };
25538
+ var DaemonInstallationObserver = class {
25539
+ #started = /* @__PURE__ */ new Set();
25540
+ #socket = null;
25541
+ connect = (socket) => {
25542
+ this.#socket = socket;
25543
+ };
25544
+ disconnect = (socket) => {
25545
+ if (this.#socket === socket) {
25546
+ this.#socket = null;
25547
+ }
25548
+ };
25549
+ #send = (socket, message) => {
25550
+ try {
25551
+ send(socket, message);
25552
+ } catch {
25553
+ if (this.#socket === socket) {
25554
+ this.#socket = null;
25555
+ }
25556
+ }
25557
+ };
25558
+ onState = (state) => {
25559
+ const socket = this.#socket;
25560
+ if (socket === null || socket.readyState !== WebSocket2.OPEN) {
25561
+ return Promise.resolve();
25562
+ }
25563
+ if (state instanceof RequestedInstallation) {
25564
+ return Promise.resolve();
25565
+ }
25566
+ if (state instanceof AwaitingLocalApproval) {
25567
+ this.#send(
25568
+ socket,
25569
+ encodeAgentInstallAwaitingApprovalEvent({
25570
+ agentId: state.agentId,
25571
+ kind: AGENT_INSTALL_AWAITING_APPROVAL_KIND,
25572
+ protocolVersion: MOBIUS_PROTOCOL_VERSION,
25573
+ requestId: state.requestId
25574
+ })
25575
+ );
25576
+ return Promise.resolve();
25577
+ }
25578
+ if (state instanceof RunningInstallation) {
25579
+ if (!this.#started.has(state.requestId)) {
25580
+ this.#started.add(state.requestId);
25581
+ this.#send(
25582
+ socket,
25583
+ encodeAgentInstallStartedEvent({
25584
+ agentId: state.agentId,
25585
+ kind: AGENT_INSTALL_STARTED_KIND,
25586
+ protocolVersion: MOBIUS_PROTOCOL_VERSION,
25587
+ requestId: state.requestId
25588
+ })
25589
+ );
25590
+ }
25591
+ this.#send(
25592
+ socket,
25593
+ encodeAgentInstallProgressEvent({
25594
+ agentId: state.agentId,
25595
+ kind: AGENT_INSTALL_PROGRESS_KIND,
25596
+ protocolVersion: MOBIUS_PROTOCOL_VERSION,
25597
+ requestId: state.requestId,
25598
+ stage: state.stage
25599
+ })
25600
+ );
25601
+ return Promise.resolve();
25602
+ }
25603
+ if (state instanceof SucceededInstallation) {
25604
+ this.#send(
25605
+ socket,
25606
+ encodeAgentInstallCompletedEvent({
25607
+ agentId: state.agentId,
25608
+ authenticationMayBeRequired: state.authenticationMayBeRequired,
25609
+ kind: AGENT_INSTALL_COMPLETED_KIND,
25610
+ protocolVersion: MOBIUS_PROTOCOL_VERSION,
25611
+ requestId: state.requestId
25612
+ })
25613
+ );
25614
+ return Promise.resolve();
25615
+ }
25616
+ if (state instanceof FailedInstallation) {
25617
+ this.#send(
25618
+ socket,
25619
+ encodeAgentInstallFailedEvent({
25620
+ agentId: state.agentId,
25621
+ code: state.code,
25622
+ kind: AGENT_INSTALL_FAILED_KIND,
25623
+ message: state.message,
25624
+ protocolVersion: MOBIUS_PROTOCOL_VERSION,
25625
+ requestId: state.requestId
25626
+ })
25627
+ );
25628
+ return Promise.resolve();
25629
+ }
25630
+ if (state instanceof CancelledInstallation) {
25631
+ this.#send(
25632
+ socket,
25633
+ encodeAgentInstallCancelledEvent({
25634
+ agentId: state.agentId,
25635
+ kind: AGENT_INSTALL_CANCELLED_KIND,
25636
+ protocolVersion: MOBIUS_PROTOCOL_VERSION,
25637
+ requestId: state.requestId
25638
+ })
25639
+ );
25640
+ return Promise.resolve();
25641
+ }
25642
+ throw new Error("Unsupported Agent installation state");
25643
+ };
25644
+ };
22727
25645
  var ResolvedSessionDirectories = class {
22728
25646
  additionalDirectories;
22729
25647
  additionalDirectoryEntries;
@@ -22780,13 +25698,13 @@ function reportFailure(socket, command, code, message) {
22780
25698
  );
22781
25699
  }
22782
25700
  function operationErrorCode(error61) {
22783
- if (error61 instanceof AcpExecutionError || error61 instanceof LocalAuthorizationError) {
25701
+ if (error61 instanceof AcpExecutionError || error61 instanceof LocalAuthorizationError || error61 instanceof LocalAgentCatalogError) {
22784
25702
  return error61.code;
22785
25703
  }
22786
25704
  return "session_operation_failed";
22787
25705
  }
22788
25706
  function operationErrorMessage(error61) {
22789
- if (error61 instanceof AcpExecutionError || error61 instanceof LocalAuthorizationError) {
25707
+ if (error61 instanceof AcpExecutionError || error61 instanceof LocalAuthorizationError || error61 instanceof LocalAgentCatalogError) {
22790
25708
  return error61.message;
22791
25709
  }
22792
25710
  return "The local agent operation could not be completed";
@@ -22871,7 +25789,7 @@ async function executeSessionPrepare(socket, command, config2, directories, audi
22871
25789
  command.directoryId,
22872
25790
  command.additionalDirectoryIds
22873
25791
  );
22874
- const agent = findLocalAgent(config2.agents, command.agentId);
25792
+ const agent = config2.catalog.find(command.agentId);
22875
25793
  const prepared = await sessions.prepareSession({
22876
25794
  additionalDirectories: resolved.additionalDirectories,
22877
25795
  agent,
@@ -22921,7 +25839,7 @@ async function executeSessionConfigure(socket, command, config2, directories, au
22921
25839
  );
22922
25840
  const configOptions = await sessions.configureSession({
22923
25841
  additionalDirectories: resolved.additionalDirectories,
22924
- agent: findLocalAgent(config2.agents, command.agentId),
25842
+ agent: config2.catalog.find(command.agentId),
22925
25843
  booleanValue: command.booleanValue,
22926
25844
  configId: command.configId,
22927
25845
  cwd: resolved.cwd,
@@ -22958,8 +25876,8 @@ async function executePrompt(socket, command, config2, directories, audit, sessi
22958
25876
  command.directoryId,
22959
25877
  command.additionalDirectoryIds
22960
25878
  );
22961
- const agent = findLocalAgent(config2.agents, command.agentId);
22962
- const processAttemptId = randomUUID2();
25879
+ const agent = config2.catalog.find(command.agentId);
25880
+ const processAttemptId = randomUUID4();
22963
25881
  audit.record({
22964
25882
  commandId: command.commandId,
22965
25883
  event: "session_authorized",
@@ -23043,41 +25961,48 @@ async function executePrompt(socket, command, config2, directories, audit, sessi
23043
25961
  event: "session_failed",
23044
25962
  sessionId: command.sessionId
23045
25963
  });
25964
+ const code = operationErrorCode(error61);
23046
25965
  reportFailure(
23047
25966
  socket,
23048
25967
  command,
23049
- "agent_execution_failed",
23050
- "The local agent could not complete the request"
25968
+ code === "session_operation_failed" ? "agent_execution_failed" : code,
25969
+ code === "session_operation_failed" ? "The local agent could not complete the request" : operationErrorMessage(error61)
23051
25970
  );
23052
25971
  console.error("Mobius daemon command failed", error61);
23053
25972
  }
23054
25973
  }
23055
- function waitForRelayRetry(signal) {
25974
+ function waitForRelayRetry(signal, timer) {
23056
25975
  if (signal.aborted) {
23057
25976
  return Promise.resolve();
23058
25977
  }
23059
- return new Promise((resolve2) => {
23060
- const timeout = setTimeout(finish, RELAY_RECONNECT_DELAY_MILLISECONDS);
25978
+ return new Promise((resolve6) => {
25979
+ const retry = timer.schedule(finish, RELAY_RECONNECT_DELAY_MILLISECONDS);
23061
25980
  function finish() {
23062
- clearTimeout(timeout);
25981
+ retry.cancel();
23063
25982
  signal.removeEventListener("abort", finish);
23064
- resolve2();
25983
+ resolve6();
23065
25984
  }
23066
25985
  signal.addEventListener("abort", finish, { once: true });
23067
25986
  });
23068
25987
  }
23069
- async function runRelayConnection(config2, directories, audit, sessions, signal) {
23070
- const socket = await connectRelay(config2.relayUrl, signal);
25988
+ async function runRelayConnection(config2, directories, audit, sessions, installations, installationObserver, signal, timer) {
25989
+ const relayUrl = new URL(config2.relayUrl);
25990
+ relayUrl.searchParams.set("liveness", "1");
25991
+ const socket = await connectRelay(relayUrl, signal);
23071
25992
  const activeTurns = /* @__PURE__ */ new Map();
23072
25993
  const connectionController = new AbortController();
23073
25994
  sessions.setObserver(new DaemonAcpObserver(socket));
23074
- await new Promise((resolve2, reject) => {
25995
+ installationObserver.connect(socket);
25996
+ await new Promise((resolve6, reject) => {
23075
25997
  let finished = false;
23076
25998
  const cleanup = () => {
25999
+ connectionRefresh.close();
26000
+ connectionLiveness.close();
23077
26001
  signal.removeEventListener("abort", stop);
23078
26002
  socket.removeEventListener("close", handleClose);
23079
26003
  socket.removeEventListener("error", handleError);
23080
26004
  socket.removeEventListener("message", handleMessage);
26005
+ installationObserver.disconnect(socket);
23081
26006
  connectionController.abort();
23082
26007
  for (const turn of activeTurns.values()) {
23083
26008
  turn.abortConnection();
@@ -23090,7 +26015,7 @@ async function runRelayConnection(config2, directories, audit, sessions, signal)
23090
26015
  }
23091
26016
  finished = true;
23092
26017
  cleanup();
23093
- resolve2();
26018
+ resolve6();
23094
26019
  };
23095
26020
  const rejectConnection = () => {
23096
26021
  if (finished) {
@@ -23104,6 +26029,22 @@ async function runRelayConnection(config2, directories, audit, sessions, signal)
23104
26029
  socket.close(1e3, "Daemon stopped");
23105
26030
  resolveConnection();
23106
26031
  };
26032
+ const refreshConnection = () => {
26033
+ socket.close(1e3, "Refreshing idle relay connection");
26034
+ resolveConnection();
26035
+ };
26036
+ const connectionLiveness = new RelayConnectionLiveness(timer, {
26037
+ send: (message) => send(socket, message),
26038
+ unresponsive: () => {
26039
+ socket.close(4001, "Relay liveness check timed out");
26040
+ resolveConnection();
26041
+ }
26042
+ });
26043
+ const connectionRefresh = new RelayConnectionRefresh(
26044
+ timer,
26045
+ () => activeTurns.size === 0,
26046
+ refreshConnection
26047
+ );
23107
26048
  signal.addEventListener("abort", stop, { once: true });
23108
26049
  const handleClose = () => {
23109
26050
  resolveConnection();
@@ -23118,11 +26059,23 @@ async function runRelayConnection(config2, directories, audit, sessions, signal)
23118
26059
  const handleMessage = (event) => {
23119
26060
  const encoded = event.data;
23120
26061
  if (typeof encoded !== "string") {
23121
- socket.close(1003, "Text message required");
26062
+ socket.close(4003, "Text message required");
26063
+ rejectConnection();
23122
26064
  return;
23123
26065
  }
23124
26066
  try {
23125
26067
  const kind = decodeMessageKind(encoded);
26068
+ if (kind === RUNTIME_LIVENESS_READY_KIND) {
26069
+ decodeRuntimeLivenessReadyEvent(encoded);
26070
+ connectionLiveness.start();
26071
+ return;
26072
+ }
26073
+ if (kind === RUNTIME_HEARTBEAT_ACK_KIND) {
26074
+ connectionLiveness.acknowledge(
26075
+ decodeRuntimeHeartbeatAckEvent(encoded)
26076
+ );
26077
+ return;
26078
+ }
23126
26079
  if (kind === SESSION_CANCEL_KIND) {
23127
26080
  const cancellation = decodeSessionCancelCommand(encoded);
23128
26081
  const turn2 = activeTurns.get(cancellation.commandId);
@@ -23215,10 +26168,23 @@ async function runRelayConnection(config2, directories, audit, sessions, signal)
23215
26168
  });
23216
26169
  return;
23217
26170
  }
23218
- if (kind !== SESSION_PROMPT_KIND) {
23219
- socket.close(1008, "Unsupported protocol message");
26171
+ if (kind === AGENT_INSTALL_REQUEST_KIND) {
26172
+ const command2 = decodeAgentInstallRequestCommand(encoded);
26173
+ void installations.request(command2, signal).catch((error61) => {
26174
+ console.error("Mobius Agent installation failed", error61);
26175
+ });
26176
+ return;
26177
+ }
26178
+ if (kind === AGENT_INSTALL_CANCEL_KIND) {
26179
+ const command2 = decodeAgentInstallCancelCommand(encoded);
26180
+ void installations.cancel(command2.requestId).catch((error61) => {
26181
+ reportOperationFailure(socket, command2.requestId, "", error61);
26182
+ });
23220
26183
  return;
23221
26184
  }
26185
+ if (kind !== SESSION_PROMPT_KIND) {
26186
+ throw new Error("Unsupported protocol message");
26187
+ }
23222
26188
  const command = decodeSessionPromptCommand(encoded);
23223
26189
  if (activeTurns.has(command.commandId)) {
23224
26190
  reportFailure(
@@ -23243,10 +26209,12 @@ async function runRelayConnection(config2, directories, audit, sessions, signal)
23243
26209
  console.error("Mobius daemon turn failed", error61);
23244
26210
  }).finally(() => {
23245
26211
  activeTurns.delete(command.commandId);
26212
+ connectionRefresh.afterTurnFinished();
23246
26213
  });
23247
26214
  } catch (error61) {
23248
26215
  console.error("Mobius daemon rejected a message", error61);
23249
- socket.close(1008, "Invalid protocol message");
26216
+ socket.close(4008, "Invalid protocol message");
26217
+ rejectConnection();
23250
26218
  }
23251
26219
  };
23252
26220
  socket.addEventListener("close", handleClose);
@@ -23257,7 +26225,32 @@ async function runRelayConnection(config2, directories, audit, sessions, signal)
23257
26225
  }
23258
26226
  });
23259
26227
  }
23260
- async function runDaemon(config2, audit, signal) {
26228
+ async function runDaemon(config2, audit, signal, timer = systemDaemonTimer) {
26229
+ const installationAudit = new AuditInstallationEvents(audit);
26230
+ const stateDirectory = readStateDirectory();
26231
+ const specifications = supportedLocalAgentSpecifications();
26232
+ const installationExecutor = new FixedInstallationExecutor({
26233
+ approvedRoot: config2.approvedRoot,
26234
+ audit: installationAudit,
26235
+ catalog: config2.catalog,
26236
+ environment: process.env,
26237
+ publishCatalog: async (installationSignal) => await publishAgentCatalog(config2, installationSignal),
26238
+ stateDirectory
26239
+ });
26240
+ await new AutomaticAdapterProvisioner(
26241
+ {
26242
+ approvedRoot: config2.approvedRoot,
26243
+ audit: installationAudit,
26244
+ catalog: config2.catalog,
26245
+ environment: process.env
26246
+ },
26247
+ new ManagedAdapterInstaller({
26248
+ approvedRoot: config2.approvedRoot,
26249
+ environment: process.env,
26250
+ stateDirectory
26251
+ }),
26252
+ specifications
26253
+ ).prepare(signal);
23261
26254
  const sessionStore = new SqliteAcpSessionStore(config2.auditDatabasePath);
23262
26255
  const sessions = new AcpSessionManager(
23263
26256
  new NullAcpSessionObserver(),
@@ -23267,11 +26260,29 @@ async function runDaemon(config2, audit, signal) {
23267
26260
  config2.bindingId,
23268
26261
  config2.approvedRoot
23269
26262
  );
26263
+ const installationObserver = new DaemonInstallationObserver();
26264
+ const installations = new AgentInstallationManager(
26265
+ specifications,
26266
+ new ConsoleInstallationAuthorizer(stateDirectory),
26267
+ installationExecutor,
26268
+ installationObserver,
26269
+ installationAudit
26270
+ );
23270
26271
  let retryAnnounced = false;
23271
26272
  try {
23272
26273
  while (!signal.aborted) {
23273
26274
  try {
23274
- await runRelayConnection(config2, directories, audit, sessions, signal);
26275
+ await publishAgentCatalog(config2, signal);
26276
+ await runRelayConnection(
26277
+ config2,
26278
+ directories,
26279
+ audit,
26280
+ sessions,
26281
+ installations,
26282
+ installationObserver,
26283
+ signal,
26284
+ timer
26285
+ );
23275
26286
  retryAnnounced = false;
23276
26287
  } catch {
23277
26288
  if (signal.aborted) {
@@ -23282,11 +26293,18 @@ async function runDaemon(config2, audit, signal) {
23282
26293
  retryAnnounced = true;
23283
26294
  }
23284
26295
  }
23285
- await waitForRelayRetry(signal);
26296
+ await waitForRelayRetry(signal, timer);
23286
26297
  }
23287
26298
  } finally {
23288
- await sessions.close();
23289
- sessionStore.close();
26299
+ try {
26300
+ await installations.close();
26301
+ } finally {
26302
+ try {
26303
+ await sessions.close();
26304
+ } finally {
26305
+ sessionStore.close();
26306
+ }
26307
+ }
23290
26308
  }
23291
26309
  }
23292
26310
  export {