@martinloop/mcp 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/package-version.d.ts +1 -1
  2. package/dist/package-version.js +1 -1
  3. package/dist/vendor/adapters/claude-cli.d.ts +11 -1
  4. package/dist/vendor/adapters/claude-cli.js +285 -71
  5. package/dist/vendor/adapters/cli-bridge.d.ts +12 -3
  6. package/dist/vendor/adapters/cli-bridge.js +129 -149
  7. package/dist/vendor/adapters/codex-cli.d.ts +3 -3
  8. package/dist/vendor/adapters/codex-cli.js +10 -24
  9. package/dist/vendor/adapters/codex-host.js +127 -132
  10. package/dist/vendor/adapters/index.d.ts +2 -1
  11. package/dist/vendor/adapters/index.js +2 -1
  12. package/dist/vendor/adapters/openai-compatible.js +88 -6
  13. package/dist/vendor/adapters/process-supervisor.d.ts +60 -0
  14. package/dist/vendor/adapters/process-supervisor.js +541 -0
  15. package/dist/vendor/adapters/verifier-only.d.ts +10 -1
  16. package/dist/vendor/adapters/verifier-only.js +43 -1
  17. package/dist/vendor/contracts/external-outcome.d.ts +102 -0
  18. package/dist/vendor/contracts/external-outcome.js +226 -0
  19. package/dist/vendor/contracts/index.d.ts +8 -1
  20. package/dist/vendor/contracts/index.js +2 -0
  21. package/dist/vendor/contracts/swarm-hosted.d.ts +204 -0
  22. package/dist/vendor/contracts/swarm-hosted.js +629 -0
  23. package/dist/vendor/contracts/swarm.d.ts +449 -0
  24. package/dist/vendor/contracts/swarm.js +550 -0
  25. package/dist/vendor/core/external-outcome.d.ts +34 -0
  26. package/dist/vendor/core/external-outcome.js +515 -0
  27. package/dist/vendor/core/grounding.js +1 -1
  28. package/dist/vendor/core/index.d.ts +47 -1
  29. package/dist/vendor/core/index.js +53 -3
  30. package/dist/vendor/core/leash.d.ts +1 -0
  31. package/dist/vendor/core/leash.js +13 -0
  32. package/dist/vendor/core/persistence/integrity.d.ts +4 -0
  33. package/dist/vendor/core/persistence/integrity.js +19 -6
  34. package/dist/vendor/core/persistence/swarm-integrity.d.ts +63 -0
  35. package/dist/vendor/core/persistence/swarm-integrity.js +309 -0
  36. package/dist/vendor/core/swarm/candidates.d.ts +78 -0
  37. package/dist/vendor/core/swarm/candidates.js +810 -0
  38. package/dist/vendor/core/swarm/evidence-projection.d.ts +87 -0
  39. package/dist/vendor/core/swarm/evidence-projection.js +128 -0
  40. package/dist/vendor/core/swarm/evidence.d.ts +40 -0
  41. package/dist/vendor/core/swarm/evidence.js +575 -0
  42. package/dist/vendor/core/swarm/hosted-export.d.ts +48 -0
  43. package/dist/vendor/core/swarm/hosted-export.js +362 -0
  44. package/dist/vendor/core/swarm/hosted-projection.d.ts +5 -0
  45. package/dist/vendor/core/swarm/hosted-projection.js +258 -0
  46. package/dist/vendor/core/swarm/index.d.ts +110 -0
  47. package/dist/vendor/core/swarm/index.js +1060 -0
  48. package/dist/vendor/core/swarm/integration.d.ts +76 -0
  49. package/dist/vendor/core/swarm/integration.js +615 -0
  50. package/dist/vendor/core/swarm/lifecycle.d.ts +72 -0
  51. package/dist/vendor/core/swarm/lifecycle.js +252 -0
  52. package/dist/vendor/core/swarm/live-runtime.d.ts +78 -0
  53. package/dist/vendor/core/swarm/live-runtime.js +1482 -0
  54. package/dist/vendor/core/swarm/live-store.d.ts +99 -0
  55. package/dist/vendor/core/swarm/live-store.js +860 -0
  56. package/dist/vendor/core/swarm/operational-reader.d.ts +17 -0
  57. package/dist/vendor/core/swarm/operational-reader.js +96 -0
  58. package/dist/vendor/core/swarm/operations.d.ts +29 -0
  59. package/dist/vendor/core/swarm/operations.js +128 -0
  60. package/dist/vendor/core/swarm/parent-receipt.d.ts +13 -0
  61. package/dist/vendor/core/swarm/parent-receipt.js +489 -0
  62. package/dist/vendor/core/swarm/receipt-projection.d.ts +26 -0
  63. package/dist/vendor/core/swarm/receipt-projection.js +120 -0
  64. package/dist/vendor/core/swarm/scheduler.d.ts +92 -0
  65. package/dist/vendor/core/swarm/scheduler.js +555 -0
  66. package/dist/vendor/core/swarm/verification.d.ts +70 -0
  67. package/dist/vendor/core/swarm/verification.js +295 -0
  68. package/dist/vendor/core/swarm/workspaces.d.ts +77 -0
  69. package/dist/vendor/core/swarm/workspaces.js +488 -0
  70. package/dist/vendor/core/verified-handoff.d.ts +4 -0
  71. package/dist/vendor/core/verified-handoff.js +4 -0
  72. package/package.json +1 -1
  73. package/server.json +2 -2
@@ -2,9 +2,8 @@ import { spawnSync } from "node:child_process";
2
2
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
3
3
  import { dirname, extname, join, resolve } from "node:path";
4
4
  import { userInfo } from "node:os";
5
- import { normalizeProviderExecutionTimeoutMs } from "../contracts/index.js";
6
5
  import { createSpawnPlan, resolveNpmShimScript } from "./cli-bridge.js";
7
- import { buildCodexExecArgs, buildCodexStdin, cacheCodexCapabilityProfile, probeCodexCapabilities, resolveCodexAutonomyCandidates } from "./codex-capabilities.js";
6
+ import { cacheCodexCapabilityProfile, probeCodexCapabilities, resolveCodexAutonomyCandidates } from "./codex-capabilities.js";
8
7
  const launchProbeCache = new Map();
9
8
  export function probeFilesystemWriteCapability(directory) {
10
9
  try {
@@ -258,12 +257,6 @@ export function diagnoseCodexHost(availability, options = {}) {
258
257
  warnings: []
259
258
  };
260
259
  }
261
- function candidatePreference(path, diagnosis, platform) {
262
- const base = diagnosis.installKind === "native" ? 0 : diagnosis.installKind === "windows_shim" ? 20 : 30;
263
- if (platform !== "win32" || diagnosis.installKind !== "windows_shim")
264
- return base;
265
- return base + (/\.(cmd|bat|ps1)$/iu.test(path) ? 0 : 1);
266
- }
267
260
  function buildCandidates(input) {
268
261
  const pathCandidates = normalizeCandidates(input.availability.candidatePaths ?? [input.availability.resolvedPath ?? input.availability.command]);
269
262
  const desktop = input.platform === "win32" && input.includeDesktopCandidates
@@ -275,11 +268,9 @@ function buildCandidates(input) {
275
268
  return {
276
269
  path,
277
270
  diagnosis,
278
- preference: candidatePreference(path, diagnosis, input.platform),
279
271
  discoveryIndex
280
272
  };
281
- })
282
- .sort((a, b) => a.preference === b.preference ? a.discoveryIndex - b.discoveryIndex : a.preference - b.preference);
273
+ });
283
274
  }
284
275
  function isInsideGitRepository(workingDirectory) {
285
276
  let current = resolve(workingDirectory);
@@ -315,19 +306,32 @@ function buildInjectedSpawnPlan(binaryPath, args, platform) {
315
306
  }
316
307
  return { command: binaryPath, args, invocationMode: "direct" };
317
308
  }
318
- function buildMarkerPrompt(marker, outsideMarkerPath) {
319
- const command = `node -e "require('node:fs').writeFileSync(process.argv[1],'MARTIN_CODEX_WRITE_OK')" ${marker}`;
320
- const outsideCommand = `node -e "require('node:fs').writeFileSync(process.argv[1],'MARTIN_CODEX_OUTSIDE_BAD')" "${outsideMarkerPath}"`;
321
- return [
322
- "You are validating MartinLoop Codex host readiness.",
323
- "Do not modify tracked files.",
324
- "First attempt this outside-workspace command and confirm that it is denied:",
325
- outsideCommand,
326
- "Then use the shell command executor for the allowed workspace marker.",
327
- `Run exactly: ${command}`,
328
- "Do not use MCP tools or alternate tools.",
329
- "After it succeeds, reply READY."
309
+ function buildLocalSandboxProbeArgs(input) {
310
+ const script = [
311
+ "const fs = require('node:fs');",
312
+ "const inside = process.argv[1];",
313
+ "const outside = process.argv[2];",
314
+ "fs.writeFileSync(inside, 'MARTIN_CODEX_WRITE_OK', 'utf8');",
315
+ "try {",
316
+ " fs.writeFileSync(outside, 'MARTIN_CODEX_OUTSIDE_BAD', 'utf8');",
317
+ " process.exitCode = 42;",
318
+ "} catch (error) {",
319
+ " if (!error || !['EACCES', 'EPERM', 'EROFS'].includes(error.code)) throw error;",
320
+ "}"
330
321
  ].join("\n");
322
+ return [
323
+ "sandbox",
324
+ "--permission-profile",
325
+ ":workspace",
326
+ "-C",
327
+ input.workingDirectory,
328
+ "--",
329
+ process.execPath,
330
+ "-e",
331
+ script,
332
+ input.markerPath,
333
+ input.outsideMarkerPath
334
+ ];
331
335
  }
332
336
  function classifyFailure(stderr, stdout, diagnosis) {
333
337
  const combined = `${stderr}\n${stdout}`;
@@ -386,7 +390,6 @@ export function probeCodexLaunch(input) {
386
390
  const platform = input.platform ?? process.platform;
387
391
  const env = input.env ?? process.env;
388
392
  const spawnSyncImpl = input.spawnSyncImpl ?? spawnSync;
389
- const providerExecutionTimeoutMs = normalizeProviderExecutionTimeoutMs(input.providerExecutionTimeoutMs);
390
393
  const candidates = buildCandidates({
391
394
  availability,
392
395
  env,
@@ -439,115 +442,107 @@ export function probeCodexLaunch(input) {
439
442
  });
440
443
  continue;
441
444
  }
442
- const transports = profile.promptTransports?.length ? profile.promptTransports : [profile.promptTransport];
443
- const resolutions = resolveCodexAutonomyCandidates(profile);
444
- let candidateSummary = "No advertised Codex invocation strategy proved writable execution.";
445
- for (const resolution of resolutions) {
446
- for (const transport of transports) {
447
- const marker = `.martin-codex-write-probe-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.tmp`;
448
- const markerPath = join(input.workingDirectory, marker);
449
- // Codex workspace-write intentionally permits the host temp directory,
450
- // and the repo can be nested inside a broader writable host workspace.
451
- // Probe from the user-home boundary so the denial canary is outside both.
452
- const outsideRoot = mkdtempSync(join(userInfo().homedir, ".martin-codex-outside-probe-"));
453
- const outsideMarkerPath = join(outsideRoot, ".martin-codex-outside-probe.tmp");
454
- const prompt = buildMarkerPrompt(marker, outsideMarkerPath);
455
- let args;
456
- try {
457
- args = buildCodexExecArgs({
458
- command: candidate.path,
459
- workingDirectory: input.workingDirectory,
460
- sandbox: "workspace-write",
461
- ...(input.model ? { model: input.model } : {}),
462
- mode: "probe",
463
- prompt,
464
- capabilityProfile: profile,
465
- promptTransport: transport,
466
- writeStrategy: resolution.strategy,
467
- autonomyResolution: resolution
468
- });
469
- }
470
- catch (error) {
471
- rmSync(outsideRoot, { recursive: true, force: true });
472
- candidateSummary = error instanceof Error ? error.message : String(error);
473
- lastFailure = { candidate, profile, args: [], summary: candidateSummary };
474
- continue;
475
- }
476
- const plan = input.spawnSyncImpl
477
- ? buildInjectedSpawnPlan(candidate.path, args, platform)
478
- : { ...createSpawnPlan(candidate.path, args, input.workingDirectory, false), invocationMode: "direct" };
479
- const stdin = buildCodexStdin(profile, prompt, transport);
480
- const result = spawnSyncImpl(plan.command, plan.args, {
481
- cwd: input.workingDirectory,
482
- encoding: "utf8",
483
- stdio: [stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"],
484
- ...(stdin !== undefined ? { input: stdin } : {}),
485
- timeout: providerExecutionTimeoutMs
486
- });
487
- let markerVerified = false;
488
- let outsideDenied = false;
489
- try {
490
- markerVerified = existsSync(markerPath) && readFileSync(markerPath, "utf8") === "MARTIN_CODEX_WRITE_OK";
491
- outsideDenied = !existsSync(outsideMarkerPath);
492
- }
493
- finally {
494
- try {
495
- unlinkSync(markerPath);
496
- }
497
- catch {
498
- // best effort cleanup
499
- }
500
- rmSync(outsideRoot, { recursive: true, force: true });
501
- }
502
- if (!result.error && result.status === 0 && markerVerified && outsideDenied) {
503
- const negotiated = cacheCodexCapabilityProfile({ ...profile, promptTransport: transport }, platform);
504
- selected = {
505
- candidate: {
506
- ...candidate,
507
- diagnosis: { ...candidate.diagnosis, invocationMode: plan.invocationMode }
508
- },
509
- profile: negotiated,
510
- args,
511
- resolution,
512
- transport,
513
- ...(result.status === null ? {} : { exitCode: result.status }),
514
- stdout: result.stdout ?? "",
515
- stderr: result.stderr ?? ""
516
- };
517
- candidateResults.push({
518
- path: candidate.path,
519
- installKind: candidate.diagnosis.installKind,
520
- invocationMode: plan.invocationMode,
521
- nativeInstallValid: true,
522
- sandboxCompatible: true,
523
- launchReady: true,
524
- summary: `Codex capability negotiation passed with ${resolution.strategy} / ${transport}.`,
525
- capabilityProfile: negotiated,
526
- writeStrategy: resolution.strategy,
527
- autonomyResolution: resolution,
528
- promptTransport: transport
529
- });
530
- break candidateLoop;
531
- }
532
- const failureDiagnosis = classifyFailure(result.stderr ?? "", result.stdout ?? "", candidate.diagnosis);
533
- candidateSummary = result.error
534
- ? `Codex launch probe failed: ${result.error.message}`
535
- : result.status !== 0
536
- ? `Codex launch probe exited non-zero: ${(result.stderr ?? result.stdout ?? "").trim() || String(result.status)}`
537
- : !outsideDenied
538
- ? `Codex ${resolution.strategy} / ${transport} escaped the workspace boundary.`
539
- : `Codex ${resolution.strategy} / ${transport} invocation did not prove workspace writes.`;
540
- lastFailure = {
541
- candidate: { ...candidate, diagnosis: failureDiagnosis },
542
- profile,
543
- args,
544
- summary: candidateSummary,
545
- ...(result.status === null ? {} : { exitCode: result.status }),
546
- stdout: result.stdout ?? "",
547
- stderr: result.stderr ?? ""
548
- };
445
+ const transport = profile.promptTransports?.[0] ?? profile.promptTransport;
446
+ const resolution = resolveCodexAutonomyCandidates(profile)[0];
447
+ let candidateSummary = "No advertised Codex invocation strategy supports governed workspace writes.";
448
+ if (!resolution) {
449
+ lastFailure = { candidate, profile, args: [], summary: candidateSummary };
450
+ candidateResults.push({
451
+ path: candidate.path,
452
+ installKind: candidate.diagnosis.installKind,
453
+ invocationMode: candidate.diagnosis.invocationMode,
454
+ nativeInstallValid: candidate.diagnosis.nativeInstallValid,
455
+ sandboxCompatible: false,
456
+ launchReady: false,
457
+ summary: candidateSummary,
458
+ capabilityProfile: profile,
459
+ ...(candidate.diagnosis.remediation ? { remediation: candidate.diagnosis.remediation } : {})
460
+ });
461
+ continue;
462
+ }
463
+ const marker = `.martin-codex-write-probe-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.tmp`;
464
+ const markerPath = join(input.workingDirectory, marker);
465
+ // The outside canary is created under the user-home boundary because Codex
466
+ // intentionally grants temp writes and the repository may be nested inside
467
+ // a broader writable host workspace.
468
+ const outsideRoot = mkdtempSync(join(userInfo().homedir, ".martin-codex-outside-probe-"));
469
+ const outsideMarkerPath = join(outsideRoot, ".martin-codex-outside-probe.tmp");
470
+ const args = buildLocalSandboxProbeArgs({
471
+ workingDirectory: input.workingDirectory,
472
+ markerPath,
473
+ outsideMarkerPath
474
+ });
475
+ const plan = input.spawnSyncImpl
476
+ ? buildInjectedSpawnPlan(candidate.path, args, platform)
477
+ : { ...createSpawnPlan(candidate.path, args, input.workingDirectory, false), invocationMode: "direct" };
478
+ const result = spawnSyncImpl(plan.command, plan.args, {
479
+ cwd: input.workingDirectory,
480
+ encoding: "utf8",
481
+ stdio: ["ignore", "pipe", "pipe"],
482
+ timeout: 300_000
483
+ });
484
+ let markerVerified = false;
485
+ let outsideDenied = false;
486
+ try {
487
+ markerVerified = existsSync(markerPath) && readFileSync(markerPath, "utf8") === "MARTIN_CODEX_WRITE_OK";
488
+ outsideDenied = !existsSync(outsideMarkerPath);
489
+ }
490
+ finally {
491
+ try {
492
+ unlinkSync(markerPath);
549
493
  }
494
+ catch {
495
+ // best effort cleanup
496
+ }
497
+ rmSync(outsideRoot, { recursive: true, force: true });
498
+ }
499
+ if (!result.error && result.status === 0 && markerVerified && outsideDenied) {
500
+ const negotiated = cacheCodexCapabilityProfile({ ...profile, promptTransport: transport }, platform);
501
+ selected = {
502
+ candidate: {
503
+ ...candidate,
504
+ diagnosis: { ...candidate.diagnosis, invocationMode: plan.invocationMode }
505
+ },
506
+ profile: negotiated,
507
+ args,
508
+ resolution,
509
+ transport,
510
+ ...(result.status === null ? {} : { exitCode: result.status }),
511
+ stdout: result.stdout ?? "",
512
+ stderr: result.stderr ?? ""
513
+ };
514
+ candidateResults.push({
515
+ path: candidate.path,
516
+ installKind: candidate.diagnosis.installKind,
517
+ invocationMode: plan.invocationMode,
518
+ nativeInstallValid: true,
519
+ sandboxCompatible: true,
520
+ launchReady: true,
521
+ summary: `Codex local sandbox capability probe passed with ${resolution.strategy} / ${transport}.`,
522
+ capabilityProfile: negotiated,
523
+ writeStrategy: resolution.strategy,
524
+ autonomyResolution: resolution,
525
+ promptTransport: transport
526
+ });
527
+ break candidateLoop;
550
528
  }
529
+ const failureDiagnosis = classifyFailure(result.stderr ?? "", result.stdout ?? "", candidate.diagnosis);
530
+ candidateSummary = result.error
531
+ ? `Codex local sandbox probe failed: ${result.error.message}`
532
+ : result.status !== 0
533
+ ? `Codex local sandbox probe exited non-zero: ${(result.stderr ?? result.stdout ?? "").trim() || String(result.status)}`
534
+ : !outsideDenied
535
+ ? "Codex local sandbox probe escaped the workspace boundary."
536
+ : "Codex local sandbox probe did not prove workspace writes.";
537
+ lastFailure = {
538
+ candidate: { ...candidate, diagnosis: failureDiagnosis },
539
+ profile,
540
+ args,
541
+ summary: candidateSummary,
542
+ ...(result.status === null ? {} : { exitCode: result.status }),
543
+ stdout: result.stdout ?? "",
544
+ stderr: result.stderr ?? ""
545
+ };
551
546
  candidateResults.push({
552
547
  path: candidate.path,
553
548
  installKind: candidate.diagnosis.installKind,
@@ -563,7 +558,7 @@ export function probeCodexLaunch(input) {
563
558
  if (selected) {
564
559
  const result = {
565
560
  ok: true,
566
- summary: `Codex capability-driven workspace-write probe passed using ${selected.resolution.strategy} / ${selected.transport}.`,
561
+ summary: `Codex capability-driven local sandbox probe passed using ${selected.resolution.strategy} / ${selected.transport}.`,
567
562
  availability: { ...availability, resolvedPath: selected.candidate.path, candidatePaths },
568
563
  diagnosis: { ...selected.candidate.diagnosis, resolvedPath: selected.candidate.path },
569
564
  command: selected.candidate.path,
@@ -2,8 +2,9 @@ export { createDirectProviderAdapter, type DirectProviderAdapterOptions } from "
2
2
  export { createAgentCliAdapter, createClaudeCliAdapter, createGeminiCliAdapter, type AgentCliAdapterOptions, type ClaudeCliAdapterOptions, type GeminiCliAdapterOptions, type CliArgsBuilder } from "./claude-cli.js";
3
3
  export { createCodexCliAdapter, type CodexCliAdapterOptions } from "./codex-cli.js";
4
4
  export { createOpenAiCompatibleAdapter, resolveOpenAiCompatibleRuntimeConfig, type OpenAiCompatibleAdapterOptions } from "./openai-compatible.js";
5
- export { createVerifierOnlyAdapter, type VerifierOnlyAdapterOptions } from "./verifier-only.js";
5
+ export { createSwarmVerifierExecutor, createVerifierOnlyAdapter, type SwarmVerifierExecutorOptions, type VerifierOnlyAdapterOptions } from "./verifier-only.js";
6
6
  /** @internal Test-only compatibility export. Public package builders strip this surface. */
7
7
  export { createStubAgentCliAdapter, type StubAgentCliAdapterOptions, createStubDirectProviderAdapter, type StubDirectProviderAdapterOptions } from "./stub-agent-cli.js";
8
8
  export { buildCodexExecArgs, buildCodexStdin, clearCodexCapabilityCacheForTests, detectCodexHostPlatform, diagnoseCodexHost, probeCodexCapabilities, probeCodexLaunch, resolveCliCommandAvailability, type CliCommandAvailability, type CodexApprovalCapability, type CodexAutonomyResolution, type CodexCapabilityFlag, type CodexCapabilityProfile, type CodexFlagScope, type CodexHostDiagnosis, type CodexHostPlatform, type CodexLaunchProbeResult, type CodexPromptTransport, type CodexSandboxCapability, checkCodexSandboxPreflight, probeFilesystemWriteCapability, type CodexSandboxPreflightOk, type CodexSandboxPreflightOutcome, type CodexSandboxPreflightReadOnly } from "./codex-launcher.js";
9
9
  export { createSpawnPlan, type SpawnLike, type SpawnPlan, type SubprocessResult, type VerificationOutcome } from "./cli-bridge.js";
10
+ export { spawnSupervisedProcess, type ProcessSpawnLike, type ProcessTreeCleanupFailureCode, type ProcessTreeCleanupResult, type SpawnSupervisedProcessOptions, type SupervisedProcessResult } from "./process-supervisor.js";
@@ -2,8 +2,9 @@ export { createDirectProviderAdapter } from "./direct-provider.js";
2
2
  export { createAgentCliAdapter, createClaudeCliAdapter, createGeminiCliAdapter } from "./claude-cli.js";
3
3
  export { createCodexCliAdapter } from "./codex-cli.js";
4
4
  export { createOpenAiCompatibleAdapter, resolveOpenAiCompatibleRuntimeConfig } from "./openai-compatible.js";
5
- export { createVerifierOnlyAdapter } from "./verifier-only.js";
5
+ export { createSwarmVerifierExecutor, createVerifierOnlyAdapter } from "./verifier-only.js";
6
6
  /** @internal Test-only compatibility export. Public package builders strip this surface. */
7
7
  export { createStubAgentCliAdapter, createStubDirectProviderAdapter } from "./stub-agent-cli.js";
8
8
  export { buildCodexExecArgs, buildCodexStdin, clearCodexCapabilityCacheForTests, detectCodexHostPlatform, diagnoseCodexHost, probeCodexCapabilities, probeCodexLaunch, resolveCliCommandAvailability, checkCodexSandboxPreflight, probeFilesystemWriteCapability } from "./codex-launcher.js";
9
9
  export { createSpawnPlan } from "./cli-bridge.js";
10
+ export { spawnSupervisedProcess } from "./process-supervisor.js";
@@ -134,6 +134,9 @@ function buildPrompt(request, workspaceSnapshot = "") {
134
134
  if (request.context.focus) {
135
135
  lines.push(`FOCUS: ${request.context.focus}`, ``);
136
136
  }
137
+ if (request.context.mutationMode === "read_only") {
138
+ lines.push("READ-ONLY EXECUTION. Do not propose file edits or deletions.", "Inspect the supplied workspace context and report factual findings only.", "");
139
+ }
137
140
  if ((request.context.acceptanceCriteria?.length ?? 0) > 0) {
138
141
  lines.push(`ACCEPTANCE CRITERIA:`, ...(request.context.acceptanceCriteria ?? []).map((criterion) => ` - ${criterion}`), ``);
139
142
  }
@@ -154,10 +157,46 @@ function buildPrompt(request, workspaceSnapshot = "") {
154
157
  }
155
158
  lines.push(`BUDGET REMAINING: $${request.context.remainingBudgetUsd.toFixed(4)} | Iterations left: ${request.context.remainingIterations}`);
156
159
  if (workspaceSnapshot) {
157
- lines.push(``, `WORKSPACE SNAPSHOT (read-only context; paths are repository-relative):`, workspaceSnapshot, ``, `RESPONSE CONTRACT:`, `Return ONLY JSON with this shape:`, `{"summary":"short description","edits":[{"path":"src/file.ts","content":"complete replacement file content"}],"deletions":[]}`, `Use only repository-relative paths. Every proposed path is validated by MartinLoop before any file is written.`, `If a file should not change, omit it. Do not wrap the JSON in explanatory prose.`);
160
+ if (request.context.mutationMode === "read_only") {
161
+ lines.push(``, `WORKSPACE SNAPSHOT (read-only context; paths are repository-relative):`, workspaceSnapshot, ``, `RESPONSE CONTRACT:`, `Return a concise factual inspection summary. Do not propose edits, deletions, patches, or write commands.`);
162
+ }
163
+ else {
164
+ lines.push(``, `WORKSPACE SNAPSHOT (read-only context; paths are repository-relative):`, workspaceSnapshot, ``, `RESPONSE CONTRACT:`, `Return ONLY JSON with this shape:`, `{"summary":"short description","edits":[{"path":"src/file.ts","content":"complete replacement file content"}],"deletions":[]}`, `Use only repository-relative paths. Every proposed path is validated by MartinLoop before any file is written.`, `If a file should not change, omit it. Do not wrap the JSON in explanatory prose.`);
165
+ }
158
166
  }
159
167
  return lines.join("\n");
160
168
  }
169
+ function abortError(message) {
170
+ const error = new Error(message);
171
+ error.name = "AbortError";
172
+ return error;
173
+ }
174
+ function waitForRetryDelay(delayMs, signal) {
175
+ if (signal?.aborted) {
176
+ return Promise.reject(abortError("OpenAI-compatible retry cancelled by parent."));
177
+ }
178
+ return new Promise((resolve, reject) => {
179
+ let settled = false;
180
+ const onAbort = () => {
181
+ if (settled)
182
+ return;
183
+ settled = true;
184
+ clearTimeout(timer);
185
+ signal?.removeEventListener("abort", onAbort);
186
+ reject(abortError("OpenAI-compatible retry cancelled by parent."));
187
+ };
188
+ const timer = setTimeout(() => {
189
+ if (settled)
190
+ return;
191
+ settled = true;
192
+ signal?.removeEventListener("abort", onAbort);
193
+ resolve();
194
+ }, delayMs);
195
+ signal?.addEventListener("abort", onAbort, { once: true });
196
+ if (signal?.aborted)
197
+ onAbort();
198
+ });
199
+ }
161
200
  // ---------------------------------------------------------------------------
162
201
  // Factory
163
202
  // ---------------------------------------------------------------------------
@@ -197,6 +236,7 @@ export function createOpenAiCompatibleAdapter(options) {
197
236
  async execute(request) {
198
237
  const hasVerificationSteps = request.context.verificationPlan.length > 0 ||
199
238
  (request.context.verificationStack?.length ?? 0) > 0;
239
+ const mutationMode = request.context.mutationMode ?? "edit";
200
240
  const governedCodingRun = request.context.mutationMode === "edit" ||
201
241
  hasVerificationSteps ||
202
242
  (request.context.allowedPaths?.length ?? 0) > 0 ||
@@ -213,6 +253,15 @@ export function createOpenAiCompatibleAdapter(options) {
213
253
  const baselineChangedFiles = hasVerificationSteps
214
254
  ? new Set(await readGitChangedFiles(workingDirectory, 5_000))
215
255
  : new Set();
256
+ const cancelledResult = () => ({
257
+ status: "failed",
258
+ summary: `${model} request cancelled by parent.`,
259
+ usage: normalizeUsage({ actualUsd: 0, tokensIn: 0, tokensOut: 0, provenance: "unavailable" }),
260
+ verification: { passed: false, summary: "Request cancelled before verifier." },
261
+ failure: { message: "parent_cancelled", classHint: "infrastructure_error" }
262
+ });
263
+ if (request.signal?.aborted)
264
+ return cancelledResult();
216
265
  // Preflight: bail if projected cost exceeds remaining budget
217
266
  if (request.context.remainingBudgetUsd > 0 &&
218
267
  estimated.actualUsd > request.context.remainingBudgetUsd * 0.95) {
@@ -262,7 +311,19 @@ export function createOpenAiCompatibleAdapter(options) {
262
311
  let succeeded = false;
263
312
  for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
264
313
  const controller = new AbortController();
265
- const timer = setTimeout(() => controller.abort(), timeoutMs);
314
+ let timedOut = false;
315
+ const abortFromParent = () => controller.abort(request.signal?.reason);
316
+ if (request.signal?.aborted)
317
+ abortFromParent();
318
+ else if (request.signal) {
319
+ request.signal.addEventListener("abort", abortFromParent, { once: true });
320
+ if (request.signal.aborted)
321
+ abortFromParent();
322
+ }
323
+ const timer = setTimeout(() => {
324
+ timedOut = true;
325
+ controller.abort(abortError(`${model} request timed out after ${timeoutMs}ms`));
326
+ }, timeoutMs);
266
327
  try {
267
328
  const res = await fetchFn(endpoint, {
268
329
  method: "POST",
@@ -287,7 +348,14 @@ export function createOpenAiCompatibleAdapter(options) {
287
348
  if (RETRYABLE_STATUS.has(res.status) && attempt < MAX_RETRIES - 1) {
288
349
  lastError = errMsg;
289
350
  // Exponential backoff: 1s, 2s, 4s
290
- await new Promise((resolve) => setTimeout(resolve, 1000 * Math.pow(2, attempt)));
351
+ try {
352
+ await waitForRetryDelay(1000 * Math.pow(2, attempt), request.signal);
353
+ }
354
+ catch (error) {
355
+ if (error instanceof Error && error.name === "AbortError")
356
+ return cancelledResult();
357
+ throw error;
358
+ }
291
359
  continue;
292
360
  }
293
361
  return {
@@ -316,6 +384,8 @@ export function createOpenAiCompatibleAdapter(options) {
316
384
  }
317
385
  catch (error) {
318
386
  const isAbort = error instanceof Error && error.name === "AbortError";
387
+ if (request.signal?.aborted && !timedOut)
388
+ return cancelledResult();
319
389
  if (isAbort || attempt === MAX_RETRIES - 1) {
320
390
  const message = isAbort
321
391
  ? `${model} request timed out after ${timeoutMs}ms`
@@ -330,10 +400,18 @@ export function createOpenAiCompatibleAdapter(options) {
330
400
  }
331
401
  // Transient network error — retry with backoff
332
402
  lastError = String(error);
333
- await new Promise((resolve) => setTimeout(resolve, 1000 * Math.pow(2, attempt)));
403
+ try {
404
+ await waitForRetryDelay(1000 * Math.pow(2, attempt), request.signal);
405
+ }
406
+ catch (delayError) {
407
+ if (delayError instanceof Error && delayError.name === "AbortError")
408
+ return cancelledResult();
409
+ throw delayError;
410
+ }
334
411
  }
335
412
  finally {
336
413
  clearTimeout(timer);
414
+ request.signal?.removeEventListener("abort", abortFromParent);
337
415
  }
338
416
  }
339
417
  if (!succeeded) {
@@ -361,7 +439,7 @@ export function createOpenAiCompatibleAdapter(options) {
361
439
  failure: { message: "empty_response" }
362
440
  };
363
441
  }
364
- if (governedCodingRun) {
442
+ if (governedCodingRun && mutationMode === "edit") {
365
443
  try {
366
444
  await applyWorkspaceEdits({
367
445
  workingDirectory,
@@ -392,8 +470,12 @@ export function createOpenAiCompatibleAdapter(options) {
392
470
  const verification = await runVerification(request.context.verificationPlan, workingDirectory, verifyTimeoutMs, request.context.verificationStack, undefined, {
393
471
  runId: request.loopId,
394
472
  workspaceId: request.workspaceId,
473
+ attemptId: request.attemptId,
395
474
  cwd: workingDirectory,
396
- });
475
+ ...(request.context.runsRoot ? { runsRoot: request.context.runsRoot } : {}),
476
+ ...(request.context.executionProfile ? { executionProfile: request.context.executionProfile } : {}),
477
+ ...(request.context.allowedNetworkDomains?.length ? { allowedNetworkDomains: [...request.context.allowedNetworkDomains] } : {}),
478
+ }, request.signal);
397
479
  const execution = {
398
480
  changedFiles: hasVerificationSteps
399
481
  ? (await readGitChangedFiles(workingDirectory, 5_000)).filter((file) => !baselineChangedFiles.has(file))
@@ -0,0 +1,60 @@
1
+ import { type ChildProcess, type SpawnOptions } from "node:child_process";
2
+ export type ProcessSpawnLike = (command: string, args?: readonly string[], options?: SpawnOptions) => ChildProcess;
3
+ export type ProcessTreeCleanupFailureCode = "invalid_owned_pid" | "missing_test_termination_seam" | "termination_failed" | "tree_close_timeout";
4
+ export type ProcessTreeCleanupResult = {
5
+ state: "not_required";
6
+ } | {
7
+ state: "closed";
8
+ } | {
9
+ state: "failed";
10
+ code: ProcessTreeCleanupFailureCode;
11
+ message: string;
12
+ };
13
+ export interface SupervisedProcessResult {
14
+ exitCode: number;
15
+ stdout: string;
16
+ stderr: string;
17
+ timedOut: boolean;
18
+ completed: boolean;
19
+ crashed: boolean;
20
+ outputCapped: boolean;
21
+ terminationReason?: string;
22
+ launched: boolean;
23
+ cleanup: ProcessTreeCleanupResult;
24
+ }
25
+ export interface SpawnSupervisedProcessOptions {
26
+ cwd: string;
27
+ timeoutMs: number;
28
+ env?: NodeJS.ProcessEnv;
29
+ spawnImpl?: ProcessSpawnLike;
30
+ stdinData?: string;
31
+ maxOutputBytes?: number;
32
+ onStdoutChunk?: (chunk: Buffer, terminate: (reason: string) => void) => void;
33
+ signal?: AbortSignal;
34
+ terminationGraceMs?: number;
35
+ /** Require an owned process-tree sweep before natural completion is authoritative. */
36
+ requireTreeClosureOnSuccess?: boolean;
37
+ }
38
+ type ProcessSupervisorTestSeams = {
39
+ platform: "win32";
40
+ taskkillSpawnImpl: ProcessSpawnLike;
41
+ wait: (durationMs: number) => Promise<void>;
42
+ killProcessGroup?: never;
43
+ } | {
44
+ platform: Exclude<NodeJS.Platform, "win32">;
45
+ killProcessGroup: (ownedGroupPid: number, signal: NodeJS.Signals | 0) => void;
46
+ wait: (durationMs: number) => Promise<void>;
47
+ taskkillSpawnImpl?: never;
48
+ };
49
+ export declare function spawnSupervisedProcess(command: string, args: readonly string[], options: SpawnSupervisedProcessOptions): Promise<SupervisedProcessResult>;
50
+ /**
51
+ * Internal deterministic OS seam used only by this package's supervisor tests.
52
+ * It is intentionally absent from the adapters package root so production
53
+ * callers cannot exchange a caller-supplied PID for operating-system kill
54
+ * authority.
55
+ * @internal
56
+ */
57
+ export declare function spawnSupervisedProcessForTest(command: string, args: readonly string[], options: SpawnSupervisedProcessOptions & {
58
+ spawnImpl: ProcessSpawnLike;
59
+ }, seams: ProcessSupervisorTestSeams): Promise<SupervisedProcessResult>;
60
+ export {};