@martinloop/mcp 0.8.8 → 0.8.10
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/server-validation.js +18 -0
- package/dist/server.js +10 -1
- package/dist/tools/run-loop.d.ts +2 -0
- package/dist/tools/run-loop.js +169 -71
- package/dist/vendor/adapters/process-supervisor.d.ts +12 -0
- package/dist/vendor/adapters/process-supervisor.js +40 -2
- package/dist/vendor/adapters/verifier-only.js +4 -0
- package/dist/vendor/core/index.d.ts +14 -1
- package/dist/vendor/core/index.js +1679 -1595
- package/dist/vendor/core/policy.js +11 -1
- package/dist/vendor/core/resume.d.ts +39 -0
- package/dist/vendor/core/resume.js +196 -0
- package/dist/vendor/core/verified-handoff.js +62 -2
- package/package.json +3 -7
- package/server.json +2 -2
|
@@ -385,7 +385,10 @@ function roundUsd(value) {
|
|
|
385
385
|
return Math.round(value * 100) / 100;
|
|
386
386
|
}
|
|
387
387
|
export function evaluateBudgetPreflight(input) {
|
|
388
|
-
const
|
|
388
|
+
const declaredPrice = input.pricePerMTokenUsd;
|
|
389
|
+
const validDeclaredPrice = declaredPrice === undefined
|
|
390
|
+
|| (Number.isFinite(declaredPrice) && declaredPrice >= 0);
|
|
391
|
+
const pricePerMToken = validDeclaredPrice ? (declaredPrice ?? 3.0) : 3.0;
|
|
389
392
|
const rawPromptTokens = Math.ceil(input.promptCharCount / 4);
|
|
390
393
|
const estimatedPromptTokens = Math.ceil(rawPromptTokens * 1.2);
|
|
391
394
|
const estimatedToolOverheadTokens = 800 + input.attemptCount * 200;
|
|
@@ -407,6 +410,13 @@ export function evaluateBudgetPreflight(input) {
|
|
|
407
410
|
estimatedAttemptCostUsd,
|
|
408
411
|
provenance
|
|
409
412
|
};
|
|
413
|
+
if (!validDeclaredPrice) {
|
|
414
|
+
return {
|
|
415
|
+
allowed: false,
|
|
416
|
+
reason: "Preflight: invalid adapter price; pricePerMTokenUsd must be finite and non-negative.",
|
|
417
|
+
estimate
|
|
418
|
+
};
|
|
419
|
+
}
|
|
410
420
|
if (input.remainingTokens !== undefined
|
|
411
421
|
&& estimatedMinimumViableTokens > 0
|
|
412
422
|
&& estimatedTotalTokens > input.remainingTokens) {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { LoopRecord, RollbackBoundaryArtifact } from "../contracts/index.js";
|
|
2
|
+
export interface RunExecutionLease {
|
|
3
|
+
schemaVersion: "martin.execution-lease.v1";
|
|
4
|
+
runId: string;
|
|
5
|
+
token: string;
|
|
6
|
+
owner: string;
|
|
7
|
+
pid: number;
|
|
8
|
+
hostname: string;
|
|
9
|
+
acquiredAt: string;
|
|
10
|
+
}
|
|
11
|
+
export interface AcquiredRunExecutionLease {
|
|
12
|
+
lease: RunExecutionLease;
|
|
13
|
+
path: string;
|
|
14
|
+
release(): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
export interface ResumeVerifierFailure {
|
|
17
|
+
command: string;
|
|
18
|
+
exitCode?: number;
|
|
19
|
+
crashed?: boolean;
|
|
20
|
+
timedOut?: boolean;
|
|
21
|
+
failureExcerpt: string;
|
|
22
|
+
attemptId: string;
|
|
23
|
+
attemptIndex: number;
|
|
24
|
+
}
|
|
25
|
+
export interface ResumabilityDecision {
|
|
26
|
+
allowed: boolean;
|
|
27
|
+
reason: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function evaluateRunResumability(loop: LoopRecord): ResumabilityDecision;
|
|
30
|
+
export declare function loadResumeCandidateBoundary(runsRoot: string, loop: LoopRecord): Promise<RollbackBoundaryArtifact | undefined>;
|
|
31
|
+
export declare function loadResumeVerifierFailure(runsRoot: string, loop: LoopRecord): Promise<ResumeVerifierFailure | undefined>;
|
|
32
|
+
export declare function acquireRunExecutionLease(input: {
|
|
33
|
+
runsRoot: string;
|
|
34
|
+
runId: string;
|
|
35
|
+
owner: string;
|
|
36
|
+
now?: () => string;
|
|
37
|
+
host?: string;
|
|
38
|
+
pid?: number;
|
|
39
|
+
}): Promise<AcquiredRunExecutionLease>;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
+
import { mkdir, open, readFile, unlink } from "node:fs/promises";
|
|
4
|
+
import { hostname } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { artifactDir } from "./persistence/index.js";
|
|
7
|
+
export function evaluateRunResumability(loop) {
|
|
8
|
+
if (loop.terminationEnvelope) {
|
|
9
|
+
return { allowed: false, reason: "The run already has authoritative terminal evidence." };
|
|
10
|
+
}
|
|
11
|
+
if (!["queued", "running", "verifying"].includes(loop.status)) {
|
|
12
|
+
return { allowed: false, reason: `Run status ${loop.status} is terminal and cannot be continued.` };
|
|
13
|
+
}
|
|
14
|
+
if (!["created", "running", "verifying"].includes(loop.lifecycleState)) {
|
|
15
|
+
return {
|
|
16
|
+
allowed: false,
|
|
17
|
+
reason: `Run lifecycle ${loop.lifecycleState} is terminal and cannot be continued.`
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
if (loop.task.taskClass === "irreversible" || loop.task.approvalPolicy?.externalWrites === true) {
|
|
21
|
+
return {
|
|
22
|
+
allowed: false,
|
|
23
|
+
reason: "Automatic execution continuation is disabled for irreversible/external-write tasks."
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if (loop.attempts.length >= loop.budget.maxIterations) {
|
|
27
|
+
return { allowed: false, reason: "The run has no remaining iteration authority." };
|
|
28
|
+
}
|
|
29
|
+
if (loop.cost.actualUsd >= loop.budget.maxUsd) {
|
|
30
|
+
return { allowed: false, reason: "The run has no remaining USD budget authority." };
|
|
31
|
+
}
|
|
32
|
+
if (loop.budget.maxTokens !== undefined &&
|
|
33
|
+
loop.cost.tokensIn + loop.cost.tokensOut >= loop.budget.maxTokens) {
|
|
34
|
+
return { allowed: false, reason: "The run has no remaining token budget authority." };
|
|
35
|
+
}
|
|
36
|
+
const started = new Set(loop.events
|
|
37
|
+
.filter((event) => event.type === "attempt.started")
|
|
38
|
+
.map((event) => String(event.payload["attemptId"] ?? ""))
|
|
39
|
+
.filter(Boolean));
|
|
40
|
+
for (const event of loop.events) {
|
|
41
|
+
if (event.type === "attempt.completed") {
|
|
42
|
+
const attemptId = String(event.payload["attemptId"] ?? "");
|
|
43
|
+
if (attemptId)
|
|
44
|
+
started.delete(attemptId);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (started.size > 0) {
|
|
48
|
+
return {
|
|
49
|
+
allowed: false,
|
|
50
|
+
reason: "A persisted attempt is still marked active; provider-process ownership is uncertain."
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return { allowed: true, reason: "Persisted run has remaining safe execution authority." };
|
|
54
|
+
}
|
|
55
|
+
export async function loadResumeCandidateBoundary(runsRoot, loop) {
|
|
56
|
+
const latestAttempt = loop.attempts.at(-1);
|
|
57
|
+
if (latestAttempt?.candidateRetained !== true)
|
|
58
|
+
return undefined;
|
|
59
|
+
// Recover the first rollback boundary in the current contiguous retained-candidate
|
|
60
|
+
// suffix. Earlier retained candidates that were later discarded must not become
|
|
61
|
+
// authority for a new resume.
|
|
62
|
+
let firstRetainedAttempt = latestAttempt;
|
|
63
|
+
for (let index = loop.attempts.length - 2; index >= 0; index -= 1) {
|
|
64
|
+
const candidate = loop.attempts[index];
|
|
65
|
+
if (candidate?.candidateRetained !== true)
|
|
66
|
+
break;
|
|
67
|
+
firstRetainedAttempt = candidate;
|
|
68
|
+
}
|
|
69
|
+
const path = join(artifactDir(runsRoot, loop.loopId, firstRetainedAttempt.index), "rollback-boundary.json");
|
|
70
|
+
const raw = await readFile(path, "utf8").catch((error) => {
|
|
71
|
+
if (error.code === "ENOENT") {
|
|
72
|
+
throw new Error(`Cannot continue ${loop.loopId}: retained candidate boundary evidence is missing for attempt ${firstRetainedAttempt.index}.`);
|
|
73
|
+
}
|
|
74
|
+
throw error;
|
|
75
|
+
});
|
|
76
|
+
return JSON.parse(raw);
|
|
77
|
+
}
|
|
78
|
+
export async function loadResumeVerifierFailure(runsRoot, loop) {
|
|
79
|
+
const attempt = loop.attempts.at(-1);
|
|
80
|
+
if (!attempt || attempt.verifierScore === 1)
|
|
81
|
+
return undefined;
|
|
82
|
+
const path = join(artifactDir(runsRoot, loop.loopId, attempt.index), "verification.json");
|
|
83
|
+
const raw = await readFile(path, "utf8").catch((error) => {
|
|
84
|
+
if (error.code === "ENOENT")
|
|
85
|
+
return undefined;
|
|
86
|
+
throw error;
|
|
87
|
+
});
|
|
88
|
+
if (!raw)
|
|
89
|
+
return undefined;
|
|
90
|
+
const verification = JSON.parse(raw);
|
|
91
|
+
const step = verification.steps?.find((candidate) => candidate.launched === true &&
|
|
92
|
+
(candidate.crashed === true ||
|
|
93
|
+
candidate.timedOut === true ||
|
|
94
|
+
(candidate.exitCode !== undefined && candidate.exitCode !== 0)));
|
|
95
|
+
const excerpt = (step?.detail ??
|
|
96
|
+
[verification.summary, ...(verification.warnings ?? [])].filter(Boolean).join("\n")).trim();
|
|
97
|
+
return {
|
|
98
|
+
command: step?.command ?? loop.task.verificationPlan[0] ?? "unknown",
|
|
99
|
+
...(!step?.crashed && !step?.timedOut && step?.exitCode !== undefined
|
|
100
|
+
? { exitCode: step.exitCode }
|
|
101
|
+
: {}),
|
|
102
|
+
...(step?.crashed ? { crashed: true } : {}),
|
|
103
|
+
...(step?.timedOut ? { timedOut: true } : {}),
|
|
104
|
+
failureExcerpt: excerpt.slice(-1500),
|
|
105
|
+
attemptId: attempt.attemptId,
|
|
106
|
+
attemptIndex: attempt.index
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
export async function acquireRunExecutionLease(input) {
|
|
110
|
+
const now = input.now ?? (() => new Date().toISOString());
|
|
111
|
+
const host = input.host ?? hostname();
|
|
112
|
+
const pid = input.pid ?? process.pid;
|
|
113
|
+
const directory = join(input.runsRoot, ".execution-leases");
|
|
114
|
+
const leaseFile = `${createHash("sha256").update(input.runId).digest("hex")}.json`;
|
|
115
|
+
const path = join(directory, leaseFile);
|
|
116
|
+
await mkdir(directory, { recursive: true });
|
|
117
|
+
const token = `${pid}-${randomBytes(16).toString("hex")}`;
|
|
118
|
+
const lease = {
|
|
119
|
+
schemaVersion: "martin.execution-lease.v1",
|
|
120
|
+
runId: input.runId,
|
|
121
|
+
token,
|
|
122
|
+
owner: input.owner,
|
|
123
|
+
pid,
|
|
124
|
+
hostname: host,
|
|
125
|
+
acquiredAt: now()
|
|
126
|
+
};
|
|
127
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
128
|
+
try {
|
|
129
|
+
const handle = await open(path, "wx", 0o600);
|
|
130
|
+
try {
|
|
131
|
+
await handle.writeFile(`${JSON.stringify(lease, null, 2)}\n`, "utf8");
|
|
132
|
+
await handle.sync();
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
await handle.close();
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
lease,
|
|
139
|
+
path,
|
|
140
|
+
async release() {
|
|
141
|
+
try {
|
|
142
|
+
const existing = JSON.parse(await readFile(path, "utf8"));
|
|
143
|
+
if (existing.token === token)
|
|
144
|
+
await unlink(path);
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
if (error.code !== "ENOENT")
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
if (error.code !== "EEXIST")
|
|
155
|
+
throw error;
|
|
156
|
+
const existing = await readLease(path);
|
|
157
|
+
if (!existing)
|
|
158
|
+
continue;
|
|
159
|
+
if (existing.hostname !== host) {
|
|
160
|
+
throw new Error(`Run ${input.runId} is leased by ${existing.owner} on ${existing.hostname}; refusing cross-host takeover.`);
|
|
161
|
+
}
|
|
162
|
+
if (isProcessAlive(existing.pid)) {
|
|
163
|
+
throw new Error(`Run ${input.runId} is already executing under ${existing.owner} (pid ${existing.pid}).`);
|
|
164
|
+
}
|
|
165
|
+
await unlink(path).catch((unlinkError) => {
|
|
166
|
+
if (unlinkError.code !== "ENOENT")
|
|
167
|
+
throw unlinkError;
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
throw new Error(`Unable to acquire execution lease for run ${input.runId}.`);
|
|
172
|
+
}
|
|
173
|
+
async function readLease(path) {
|
|
174
|
+
try {
|
|
175
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
if (error.code === "ENOENT")
|
|
179
|
+
return undefined;
|
|
180
|
+
throw new Error(`Existing execution lease is unreadable: ${error instanceof Error ? error.message : String(error)}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function isProcessAlive(pid) {
|
|
184
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
185
|
+
return true;
|
|
186
|
+
try {
|
|
187
|
+
process.kill(pid, 0);
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
const code = error.code;
|
|
192
|
+
if (code === "ESRCH")
|
|
193
|
+
return false;
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: MartinLoop contributors
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
+
import { posix, win32 } from "node:path";
|
|
4
5
|
// ---------------------------------------------------------------------------
|
|
5
6
|
// Mapping: granular TestIntegrityStatus → public TestIntegrityVerdict
|
|
6
7
|
//
|
|
@@ -88,6 +89,65 @@ export function deriveVerifiedHandoffLifecycleTruth(loop) {
|
|
|
88
89
|
evidenceAvailable: true,
|
|
89
90
|
};
|
|
90
91
|
}
|
|
92
|
+
function sameOptionalVerifierFilesystemPath(actual, expected) {
|
|
93
|
+
if (actual === undefined || expected === undefined) {
|
|
94
|
+
return actual === expected;
|
|
95
|
+
}
|
|
96
|
+
return sameVerifierFilesystemPath(actual, expected);
|
|
97
|
+
}
|
|
98
|
+
function classifyVerifierFilesystemPath(value) {
|
|
99
|
+
// Persisted Windows bindings are expected to be drive-absolute or UNC.
|
|
100
|
+
// Do not use win32.isAbsolute() here because it also classifies POSIX-style
|
|
101
|
+
// root paths such as "/repo" as Windows root-relative paths.
|
|
102
|
+
if (/^[A-Za-z]:[\\/]/u.test(value) || /^\\\\/u.test(value) || /^\/\//u.test(value)) {
|
|
103
|
+
return "windows";
|
|
104
|
+
}
|
|
105
|
+
if (value.startsWith("/")) {
|
|
106
|
+
return "posix";
|
|
107
|
+
}
|
|
108
|
+
return "relative";
|
|
109
|
+
}
|
|
110
|
+
function stripTrailingVerifierSeparators(value, kind) {
|
|
111
|
+
if (kind === "windows") {
|
|
112
|
+
// Preserve drive roots (C:\\) and UNC share roots while removing harmless
|
|
113
|
+
// trailing separators from ordinary persisted paths.
|
|
114
|
+
if (/^[a-z]:\\$/iu.test(value) || /^\\\\[^\\]+\\[^\\]+\\?$/u.test(value)) {
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
return value.replace(/[\\/]+$/u, "");
|
|
118
|
+
}
|
|
119
|
+
if (kind === "posix") {
|
|
120
|
+
return value === "/" ? value : value.replace(/\/+$/u, "");
|
|
121
|
+
}
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
function normalizeVerifierFilesystemPath(value) {
|
|
125
|
+
const kind = classifyVerifierFilesystemPath(value);
|
|
126
|
+
if (kind === "windows") {
|
|
127
|
+
return {
|
|
128
|
+
kind,
|
|
129
|
+
value: stripTrailingVerifierSeparators(win32.normalize(value), kind).toLowerCase(),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (kind === "posix") {
|
|
133
|
+
return {
|
|
134
|
+
kind,
|
|
135
|
+
value: stripTrailingVerifierSeparators(posix.normalize(value), kind),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
// Relative verifier bindings are legacy/non-canonical. Normalize only their
|
|
139
|
+
// separators/dot segments and keep comparison case-sensitive/fail-closed.
|
|
140
|
+
return {
|
|
141
|
+
kind,
|
|
142
|
+
value: stripTrailingVerifierSeparators(posix.normalize(value.replace(/\\/gu, "/")), kind),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function sameVerifierFilesystemPath(actual, expected) {
|
|
146
|
+
const normalizedActual = normalizeVerifierFilesystemPath(actual);
|
|
147
|
+
const normalizedExpected = normalizeVerifierFilesystemPath(expected);
|
|
148
|
+
return (normalizedActual.kind === normalizedExpected.kind &&
|
|
149
|
+
normalizedActual.value === normalizedExpected.value);
|
|
150
|
+
}
|
|
91
151
|
export function verifierActuallyPassed(evidence, expected) {
|
|
92
152
|
if (!evidence?.passed || !evidence.binding || !evidence.steps) {
|
|
93
153
|
return false;
|
|
@@ -98,8 +158,8 @@ export function verifierActuallyPassed(evidence, expected) {
|
|
|
98
158
|
if (evidence.binding.runId !== expected.runId ||
|
|
99
159
|
evidence.binding.workspaceId !== expected.workspaceId ||
|
|
100
160
|
(evidence.binding.attemptId ?? undefined) !== (expected.attemptId ?? undefined) ||
|
|
101
|
-
evidence.binding.cwd
|
|
102
|
-
(evidence.binding.runsRoot
|
|
161
|
+
!sameVerifierFilesystemPath(evidence.binding.cwd, expected.cwd) ||
|
|
162
|
+
!sameOptionalVerifierFilesystemPath(evidence.binding.runsRoot, expected.runsRoot) ||
|
|
103
163
|
(evidence.binding.executionProfile ?? undefined) !== (expected.executionProfile ?? undefined) ||
|
|
104
164
|
JSON.stringify(evidence.binding.allowedNetworkDomains ?? []) !== JSON.stringify(expected.allowedNetworkDomains ?? []) ||
|
|
105
165
|
JSON.stringify(evidence.binding.commands) !== JSON.stringify(expected.commands)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@martinloop/mcp",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.10",
|
|
4
4
|
"mcpName": "io.github.Keesan12/martin-loop",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -74,13 +74,9 @@
|
|
|
74
74
|
"mcpb:smoke": "node ./mcpb/smoke-mcpb.mjs"
|
|
75
75
|
},
|
|
76
76
|
"dependencies": {
|
|
77
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
77
|
+
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
78
78
|
"@open-policy-agent/opa-wasm": "^1.10.0",
|
|
79
|
-
"
|
|
80
|
-
"@opentelemetry/exporter-logs-otlp-http": "^0.214.0",
|
|
81
|
-
"@opentelemetry/resources": "^2.6.1",
|
|
82
|
-
"@opentelemetry/sdk-logs": "^0.214.0",
|
|
83
|
-
"ts-morph": "^21.0.0"
|
|
79
|
+
"zod": "^4.6.7"
|
|
84
80
|
},
|
|
85
81
|
"devDependencies": {
|
|
86
82
|
"@anthropic-ai/mcpb": "2.1.2",
|
package/server.json
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"url": "https://github.com/Keesan12/martin-loop",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "0.8.
|
|
10
|
+
"version": "0.8.10",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "@martinloop/mcp",
|
|
15
|
-
"version": "0.8.
|
|
15
|
+
"version": "0.8.10",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
}
|