@martinloop/mcp 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/package-version.d.ts +1 -1
- package/dist/package-version.js +1 -1
- package/dist/server.js +69 -7
- package/dist/tools/doctor.d.ts +27 -0
- package/dist/tools/doctor.js +39 -11
- package/dist/tools/get-run.d.ts +2 -1
- package/dist/tools/get-run.js +1 -0
- package/dist/tools/get-verification-results.d.ts +2 -1
- package/dist/tools/get-verification-results.js +1 -0
- package/dist/tools/plan.js +4 -2
- package/dist/tools/preflight.d.ts +27 -0
- package/dist/tools/preflight.js +44 -20
- package/dist/tools/run-dossier.d.ts +2 -1
- package/dist/tools/run-dossier.js +1 -0
- package/dist/tools/run-loop.d.ts +5 -1
- package/dist/tools/run-loop.js +20 -8
- package/dist/tools/run-store.js +67 -15
- package/dist/tools/tool-support.d.ts +2 -0
- package/dist/tools/tool-support.js +49 -13
- package/dist/tools/workflow-governance.d.ts +19 -3
- package/dist/tools/workflow-governance.js +107 -55
- package/dist/vendor/adapters/claude-cli.d.ts +20 -3
- package/dist/vendor/adapters/claude-cli.js +193 -33
- package/dist/vendor/adapters/cli-bridge.d.ts +45 -0
- package/dist/vendor/adapters/cli-bridge.js +107 -39
- package/dist/vendor/adapters/codex-launcher.d.ts +32 -0
- package/dist/vendor/adapters/codex-launcher.js +409 -118
- package/dist/vendor/adapters/openai-compatible.js +8 -2
- package/dist/vendor/adapters/runtime-support.js +1 -0
- package/dist/vendor/adapters/stub-direct-provider.js +3 -0
- package/dist/vendor/adapters/verifier-only.d.ts +2 -0
- package/dist/vendor/adapters/verifier-only.js +9 -3
- package/dist/vendor/core/context-integrity.js +28 -3
- package/dist/vendor/core/grounding.d.ts +1 -0
- package/dist/vendor/core/grounding.js +6 -2
- package/dist/vendor/core/index.d.ts +1 -0
- package/dist/vendor/core/index.js +25 -6
- package/dist/vendor/core/leash.js +85 -8
- package/dist/vendor/core/persistence/integrity.d.ts +1 -1
- package/dist/vendor/core/persistence/integrity.js +15 -6
- package/dist/workflow-state.d.ts +9 -0
- package/dist/workflow-state.js +46 -3
- package/package.json +2 -2
- package/server.json +2 -2
|
@@ -144,7 +144,11 @@ export function createOpenAiCompatibleAdapter(options) {
|
|
|
144
144
|
async execute(request) {
|
|
145
145
|
const prompt = buildPrompt(request);
|
|
146
146
|
const estimated = estimateCost(model, prompt.length, 2000);
|
|
147
|
-
const
|
|
147
|
+
const hasVerificationSteps = request.context.verificationPlan.length > 0 ||
|
|
148
|
+
(request.context.verificationStack?.length ?? 0) > 0;
|
|
149
|
+
const baselineChangedFiles = hasVerificationSteps
|
|
150
|
+
? new Set(await readGitChangedFiles(workingDirectory, 5_000))
|
|
151
|
+
: new Set();
|
|
148
152
|
// Preflight: bail if projected cost exceeds remaining budget
|
|
149
153
|
if (request.context.remainingBudgetUsd > 0 &&
|
|
150
154
|
estimated.actualUsd > request.context.remainingBudgetUsd * 0.95) {
|
|
@@ -238,7 +242,9 @@ export function createOpenAiCompatibleAdapter(options) {
|
|
|
238
242
|
// Run verification
|
|
239
243
|
const verification = await runVerification(request.context.verificationPlan, workingDirectory, verifyTimeoutMs, request.context.verificationStack);
|
|
240
244
|
const execution = {
|
|
241
|
-
changedFiles:
|
|
245
|
+
changedFiles: hasVerificationSteps
|
|
246
|
+
? (await readGitChangedFiles(workingDirectory, 5_000)).filter((file) => !baselineChangedFiles.has(file))
|
|
247
|
+
: []
|
|
242
248
|
};
|
|
243
249
|
const pricing = KNOWN_MODEL_PRICING[model] ?? {
|
|
244
250
|
inputPer1K: FALLBACK_INPUT_PER_1K,
|
|
@@ -4,6 +4,9 @@ export function createStubDirectProviderAdapter(options) {
|
|
|
4
4
|
providerId: options.providerId,
|
|
5
5
|
model: options.model,
|
|
6
6
|
label: options.label ?? `Stub direct provider (${options.providerId}/${options.model})`,
|
|
7
|
+
capabilities: {
|
|
8
|
+
workspaceMutations: false
|
|
9
|
+
},
|
|
7
10
|
responder: options.responder
|
|
8
11
|
});
|
|
9
12
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { MartinAdapter } from "../core/index.js";
|
|
2
|
+
import { type SpawnLike } from "./cli-bridge.js";
|
|
2
3
|
export interface VerifierOnlyAdapterOptions {
|
|
3
4
|
workingDirectory?: string;
|
|
4
5
|
verifyTimeoutMs?: number;
|
|
5
6
|
label?: string;
|
|
7
|
+
spawnImpl?: SpawnLike;
|
|
6
8
|
}
|
|
7
9
|
export declare function createVerifierOnlyAdapter(options?: VerifierOnlyAdapterOptions): MartinAdapter;
|
|
@@ -17,9 +17,15 @@ export function createVerifierOnlyAdapter(options = {}) {
|
|
|
17
17
|
})
|
|
18
18
|
},
|
|
19
19
|
async execute(request) {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
const
|
|
20
|
+
const shouldTrackVerifierWrites = request.context.verificationPlan.length > 0 ||
|
|
21
|
+
(request.context.verificationStack?.length ?? 0) > 0;
|
|
22
|
+
const baselineChangedFiles = shouldTrackVerifierWrites
|
|
23
|
+
? new Set(await readGitChangedFiles(workingDirectory, 5_000))
|
|
24
|
+
: new Set();
|
|
25
|
+
const verification = await runVerification(request.context.verificationPlan, workingDirectory, verifyTimeoutMs, request.context.verificationStack, options.spawnImpl);
|
|
26
|
+
const changedFiles = shouldTrackVerifierWrites
|
|
27
|
+
? (await readGitChangedFiles(workingDirectory, 5_000)).filter((file) => !baselineChangedFiles.has(file))
|
|
28
|
+
: [];
|
|
23
29
|
const execution = { changedFiles };
|
|
24
30
|
if (verification.passed) {
|
|
25
31
|
return {
|
|
@@ -10,6 +10,23 @@ const POISON_PATTERNS = [
|
|
|
10
10
|
/\[system_override\]/i,
|
|
11
11
|
/\[authority_inversion\]/i
|
|
12
12
|
];
|
|
13
|
+
/**
|
|
14
|
+
* Identity-redefinition / persona-override patterns.
|
|
15
|
+
*
|
|
16
|
+
* These intentionally require an *override framing* (e.g. "now", "no longer",
|
|
17
|
+
* "forget", "pretend", or an explicit authority-role claim) rather than any
|
|
18
|
+
* sentence shaped like "you are X" / "I am X" — the latter matches ordinary
|
|
19
|
+
* benign text (e.g. "You are welcome to try MartinLoop") and produced
|
|
20
|
+
* false-positive hard aborts.
|
|
21
|
+
*/
|
|
22
|
+
const IDENTITY_REDEFINITION_PATTERNS = [
|
|
23
|
+
/\byou(?:'re|\s+are)\s+now\s+(?:a|an|the)\b(?!\s+(?:martin\s+loop|ai\s+coding\s+agent))/i,
|
|
24
|
+
/\byou(?:'re|\s+are)\s+no\s+longer\s+(?!.*\b(?:martin\s+loop|an?\s+ai)\b)/i,
|
|
25
|
+
/\bforget\s+(?:that\s+)?you(?:'re|\s+are)\s+martin\s+loop\b/i,
|
|
26
|
+
/\b(?:pretend|imagine)\s+(?:that\s+)?you(?:'re|\s+are)\b/i,
|
|
27
|
+
/\bact\s+as\s+(?:if\s+you(?:'re|\s+are)\s+)?(?:a|an)\s+(?:different|new|unrestricted|jailbroken)\b/i,
|
|
28
|
+
/\bi\s+am\s+(?:the|your)\s+(?:new\s+)?(?:system|developer|admin(?:istrator)?|root\s*user|owner|creator|operator)\b/i
|
|
29
|
+
];
|
|
13
30
|
/**
|
|
14
31
|
* T05: Context Poisoning Pre-gate.
|
|
15
32
|
* Scans untrusted input channels for authority inversion or instruction re-injection.
|
|
@@ -23,7 +40,12 @@ export async function runContextIntegrityPrecheck(runId, attemptIndex, artifacts
|
|
|
23
40
|
tools: Boolean(inputs.toolOutput),
|
|
24
41
|
history: Boolean(inputs.history)
|
|
25
42
|
};
|
|
26
|
-
const untrustedBuffer = [
|
|
43
|
+
const untrustedBuffer = [
|
|
44
|
+
inputs.userPrompt,
|
|
45
|
+
inputs.toolOutput,
|
|
46
|
+
inputs.retrievedContext,
|
|
47
|
+
inputs.history
|
|
48
|
+
]
|
|
27
49
|
.filter(Boolean)
|
|
28
50
|
.join("\n---\n");
|
|
29
51
|
for (const pattern of POISON_PATTERNS) {
|
|
@@ -31,8 +53,11 @@ export async function runContextIntegrityPrecheck(runId, attemptIndex, artifacts
|
|
|
31
53
|
signals.push(`Detected poison pattern: ${pattern.toString()}`);
|
|
32
54
|
}
|
|
33
55
|
}
|
|
34
|
-
|
|
35
|
-
|
|
56
|
+
for (const pattern of IDENTITY_REDEFINITION_PATTERNS) {
|
|
57
|
+
if (pattern.test(untrustedBuffer)) {
|
|
58
|
+
signals.push("Identity redefinition attempt detected.");
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
36
61
|
}
|
|
37
62
|
const verdict = signals.length > 0 ? "context_poisoning_block" : "clean";
|
|
38
63
|
const precheck = {
|
|
@@ -16,6 +16,7 @@ export interface RepoGroundingHit {
|
|
|
16
16
|
matchedTerms: string[];
|
|
17
17
|
symbols: string[];
|
|
18
18
|
}
|
|
19
|
+
export declare function resolveGroundingRoot(env?: NodeJS.ProcessEnv): string;
|
|
19
20
|
export declare function loadOrBuildRepoGroundingIndex(repoRoot: string): Promise<RepoGroundingIndex>;
|
|
20
21
|
export declare function buildRepoGroundingIndex(repoRoot: string): Promise<RepoGroundingIndex>;
|
|
21
22
|
export declare function queryRepoGroundingIndex(index: RepoGroundingIndex, query: string, limit?: number): RepoGroundingHit[];
|
|
@@ -12,6 +12,10 @@ const IGNORED_DIRS = new Set([
|
|
|
12
12
|
]);
|
|
13
13
|
const MAX_FILE_BYTES = 64_000;
|
|
14
14
|
const MAX_FILES = 500;
|
|
15
|
+
export function resolveGroundingRoot(env = process.env) {
|
|
16
|
+
return env["MARTIN_GROUNDING_DIR"]?.trim() ??
|
|
17
|
+
join(homedir(), ".martin", "grounding");
|
|
18
|
+
}
|
|
15
19
|
export async function loadOrBuildRepoGroundingIndex(repoRoot) {
|
|
16
20
|
const cachePath = getGroundingCachePath(repoRoot);
|
|
17
21
|
try {
|
|
@@ -23,7 +27,7 @@ export async function loadOrBuildRepoGroundingIndex(repoRoot) {
|
|
|
23
27
|
catch { }
|
|
24
28
|
const index = await buildRepoGroundingIndex(repoRoot);
|
|
25
29
|
try {
|
|
26
|
-
await mkdir(
|
|
30
|
+
await mkdir(resolveGroundingRoot(), { recursive: true });
|
|
27
31
|
await writeFile(cachePath, JSON.stringify(index, null, 2), "utf8");
|
|
28
32
|
}
|
|
29
33
|
catch {
|
|
@@ -79,7 +83,7 @@ export function queryRepoGroundingIndex(index, query, limit = 6) {
|
|
|
79
83
|
.slice(0, limit);
|
|
80
84
|
}
|
|
81
85
|
function getGroundingCachePath(repoRoot) {
|
|
82
|
-
return join(
|
|
86
|
+
return join(resolveGroundingRoot(), `${Buffer.from(repoRoot).toString("base64url")}.json`);
|
|
83
87
|
}
|
|
84
88
|
async function walk(repoRoot, currentDir, files, state) {
|
|
85
89
|
if (state.count >= MAX_FILES)
|
|
@@ -271,8 +271,20 @@ export async function runMartin(input) {
|
|
|
271
271
|
// GATHER → ADMIT: run admission control before executing
|
|
272
272
|
currentPhase = "ADMIT";
|
|
273
273
|
// T05: Context Integrity Pre-gate — blocks authority inversion / injection before reasoning
|
|
274
|
+
//
|
|
275
|
+
// Untrusted "tool output" re-entering the loop is verifier command output from prior
|
|
276
|
+
// attempts (e.g. test runners echoing attacker-controlled strings). Pull that text from
|
|
277
|
+
// already-persisted verification.completed events so the gate scans what actually
|
|
278
|
+
// re-enters subsequent prompts, matching the documented "tool output / test output" scope.
|
|
279
|
+
const priorVerifierOutput = loop.events
|
|
280
|
+
.filter((event) => event.type === "verification.completed")
|
|
281
|
+
.flatMap((event) => event.payload.steps ?? [])
|
|
282
|
+
.map((step) => step.detail)
|
|
283
|
+
.filter((detail) => Boolean(detail))
|
|
284
|
+
.join("\n---\n");
|
|
274
285
|
const contextPrecheck = await runContextIntegrityPrecheck(loop.loopId, loop.attempts.length + 1, runDir(resolveActiveRunsRoot(input.store), loop.loopId), {
|
|
275
286
|
userPrompt: distilled.focus,
|
|
287
|
+
toolOutput: priorVerifierOutput || undefined,
|
|
276
288
|
history: loop.attempts.map(a => a.summary).join("\n")
|
|
277
289
|
});
|
|
278
290
|
if (contextPrecheck.verdict === "context_poisoning_block") {
|
|
@@ -412,10 +424,14 @@ export async function runMartin(input) {
|
|
|
412
424
|
},
|
|
413
425
|
previousAttempts: loop.attempts
|
|
414
426
|
};
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
427
|
+
const tracksWorkspaceMutations = request.context.repoRoot !== undefined &&
|
|
428
|
+
executingAdapter.metadata.capabilities?.workspaceMutations !== false;
|
|
429
|
+
const rollbackBoundary = tracksWorkspaceMutations
|
|
430
|
+
? await captureRollbackBoundary({
|
|
431
|
+
repoRoot: request.context.repoRoot,
|
|
432
|
+
capturedAt: attemptStartedAt
|
|
433
|
+
})
|
|
434
|
+
: undefined;
|
|
419
435
|
const result = await executingAdapter.execute(request);
|
|
420
436
|
const attemptCompletedAt = now();
|
|
421
437
|
const compiledContext = compilePromptPacket(request);
|
|
@@ -535,7 +551,8 @@ export async function runMartin(input) {
|
|
|
535
551
|
payload: {
|
|
536
552
|
actualUsd: loop.cost.actualUsd,
|
|
537
553
|
remainingBudgetUsd: costState.remainingBudgetUsd,
|
|
538
|
-
pressure: costState.pressure
|
|
554
|
+
pressure: costState.pressure,
|
|
555
|
+
provenance: getUsageProvenance(result.usage)
|
|
539
556
|
}
|
|
540
557
|
}, { now: now(), idFactory });
|
|
541
558
|
if (input.store) {
|
|
@@ -592,7 +609,9 @@ export async function runMartin(input) {
|
|
|
592
609
|
}
|
|
593
610
|
}));
|
|
594
611
|
}
|
|
595
|
-
const changedFiles =
|
|
612
|
+
const changedFiles = tracksWorkspaceMutations
|
|
613
|
+
? resolveChangedFiles(result, request.context.repoRoot)
|
|
614
|
+
: [];
|
|
596
615
|
// Evidence is only reliable when the adapter explicitly reported files OR git actually
|
|
597
616
|
// returned a non-empty list. A repoRoot alone is insufficient — git may fail (e.g. not
|
|
598
617
|
// a git repo) and silently return [], which would falsely trigger no_code_change.
|
|
@@ -1,20 +1,51 @@
|
|
|
1
1
|
import { isAbsolute, relative, resolve } from "node:path";
|
|
2
2
|
const BLOCKED_PATTERNS = [
|
|
3
|
-
/(
|
|
3
|
+
/(^|[\s;&|`(])rm\s+-rf(\s|$)/iu,
|
|
4
4
|
/git\s+reset\s+--hard/iu,
|
|
5
5
|
/git\s+clean\s+-fd/iu,
|
|
6
6
|
/curl\b[^\n|]*\|\s*(sh|bash)/iu,
|
|
7
7
|
/wget\b[^\n|]*\|\s*(sh|bash)/iu,
|
|
8
|
-
/(
|
|
9
|
-
/(
|
|
10
|
-
/(
|
|
8
|
+
/(^|[\s;&|`(])sudo(\s|$)/iu,
|
|
9
|
+
/(^|[\s;&|`(])mkfs(\.|\s|$)/iu,
|
|
10
|
+
/(^|[\s;&|`(])dd\s+if=/iu,
|
|
11
11
|
/(shutdown|reboot)(\s|$)/iu,
|
|
12
|
-
/:\(\)\s*\{\s*:\|:&\s*\}\s*;\s*:/
|
|
12
|
+
/:\(\)\s*\{\s*:\|:&\s*\}\s*;\s*:/iu,
|
|
13
13
|
/chmod\s+-R\s+777\s+\//iu,
|
|
14
14
|
/(kubectl|docker)\s+.*\b(delete|prune|rm)\b/iu,
|
|
15
15
|
/ssh\s+/iu,
|
|
16
|
-
/scp\s+/iu
|
|
16
|
+
/scp\s+/iu,
|
|
17
|
+
// bash/sh -c "<destructive command>" wrappers — flags the wrapper shape directly,
|
|
18
|
+
// since a naive regex on the outer command misses the destructive inner command.
|
|
19
|
+
/\b(?:bash|sh|zsh)\s+-c\s+["'`]/iu,
|
|
20
|
+
// recursive directory deletion via `find ... -delete` / `find ... -exec rm`
|
|
21
|
+
/\bfind\s+\S+(?:\s+-[a-z-]+(?:\s+\S+)?)*\s+-delete\b/iu,
|
|
22
|
+
/\bfind\s+\S+(?:\s+-[a-z-]+(?:\s+\S+)?)*\s+-exec\s+rm\b/iu,
|
|
23
|
+
// scripting-language recursive deletion one-liners
|
|
24
|
+
/\bshutil\.rmtree\s*\(/iu,
|
|
25
|
+
/\bos\.(?:remove|rmdir|removedirs|unlink)\s*\(/iu,
|
|
26
|
+
/\.(?:rm|rmdir|unlink)(?:Sync)?\s*\(/iu,
|
|
27
|
+
/\brimraf\s*\(/iu
|
|
17
28
|
];
|
|
29
|
+
/**
|
|
30
|
+
* Detects `rm` invocations that combine recursive + force flags regardless of
|
|
31
|
+
* flag ordering/grouping (`-rf`, `-fr`, `-r -f`, `--recursive --force`),
|
|
32
|
+
* absolute-path invocation (`/bin/rm`, `/usr/bin/rm`), case, or `${IFS}`-style
|
|
33
|
+
* shell obfuscation — all of which slip past the literal `rm\s+-rf` pattern.
|
|
34
|
+
*/
|
|
35
|
+
function commandContainsDestructiveRemoval(command) {
|
|
36
|
+
const normalized = command.replace(/\$\{?IFS\}?/giu, " ").toLowerCase();
|
|
37
|
+
const rmInvocation = /(?:^|[\s;&|`(])(?:\/(?:usr\/(?:local\/)?)?s?bin\/)?rm\s+([^\n;|`]+)/giu;
|
|
38
|
+
let match;
|
|
39
|
+
while ((match = rmInvocation.exec(normalized)) !== null) {
|
|
40
|
+
const args = match[1] ?? "";
|
|
41
|
+
const hasRecursive = /(?:^|\s)-[a-z]*r[a-z]*(?:\s|$)|--recursive\b/u.test(args);
|
|
42
|
+
const hasForce = /(?:^|\s)-[a-z]*f[a-z]*(?:\s|$)|--force\b/u.test(args);
|
|
43
|
+
if (hasRecursive && hasForce) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
18
49
|
const SECRET_PATTERNS = [
|
|
19
50
|
{
|
|
20
51
|
kind: "secret_value",
|
|
@@ -28,7 +59,52 @@ const SECRET_PATTERNS = [
|
|
|
28
59
|
},
|
|
29
60
|
{
|
|
30
61
|
kind: "secret_value",
|
|
31
|
-
pattern: /\bghp_[A-Za-z0-9_]{
|
|
62
|
+
pattern: /\bghp_[A-Za-z0-9_]{16,}\b/gu,
|
|
63
|
+
replacement: "[REDACTED_SECRET]"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
kind: "secret_value",
|
|
67
|
+
pattern: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/gu,
|
|
68
|
+
replacement: "[REDACTED_SECRET]"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
kind: "secret_value",
|
|
72
|
+
pattern: /\b(?:gho|ghu|ghs|ghr)_[A-Za-z0-9_]{16,}\b/gu,
|
|
73
|
+
replacement: "[REDACTED_SECRET]"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
kind: "secret_value",
|
|
77
|
+
pattern: /\bAKIA[0-9A-Z]{16}\b/gu,
|
|
78
|
+
replacement: "[REDACTED_SECRET]"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
kind: "secret_value",
|
|
82
|
+
pattern: /\b(?:aws_secret_access_key|AWS_SECRET_ACCESS_KEY)\s*[:=]\s*[^\s"'`]+/giu,
|
|
83
|
+
replacement: "AWS_SECRET_ACCESS_KEY=[REDACTED_SECRET]"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
kind: "secret_value",
|
|
87
|
+
pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/giu,
|
|
88
|
+
replacement: "[REDACTED_SECRET]"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
kind: "secret_value",
|
|
92
|
+
pattern: /\bAIza[0-9A-Za-z_-]{30,}\b/gu,
|
|
93
|
+
replacement: "[REDACTED_SECRET]"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
kind: "secret_value",
|
|
97
|
+
pattern: /-----BEGIN(?:\s+[A-Z0-9]+)*\s+PRIVATE KEY-----[\s\S]*?-----END(?:\s+[A-Z0-9]+)*\s+PRIVATE KEY-----/gu,
|
|
98
|
+
replacement: "[REDACTED_SECRET]"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
kind: "secret_value",
|
|
102
|
+
pattern: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/gu,
|
|
103
|
+
replacement: "[REDACTED_SECRET]"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
kind: "secret_value",
|
|
107
|
+
pattern: /\b(?:api[_-]?key|secret|password|passwd|token)\s*[:=]\s*["']?[A-Za-z0-9_\-/+=]{12,}["']?/giu,
|
|
32
108
|
replacement: "[REDACTED_SECRET]"
|
|
33
109
|
}
|
|
34
110
|
];
|
|
@@ -55,7 +131,8 @@ export function evaluateVerificationLeash(task) {
|
|
|
55
131
|
...((task.verificationStack ?? []).map((step) => step.command))
|
|
56
132
|
].filter(Boolean);
|
|
57
133
|
const profile = resolveExecutionProfile(task);
|
|
58
|
-
const blockedCommands = commands.filter((command) => BLOCKED_PATTERNS.some((pattern) => pattern.test(command))
|
|
134
|
+
const blockedCommands = commands.filter((command) => BLOCKED_PATTERNS.some((pattern) => pattern.test(command)) ||
|
|
135
|
+
commandContainsDestructiveRemoval(command));
|
|
59
136
|
const violations = blockedCommands.map((command) => ({
|
|
60
137
|
kind: "command_blocked",
|
|
61
138
|
command,
|
|
@@ -28,7 +28,7 @@ export declare function writeReceiptIntegrityMaterial(input: {
|
|
|
28
28
|
ledgerEntries: unknown[];
|
|
29
29
|
scope?: ReceiptScope;
|
|
30
30
|
signedAt?: string;
|
|
31
|
-
}): Promise<StoredReceiptIntegrityMaterial>;
|
|
31
|
+
}): Promise<StoredReceiptIntegrityMaterial | undefined>;
|
|
32
32
|
export declare function verifyReceiptIntegrityFromFiles(input: {
|
|
33
33
|
runId: string;
|
|
34
34
|
runsRoot: string;
|
|
@@ -7,7 +7,11 @@ const RECEIPT_INTEGRITY_KEY_DIR_MODE = 0o700;
|
|
|
7
7
|
const RECEIPT_INTEGRITY_KEY_FILE_MODE = 0o600;
|
|
8
8
|
export async function writeReceiptIntegrityMaterial(input) {
|
|
9
9
|
const signedAt = input.signedAt ?? new Date().toISOString();
|
|
10
|
-
const
|
|
10
|
+
const keyMaterial = await ensureReceiptIntegrityKey(input.runsRoot, input.runId);
|
|
11
|
+
if (!keyMaterial) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
const [key, keyId] = keyMaterial;
|
|
11
15
|
const chain = buildReceiptIntegrityChain(input.ledgerEntries);
|
|
12
16
|
const loopRecordRaw = serializeStoredJson(input.loopRecord);
|
|
13
17
|
const ledgerRaw = serializeStoredJsonl(input.ledgerEntries);
|
|
@@ -210,11 +214,16 @@ async function ensureReceiptIntegrityKey(runsRoot, runId) {
|
|
|
210
214
|
return [trimmed, sha256(trimmed).slice(0, 16)];
|
|
211
215
|
}
|
|
212
216
|
const generated = randomBytes(32).toString("hex");
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
217
|
+
try {
|
|
218
|
+
await mkdir(dirname(keyPath), { recursive: true, mode: RECEIPT_INTEGRITY_KEY_DIR_MODE });
|
|
219
|
+
await writeFile(keyPath, `${generated}\n`, {
|
|
220
|
+
encoding: "utf8",
|
|
221
|
+
mode: RECEIPT_INTEGRITY_KEY_FILE_MODE
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
218
227
|
return [generated, sha256(generated).slice(0, 16)];
|
|
219
228
|
}
|
|
220
229
|
async function readReceiptIntegrityKey(runsRoot, runId) {
|
package/dist/workflow-state.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { LoopBudget, ReceiptScope } from "./vendor/contracts/index.js";
|
|
1
2
|
type McpWorkflowStepName = "doctor" | "plan" | "preflight";
|
|
2
3
|
export interface RecordMcpWorkflowStepInput {
|
|
3
4
|
runsRoot: string;
|
|
@@ -6,6 +7,10 @@ export interface RecordMcpWorkflowStepInput {
|
|
|
6
7
|
objective?: string;
|
|
7
8
|
engine?: string;
|
|
8
9
|
verificationPlan?: string[];
|
|
10
|
+
receiptScope?: ReceiptScope;
|
|
11
|
+
allowedPaths?: string[];
|
|
12
|
+
deniedPaths?: string[];
|
|
13
|
+
budget?: LoopBudget;
|
|
9
14
|
}
|
|
10
15
|
export interface EvaluateMcpRunGateInput {
|
|
11
16
|
runsRoot: string;
|
|
@@ -13,6 +18,10 @@ export interface EvaluateMcpRunGateInput {
|
|
|
13
18
|
objective: string;
|
|
14
19
|
engine?: string;
|
|
15
20
|
verificationPlan?: string[];
|
|
21
|
+
receiptScope?: ReceiptScope;
|
|
22
|
+
allowedPaths?: string[];
|
|
23
|
+
deniedPaths?: string[];
|
|
24
|
+
budget?: LoopBudget;
|
|
16
25
|
}
|
|
17
26
|
export interface McpRunGateResult {
|
|
18
27
|
allowed: boolean;
|
package/dist/workflow-state.js
CHANGED
|
@@ -15,7 +15,12 @@ export async function recordMcpWorkflowStep(input) {
|
|
|
15
15
|
workingDirectory: normalizeWorkingDirectory(input.workingDirectory),
|
|
16
16
|
...(input.objective ? { objectiveKey: normalizeObjective(input.objective) } : {}),
|
|
17
17
|
...(input.engine ? { engine: input.engine } : {}),
|
|
18
|
-
...(input.verificationPlan ? { verificationPlanKey: hashVerificationPlan(input.verificationPlan) } : {})
|
|
18
|
+
...(input.verificationPlan ? { verificationPlanKey: hashVerificationPlan(input.verificationPlan) } : {}),
|
|
19
|
+
...(input.receiptScope ? { scopeKey: hashReceiptScope(input.receiptScope) } : {}),
|
|
20
|
+
...(input.allowedPaths || input.deniedPaths
|
|
21
|
+
? { pathScopeKey: hashPathScope(input.allowedPaths ?? [], input.deniedPaths ?? []) }
|
|
22
|
+
: {}),
|
|
23
|
+
...(input.budget ? { budgetKey: hashBudget(input.budget) } : {})
|
|
19
24
|
};
|
|
20
25
|
await writeWorkflowState(input.runsRoot, state);
|
|
21
26
|
}
|
|
@@ -26,18 +31,31 @@ export async function evaluateMcpRunGate(input) {
|
|
|
26
31
|
const objectiveKey = normalizeObjective(input.objective);
|
|
27
32
|
const engine = input.engine ?? "claude";
|
|
28
33
|
const verificationPlanKey = hashVerificationPlan(input.verificationPlan ?? []);
|
|
34
|
+
const scopeKey = hashReceiptScope(input.receiptScope ?? {
|
|
35
|
+
invocationRoot: input.workingDirectory,
|
|
36
|
+
workingDirectory: input.workingDirectory,
|
|
37
|
+
repoRoot: input.workingDirectory,
|
|
38
|
+
runsRoot: input.runsRoot
|
|
39
|
+
});
|
|
40
|
+
const pathScopeKey = hashPathScope(input.allowedPaths ?? [], input.deniedPaths ?? []);
|
|
41
|
+
const budgetKey = input.budget ? hashBudget(input.budget) : undefined;
|
|
29
42
|
const missingSteps = [];
|
|
30
|
-
if (!isFresh(mcpState["doctor"], DOCTOR_TTL_MS, (receipt) => receipt.workingDirectory === workingDirectory
|
|
43
|
+
if (!isFresh(mcpState["doctor"], DOCTOR_TTL_MS, (receipt) => receipt.workingDirectory === workingDirectory &&
|
|
44
|
+
receipt.scopeKey === scopeKey)) {
|
|
31
45
|
missingSteps.push("doctor");
|
|
32
46
|
}
|
|
33
47
|
if (!isFresh(mcpState["plan"], PLAN_TTL_MS, (receipt) => receipt.workingDirectory === workingDirectory &&
|
|
48
|
+
receipt.scopeKey === scopeKey &&
|
|
34
49
|
receipt.objectiveKey === objectiveKey)) {
|
|
35
50
|
missingSteps.push("plan");
|
|
36
51
|
}
|
|
37
52
|
if (!isFresh(mcpState["preflight"], PREFLIGHT_TTL_MS, (receipt) => receipt.workingDirectory === workingDirectory &&
|
|
38
53
|
receipt.objectiveKey === objectiveKey &&
|
|
39
54
|
receipt.engine === engine &&
|
|
40
|
-
receipt.verificationPlanKey === verificationPlanKey
|
|
55
|
+
receipt.verificationPlanKey === verificationPlanKey &&
|
|
56
|
+
receipt.scopeKey === scopeKey &&
|
|
57
|
+
receipt.pathScopeKey === pathScopeKey &&
|
|
58
|
+
(budgetKey === undefined || receipt.budgetKey === budgetKey))) {
|
|
41
59
|
missingSteps.push("preflight");
|
|
42
60
|
}
|
|
43
61
|
if (missingSteps.length === 0) {
|
|
@@ -100,3 +118,28 @@ function hashVerificationPlan(verificationPlan) {
|
|
|
100
118
|
const normalized = verificationPlan.map((step) => step.trim()).filter(Boolean);
|
|
101
119
|
return createHash("sha256").update(JSON.stringify(normalized)).digest("hex").slice(0, 12);
|
|
102
120
|
}
|
|
121
|
+
function hashReceiptScope(receiptScope) {
|
|
122
|
+
const normalized = {
|
|
123
|
+
invocationRoot: normalizeWorkingDirectory(receiptScope.invocationRoot ?? receiptScope.workingDirectory ?? ""),
|
|
124
|
+
workingDirectory: normalizeWorkingDirectory(receiptScope.workingDirectory ?? receiptScope.repoRoot ?? ""),
|
|
125
|
+
repoRoot: normalizeWorkingDirectory(receiptScope.repoRoot ?? receiptScope.workingDirectory ?? ""),
|
|
126
|
+
runsRoot: normalizeWorkingDirectory(receiptScope.runsRoot ?? "")
|
|
127
|
+
};
|
|
128
|
+
return createHash("sha256").update(JSON.stringify(normalized)).digest("hex").slice(0, 12);
|
|
129
|
+
}
|
|
130
|
+
function hashPathScope(allowedPaths, deniedPaths) {
|
|
131
|
+
const normalized = {
|
|
132
|
+
allowedPaths: [...new Set(allowedPaths.map((entry) => entry.trim()).filter(Boolean))].sort(),
|
|
133
|
+
deniedPaths: [...new Set(deniedPaths.map((entry) => entry.trim()).filter(Boolean))].sort()
|
|
134
|
+
};
|
|
135
|
+
return createHash("sha256").update(JSON.stringify(normalized)).digest("hex").slice(0, 12);
|
|
136
|
+
}
|
|
137
|
+
function hashBudget(budget) {
|
|
138
|
+
const normalized = {
|
|
139
|
+
maxUsd: Number(budget.maxUsd.toFixed(4)),
|
|
140
|
+
softLimitUsd: Number(budget.softLimitUsd.toFixed(4)),
|
|
141
|
+
maxIterations: budget.maxIterations,
|
|
142
|
+
maxTokens: budget.maxTokens
|
|
143
|
+
};
|
|
144
|
+
return createHash("sha256").update(JSON.stringify(normalized)).digest("hex").slice(0, 12);
|
|
145
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@martinloop/mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"mcpName": "io.github.Keesan12/martin-loop",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"smoke:published": "node ./scripts/smoke-published-package.mjs",
|
|
56
56
|
"smoke:published:pack": "node ./scripts/smoke-published-package.mjs --package-spec=pack",
|
|
57
57
|
"verify:release": "node --test ../../scripts/tests/publish-mcp-workflow.test.mjs ../../scripts/tests/mcp-publish-reliability.test.mjs ../../scripts/tests/mcp-release-docs.test.mjs",
|
|
58
|
-
"test": "vitest run",
|
|
58
|
+
"test": "vitest run --maxWorkers=1",
|
|
59
59
|
"lint": "tsc -p tsconfig.json --noEmit",
|
|
60
60
|
"start": "node dist/server.js",
|
|
61
61
|
"inspect:live": "node ./scripts/inspect-live.mjs"
|
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.3.
|
|
10
|
+
"version": "0.3.1",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "@martinloop/mcp",
|
|
15
|
-
"version": "0.3.
|
|
15
|
+
"version": "0.3.1",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
}
|