@martinloop/mcp 0.7.1 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/package-version.d.ts +1 -1
- package/dist/package-version.js +1 -1
- package/dist/vendor/adapters/claude-cli.d.ts +16 -1
- package/dist/vendor/adapters/claude-cli.js +286 -71
- package/dist/vendor/adapters/cli-bridge.d.ts +12 -3
- package/dist/vendor/adapters/cli-bridge.js +129 -149
- package/dist/vendor/adapters/codex-capabilities.d.ts +4 -1
- package/dist/vendor/adapters/codex-capabilities.js +36 -4
- package/dist/vendor/adapters/codex-cli.d.ts +10 -3
- package/dist/vendor/adapters/codex-cli.js +21 -24
- package/dist/vendor/adapters/codex-host.js +150 -132
- package/dist/vendor/adapters/index.d.ts +3 -2
- package/dist/vendor/adapters/index.js +3 -2
- package/dist/vendor/adapters/openai-compatible.js +88 -6
- package/dist/vendor/adapters/process-supervisor.d.ts +60 -0
- package/dist/vendor/adapters/process-supervisor.js +541 -0
- package/dist/vendor/adapters/verifier-only.d.ts +10 -1
- package/dist/vendor/adapters/verifier-only.js +43 -1
- package/dist/vendor/contracts/external-outcome.d.ts +102 -0
- package/dist/vendor/contracts/external-outcome.js +226 -0
- package/dist/vendor/contracts/index.d.ts +12 -1
- package/dist/vendor/contracts/index.js +2 -0
- package/dist/vendor/contracts/swarm-hosted.d.ts +204 -0
- package/dist/vendor/contracts/swarm-hosted.js +629 -0
- package/dist/vendor/contracts/swarm.d.ts +449 -0
- package/dist/vendor/contracts/swarm.js +550 -0
- package/dist/vendor/core/external-outcome.d.ts +34 -0
- package/dist/vendor/core/external-outcome.js +515 -0
- package/dist/vendor/core/grounding.js +1 -1
- package/dist/vendor/core/index.d.ts +52 -1
- package/dist/vendor/core/index.js +55 -3
- package/dist/vendor/core/leash.d.ts +1 -0
- package/dist/vendor/core/leash.js +13 -0
- package/dist/vendor/core/persistence/integrity.d.ts +4 -0
- package/dist/vendor/core/persistence/integrity.js +19 -6
- package/dist/vendor/core/persistence/swarm-integrity.d.ts +63 -0
- package/dist/vendor/core/persistence/swarm-integrity.js +309 -0
- package/dist/vendor/core/policy.d.ts +3 -0
- package/dist/vendor/core/policy.js +13 -0
- package/dist/vendor/core/swarm/candidates.d.ts +78 -0
- package/dist/vendor/core/swarm/candidates.js +810 -0
- package/dist/vendor/core/swarm/evidence-projection.d.ts +87 -0
- package/dist/vendor/core/swarm/evidence-projection.js +128 -0
- package/dist/vendor/core/swarm/evidence.d.ts +40 -0
- package/dist/vendor/core/swarm/evidence.js +575 -0
- package/dist/vendor/core/swarm/hosted-export.d.ts +48 -0
- package/dist/vendor/core/swarm/hosted-export.js +362 -0
- package/dist/vendor/core/swarm/hosted-projection.d.ts +5 -0
- package/dist/vendor/core/swarm/hosted-projection.js +258 -0
- package/dist/vendor/core/swarm/index.d.ts +110 -0
- package/dist/vendor/core/swarm/index.js +1060 -0
- package/dist/vendor/core/swarm/integration.d.ts +76 -0
- package/dist/vendor/core/swarm/integration.js +615 -0
- package/dist/vendor/core/swarm/lifecycle.d.ts +72 -0
- package/dist/vendor/core/swarm/lifecycle.js +252 -0
- package/dist/vendor/core/swarm/live-runtime.d.ts +78 -0
- package/dist/vendor/core/swarm/live-runtime.js +1482 -0
- package/dist/vendor/core/swarm/live-store.d.ts +99 -0
- package/dist/vendor/core/swarm/live-store.js +860 -0
- package/dist/vendor/core/swarm/operational-reader.d.ts +17 -0
- package/dist/vendor/core/swarm/operational-reader.js +96 -0
- package/dist/vendor/core/swarm/operations.d.ts +29 -0
- package/dist/vendor/core/swarm/operations.js +128 -0
- package/dist/vendor/core/swarm/parent-receipt.d.ts +13 -0
- package/dist/vendor/core/swarm/parent-receipt.js +489 -0
- package/dist/vendor/core/swarm/receipt-projection.d.ts +26 -0
- package/dist/vendor/core/swarm/receipt-projection.js +120 -0
- package/dist/vendor/core/swarm/scheduler.d.ts +92 -0
- package/dist/vendor/core/swarm/scheduler.js +555 -0
- package/dist/vendor/core/swarm/verification.d.ts +70 -0
- package/dist/vendor/core/swarm/verification.js +295 -0
- package/dist/vendor/core/swarm/workspaces.d.ts +77 -0
- package/dist/vendor/core/swarm/workspaces.js +488 -0
- package/dist/vendor/core/verified-handoff.d.ts +4 -0
- package/dist/vendor/core/verified-handoff.js +4 -0
- package/package.json +1 -1
- 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 {
|
|
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,38 @@ function buildInjectedSpawnPlan(binaryPath, args, platform) {
|
|
|
315
306
|
}
|
|
316
307
|
return { command: binaryPath, args, invocationMode: "direct" };
|
|
317
308
|
}
|
|
318
|
-
function
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
"
|
|
323
|
-
"
|
|
324
|
-
"
|
|
325
|
-
|
|
326
|
-
"
|
|
327
|
-
|
|
328
|
-
"
|
|
329
|
-
"
|
|
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
|
+
const windowsSandboxConfig = input.platform === "win32" &&
|
|
323
|
+
input.capabilityProfile.userConfigIsolation &&
|
|
324
|
+
input.capabilityProfile.sandboxConfig
|
|
325
|
+
? [input.capabilityProfile.sandboxConfig.flag, 'windows.sandbox="elevated"']
|
|
326
|
+
: [];
|
|
327
|
+
return [
|
|
328
|
+
"sandbox",
|
|
329
|
+
...windowsSandboxConfig,
|
|
330
|
+
"--permission-profile",
|
|
331
|
+
":workspace",
|
|
332
|
+
"-C",
|
|
333
|
+
input.workingDirectory,
|
|
334
|
+
"--",
|
|
335
|
+
process.execPath,
|
|
336
|
+
"-e",
|
|
337
|
+
script,
|
|
338
|
+
input.markerPath,
|
|
339
|
+
input.outsideMarkerPath
|
|
340
|
+
];
|
|
331
341
|
}
|
|
332
342
|
function classifyFailure(stderr, stdout, diagnosis) {
|
|
333
343
|
const combined = `${stderr}\n${stdout}`;
|
|
@@ -386,7 +396,6 @@ export function probeCodexLaunch(input) {
|
|
|
386
396
|
const platform = input.platform ?? process.platform;
|
|
387
397
|
const env = input.env ?? process.env;
|
|
388
398
|
const spawnSyncImpl = input.spawnSyncImpl ?? spawnSync;
|
|
389
|
-
const providerExecutionTimeoutMs = normalizeProviderExecutionTimeoutMs(input.providerExecutionTimeoutMs);
|
|
390
399
|
const candidates = buildCandidates({
|
|
391
400
|
availability,
|
|
392
401
|
env,
|
|
@@ -439,115 +448,124 @@ export function probeCodexLaunch(input) {
|
|
|
439
448
|
});
|
|
440
449
|
continue;
|
|
441
450
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
-
};
|
|
451
|
+
if (platform === "win32" && profile.userConfigIsolation && (!profile.config || !profile.sandboxConfig)) {
|
|
452
|
+
const summary = "Resolved Codex binary does not advertise configuration override support for both exec and sandbox; cannot prove isolated native Windows workspace writes.";
|
|
453
|
+
lastFailure = { candidate, profile, args: [], summary };
|
|
454
|
+
candidateResults.push({
|
|
455
|
+
path: candidate.path,
|
|
456
|
+
installKind: candidate.diagnosis.installKind,
|
|
457
|
+
invocationMode: candidate.diagnosis.invocationMode,
|
|
458
|
+
nativeInstallValid: true,
|
|
459
|
+
sandboxCompatible: false,
|
|
460
|
+
launchReady: false,
|
|
461
|
+
summary,
|
|
462
|
+
capabilityProfile: profile
|
|
463
|
+
});
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
const transport = profile.promptTransports?.[0] ?? profile.promptTransport;
|
|
467
|
+
const resolution = resolveCodexAutonomyCandidates(profile)[0];
|
|
468
|
+
let candidateSummary = "No advertised Codex invocation strategy supports governed workspace writes.";
|
|
469
|
+
if (!resolution) {
|
|
470
|
+
lastFailure = { candidate, profile, args: [], summary: candidateSummary };
|
|
471
|
+
candidateResults.push({
|
|
472
|
+
path: candidate.path,
|
|
473
|
+
installKind: candidate.diagnosis.installKind,
|
|
474
|
+
invocationMode: candidate.diagnosis.invocationMode,
|
|
475
|
+
nativeInstallValid: candidate.diagnosis.nativeInstallValid,
|
|
476
|
+
sandboxCompatible: false,
|
|
477
|
+
launchReady: false,
|
|
478
|
+
summary: candidateSummary,
|
|
479
|
+
capabilityProfile: profile,
|
|
480
|
+
...(candidate.diagnosis.remediation ? { remediation: candidate.diagnosis.remediation } : {})
|
|
481
|
+
});
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
const marker = `.martin-codex-write-probe-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.tmp`;
|
|
485
|
+
const markerPath = join(input.workingDirectory, marker);
|
|
486
|
+
// The outside canary is created under the user-home boundary because Codex
|
|
487
|
+
// intentionally grants temp writes and the repository may be nested inside
|
|
488
|
+
// a broader writable host workspace.
|
|
489
|
+
const outsideRoot = mkdtempSync(join(userInfo().homedir, ".martin-codex-outside-probe-"));
|
|
490
|
+
const outsideMarkerPath = join(outsideRoot, ".martin-codex-outside-probe.tmp");
|
|
491
|
+
const args = buildLocalSandboxProbeArgs({
|
|
492
|
+
workingDirectory: input.workingDirectory,
|
|
493
|
+
markerPath,
|
|
494
|
+
outsideMarkerPath,
|
|
495
|
+
platform,
|
|
496
|
+
capabilityProfile: profile
|
|
497
|
+
});
|
|
498
|
+
const plan = input.spawnSyncImpl
|
|
499
|
+
? buildInjectedSpawnPlan(candidate.path, args, platform)
|
|
500
|
+
: { ...createSpawnPlan(candidate.path, args, input.workingDirectory, false), invocationMode: "direct" };
|
|
501
|
+
const result = spawnSyncImpl(plan.command, plan.args, {
|
|
502
|
+
cwd: input.workingDirectory,
|
|
503
|
+
encoding: "utf8",
|
|
504
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
505
|
+
timeout: 300_000
|
|
506
|
+
});
|
|
507
|
+
let markerVerified = false;
|
|
508
|
+
let outsideDenied = false;
|
|
509
|
+
try {
|
|
510
|
+
markerVerified = existsSync(markerPath) && readFileSync(markerPath, "utf8") === "MARTIN_CODEX_WRITE_OK";
|
|
511
|
+
outsideDenied = !existsSync(outsideMarkerPath);
|
|
512
|
+
}
|
|
513
|
+
finally {
|
|
514
|
+
try {
|
|
515
|
+
unlinkSync(markerPath);
|
|
549
516
|
}
|
|
517
|
+
catch {
|
|
518
|
+
// best effort cleanup
|
|
519
|
+
}
|
|
520
|
+
rmSync(outsideRoot, { recursive: true, force: true });
|
|
521
|
+
}
|
|
522
|
+
if (!result.error && result.status === 0 && markerVerified && outsideDenied) {
|
|
523
|
+
const negotiated = cacheCodexCapabilityProfile({ ...profile, promptTransport: transport }, platform);
|
|
524
|
+
selected = {
|
|
525
|
+
candidate: {
|
|
526
|
+
...candidate,
|
|
527
|
+
diagnosis: { ...candidate.diagnosis, invocationMode: plan.invocationMode }
|
|
528
|
+
},
|
|
529
|
+
profile: negotiated,
|
|
530
|
+
args,
|
|
531
|
+
resolution,
|
|
532
|
+
transport,
|
|
533
|
+
...(result.status === null ? {} : { exitCode: result.status }),
|
|
534
|
+
stdout: result.stdout ?? "",
|
|
535
|
+
stderr: result.stderr ?? ""
|
|
536
|
+
};
|
|
537
|
+
candidateResults.push({
|
|
538
|
+
path: candidate.path,
|
|
539
|
+
installKind: candidate.diagnosis.installKind,
|
|
540
|
+
invocationMode: plan.invocationMode,
|
|
541
|
+
nativeInstallValid: true,
|
|
542
|
+
sandboxCompatible: true,
|
|
543
|
+
launchReady: true,
|
|
544
|
+
summary: `Codex local sandbox capability probe passed with ${resolution.strategy} / ${transport}.`,
|
|
545
|
+
capabilityProfile: negotiated,
|
|
546
|
+
writeStrategy: resolution.strategy,
|
|
547
|
+
autonomyResolution: resolution,
|
|
548
|
+
promptTransport: transport
|
|
549
|
+
});
|
|
550
|
+
break candidateLoop;
|
|
550
551
|
}
|
|
552
|
+
const failureDiagnosis = classifyFailure(result.stderr ?? "", result.stdout ?? "", candidate.diagnosis);
|
|
553
|
+
candidateSummary = result.error
|
|
554
|
+
? `Codex local sandbox probe failed: ${result.error.message}`
|
|
555
|
+
: result.status !== 0
|
|
556
|
+
? `Codex local sandbox probe exited non-zero: ${(result.stderr ?? result.stdout ?? "").trim() || String(result.status)}`
|
|
557
|
+
: !outsideDenied
|
|
558
|
+
? "Codex local sandbox probe escaped the workspace boundary."
|
|
559
|
+
: "Codex local sandbox probe did not prove workspace writes.";
|
|
560
|
+
lastFailure = {
|
|
561
|
+
candidate: { ...candidate, diagnosis: failureDiagnosis },
|
|
562
|
+
profile,
|
|
563
|
+
args,
|
|
564
|
+
summary: candidateSummary,
|
|
565
|
+
...(result.status === null ? {} : { exitCode: result.status }),
|
|
566
|
+
stdout: result.stdout ?? "",
|
|
567
|
+
stderr: result.stderr ?? ""
|
|
568
|
+
};
|
|
551
569
|
candidateResults.push({
|
|
552
570
|
path: candidate.path,
|
|
553
571
|
installKind: candidate.diagnosis.installKind,
|
|
@@ -563,7 +581,7 @@ export function probeCodexLaunch(input) {
|
|
|
563
581
|
if (selected) {
|
|
564
582
|
const result = {
|
|
565
583
|
ok: true,
|
|
566
|
-
summary: `Codex capability-driven
|
|
584
|
+
summary: `Codex capability-driven local sandbox probe passed using ${selected.resolution.strategy} / ${selected.transport}.`,
|
|
567
585
|
availability: { ...availability, resolvedPath: selected.candidate.path, candidatePaths },
|
|
568
586
|
diagnosis: { ...selected.candidate.diagnosis, resolvedPath: selected.candidate.path },
|
|
569
587
|
command: selected.candidate.path,
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export { createDirectProviderAdapter, type DirectProviderAdapterOptions } from "./direct-provider.js";
|
|
2
2
|
export { createAgentCliAdapter, createClaudeCliAdapter, createGeminiCliAdapter, type AgentCliAdapterOptions, type ClaudeCliAdapterOptions, type GeminiCliAdapterOptions, type CliArgsBuilder } from "./claude-cli.js";
|
|
3
|
-
export { createCodexCliAdapter, type CodexCliAdapterOptions } from "./codex-cli.js";
|
|
3
|
+
export { createCodexCliAdapter, CODEX_MINIMUM_VIABLE_TOKEN_BUDGET, CODEX_TOKEN_BUDGET_PREFLIGHT_BASIS, 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";
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export { createDirectProviderAdapter } from "./direct-provider.js";
|
|
2
2
|
export { createAgentCliAdapter, createClaudeCliAdapter, createGeminiCliAdapter } from "./claude-cli.js";
|
|
3
|
-
export { createCodexCliAdapter } from "./codex-cli.js";
|
|
3
|
+
export { createCodexCliAdapter, CODEX_MINIMUM_VIABLE_TOKEN_BUDGET, CODEX_TOKEN_BUDGET_PREFLIGHT_BASIS } 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {};
|