@martinloop/mcp 0.2.7 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +18 -2
- package/dist/tools/doctor.d.ts +12 -1
- package/dist/tools/doctor.js +37 -6
- package/dist/tools/eval.js +3 -2
- package/dist/tools/get-run.d.ts +2 -0
- package/dist/tools/get-run.js +2 -1
- package/dist/tools/get-verification-results.d.ts +2 -0
- package/dist/tools/get-verification-results.js +2 -1
- package/dist/tools/pr-tools.js +2 -1
- package/dist/tools/preflight.d.ts +14 -1
- package/dist/tools/preflight.js +36 -5
- package/dist/tools/run-dossier.d.ts +2 -0
- package/dist/tools/run-dossier.js +4 -2
- package/dist/tools/run-loop.d.ts +3 -2
- package/dist/tools/run-loop.js +48 -28
- package/dist/tools/tool-errors.js +1 -1
- package/dist/tools/tool-support.d.ts +6 -3
- package/dist/tools/tool-support.js +12 -5
- package/dist/vendor/adapters/claude-cli.d.ts +25 -0
- package/dist/vendor/adapters/claude-cli.js +279 -19
- package/dist/vendor/adapters/cli-bridge.d.ts +1 -0
- package/dist/vendor/adapters/cli-bridge.js +44 -3
- package/dist/vendor/adapters/codex-launcher.d.ts +44 -0
- package/dist/vendor/adapters/codex-launcher.js +247 -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 +44 -19
- package/dist/vendor/adapters/runtime-support.d.ts +3 -0
- package/dist/vendor/adapters/runtime-support.js +8 -1
- package/dist/vendor/adapters/verifier-only.js +4 -3
- package/dist/vendor/contracts/index.d.ts +39 -0
- package/dist/vendor/contracts/index.js +2 -0
- package/dist/vendor/core/index.d.ts +23 -3
- package/dist/vendor/core/index.js +88 -15
- 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 +239 -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/package.json +1 -1
- 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,7 +271,7 @@ 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
|
-
const contextPrecheck = await runContextIntegrityPrecheck(loop.loopId, loop.attempts.length + 1, runDir(
|
|
274
|
+
const contextPrecheck = await runContextIntegrityPrecheck(loop.loopId, loop.attempts.length + 1, runDir(resolveActiveRunsRoot(input.store), loop.loopId), {
|
|
274
275
|
userPrompt: distilled.focus,
|
|
275
276
|
history: loop.attempts.map(a => a.summary).join("\n")
|
|
276
277
|
});
|
|
@@ -418,6 +419,7 @@ export async function runMartin(input) {
|
|
|
418
419
|
const result = await executingAdapter.execute(request);
|
|
419
420
|
const attemptCompletedAt = now();
|
|
420
421
|
const compiledContext = compilePromptPacket(request);
|
|
422
|
+
const verification = normalizeVerificationOutcome(result);
|
|
421
423
|
// PATCH → VERIFY
|
|
422
424
|
currentPhase = "VERIFY";
|
|
423
425
|
let failure = result.status === "failed"
|
|
@@ -444,7 +446,16 @@ export async function runMartin(input) {
|
|
|
444
446
|
actualUsd: roundUsd(loop.cost.actualUsd + getUsageUsd(result.usage)),
|
|
445
447
|
avoidedUsd: loop.cost.avoidedUsd,
|
|
446
448
|
tokensIn: loop.cost.tokensIn + result.usage.tokensIn,
|
|
447
|
-
tokensOut: loop.cost.tokensOut + result.usage.tokensOut
|
|
449
|
+
tokensOut: loop.cost.tokensOut + result.usage.tokensOut,
|
|
450
|
+
...(result.usage.estimatedUsd !== undefined
|
|
451
|
+
? {
|
|
452
|
+
estimatedUsd: roundUsd((loop.cost.estimatedUsd ?? loop.cost.actualUsd) + result.usage.estimatedUsd)
|
|
453
|
+
}
|
|
454
|
+
: {}),
|
|
455
|
+
provenance: getUsageProvenance(result.usage),
|
|
456
|
+
...(result.usage.providerSettlement
|
|
457
|
+
? { providerSettlement: result.usage.providerSettlement }
|
|
458
|
+
: {})
|
|
448
459
|
},
|
|
449
460
|
updatedAt: attemptCompletedAt
|
|
450
461
|
};
|
|
@@ -503,11 +514,14 @@ export async function runMartin(input) {
|
|
|
503
514
|
}
|
|
504
515
|
loop = appendLoopEvent(loop, {
|
|
505
516
|
type: "verification.completed",
|
|
506
|
-
lifecycleState:
|
|
517
|
+
lifecycleState: verification.passed ? "completed" : "verifying",
|
|
507
518
|
payload: {
|
|
508
519
|
attemptId,
|
|
509
|
-
|
|
510
|
-
|
|
520
|
+
attemptIndex: currentAttemptIndex,
|
|
521
|
+
passed: verification.passed,
|
|
522
|
+
summary: verification.summary,
|
|
523
|
+
...(verification.steps?.length ? { steps: verification.steps } : {}),
|
|
524
|
+
...(verification.warnings?.length ? { warnings: verification.warnings } : {})
|
|
511
525
|
}
|
|
512
526
|
}, { now: attemptCompletedAt, idFactory });
|
|
513
527
|
const costState = evaluateCostGovernor({
|
|
@@ -534,6 +548,7 @@ export async function runMartin(input) {
|
|
|
534
548
|
});
|
|
535
549
|
await input.store.writeAttemptArtifacts(loop.loopId, currentAttemptIndex, {
|
|
536
550
|
compiledContext,
|
|
551
|
+
verification,
|
|
537
552
|
...(rollbackBoundary ? { rollbackBoundary } : {})
|
|
538
553
|
});
|
|
539
554
|
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
@@ -546,7 +561,13 @@ export async function runMartin(input) {
|
|
|
546
561
|
kind: "verification.completed",
|
|
547
562
|
runId: loop.loopId,
|
|
548
563
|
attemptIndex: currentAttemptIndex,
|
|
549
|
-
payload: {
|
|
564
|
+
payload: {
|
|
565
|
+
attemptId,
|
|
566
|
+
passed: verification.passed,
|
|
567
|
+
summary: verification.summary,
|
|
568
|
+
...(verification.steps?.length ? { steps: verification.steps } : {}),
|
|
569
|
+
...(verification.warnings?.length ? { warnings: verification.warnings } : {})
|
|
570
|
+
}
|
|
550
571
|
}));
|
|
551
572
|
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
552
573
|
kind: "budget.settled",
|
|
@@ -557,10 +578,13 @@ export async function runMartin(input) {
|
|
|
557
578
|
estimatedUsd: result.usage.estimatedUsd,
|
|
558
579
|
tokensIn: result.usage.tokensIn,
|
|
559
580
|
tokensOut: result.usage.tokensOut,
|
|
581
|
+
cachedInputTokens: result.usage.cachedInputTokens,
|
|
582
|
+
reasoningTokensOut: result.usage.reasoningTokensOut,
|
|
560
583
|
provenance: getUsageProvenance(result.usage),
|
|
561
584
|
transport: getAdapterTransport(executingAdapter),
|
|
562
585
|
providerId: executingAdapter.metadata.providerId,
|
|
563
586
|
model: executingAdapter.metadata.model,
|
|
587
|
+
providerSettlement: result.usage.providerSettlement,
|
|
564
588
|
patchCost: settlement.patchCost,
|
|
565
589
|
verificationCost: settlement.verificationCost,
|
|
566
590
|
varianceUsd: settlement.varianceUsd,
|
|
@@ -573,11 +597,13 @@ export async function runMartin(input) {
|
|
|
573
597
|
// returned a non-empty list. A repoRoot alone is insufficient — git may fail (e.g. not
|
|
574
598
|
// a git repo) and silently return [], which would falsely trigger no_code_change.
|
|
575
599
|
const changedFileEvidenceAvailable = result.execution?.changedFiles !== undefined || changedFiles.length > 0;
|
|
600
|
+
const isVerifierOnlyAdapter = executingAdapter.adapterId === "direct:verifier:verify-only";
|
|
601
|
+
const patchTruthCountsEdits = !isVerifyOnly && !isVerifierOnlyAdapter && changedFileEvidenceAvailable;
|
|
576
602
|
if (isVerifyOnly && changedFiles.length > 0) {
|
|
577
603
|
const patchDecision = evaluatePatchDecision({
|
|
578
|
-
verificationPassed:
|
|
604
|
+
verificationPassed: verification.passed,
|
|
579
605
|
previousVerifierScore,
|
|
580
|
-
verifierScore:
|
|
606
|
+
verifierScore: verification.passed ? 1 : 0,
|
|
581
607
|
scopeViolationCount: changedFiles.length,
|
|
582
608
|
changedFileCount: changedFiles.length,
|
|
583
609
|
diffNovelty: 1,
|
|
@@ -842,12 +868,12 @@ export async function runMartin(input) {
|
|
|
842
868
|
let patchDecision;
|
|
843
869
|
if (result.status === "completed") {
|
|
844
870
|
patchDecision = evaluatePatchDecision({
|
|
845
|
-
verificationPassed:
|
|
871
|
+
verificationPassed: verification.passed,
|
|
846
872
|
previousVerifierScore,
|
|
847
|
-
verifierScore:
|
|
873
|
+
verifierScore: verification.passed ? 1 : 0,
|
|
848
874
|
groundingViolationCount: groundingScanResult?.violations.length ?? 0,
|
|
849
|
-
changedFileCount:
|
|
850
|
-
diffNovelty:
|
|
875
|
+
changedFileCount: patchTruthCountsEdits ? changedFiles.length : undefined,
|
|
876
|
+
diffNovelty: patchTruthCountsEdits ? (changedFiles.length > 0 ? 1 : 0) : undefined,
|
|
851
877
|
diffStats: result.execution?.diffStats,
|
|
852
878
|
costUsd: getUsageUsd(result.usage),
|
|
853
879
|
summary: result.summary
|
|
@@ -897,10 +923,10 @@ export async function runMartin(input) {
|
|
|
897
923
|
}
|
|
898
924
|
else {
|
|
899
925
|
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
900
|
-
kind:
|
|
926
|
+
kind: verification.passed ? "attempt.kept" : "attempt.discarded",
|
|
901
927
|
runId: loop.loopId,
|
|
902
928
|
attemptIndex: currentAttemptIndex,
|
|
903
|
-
payload: { reason:
|
|
929
|
+
payload: { reason: verification.summary }
|
|
904
930
|
}));
|
|
905
931
|
}
|
|
906
932
|
}
|
|
@@ -1145,6 +1171,9 @@ function createBudgetSettlement(input) {
|
|
|
1145
1171
|
usd: 0,
|
|
1146
1172
|
provenance: "unavailable"
|
|
1147
1173
|
},
|
|
1174
|
+
...(input.usage.providerSettlement
|
|
1175
|
+
? { providerSettlement: input.usage.providerSettlement }
|
|
1176
|
+
: {}),
|
|
1148
1177
|
totalActualUsd,
|
|
1149
1178
|
preflightEstimateUsd: input.estimate.estimatedAttemptCostUsd,
|
|
1150
1179
|
varianceUsd: roundUsd(totalActualUsd - input.estimate.estimatedAttemptCostUsd),
|
|
@@ -1185,6 +1214,50 @@ function getLastVerifierScore(loop) {
|
|
|
1185
1214
|
}
|
|
1186
1215
|
return 0;
|
|
1187
1216
|
}
|
|
1217
|
+
function truncateVerificationDetail(text, maxLength) {
|
|
1218
|
+
return text.length <= maxLength ? text : `${text.slice(0, maxLength - 1)}…`;
|
|
1219
|
+
}
|
|
1220
|
+
function normalizeVerificationOutcome(result) {
|
|
1221
|
+
const warnings = [...(result.verification.warnings ?? [])];
|
|
1222
|
+
const contradiction = detectVerificationContradiction(result);
|
|
1223
|
+
if (contradiction && !warnings.includes(contradiction)) {
|
|
1224
|
+
warnings.push(contradiction);
|
|
1225
|
+
}
|
|
1226
|
+
return {
|
|
1227
|
+
passed: result.verification.passed,
|
|
1228
|
+
summary: result.verification.summary,
|
|
1229
|
+
...(result.verification.steps?.length ? { steps: result.verification.steps } : {}),
|
|
1230
|
+
...(warnings.length ? { warnings } : {})
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
function detectVerificationContradiction(result) {
|
|
1234
|
+
if (!result.verification.passed) {
|
|
1235
|
+
return undefined;
|
|
1236
|
+
}
|
|
1237
|
+
const sources = [result.summary, result.failure?.message]
|
|
1238
|
+
.filter((value) => typeof value === "string" && value.trim().length > 0);
|
|
1239
|
+
const contradictionPatterns = [
|
|
1240
|
+
/createprocessasuserw failed:\s*\d+/iu,
|
|
1241
|
+
/\bverifier not run\b/iu,
|
|
1242
|
+
/\bfailed before verifier\b/iu,
|
|
1243
|
+
/\bnever launched\b/iu,
|
|
1244
|
+
/\bfailed to launch\b/iu,
|
|
1245
|
+
/\bcould not launch\b/iu
|
|
1246
|
+
];
|
|
1247
|
+
for (const source of sources) {
|
|
1248
|
+
const match = contradictionPatterns.find((pattern) => pattern.test(source));
|
|
1249
|
+
if (!match) {
|
|
1250
|
+
continue;
|
|
1251
|
+
}
|
|
1252
|
+
const excerpt = truncateVerificationDetail(source.trim().replace(/\s+/gu, " "), 220);
|
|
1253
|
+
return `Adapter output reported a tool-launch problem before MartinLoop ran its own verifier: ${excerpt}`;
|
|
1254
|
+
}
|
|
1255
|
+
return undefined;
|
|
1256
|
+
}
|
|
1257
|
+
function resolveActiveRunsRoot(store) {
|
|
1258
|
+
const configuredRunsRoot = store?.runsRoot?.trim();
|
|
1259
|
+
return configuredRunsRoot && configuredRunsRoot.length > 0 ? configuredRunsRoot : resolveRunsRoot();
|
|
1260
|
+
}
|
|
1188
1261
|
function toPatchDecisionArtifact(decision) {
|
|
1189
1262
|
return {
|
|
1190
1263
|
decision: decision.decision,
|
|
@@ -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>;
|
|
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,239 @@
|
|
|
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 [key, keyId] = await ensureReceiptIntegrityKey(input.runsRoot, input.runId);
|
|
11
|
+
const chain = buildReceiptIntegrityChain(input.ledgerEntries);
|
|
12
|
+
const loopRecordRaw = serializeStoredJson(input.loopRecord);
|
|
13
|
+
const ledgerRaw = serializeStoredJsonl(input.ledgerEntries);
|
|
14
|
+
const materialBase = {
|
|
15
|
+
schemaVersion: RECEIPT_INTEGRITY_SCHEMA_VERSION,
|
|
16
|
+
runId: input.runId,
|
|
17
|
+
keyId,
|
|
18
|
+
signedAt,
|
|
19
|
+
...(input.scope ? { scope: input.scope } : {}),
|
|
20
|
+
loopRecordSha256: sha256(loopRecordRaw),
|
|
21
|
+
ledgerSha256: sha256(ledgerRaw),
|
|
22
|
+
ledgerHeadHash: chain.at(-1)?.entryHash ?? "root",
|
|
23
|
+
entryCount: chain.length,
|
|
24
|
+
chain
|
|
25
|
+
};
|
|
26
|
+
const material = {
|
|
27
|
+
...materialBase,
|
|
28
|
+
signatureHmacSha256: createReceiptIntegritySignature(key, materialBase)
|
|
29
|
+
};
|
|
30
|
+
await mkdir(join(input.runsRoot, input.runId), { recursive: true });
|
|
31
|
+
await writeFile(resolveReceiptIntegrityPath(input.runsRoot, input.runId), serializeStoredJson(material), "utf8");
|
|
32
|
+
return material;
|
|
33
|
+
}
|
|
34
|
+
export async function verifyReceiptIntegrityFromFiles(input) {
|
|
35
|
+
const integrityPath = resolveReceiptIntegrityPath(input.runsRoot, input.runId);
|
|
36
|
+
const [rawMaterial, rawLoopRecord, rawLedger, key] = await Promise.all([
|
|
37
|
+
readFile(integrityPath, "utf8").catch(() => null),
|
|
38
|
+
readFile(input.loopRecordPath, "utf8").catch(() => null),
|
|
39
|
+
readFile(input.ledgerPath, "utf8").catch(() => null),
|
|
40
|
+
readReceiptIntegrityKey(input.runsRoot, input.runId).catch(() => null)
|
|
41
|
+
]);
|
|
42
|
+
if (!rawMaterial || !rawLoopRecord || rawLedger === null || !key) {
|
|
43
|
+
return {
|
|
44
|
+
state: "unsigned",
|
|
45
|
+
reason: "Receipt integrity material is missing for this run.",
|
|
46
|
+
warnings: ["Receipts are local-only and unsigned; trust claims are unavailable."]
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
let material;
|
|
50
|
+
try {
|
|
51
|
+
material = JSON.parse(rawMaterial);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return {
|
|
55
|
+
state: "tamper_detected",
|
|
56
|
+
reason: "Receipt integrity metadata is unreadable."
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const actualLoopRecordSha256 = sha256(rawLoopRecord);
|
|
60
|
+
if (actualLoopRecordSha256 !== material.loopRecordSha256) {
|
|
61
|
+
return {
|
|
62
|
+
state: "tamper_detected",
|
|
63
|
+
keyId: material.keyId,
|
|
64
|
+
signedAt: material.signedAt,
|
|
65
|
+
loopRecordSha256: material.loopRecordSha256,
|
|
66
|
+
ledgerSha256: material.ledgerSha256,
|
|
67
|
+
ledgerHeadHash: material.ledgerHeadHash,
|
|
68
|
+
entryCount: material.entryCount,
|
|
69
|
+
reason: "loop_record_hash_mismatch"
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const actualLedgerSha256 = sha256(rawLedger);
|
|
73
|
+
if (actualLedgerSha256 !== material.ledgerSha256) {
|
|
74
|
+
return {
|
|
75
|
+
state: "tamper_detected",
|
|
76
|
+
keyId: material.keyId,
|
|
77
|
+
signedAt: material.signedAt,
|
|
78
|
+
loopRecordSha256: material.loopRecordSha256,
|
|
79
|
+
ledgerSha256: material.ledgerSha256,
|
|
80
|
+
ledgerHeadHash: material.ledgerHeadHash,
|
|
81
|
+
entryCount: material.entryCount,
|
|
82
|
+
reason: "ledger_hash_mismatch"
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
let parsedLedgerEntries;
|
|
86
|
+
try {
|
|
87
|
+
parsedLedgerEntries = rawLedger
|
|
88
|
+
.split(/\r?\n/u)
|
|
89
|
+
.map((line) => line.trim())
|
|
90
|
+
.filter(Boolean)
|
|
91
|
+
.map((line) => JSON.parse(line));
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return {
|
|
95
|
+
state: "tamper_detected",
|
|
96
|
+
keyId: material.keyId,
|
|
97
|
+
signedAt: material.signedAt,
|
|
98
|
+
loopRecordSha256: material.loopRecordSha256,
|
|
99
|
+
ledgerSha256: material.ledgerSha256,
|
|
100
|
+
ledgerHeadHash: material.ledgerHeadHash,
|
|
101
|
+
entryCount: material.entryCount,
|
|
102
|
+
reason: "ledger_entry_parse_error"
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const actualChain = buildReceiptIntegrityChain(parsedLedgerEntries);
|
|
106
|
+
if (actualChain.length !== material.chain.length) {
|
|
107
|
+
return {
|
|
108
|
+
state: "tamper_detected",
|
|
109
|
+
keyId: material.keyId,
|
|
110
|
+
signedAt: material.signedAt,
|
|
111
|
+
loopRecordSha256: material.loopRecordSha256,
|
|
112
|
+
ledgerSha256: material.ledgerSha256,
|
|
113
|
+
ledgerHeadHash: material.ledgerHeadHash,
|
|
114
|
+
entryCount: material.entryCount,
|
|
115
|
+
reason: "ledger_entry_count_mismatch"
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
for (let index = 0; index < actualChain.length; index += 1) {
|
|
119
|
+
const expected = material.chain[index];
|
|
120
|
+
const actual = actualChain[index];
|
|
121
|
+
if (!expected ||
|
|
122
|
+
!actual ||
|
|
123
|
+
expected.entryHash !== actual.entryHash ||
|
|
124
|
+
expected.prevHash !== actual.prevHash) {
|
|
125
|
+
return {
|
|
126
|
+
state: "tamper_detected",
|
|
127
|
+
keyId: material.keyId,
|
|
128
|
+
signedAt: material.signedAt,
|
|
129
|
+
loopRecordSha256: material.loopRecordSha256,
|
|
130
|
+
ledgerSha256: material.ledgerSha256,
|
|
131
|
+
ledgerHeadHash: material.ledgerHeadHash,
|
|
132
|
+
entryCount: material.entryCount,
|
|
133
|
+
reason: `ledger_chain_mismatch:${index}`
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const signatureBase = {
|
|
138
|
+
schemaVersion: material.schemaVersion,
|
|
139
|
+
runId: material.runId,
|
|
140
|
+
keyId: material.keyId,
|
|
141
|
+
signedAt: material.signedAt,
|
|
142
|
+
...(material.scope ? { scope: material.scope } : {}),
|
|
143
|
+
loopRecordSha256: material.loopRecordSha256,
|
|
144
|
+
ledgerSha256: material.ledgerSha256,
|
|
145
|
+
ledgerHeadHash: material.ledgerHeadHash,
|
|
146
|
+
entryCount: material.entryCount,
|
|
147
|
+
chain: material.chain
|
|
148
|
+
};
|
|
149
|
+
const expectedSignature = createReceiptIntegritySignature(key, signatureBase);
|
|
150
|
+
if (expectedSignature !== material.signatureHmacSha256) {
|
|
151
|
+
return {
|
|
152
|
+
state: "tamper_detected",
|
|
153
|
+
keyId: material.keyId,
|
|
154
|
+
signedAt: material.signedAt,
|
|
155
|
+
loopRecordSha256: material.loopRecordSha256,
|
|
156
|
+
ledgerSha256: material.ledgerSha256,
|
|
157
|
+
ledgerHeadHash: material.ledgerHeadHash,
|
|
158
|
+
entryCount: material.entryCount,
|
|
159
|
+
reason: "signature_mismatch"
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
state: "verified",
|
|
164
|
+
keyId: material.keyId,
|
|
165
|
+
signedAt: material.signedAt,
|
|
166
|
+
loopRecordSha256: material.loopRecordSha256,
|
|
167
|
+
ledgerSha256: material.ledgerSha256,
|
|
168
|
+
ledgerHeadHash: material.ledgerHeadHash,
|
|
169
|
+
entryCount: material.entryCount
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
export function resolveReceiptIntegrityPath(runsRoot, runId) {
|
|
173
|
+
return join(runsRoot, runId, "receipt-integrity.json");
|
|
174
|
+
}
|
|
175
|
+
function buildReceiptIntegrityChain(entries) {
|
|
176
|
+
let previousHash = "root";
|
|
177
|
+
return entries.map((entry, index) => {
|
|
178
|
+
const normalizedEntry = JSON.stringify(entry);
|
|
179
|
+
const entryHash = sha256(`${previousHash}\n${normalizedEntry}`);
|
|
180
|
+
const candidate = entry;
|
|
181
|
+
const chainEntry = {
|
|
182
|
+
index,
|
|
183
|
+
prevHash: previousHash,
|
|
184
|
+
entryHash
|
|
185
|
+
};
|
|
186
|
+
if (typeof candidate.eventId === "string") {
|
|
187
|
+
chainEntry.eventId = candidate.eventId;
|
|
188
|
+
}
|
|
189
|
+
if (typeof candidate.type === "string") {
|
|
190
|
+
chainEntry.type = candidate.type;
|
|
191
|
+
}
|
|
192
|
+
if (typeof candidate.kind === "string") {
|
|
193
|
+
chainEntry.kind = candidate.kind;
|
|
194
|
+
}
|
|
195
|
+
if (typeof candidate.timestamp === "string") {
|
|
196
|
+
chainEntry.timestamp = candidate.timestamp;
|
|
197
|
+
}
|
|
198
|
+
previousHash = entryHash;
|
|
199
|
+
return chainEntry;
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
function createReceiptIntegritySignature(key, material) {
|
|
203
|
+
return createHmac("sha256", key).update(JSON.stringify(material)).digest("hex");
|
|
204
|
+
}
|
|
205
|
+
async function ensureReceiptIntegrityKey(runsRoot, runId) {
|
|
206
|
+
const keyPath = resolveReceiptIntegrityKeyPath(runsRoot, runId);
|
|
207
|
+
const existing = await readFile(keyPath, "utf8").catch(() => null);
|
|
208
|
+
if (existing) {
|
|
209
|
+
const trimmed = existing.trim();
|
|
210
|
+
return [trimmed, sha256(trimmed).slice(0, 16)];
|
|
211
|
+
}
|
|
212
|
+
const generated = randomBytes(32).toString("hex");
|
|
213
|
+
await mkdir(dirname(keyPath), { recursive: true, mode: RECEIPT_INTEGRITY_KEY_DIR_MODE });
|
|
214
|
+
await writeFile(keyPath, `${generated}\n`, {
|
|
215
|
+
encoding: "utf8",
|
|
216
|
+
mode: RECEIPT_INTEGRITY_KEY_FILE_MODE
|
|
217
|
+
});
|
|
218
|
+
return [generated, sha256(generated).slice(0, 16)];
|
|
219
|
+
}
|
|
220
|
+
async function readReceiptIntegrityKey(runsRoot, runId) {
|
|
221
|
+
const raw = await readFile(resolveReceiptIntegrityKeyPath(runsRoot, runId), "utf8");
|
|
222
|
+
return raw.trim();
|
|
223
|
+
}
|
|
224
|
+
function resolveReceiptIntegrityKeyPath(runsRoot, runId) {
|
|
225
|
+
const rootHash = sha256(runsRoot).slice(0, 16);
|
|
226
|
+
return join(homedir(), ".martin", "receipt-integrity", rootHash, `${runId}.key`);
|
|
227
|
+
}
|
|
228
|
+
function serializeStoredJson(value) {
|
|
229
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
230
|
+
}
|
|
231
|
+
function serializeStoredJsonl(entries) {
|
|
232
|
+
if (entries.length === 0) {
|
|
233
|
+
return "";
|
|
234
|
+
}
|
|
235
|
+
return `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`;
|
|
236
|
+
}
|
|
237
|
+
function sha256(value) {
|
|
238
|
+
return createHash("sha256").update(value).digest("hex");
|
|
239
|
+
}
|
|
@@ -12,6 +12,8 @@ export interface RunContract {
|
|
|
12
12
|
export interface AttemptArtifacts {
|
|
13
13
|
/** Compiled PromptPacket written as compiled-context.json */
|
|
14
14
|
compiledContext: unknown;
|
|
15
|
+
/** Structured verification evidence captured from the authoritative MartinLoop verifier (optional) */
|
|
16
|
+
verification?: unknown;
|
|
15
17
|
/** Unified diff string from the patch (optional) */
|
|
16
18
|
diff?: string;
|
|
17
19
|
/** Raw verifier command output (optional) */
|
|
@@ -35,6 +37,11 @@ export interface AttemptArtifacts {
|
|
|
35
37
|
* is durably written before the run proceeds to the next step.
|
|
36
38
|
*/
|
|
37
39
|
export interface RunStore {
|
|
40
|
+
/**
|
|
41
|
+
* Optional runs root hint for filesystem-backed stores.
|
|
42
|
+
* Orchestration should prefer this over recomputing the default home store.
|
|
43
|
+
*/
|
|
44
|
+
runsRoot?: string;
|
|
38
45
|
/**
|
|
39
46
|
* Write contract.json for a new run. Called once at run start.
|
|
40
47
|
* The contract is immutable after this point.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
|
-
import { appendFile, mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { writeReceiptIntegrityMaterial } from "./integrity.js";
|
|
5
6
|
// ─── FileRunStore implementation ─────────────────────────────────────────────
|
|
6
7
|
export function resolveRunsRoot(env = process.env) {
|
|
7
8
|
return env["MARTIN_RUNS_DIR"]?.trim() ??
|
|
@@ -31,6 +32,7 @@ export function artifactDir(runsRoot, runId, attemptIndex) {
|
|
|
31
32
|
export function createFileRunStore(options = {}) {
|
|
32
33
|
const runsRoot = options.runsRoot ?? resolveRunsRoot();
|
|
33
34
|
return {
|
|
35
|
+
runsRoot,
|
|
34
36
|
async initRun(contract) {
|
|
35
37
|
const dir = runDir(runsRoot, contract.runId);
|
|
36
38
|
await mkdir(dir, { recursive: true });
|
|
@@ -50,6 +52,9 @@ export function createFileRunStore(options = {}) {
|
|
|
50
52
|
const dir = artifactDir(runsRoot, runId, attemptIndex);
|
|
51
53
|
await mkdir(dir, { recursive: true });
|
|
52
54
|
await writeJsonFile(join(dir, "compiled-context.json"), artifacts.compiledContext);
|
|
55
|
+
if (artifacts.verification !== undefined) {
|
|
56
|
+
await writeJsonFile(join(dir, "verification.json"), artifacts.verification);
|
|
57
|
+
}
|
|
53
58
|
if (artifacts.diff !== undefined) {
|
|
54
59
|
await writeFile(join(dir, "diff.patch"), artifacts.diff, "utf8");
|
|
55
60
|
}
|
|
@@ -79,6 +84,25 @@ export function createFileRunStore(options = {}) {
|
|
|
79
84
|
const dir = runDir(runsRoot, runId);
|
|
80
85
|
await mkdir(dir, { recursive: true });
|
|
81
86
|
await writeJsonFile(join(dir, "loop-record.json"), loop);
|
|
87
|
+
const ledgerRaw = await readFile(join(dir, "ledger.jsonl"), "utf8").catch(() => "");
|
|
88
|
+
const ledgerEntries = ledgerRaw
|
|
89
|
+
.split(/\r?\n/u)
|
|
90
|
+
.map((line) => line.trim())
|
|
91
|
+
.filter(Boolean)
|
|
92
|
+
.map((line) => JSON.parse(line));
|
|
93
|
+
await writeReceiptIntegrityMaterial({
|
|
94
|
+
runId,
|
|
95
|
+
runsRoot,
|
|
96
|
+
loopRecord: loop,
|
|
97
|
+
ledgerEntries,
|
|
98
|
+
scope: loop.receiptScope ??
|
|
99
|
+
{
|
|
100
|
+
...(loop.task.repoRoot ? { repoRoot: loop.task.repoRoot } : {}),
|
|
101
|
+
...(loop.task.repoRoot ? { workingDirectory: loop.task.repoRoot } : {}),
|
|
102
|
+
runsRoot
|
|
103
|
+
},
|
|
104
|
+
signedAt: loop.updatedAt
|
|
105
|
+
});
|
|
82
106
|
}
|
|
83
107
|
};
|
|
84
108
|
}
|
|
@@ -38,6 +38,15 @@ export interface MartinAdapterResultLike {
|
|
|
38
38
|
verification: {
|
|
39
39
|
passed: boolean;
|
|
40
40
|
summary: string;
|
|
41
|
+
steps?: Array<{
|
|
42
|
+
command: string;
|
|
43
|
+
launched: boolean;
|
|
44
|
+
exitCode?: number;
|
|
45
|
+
timedOut: boolean;
|
|
46
|
+
fastFail?: boolean;
|
|
47
|
+
detail?: string;
|
|
48
|
+
}>;
|
|
49
|
+
warnings?: string[];
|
|
41
50
|
};
|
|
42
51
|
failure?: {
|
|
43
52
|
message: string;
|
package/package.json
CHANGED
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.
|
|
10
|
+
"version": "0.3.0",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "@martinloop/mcp",
|
|
15
|
-
"version": "0.
|
|
15
|
+
"version": "0.3.0",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
}
|