@martinloop/mcp 0.2.7 → 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 +49 -104
- package/dist/package-version.d.ts +1 -1
- package/dist/package-version.js +1 -1
- package/dist/prompts.d.ts +1 -1
- package/dist/resources.d.ts +1 -1
- package/dist/resources.js +2 -2
- package/dist/server-validation.d.ts +1 -0
- package/dist/server-validation.js +8 -0
- package/dist/server.js +87 -9
- package/dist/tools/doctor.d.ts +39 -1
- package/dist/tools/doctor.js +68 -9
- package/dist/tools/eval.js +3 -2
- package/dist/tools/get-run.d.ts +3 -0
- package/dist/tools/get-run.js +3 -1
- package/dist/tools/get-verification-results.d.ts +3 -0
- package/dist/tools/get-verification-results.js +3 -1
- package/dist/tools/plan.js +4 -2
- package/dist/tools/pr-tools.js +2 -1
- package/dist/tools/preflight.d.ts +41 -1
- package/dist/tools/preflight.js +74 -19
- package/dist/tools/run-dossier.d.ts +3 -0
- package/dist/tools/run-dossier.js +5 -2
- package/dist/tools/run-loop.d.ts +7 -2
- package/dist/tools/run-loop.js +67 -35
- package/dist/tools/run-store.js +67 -15
- package/dist/tools/tool-errors.js +1 -1
- package/dist/tools/tool-support.d.ts +8 -3
- package/dist/tools/tool-support.js +61 -18
- 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 +45 -3
- package/dist/vendor/adapters/claude-cli.js +465 -45
- package/dist/vendor/adapters/cli-bridge.d.ts +46 -0
- package/dist/vendor/adapters/cli-bridge.js +147 -38
- package/dist/vendor/adapters/codex-launcher.d.ts +76 -0
- package/dist/vendor/adapters/codex-launcher.js +538 -0
- package/dist/vendor/adapters/index.d.ts +3 -2
- package/dist/vendor/adapters/index.js +3 -2
- package/dist/vendor/adapters/openai-compatible.d.ts +19 -4
- package/dist/vendor/adapters/openai-compatible.js +50 -19
- package/dist/vendor/adapters/runtime-support.d.ts +3 -0
- package/dist/vendor/adapters/runtime-support.js +9 -1
- 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 +11 -4
- package/dist/vendor/contracts/index.d.ts +39 -0
- package/dist/vendor/contracts/index.js +2 -0
- 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 +24 -3
- package/dist/vendor/core/index.js +113 -21
- package/dist/vendor/core/leash.js +85 -8
- package/dist/vendor/core/persistence/index.d.ts +2 -0
- package/dist/vendor/core/persistence/index.js +1 -0
- package/dist/vendor/core/persistence/integrity.d.ts +38 -0
- package/dist/vendor/core/persistence/integrity.js +248 -0
- package/dist/vendor/core/persistence/store.d.ts +7 -0
- package/dist/vendor/core/persistence/store.js +25 -1
- package/dist/vendor/core/policy.d.ts +9 -0
- 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
|
@@ -13,7 +13,7 @@ export { runContextIntegrityPrecheck } from "./context-integrity.js";
|
|
|
13
13
|
// ─── Prompt packet compiler ──────────────────────────────────────────────────
|
|
14
14
|
export { compilePromptPacket } from "./compiler.js";
|
|
15
15
|
// ─── Persistence (RunStore, LedgerEvent, FileRunStore) ──────────────────────
|
|
16
|
-
export { createFileRunStore, makeLedgerEvent, readAllLoopRecords, readLatestLoopRecord, readLatestLoopRecordFromFile, readLoopRecordsFromFile, resolveRunsRoot } from "./persistence/index.js";
|
|
16
|
+
export { createFileRunStore, makeLedgerEvent, readAllLoopRecords, readLatestLoopRecord, readLatestLoopRecordFromFile, readLoopRecordsFromFile, resolveRunsRoot, resolveReceiptIntegrityPath, verifyReceiptIntegrityFromFiles, writeReceiptIntegrityMaterial } from "./persistence/index.js";
|
|
17
17
|
export { compileAndPersistContext } from "./persistence/index.js";
|
|
18
18
|
/**
|
|
19
19
|
* Admission gate — must pass before any attempt is executed.
|
|
@@ -101,6 +101,7 @@ export async function runMartin(input) {
|
|
|
101
101
|
projectId: input.projectId,
|
|
102
102
|
task: input.task,
|
|
103
103
|
budget: input.budget,
|
|
104
|
+
...(input.receiptScope ? { receiptScope: input.receiptScope } : {}),
|
|
104
105
|
...(input.teamId ? { teamId: input.teamId } : {}),
|
|
105
106
|
...(input.metadata ? { metadata: input.metadata } : {})
|
|
106
107
|
}, { now: now(), idFactory });
|
|
@@ -270,8 +271,20 @@ export async function runMartin(input) {
|
|
|
270
271
|
// GATHER → ADMIT: run admission control before executing
|
|
271
272
|
currentPhase = "ADMIT";
|
|
272
273
|
// T05: Context Integrity Pre-gate — blocks authority inversion / injection before reasoning
|
|
273
|
-
|
|
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");
|
|
285
|
+
const contextPrecheck = await runContextIntegrityPrecheck(loop.loopId, loop.attempts.length + 1, runDir(resolveActiveRunsRoot(input.store), loop.loopId), {
|
|
274
286
|
userPrompt: distilled.focus,
|
|
287
|
+
toolOutput: priorVerifierOutput || undefined,
|
|
275
288
|
history: loop.attempts.map(a => a.summary).join("\n")
|
|
276
289
|
});
|
|
277
290
|
if (contextPrecheck.verdict === "context_poisoning_block") {
|
|
@@ -411,13 +424,18 @@ export async function runMartin(input) {
|
|
|
411
424
|
},
|
|
412
425
|
previousAttempts: loop.attempts
|
|
413
426
|
};
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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;
|
|
418
435
|
const result = await executingAdapter.execute(request);
|
|
419
436
|
const attemptCompletedAt = now();
|
|
420
437
|
const compiledContext = compilePromptPacket(request);
|
|
438
|
+
const verification = normalizeVerificationOutcome(result);
|
|
421
439
|
// PATCH → VERIFY
|
|
422
440
|
currentPhase = "VERIFY";
|
|
423
441
|
let failure = result.status === "failed"
|
|
@@ -444,7 +462,16 @@ export async function runMartin(input) {
|
|
|
444
462
|
actualUsd: roundUsd(loop.cost.actualUsd + getUsageUsd(result.usage)),
|
|
445
463
|
avoidedUsd: loop.cost.avoidedUsd,
|
|
446
464
|
tokensIn: loop.cost.tokensIn + result.usage.tokensIn,
|
|
447
|
-
tokensOut: loop.cost.tokensOut + result.usage.tokensOut
|
|
465
|
+
tokensOut: loop.cost.tokensOut + result.usage.tokensOut,
|
|
466
|
+
...(result.usage.estimatedUsd !== undefined
|
|
467
|
+
? {
|
|
468
|
+
estimatedUsd: roundUsd((loop.cost.estimatedUsd ?? loop.cost.actualUsd) + result.usage.estimatedUsd)
|
|
469
|
+
}
|
|
470
|
+
: {}),
|
|
471
|
+
provenance: getUsageProvenance(result.usage),
|
|
472
|
+
...(result.usage.providerSettlement
|
|
473
|
+
? { providerSettlement: result.usage.providerSettlement }
|
|
474
|
+
: {})
|
|
448
475
|
},
|
|
449
476
|
updatedAt: attemptCompletedAt
|
|
450
477
|
};
|
|
@@ -503,11 +530,14 @@ export async function runMartin(input) {
|
|
|
503
530
|
}
|
|
504
531
|
loop = appendLoopEvent(loop, {
|
|
505
532
|
type: "verification.completed",
|
|
506
|
-
lifecycleState:
|
|
533
|
+
lifecycleState: verification.passed ? "completed" : "verifying",
|
|
507
534
|
payload: {
|
|
508
535
|
attemptId,
|
|
509
|
-
|
|
510
|
-
|
|
536
|
+
attemptIndex: currentAttemptIndex,
|
|
537
|
+
passed: verification.passed,
|
|
538
|
+
summary: verification.summary,
|
|
539
|
+
...(verification.steps?.length ? { steps: verification.steps } : {}),
|
|
540
|
+
...(verification.warnings?.length ? { warnings: verification.warnings } : {})
|
|
511
541
|
}
|
|
512
542
|
}, { now: attemptCompletedAt, idFactory });
|
|
513
543
|
const costState = evaluateCostGovernor({
|
|
@@ -521,7 +551,8 @@ export async function runMartin(input) {
|
|
|
521
551
|
payload: {
|
|
522
552
|
actualUsd: loop.cost.actualUsd,
|
|
523
553
|
remainingBudgetUsd: costState.remainingBudgetUsd,
|
|
524
|
-
pressure: costState.pressure
|
|
554
|
+
pressure: costState.pressure,
|
|
555
|
+
provenance: getUsageProvenance(result.usage)
|
|
525
556
|
}
|
|
526
557
|
}, { now: now(), idFactory });
|
|
527
558
|
if (input.store) {
|
|
@@ -534,6 +565,7 @@ export async function runMartin(input) {
|
|
|
534
565
|
});
|
|
535
566
|
await input.store.writeAttemptArtifacts(loop.loopId, currentAttemptIndex, {
|
|
536
567
|
compiledContext,
|
|
568
|
+
verification,
|
|
537
569
|
...(rollbackBoundary ? { rollbackBoundary } : {})
|
|
538
570
|
});
|
|
539
571
|
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
@@ -546,7 +578,13 @@ export async function runMartin(input) {
|
|
|
546
578
|
kind: "verification.completed",
|
|
547
579
|
runId: loop.loopId,
|
|
548
580
|
attemptIndex: currentAttemptIndex,
|
|
549
|
-
payload: {
|
|
581
|
+
payload: {
|
|
582
|
+
attemptId,
|
|
583
|
+
passed: verification.passed,
|
|
584
|
+
summary: verification.summary,
|
|
585
|
+
...(verification.steps?.length ? { steps: verification.steps } : {}),
|
|
586
|
+
...(verification.warnings?.length ? { warnings: verification.warnings } : {})
|
|
587
|
+
}
|
|
550
588
|
}));
|
|
551
589
|
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
552
590
|
kind: "budget.settled",
|
|
@@ -557,10 +595,13 @@ export async function runMartin(input) {
|
|
|
557
595
|
estimatedUsd: result.usage.estimatedUsd,
|
|
558
596
|
tokensIn: result.usage.tokensIn,
|
|
559
597
|
tokensOut: result.usage.tokensOut,
|
|
598
|
+
cachedInputTokens: result.usage.cachedInputTokens,
|
|
599
|
+
reasoningTokensOut: result.usage.reasoningTokensOut,
|
|
560
600
|
provenance: getUsageProvenance(result.usage),
|
|
561
601
|
transport: getAdapterTransport(executingAdapter),
|
|
562
602
|
providerId: executingAdapter.metadata.providerId,
|
|
563
603
|
model: executingAdapter.metadata.model,
|
|
604
|
+
providerSettlement: result.usage.providerSettlement,
|
|
564
605
|
patchCost: settlement.patchCost,
|
|
565
606
|
verificationCost: settlement.verificationCost,
|
|
566
607
|
varianceUsd: settlement.varianceUsd,
|
|
@@ -568,16 +609,20 @@ export async function runMartin(input) {
|
|
|
568
609
|
}
|
|
569
610
|
}));
|
|
570
611
|
}
|
|
571
|
-
const changedFiles =
|
|
612
|
+
const changedFiles = tracksWorkspaceMutations
|
|
613
|
+
? resolveChangedFiles(result, request.context.repoRoot)
|
|
614
|
+
: [];
|
|
572
615
|
// Evidence is only reliable when the adapter explicitly reported files OR git actually
|
|
573
616
|
// returned a non-empty list. A repoRoot alone is insufficient — git may fail (e.g. not
|
|
574
617
|
// a git repo) and silently return [], which would falsely trigger no_code_change.
|
|
575
618
|
const changedFileEvidenceAvailable = result.execution?.changedFiles !== undefined || changedFiles.length > 0;
|
|
619
|
+
const isVerifierOnlyAdapter = executingAdapter.adapterId === "direct:verifier:verify-only";
|
|
620
|
+
const patchTruthCountsEdits = !isVerifyOnly && !isVerifierOnlyAdapter && changedFileEvidenceAvailable;
|
|
576
621
|
if (isVerifyOnly && changedFiles.length > 0) {
|
|
577
622
|
const patchDecision = evaluatePatchDecision({
|
|
578
|
-
verificationPassed:
|
|
623
|
+
verificationPassed: verification.passed,
|
|
579
624
|
previousVerifierScore,
|
|
580
|
-
verifierScore:
|
|
625
|
+
verifierScore: verification.passed ? 1 : 0,
|
|
581
626
|
scopeViolationCount: changedFiles.length,
|
|
582
627
|
changedFileCount: changedFiles.length,
|
|
583
628
|
diffNovelty: 1,
|
|
@@ -842,12 +887,12 @@ export async function runMartin(input) {
|
|
|
842
887
|
let patchDecision;
|
|
843
888
|
if (result.status === "completed") {
|
|
844
889
|
patchDecision = evaluatePatchDecision({
|
|
845
|
-
verificationPassed:
|
|
890
|
+
verificationPassed: verification.passed,
|
|
846
891
|
previousVerifierScore,
|
|
847
|
-
verifierScore:
|
|
892
|
+
verifierScore: verification.passed ? 1 : 0,
|
|
848
893
|
groundingViolationCount: groundingScanResult?.violations.length ?? 0,
|
|
849
|
-
changedFileCount:
|
|
850
|
-
diffNovelty:
|
|
894
|
+
changedFileCount: patchTruthCountsEdits ? changedFiles.length : undefined,
|
|
895
|
+
diffNovelty: patchTruthCountsEdits ? (changedFiles.length > 0 ? 1 : 0) : undefined,
|
|
851
896
|
diffStats: result.execution?.diffStats,
|
|
852
897
|
costUsd: getUsageUsd(result.usage),
|
|
853
898
|
summary: result.summary
|
|
@@ -897,10 +942,10 @@ export async function runMartin(input) {
|
|
|
897
942
|
}
|
|
898
943
|
else {
|
|
899
944
|
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
900
|
-
kind:
|
|
945
|
+
kind: verification.passed ? "attempt.kept" : "attempt.discarded",
|
|
901
946
|
runId: loop.loopId,
|
|
902
947
|
attemptIndex: currentAttemptIndex,
|
|
903
|
-
payload: { reason:
|
|
948
|
+
payload: { reason: verification.summary }
|
|
904
949
|
}));
|
|
905
950
|
}
|
|
906
951
|
}
|
|
@@ -1145,6 +1190,9 @@ function createBudgetSettlement(input) {
|
|
|
1145
1190
|
usd: 0,
|
|
1146
1191
|
provenance: "unavailable"
|
|
1147
1192
|
},
|
|
1193
|
+
...(input.usage.providerSettlement
|
|
1194
|
+
? { providerSettlement: input.usage.providerSettlement }
|
|
1195
|
+
: {}),
|
|
1148
1196
|
totalActualUsd,
|
|
1149
1197
|
preflightEstimateUsd: input.estimate.estimatedAttemptCostUsd,
|
|
1150
1198
|
varianceUsd: roundUsd(totalActualUsd - input.estimate.estimatedAttemptCostUsd),
|
|
@@ -1185,6 +1233,50 @@ function getLastVerifierScore(loop) {
|
|
|
1185
1233
|
}
|
|
1186
1234
|
return 0;
|
|
1187
1235
|
}
|
|
1236
|
+
function truncateVerificationDetail(text, maxLength) {
|
|
1237
|
+
return text.length <= maxLength ? text : `${text.slice(0, maxLength - 1)}…`;
|
|
1238
|
+
}
|
|
1239
|
+
function normalizeVerificationOutcome(result) {
|
|
1240
|
+
const warnings = [...(result.verification.warnings ?? [])];
|
|
1241
|
+
const contradiction = detectVerificationContradiction(result);
|
|
1242
|
+
if (contradiction && !warnings.includes(contradiction)) {
|
|
1243
|
+
warnings.push(contradiction);
|
|
1244
|
+
}
|
|
1245
|
+
return {
|
|
1246
|
+
passed: result.verification.passed,
|
|
1247
|
+
summary: result.verification.summary,
|
|
1248
|
+
...(result.verification.steps?.length ? { steps: result.verification.steps } : {}),
|
|
1249
|
+
...(warnings.length ? { warnings } : {})
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
function detectVerificationContradiction(result) {
|
|
1253
|
+
if (!result.verification.passed) {
|
|
1254
|
+
return undefined;
|
|
1255
|
+
}
|
|
1256
|
+
const sources = [result.summary, result.failure?.message]
|
|
1257
|
+
.filter((value) => typeof value === "string" && value.trim().length > 0);
|
|
1258
|
+
const contradictionPatterns = [
|
|
1259
|
+
/createprocessasuserw failed:\s*\d+/iu,
|
|
1260
|
+
/\bverifier not run\b/iu,
|
|
1261
|
+
/\bfailed before verifier\b/iu,
|
|
1262
|
+
/\bnever launched\b/iu,
|
|
1263
|
+
/\bfailed to launch\b/iu,
|
|
1264
|
+
/\bcould not launch\b/iu
|
|
1265
|
+
];
|
|
1266
|
+
for (const source of sources) {
|
|
1267
|
+
const match = contradictionPatterns.find((pattern) => pattern.test(source));
|
|
1268
|
+
if (!match) {
|
|
1269
|
+
continue;
|
|
1270
|
+
}
|
|
1271
|
+
const excerpt = truncateVerificationDetail(source.trim().replace(/\s+/gu, " "), 220);
|
|
1272
|
+
return `Adapter output reported a tool-launch problem before MartinLoop ran its own verifier: ${excerpt}`;
|
|
1273
|
+
}
|
|
1274
|
+
return undefined;
|
|
1275
|
+
}
|
|
1276
|
+
function resolveActiveRunsRoot(store) {
|
|
1277
|
+
const configuredRunsRoot = store?.runsRoot?.trim();
|
|
1278
|
+
return configuredRunsRoot && configuredRunsRoot.length > 0 ? configuredRunsRoot : resolveRunsRoot();
|
|
1279
|
+
}
|
|
1188
1280
|
function toPatchDecisionArtifact(decision) {
|
|
1189
1281
|
return {
|
|
1190
1282
|
decision: decision.decision,
|
|
@@ -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,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { makeLedgerEvent } from "./ledger.js";
|
|
2
2
|
export type { LedgerEvent, LedgerEventDraft, LedgerEventKind } from "./ledger.js";
|
|
3
|
+
export { resolveReceiptIntegrityPath, verifyReceiptIntegrityFromFiles, writeReceiptIntegrityMaterial } from "./integrity.js";
|
|
4
|
+
export type { ReceiptIntegrityChainEntry, StoredReceiptIntegrityMaterial } from "./integrity.js";
|
|
3
5
|
export { artifactDir, createFileRunStore, resolveRunsRoot, runDir } from "./store.js";
|
|
4
6
|
export type { AttemptArtifacts, RunContract, RunStore } from "./store.js";
|
|
5
7
|
export { readAllLoopRecords, readLatestLoopRecord, readLatestLoopRecordFromFile, readLoopRecordsFromFile } from "./runs-reader.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { makeLedgerEvent } from "./ledger.js";
|
|
2
|
+
export { resolveReceiptIntegrityPath, verifyReceiptIntegrityFromFiles, writeReceiptIntegrityMaterial } from "./integrity.js";
|
|
2
3
|
export { artifactDir, createFileRunStore, resolveRunsRoot, runDir } from "./store.js";
|
|
3
4
|
export { readAllLoopRecords, readLatestLoopRecord, readLatestLoopRecordFromFile, readLoopRecordsFromFile } from "./runs-reader.js";
|
|
4
5
|
export { compileAndPersistContext } from "./compiler.js";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { LoopRecord, ReceiptIntegritySummary, ReceiptScope } from "../../contracts/index.js";
|
|
2
|
+
export interface ReceiptIntegrityChainEntry {
|
|
3
|
+
index: number;
|
|
4
|
+
eventId?: string;
|
|
5
|
+
type?: string;
|
|
6
|
+
kind?: string;
|
|
7
|
+
timestamp?: string;
|
|
8
|
+
prevHash: string;
|
|
9
|
+
entryHash: string;
|
|
10
|
+
}
|
|
11
|
+
export interface StoredReceiptIntegrityMaterial {
|
|
12
|
+
schemaVersion: "martin.receipt-integrity.v1";
|
|
13
|
+
runId: string;
|
|
14
|
+
keyId: string;
|
|
15
|
+
signedAt: string;
|
|
16
|
+
scope?: ReceiptScope;
|
|
17
|
+
loopRecordSha256: string;
|
|
18
|
+
ledgerSha256: string;
|
|
19
|
+
ledgerHeadHash: string;
|
|
20
|
+
entryCount: number;
|
|
21
|
+
chain: ReceiptIntegrityChainEntry[];
|
|
22
|
+
signatureHmacSha256: string;
|
|
23
|
+
}
|
|
24
|
+
export declare function writeReceiptIntegrityMaterial(input: {
|
|
25
|
+
runId: string;
|
|
26
|
+
runsRoot: string;
|
|
27
|
+
loopRecord: LoopRecord;
|
|
28
|
+
ledgerEntries: unknown[];
|
|
29
|
+
scope?: ReceiptScope;
|
|
30
|
+
signedAt?: string;
|
|
31
|
+
}): Promise<StoredReceiptIntegrityMaterial | undefined>;
|
|
32
|
+
export declare function verifyReceiptIntegrityFromFiles(input: {
|
|
33
|
+
runId: string;
|
|
34
|
+
runsRoot: string;
|
|
35
|
+
loopRecordPath: string;
|
|
36
|
+
ledgerPath: string;
|
|
37
|
+
}): Promise<ReceiptIntegritySummary>;
|
|
38
|
+
export declare function resolveReceiptIntegrityPath(runsRoot: string, runId: string): string;
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { createHash, createHmac, randomBytes } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
const RECEIPT_INTEGRITY_SCHEMA_VERSION = "martin.receipt-integrity.v1";
|
|
6
|
+
const RECEIPT_INTEGRITY_KEY_DIR_MODE = 0o700;
|
|
7
|
+
const RECEIPT_INTEGRITY_KEY_FILE_MODE = 0o600;
|
|
8
|
+
export async function writeReceiptIntegrityMaterial(input) {
|
|
9
|
+
const signedAt = input.signedAt ?? new Date().toISOString();
|
|
10
|
+
const keyMaterial = await ensureReceiptIntegrityKey(input.runsRoot, input.runId);
|
|
11
|
+
if (!keyMaterial) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
const [key, keyId] = keyMaterial;
|
|
15
|
+
const chain = buildReceiptIntegrityChain(input.ledgerEntries);
|
|
16
|
+
const loopRecordRaw = serializeStoredJson(input.loopRecord);
|
|
17
|
+
const ledgerRaw = serializeStoredJsonl(input.ledgerEntries);
|
|
18
|
+
const materialBase = {
|
|
19
|
+
schemaVersion: RECEIPT_INTEGRITY_SCHEMA_VERSION,
|
|
20
|
+
runId: input.runId,
|
|
21
|
+
keyId,
|
|
22
|
+
signedAt,
|
|
23
|
+
...(input.scope ? { scope: input.scope } : {}),
|
|
24
|
+
loopRecordSha256: sha256(loopRecordRaw),
|
|
25
|
+
ledgerSha256: sha256(ledgerRaw),
|
|
26
|
+
ledgerHeadHash: chain.at(-1)?.entryHash ?? "root",
|
|
27
|
+
entryCount: chain.length,
|
|
28
|
+
chain
|
|
29
|
+
};
|
|
30
|
+
const material = {
|
|
31
|
+
...materialBase,
|
|
32
|
+
signatureHmacSha256: createReceiptIntegritySignature(key, materialBase)
|
|
33
|
+
};
|
|
34
|
+
await mkdir(join(input.runsRoot, input.runId), { recursive: true });
|
|
35
|
+
await writeFile(resolveReceiptIntegrityPath(input.runsRoot, input.runId), serializeStoredJson(material), "utf8");
|
|
36
|
+
return material;
|
|
37
|
+
}
|
|
38
|
+
export async function verifyReceiptIntegrityFromFiles(input) {
|
|
39
|
+
const integrityPath = resolveReceiptIntegrityPath(input.runsRoot, input.runId);
|
|
40
|
+
const [rawMaterial, rawLoopRecord, rawLedger, key] = await Promise.all([
|
|
41
|
+
readFile(integrityPath, "utf8").catch(() => null),
|
|
42
|
+
readFile(input.loopRecordPath, "utf8").catch(() => null),
|
|
43
|
+
readFile(input.ledgerPath, "utf8").catch(() => null),
|
|
44
|
+
readReceiptIntegrityKey(input.runsRoot, input.runId).catch(() => null)
|
|
45
|
+
]);
|
|
46
|
+
if (!rawMaterial || !rawLoopRecord || rawLedger === null || !key) {
|
|
47
|
+
return {
|
|
48
|
+
state: "unsigned",
|
|
49
|
+
reason: "Receipt integrity material is missing for this run.",
|
|
50
|
+
warnings: ["Receipts are local-only and unsigned; trust claims are unavailable."]
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
let material;
|
|
54
|
+
try {
|
|
55
|
+
material = JSON.parse(rawMaterial);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return {
|
|
59
|
+
state: "tamper_detected",
|
|
60
|
+
reason: "Receipt integrity metadata is unreadable."
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const actualLoopRecordSha256 = sha256(rawLoopRecord);
|
|
64
|
+
if (actualLoopRecordSha256 !== material.loopRecordSha256) {
|
|
65
|
+
return {
|
|
66
|
+
state: "tamper_detected",
|
|
67
|
+
keyId: material.keyId,
|
|
68
|
+
signedAt: material.signedAt,
|
|
69
|
+
loopRecordSha256: material.loopRecordSha256,
|
|
70
|
+
ledgerSha256: material.ledgerSha256,
|
|
71
|
+
ledgerHeadHash: material.ledgerHeadHash,
|
|
72
|
+
entryCount: material.entryCount,
|
|
73
|
+
reason: "loop_record_hash_mismatch"
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const actualLedgerSha256 = sha256(rawLedger);
|
|
77
|
+
if (actualLedgerSha256 !== material.ledgerSha256) {
|
|
78
|
+
return {
|
|
79
|
+
state: "tamper_detected",
|
|
80
|
+
keyId: material.keyId,
|
|
81
|
+
signedAt: material.signedAt,
|
|
82
|
+
loopRecordSha256: material.loopRecordSha256,
|
|
83
|
+
ledgerSha256: material.ledgerSha256,
|
|
84
|
+
ledgerHeadHash: material.ledgerHeadHash,
|
|
85
|
+
entryCount: material.entryCount,
|
|
86
|
+
reason: "ledger_hash_mismatch"
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
let parsedLedgerEntries;
|
|
90
|
+
try {
|
|
91
|
+
parsedLedgerEntries = rawLedger
|
|
92
|
+
.split(/\r?\n/u)
|
|
93
|
+
.map((line) => line.trim())
|
|
94
|
+
.filter(Boolean)
|
|
95
|
+
.map((line) => JSON.parse(line));
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return {
|
|
99
|
+
state: "tamper_detected",
|
|
100
|
+
keyId: material.keyId,
|
|
101
|
+
signedAt: material.signedAt,
|
|
102
|
+
loopRecordSha256: material.loopRecordSha256,
|
|
103
|
+
ledgerSha256: material.ledgerSha256,
|
|
104
|
+
ledgerHeadHash: material.ledgerHeadHash,
|
|
105
|
+
entryCount: material.entryCount,
|
|
106
|
+
reason: "ledger_entry_parse_error"
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const actualChain = buildReceiptIntegrityChain(parsedLedgerEntries);
|
|
110
|
+
if (actualChain.length !== material.chain.length) {
|
|
111
|
+
return {
|
|
112
|
+
state: "tamper_detected",
|
|
113
|
+
keyId: material.keyId,
|
|
114
|
+
signedAt: material.signedAt,
|
|
115
|
+
loopRecordSha256: material.loopRecordSha256,
|
|
116
|
+
ledgerSha256: material.ledgerSha256,
|
|
117
|
+
ledgerHeadHash: material.ledgerHeadHash,
|
|
118
|
+
entryCount: material.entryCount,
|
|
119
|
+
reason: "ledger_entry_count_mismatch"
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
for (let index = 0; index < actualChain.length; index += 1) {
|
|
123
|
+
const expected = material.chain[index];
|
|
124
|
+
const actual = actualChain[index];
|
|
125
|
+
if (!expected ||
|
|
126
|
+
!actual ||
|
|
127
|
+
expected.entryHash !== actual.entryHash ||
|
|
128
|
+
expected.prevHash !== actual.prevHash) {
|
|
129
|
+
return {
|
|
130
|
+
state: "tamper_detected",
|
|
131
|
+
keyId: material.keyId,
|
|
132
|
+
signedAt: material.signedAt,
|
|
133
|
+
loopRecordSha256: material.loopRecordSha256,
|
|
134
|
+
ledgerSha256: material.ledgerSha256,
|
|
135
|
+
ledgerHeadHash: material.ledgerHeadHash,
|
|
136
|
+
entryCount: material.entryCount,
|
|
137
|
+
reason: `ledger_chain_mismatch:${index}`
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const signatureBase = {
|
|
142
|
+
schemaVersion: material.schemaVersion,
|
|
143
|
+
runId: material.runId,
|
|
144
|
+
keyId: material.keyId,
|
|
145
|
+
signedAt: material.signedAt,
|
|
146
|
+
...(material.scope ? { scope: material.scope } : {}),
|
|
147
|
+
loopRecordSha256: material.loopRecordSha256,
|
|
148
|
+
ledgerSha256: material.ledgerSha256,
|
|
149
|
+
ledgerHeadHash: material.ledgerHeadHash,
|
|
150
|
+
entryCount: material.entryCount,
|
|
151
|
+
chain: material.chain
|
|
152
|
+
};
|
|
153
|
+
const expectedSignature = createReceiptIntegritySignature(key, signatureBase);
|
|
154
|
+
if (expectedSignature !== material.signatureHmacSha256) {
|
|
155
|
+
return {
|
|
156
|
+
state: "tamper_detected",
|
|
157
|
+
keyId: material.keyId,
|
|
158
|
+
signedAt: material.signedAt,
|
|
159
|
+
loopRecordSha256: material.loopRecordSha256,
|
|
160
|
+
ledgerSha256: material.ledgerSha256,
|
|
161
|
+
ledgerHeadHash: material.ledgerHeadHash,
|
|
162
|
+
entryCount: material.entryCount,
|
|
163
|
+
reason: "signature_mismatch"
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
state: "verified",
|
|
168
|
+
keyId: material.keyId,
|
|
169
|
+
signedAt: material.signedAt,
|
|
170
|
+
loopRecordSha256: material.loopRecordSha256,
|
|
171
|
+
ledgerSha256: material.ledgerSha256,
|
|
172
|
+
ledgerHeadHash: material.ledgerHeadHash,
|
|
173
|
+
entryCount: material.entryCount
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
export function resolveReceiptIntegrityPath(runsRoot, runId) {
|
|
177
|
+
return join(runsRoot, runId, "receipt-integrity.json");
|
|
178
|
+
}
|
|
179
|
+
function buildReceiptIntegrityChain(entries) {
|
|
180
|
+
let previousHash = "root";
|
|
181
|
+
return entries.map((entry, index) => {
|
|
182
|
+
const normalizedEntry = JSON.stringify(entry);
|
|
183
|
+
const entryHash = sha256(`${previousHash}\n${normalizedEntry}`);
|
|
184
|
+
const candidate = entry;
|
|
185
|
+
const chainEntry = {
|
|
186
|
+
index,
|
|
187
|
+
prevHash: previousHash,
|
|
188
|
+
entryHash
|
|
189
|
+
};
|
|
190
|
+
if (typeof candidate.eventId === "string") {
|
|
191
|
+
chainEntry.eventId = candidate.eventId;
|
|
192
|
+
}
|
|
193
|
+
if (typeof candidate.type === "string") {
|
|
194
|
+
chainEntry.type = candidate.type;
|
|
195
|
+
}
|
|
196
|
+
if (typeof candidate.kind === "string") {
|
|
197
|
+
chainEntry.kind = candidate.kind;
|
|
198
|
+
}
|
|
199
|
+
if (typeof candidate.timestamp === "string") {
|
|
200
|
+
chainEntry.timestamp = candidate.timestamp;
|
|
201
|
+
}
|
|
202
|
+
previousHash = entryHash;
|
|
203
|
+
return chainEntry;
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
function createReceiptIntegritySignature(key, material) {
|
|
207
|
+
return createHmac("sha256", key).update(JSON.stringify(material)).digest("hex");
|
|
208
|
+
}
|
|
209
|
+
async function ensureReceiptIntegrityKey(runsRoot, runId) {
|
|
210
|
+
const keyPath = resolveReceiptIntegrityKeyPath(runsRoot, runId);
|
|
211
|
+
const existing = await readFile(keyPath, "utf8").catch(() => null);
|
|
212
|
+
if (existing) {
|
|
213
|
+
const trimmed = existing.trim();
|
|
214
|
+
return [trimmed, sha256(trimmed).slice(0, 16)];
|
|
215
|
+
}
|
|
216
|
+
const generated = randomBytes(32).toString("hex");
|
|
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
|
+
}
|
|
227
|
+
return [generated, sha256(generated).slice(0, 16)];
|
|
228
|
+
}
|
|
229
|
+
async function readReceiptIntegrityKey(runsRoot, runId) {
|
|
230
|
+
const raw = await readFile(resolveReceiptIntegrityKeyPath(runsRoot, runId), "utf8");
|
|
231
|
+
return raw.trim();
|
|
232
|
+
}
|
|
233
|
+
function resolveReceiptIntegrityKeyPath(runsRoot, runId) {
|
|
234
|
+
const rootHash = sha256(runsRoot).slice(0, 16);
|
|
235
|
+
return join(homedir(), ".martin", "receipt-integrity", rootHash, `${runId}.key`);
|
|
236
|
+
}
|
|
237
|
+
function serializeStoredJson(value) {
|
|
238
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
239
|
+
}
|
|
240
|
+
function serializeStoredJsonl(entries) {
|
|
241
|
+
if (entries.length === 0) {
|
|
242
|
+
return "";
|
|
243
|
+
}
|
|
244
|
+
return `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`;
|
|
245
|
+
}
|
|
246
|
+
function sha256(value) {
|
|
247
|
+
return createHash("sha256").update(value).digest("hex");
|
|
248
|
+
}
|