@martinloop/mcp 0.3.6 → 0.3.8
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/LICENSE +216 -0
- package/README.md +105 -97
- package/dist/discovery-metadata.d.ts +1 -1
- package/dist/discovery-metadata.js +1 -1
- package/dist/package-version.d.ts +1 -1
- package/dist/package-version.js +1 -1
- package/dist/resources.d.ts +2 -1
- package/dist/resources.js +251 -197
- package/dist/server-validation.d.ts +1 -1
- package/dist/server-validation.js +11 -16
- package/dist/server.d.ts +0 -23
- package/dist/server.js +7 -152
- package/dist/tools/preflight.d.ts +2 -0
- package/dist/tools/preflight.js +42 -2
- package/dist/tools/run-loop.d.ts +2 -1
- package/dist/tools/run-loop.js +5 -1
- package/dist/tools/tool-support.js +12 -1
- package/dist/vendor/adapters/adapters/src/claude-cli.d.ts +146 -0
- package/dist/vendor/adapters/adapters/src/claude-cli.js +1100 -0
- package/dist/vendor/adapters/adapters/src/cli-bridge.d.ts +103 -0
- package/dist/vendor/adapters/adapters/src/cli-bridge.js +569 -0
- package/dist/vendor/adapters/adapters/src/codex-launcher.d.ts +78 -0
- package/dist/vendor/adapters/adapters/src/codex-launcher.js +663 -0
- package/dist/vendor/adapters/adapters/src/direct-provider.d.ts +10 -0
- package/dist/vendor/adapters/adapters/src/direct-provider.js +40 -0
- package/dist/vendor/adapters/adapters/src/index.d.ts +8 -0
- package/dist/vendor/adapters/adapters/src/index.js +8 -0
- package/dist/vendor/adapters/adapters/src/openai-compatible.d.ts +62 -0
- package/dist/vendor/adapters/adapters/src/openai-compatible.js +306 -0
- package/dist/vendor/adapters/adapters/src/runtime-support.d.ts +17 -0
- package/dist/vendor/adapters/adapters/src/runtime-support.js +59 -0
- package/dist/vendor/adapters/adapters/src/stub-agent-cli.d.ts +8 -0
- package/dist/vendor/adapters/adapters/src/stub-agent-cli.js +40 -0
- package/dist/vendor/adapters/adapters/src/stub-direct-provider.d.ts +8 -0
- package/dist/vendor/adapters/adapters/src/stub-direct-provider.js +12 -0
- package/dist/vendor/adapters/adapters/src/verifier-only.d.ts +9 -0
- package/dist/vendor/adapters/adapters/src/verifier-only.js +63 -0
- package/dist/vendor/adapters/claude-cli.js +130 -68
- package/dist/vendor/adapters/codex-launcher.js +10 -1
- package/dist/vendor/adapters/contracts/src/context-graph.d.ts +48 -0
- package/dist/vendor/adapters/contracts/src/context-graph.js +26 -0
- package/dist/vendor/adapters/contracts/src/execution-policy.d.ts +78 -0
- package/dist/vendor/adapters/contracts/src/execution-policy.js +43 -0
- package/dist/vendor/adapters/contracts/src/governance.d.ts +24 -0
- package/dist/vendor/adapters/contracts/src/governance.js +11 -0
- package/dist/vendor/adapters/contracts/src/identity.d.ts +29 -0
- package/dist/vendor/adapters/contracts/src/identity.js +30 -0
- package/dist/vendor/adapters/contracts/src/index.d.ts +412 -0
- package/dist/vendor/adapters/contracts/src/index.js +225 -0
- package/dist/vendor/adapters/contracts/src/operator.d.ts +19 -0
- package/dist/vendor/adapters/contracts/src/operator.js +11 -0
- package/dist/vendor/adapters/contracts/src/trajectory.d.ts +26 -0
- package/dist/vendor/adapters/contracts/src/trajectory.js +27 -0
- package/dist/vendor/adapters/core/src/compiler.d.ts +52 -0
- package/dist/vendor/adapters/core/src/compiler.js +52 -0
- package/dist/vendor/adapters/core/src/context-graph.d.ts +3 -0
- package/dist/vendor/adapters/core/src/context-graph.js +371 -0
- package/dist/vendor/adapters/core/src/context-integrity.d.ts +26 -0
- package/dist/vendor/adapters/core/src/context-integrity.js +80 -0
- package/dist/vendor/adapters/core/src/grounding.d.ts +38 -0
- package/dist/vendor/adapters/core/src/grounding.js +273 -0
- package/dist/vendor/adapters/core/src/identity.d.ts +20 -0
- package/dist/vendor/adapters/core/src/identity.js +86 -0
- package/dist/vendor/adapters/core/src/index.d.ts +186 -0
- package/dist/vendor/adapters/core/src/index.js +1495 -0
- package/dist/vendor/adapters/core/src/leash.d.ts +48 -0
- package/dist/vendor/adapters/core/src/leash.js +497 -0
- package/dist/vendor/adapters/core/src/persistence/compiler.d.ts +18 -0
- package/dist/vendor/adapters/core/src/persistence/compiler.js +34 -0
- package/dist/vendor/adapters/core/src/persistence/index.d.ts +10 -0
- package/dist/vendor/adapters/core/src/persistence/index.js +5 -0
- package/dist/vendor/adapters/core/src/persistence/integrity.d.ts +39 -0
- package/dist/vendor/adapters/core/src/persistence/integrity.js +252 -0
- package/dist/vendor/adapters/core/src/persistence/ledger.d.ts +23 -0
- package/dist/vendor/adapters/core/src/persistence/ledger.js +9 -0
- package/dist/vendor/adapters/core/src/persistence/memory-store.d.ts +63 -0
- package/dist/vendor/adapters/core/src/persistence/memory-store.js +115 -0
- package/dist/vendor/adapters/core/src/persistence/runs-reader.d.ts +66 -0
- package/dist/vendor/adapters/core/src/persistence/runs-reader.js +140 -0
- package/dist/vendor/adapters/core/src/persistence/store.d.ts +89 -0
- package/dist/vendor/adapters/core/src/persistence/store.js +125 -0
- package/dist/vendor/adapters/core/src/persistence/trace-store.d.ts +85 -0
- package/dist/vendor/adapters/core/src/persistence/trace-store.js +125 -0
- package/dist/vendor/adapters/core/src/policy-compiler.d.ts +2 -0
- package/dist/vendor/adapters/core/src/policy-compiler.js +195 -0
- package/dist/vendor/adapters/core/src/policy.d.ts +156 -0
- package/dist/vendor/adapters/core/src/policy.js +704 -0
- package/dist/vendor/adapters/core/src/repo-analyzer.d.ts +53 -0
- package/dist/vendor/adapters/core/src/repo-analyzer.js +263 -0
- package/dist/vendor/adapters/core/src/rollback.d.ts +15 -0
- package/dist/vendor/adapters/core/src/rollback.js +253 -0
- package/dist/vendor/adapters/core/src/routing.d.ts +82 -0
- package/dist/vendor/adapters/core/src/routing.js +336 -0
- package/dist/vendor/adapters/core/src/test-integrity.d.ts +64 -0
- package/dist/vendor/adapters/core/src/test-integrity.js +197 -0
- package/dist/vendor/adapters/core/src/trajectory.d.ts +18 -0
- package/dist/vendor/adapters/core/src/trajectory.js +295 -0
- package/dist/vendor/adapters/openai-compatible.js +6 -6
- package/dist/vendor/adapters/verifier-only.js +2 -2
- package/dist/vendor/contracts/delivery.d.ts +47 -0
- package/dist/vendor/contracts/delivery.js +18 -0
- package/dist/vendor/contracts/index.d.ts +3 -1
- package/dist/vendor/contracts/index.js +2 -0
- package/dist/vendor/core/compiler.js +4 -9
- package/dist/vendor/core/delivery/index.d.ts +6 -0
- package/dist/vendor/core/delivery/index.js +4 -0
- package/dist/vendor/core/delivery/message-client.d.ts +18 -0
- package/dist/vendor/core/delivery/message-client.js +38 -0
- package/dist/vendor/core/delivery/message-ledger.d.ts +35 -0
- package/dist/vendor/core/delivery/message-ledger.js +97 -0
- package/dist/vendor/core/delivery/message-schema.d.ts +16 -0
- package/dist/vendor/core/delivery/message-schema.js +111 -0
- package/dist/vendor/core/delivery/update-check.d.ts +19 -0
- package/dist/vendor/core/delivery/update-check.js +107 -0
- package/dist/vendor/core/index.d.ts +2 -0
- package/dist/vendor/core/index.js +3 -88
- package/dist/vendor/core/rollback.js +2 -3
- package/package.json +83 -76
- package/server.json +21 -21
|
@@ -0,0 +1,252 @@
|
|
|
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 function resolveReceiptIntegrityRoot(env = process.env) {
|
|
9
|
+
return env["MARTIN_INTEGRITY_KEY_DIR"]?.trim() ??
|
|
10
|
+
join(homedir(), ".martin", "receipt-integrity");
|
|
11
|
+
}
|
|
12
|
+
export async function writeReceiptIntegrityMaterial(input) {
|
|
13
|
+
const signedAt = input.signedAt ?? new Date().toISOString();
|
|
14
|
+
const keyMaterial = await ensureReceiptIntegrityKey(input.runsRoot, input.runId);
|
|
15
|
+
if (!keyMaterial) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
const [key, keyId] = keyMaterial;
|
|
19
|
+
const chain = buildReceiptIntegrityChain(input.ledgerEntries);
|
|
20
|
+
const loopRecordRaw = serializeStoredJson(input.loopRecord);
|
|
21
|
+
const ledgerRaw = serializeStoredJsonl(input.ledgerEntries);
|
|
22
|
+
const materialBase = {
|
|
23
|
+
schemaVersion: RECEIPT_INTEGRITY_SCHEMA_VERSION,
|
|
24
|
+
runId: input.runId,
|
|
25
|
+
keyId,
|
|
26
|
+
signedAt,
|
|
27
|
+
...(input.scope ? { scope: input.scope } : {}),
|
|
28
|
+
loopRecordSha256: sha256(loopRecordRaw),
|
|
29
|
+
ledgerSha256: sha256(ledgerRaw),
|
|
30
|
+
ledgerHeadHash: chain.at(-1)?.entryHash ?? "root",
|
|
31
|
+
entryCount: chain.length,
|
|
32
|
+
chain
|
|
33
|
+
};
|
|
34
|
+
const material = {
|
|
35
|
+
...materialBase,
|
|
36
|
+
signatureHmacSha256: createReceiptIntegritySignature(key, materialBase)
|
|
37
|
+
};
|
|
38
|
+
await mkdir(join(input.runsRoot, input.runId), { recursive: true });
|
|
39
|
+
await writeFile(resolveReceiptIntegrityPath(input.runsRoot, input.runId), serializeStoredJson(material), "utf8");
|
|
40
|
+
return material;
|
|
41
|
+
}
|
|
42
|
+
export async function verifyReceiptIntegrityFromFiles(input) {
|
|
43
|
+
const integrityPath = resolveReceiptIntegrityPath(input.runsRoot, input.runId);
|
|
44
|
+
const [rawMaterial, rawLoopRecord, rawLedger, key] = await Promise.all([
|
|
45
|
+
readFile(integrityPath, "utf8").catch(() => null),
|
|
46
|
+
readFile(input.loopRecordPath, "utf8").catch(() => null),
|
|
47
|
+
readFile(input.ledgerPath, "utf8").catch(() => null),
|
|
48
|
+
readReceiptIntegrityKey(input.runsRoot, input.runId).catch(() => null)
|
|
49
|
+
]);
|
|
50
|
+
if (!rawMaterial || !rawLoopRecord || rawLedger === null || !key) {
|
|
51
|
+
return {
|
|
52
|
+
state: "material_missing",
|
|
53
|
+
reason: "Receipt integrity material is missing for this run.",
|
|
54
|
+
warnings: ["Receipt integrity material is incomplete; trust claims are unavailable."]
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
let material;
|
|
58
|
+
try {
|
|
59
|
+
material = JSON.parse(rawMaterial);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return {
|
|
63
|
+
state: "tamper_detected",
|
|
64
|
+
reason: "Receipt integrity metadata is unreadable."
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const actualLoopRecordSha256 = sha256(rawLoopRecord);
|
|
68
|
+
if (actualLoopRecordSha256 !== material.loopRecordSha256) {
|
|
69
|
+
return {
|
|
70
|
+
state: "tamper_detected",
|
|
71
|
+
keyId: material.keyId,
|
|
72
|
+
signedAt: material.signedAt,
|
|
73
|
+
loopRecordSha256: material.loopRecordSha256,
|
|
74
|
+
ledgerSha256: material.ledgerSha256,
|
|
75
|
+
ledgerHeadHash: material.ledgerHeadHash,
|
|
76
|
+
entryCount: material.entryCount,
|
|
77
|
+
reason: "loop_record_hash_mismatch"
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const actualLedgerSha256 = sha256(rawLedger);
|
|
81
|
+
if (actualLedgerSha256 !== material.ledgerSha256) {
|
|
82
|
+
return {
|
|
83
|
+
state: "tamper_detected",
|
|
84
|
+
keyId: material.keyId,
|
|
85
|
+
signedAt: material.signedAt,
|
|
86
|
+
loopRecordSha256: material.loopRecordSha256,
|
|
87
|
+
ledgerSha256: material.ledgerSha256,
|
|
88
|
+
ledgerHeadHash: material.ledgerHeadHash,
|
|
89
|
+
entryCount: material.entryCount,
|
|
90
|
+
reason: "ledger_hash_mismatch"
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
let parsedLedgerEntries;
|
|
94
|
+
try {
|
|
95
|
+
parsedLedgerEntries = rawLedger
|
|
96
|
+
.split(/\r?\n/u)
|
|
97
|
+
.map((line) => line.trim())
|
|
98
|
+
.filter(Boolean)
|
|
99
|
+
.map((line) => JSON.parse(line));
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return {
|
|
103
|
+
state: "tamper_detected",
|
|
104
|
+
keyId: material.keyId,
|
|
105
|
+
signedAt: material.signedAt,
|
|
106
|
+
loopRecordSha256: material.loopRecordSha256,
|
|
107
|
+
ledgerSha256: material.ledgerSha256,
|
|
108
|
+
ledgerHeadHash: material.ledgerHeadHash,
|
|
109
|
+
entryCount: material.entryCount,
|
|
110
|
+
reason: "ledger_entry_parse_error"
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const actualChain = buildReceiptIntegrityChain(parsedLedgerEntries);
|
|
114
|
+
if (actualChain.length !== material.chain.length) {
|
|
115
|
+
return {
|
|
116
|
+
state: "tamper_detected",
|
|
117
|
+
keyId: material.keyId,
|
|
118
|
+
signedAt: material.signedAt,
|
|
119
|
+
loopRecordSha256: material.loopRecordSha256,
|
|
120
|
+
ledgerSha256: material.ledgerSha256,
|
|
121
|
+
ledgerHeadHash: material.ledgerHeadHash,
|
|
122
|
+
entryCount: material.entryCount,
|
|
123
|
+
reason: "ledger_entry_count_mismatch"
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
for (let index = 0; index < actualChain.length; index += 1) {
|
|
127
|
+
const expected = material.chain[index];
|
|
128
|
+
const actual = actualChain[index];
|
|
129
|
+
if (!expected ||
|
|
130
|
+
!actual ||
|
|
131
|
+
expected.entryHash !== actual.entryHash ||
|
|
132
|
+
expected.prevHash !== actual.prevHash) {
|
|
133
|
+
return {
|
|
134
|
+
state: "tamper_detected",
|
|
135
|
+
keyId: material.keyId,
|
|
136
|
+
signedAt: material.signedAt,
|
|
137
|
+
loopRecordSha256: material.loopRecordSha256,
|
|
138
|
+
ledgerSha256: material.ledgerSha256,
|
|
139
|
+
ledgerHeadHash: material.ledgerHeadHash,
|
|
140
|
+
entryCount: material.entryCount,
|
|
141
|
+
reason: `ledger_chain_mismatch:${index}`
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const signatureBase = {
|
|
146
|
+
schemaVersion: material.schemaVersion,
|
|
147
|
+
runId: material.runId,
|
|
148
|
+
keyId: material.keyId,
|
|
149
|
+
signedAt: material.signedAt,
|
|
150
|
+
...(material.scope ? { scope: material.scope } : {}),
|
|
151
|
+
loopRecordSha256: material.loopRecordSha256,
|
|
152
|
+
ledgerSha256: material.ledgerSha256,
|
|
153
|
+
ledgerHeadHash: material.ledgerHeadHash,
|
|
154
|
+
entryCount: material.entryCount,
|
|
155
|
+
chain: material.chain
|
|
156
|
+
};
|
|
157
|
+
const expectedSignature = createReceiptIntegritySignature(key, signatureBase);
|
|
158
|
+
if (expectedSignature !== material.signatureHmacSha256) {
|
|
159
|
+
return {
|
|
160
|
+
state: "tamper_detected",
|
|
161
|
+
keyId: material.keyId,
|
|
162
|
+
signedAt: material.signedAt,
|
|
163
|
+
loopRecordSha256: material.loopRecordSha256,
|
|
164
|
+
ledgerSha256: material.ledgerSha256,
|
|
165
|
+
ledgerHeadHash: material.ledgerHeadHash,
|
|
166
|
+
entryCount: material.entryCount,
|
|
167
|
+
reason: "signature_mismatch"
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
state: "verified",
|
|
172
|
+
keyId: material.keyId,
|
|
173
|
+
signedAt: material.signedAt,
|
|
174
|
+
loopRecordSha256: material.loopRecordSha256,
|
|
175
|
+
ledgerSha256: material.ledgerSha256,
|
|
176
|
+
ledgerHeadHash: material.ledgerHeadHash,
|
|
177
|
+
entryCount: material.entryCount
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
export function resolveReceiptIntegrityPath(runsRoot, runId) {
|
|
181
|
+
return join(runsRoot, runId, "receipt-integrity.json");
|
|
182
|
+
}
|
|
183
|
+
function buildReceiptIntegrityChain(entries) {
|
|
184
|
+
let previousHash = "root";
|
|
185
|
+
return entries.map((entry, index) => {
|
|
186
|
+
const normalizedEntry = JSON.stringify(entry);
|
|
187
|
+
const entryHash = sha256(`${previousHash}\n${normalizedEntry}`);
|
|
188
|
+
const candidate = entry;
|
|
189
|
+
const chainEntry = {
|
|
190
|
+
index,
|
|
191
|
+
prevHash: previousHash,
|
|
192
|
+
entryHash
|
|
193
|
+
};
|
|
194
|
+
if (typeof candidate.eventId === "string") {
|
|
195
|
+
chainEntry.eventId = candidate.eventId;
|
|
196
|
+
}
|
|
197
|
+
if (typeof candidate.type === "string") {
|
|
198
|
+
chainEntry.type = candidate.type;
|
|
199
|
+
}
|
|
200
|
+
if (typeof candidate.kind === "string") {
|
|
201
|
+
chainEntry.kind = candidate.kind;
|
|
202
|
+
}
|
|
203
|
+
if (typeof candidate.timestamp === "string") {
|
|
204
|
+
chainEntry.timestamp = candidate.timestamp;
|
|
205
|
+
}
|
|
206
|
+
previousHash = entryHash;
|
|
207
|
+
return chainEntry;
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
function createReceiptIntegritySignature(key, material) {
|
|
211
|
+
return createHmac("sha256", key).update(JSON.stringify(material)).digest("hex");
|
|
212
|
+
}
|
|
213
|
+
async function ensureReceiptIntegrityKey(runsRoot, runId) {
|
|
214
|
+
const keyPath = resolveReceiptIntegrityKeyPath(runsRoot, runId);
|
|
215
|
+
const existing = await readFile(keyPath, "utf8").catch(() => null);
|
|
216
|
+
if (existing) {
|
|
217
|
+
const trimmed = existing.trim();
|
|
218
|
+
return [trimmed, sha256(trimmed).slice(0, 16)];
|
|
219
|
+
}
|
|
220
|
+
const generated = randomBytes(32).toString("hex");
|
|
221
|
+
try {
|
|
222
|
+
await mkdir(dirname(keyPath), { recursive: true, mode: RECEIPT_INTEGRITY_KEY_DIR_MODE });
|
|
223
|
+
await writeFile(keyPath, `${generated}\n`, {
|
|
224
|
+
encoding: "utf8",
|
|
225
|
+
mode: RECEIPT_INTEGRITY_KEY_FILE_MODE
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
return [generated, sha256(generated).slice(0, 16)];
|
|
232
|
+
}
|
|
233
|
+
async function readReceiptIntegrityKey(runsRoot, runId) {
|
|
234
|
+
const raw = await readFile(resolveReceiptIntegrityKeyPath(runsRoot, runId), "utf8");
|
|
235
|
+
return raw.trim();
|
|
236
|
+
}
|
|
237
|
+
function resolveReceiptIntegrityKeyPath(runsRoot, runId) {
|
|
238
|
+
const rootHash = sha256(runsRoot).slice(0, 16);
|
|
239
|
+
return join(resolveReceiptIntegrityRoot(), rootHash, `${runId}.key`);
|
|
240
|
+
}
|
|
241
|
+
function serializeStoredJson(value) {
|
|
242
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
243
|
+
}
|
|
244
|
+
function serializeStoredJsonl(entries) {
|
|
245
|
+
if (entries.length === 0) {
|
|
246
|
+
return "";
|
|
247
|
+
}
|
|
248
|
+
return `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`;
|
|
249
|
+
}
|
|
250
|
+
function sha256(value) {
|
|
251
|
+
return createHash("sha256").update(value).digest("hex");
|
|
252
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ledger event types for the Phase 3 persistence stack.
|
|
3
|
+
* These are WRITE-side events that go into ledger.jsonl — separate from the
|
|
4
|
+
* read-model LoopEventType used by the control plane.
|
|
5
|
+
*/
|
|
6
|
+
export type LedgerEventKind = "contract.created" | "attempt.admitted" | "attempt.rejected" | "prompt.compiled" | "patch.generated" | "verification.completed" | "grounding.violations_found" | "safety.violations_found" | "budget.settled" | "attempt.kept" | "attempt.discarded" | "run.exited";
|
|
7
|
+
export interface LedgerEvent {
|
|
8
|
+
kind: LedgerEventKind;
|
|
9
|
+
runId: string;
|
|
10
|
+
attemptIndex?: number;
|
|
11
|
+
timestamp: string;
|
|
12
|
+
payload: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
export interface LedgerEventDraft {
|
|
15
|
+
kind: LedgerEventKind;
|
|
16
|
+
runId: string;
|
|
17
|
+
attemptIndex?: number;
|
|
18
|
+
payload: Record<string, unknown>;
|
|
19
|
+
timestamp?: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function makeLedgerEvent(draft: LedgerEventDraft, options?: {
|
|
22
|
+
now?: string;
|
|
23
|
+
}): LedgerEvent;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function makeLedgerEvent(draft, options = {}) {
|
|
2
|
+
return {
|
|
3
|
+
kind: draft.kind,
|
|
4
|
+
runId: draft.runId,
|
|
5
|
+
timestamp: draft.timestamp ?? options.now ?? new Date().toISOString(),
|
|
6
|
+
payload: draft.payload,
|
|
7
|
+
...(draft.attemptIndex !== undefined ? { attemptIndex: draft.attemptIndex } : {})
|
|
8
|
+
};
|
|
9
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MartinLoop Memory Store
|
|
3
|
+
*
|
|
4
|
+
* Append-only, never-overwriting persistent memory. Records user consent
|
|
5
|
+
* signals, preferences, behavioral patterns, and learning outcomes so
|
|
6
|
+
* MartinLoop gets smarter with every session.
|
|
7
|
+
*
|
|
8
|
+
* Design principles:
|
|
9
|
+
* - Append-only JSONL — entries are never deleted or overwritten
|
|
10
|
+
* - Structured by kind — each memory entry has a type and a key
|
|
11
|
+
* - Source-aware — distinguishes explicit user input from inferred patterns
|
|
12
|
+
* - Confidence-scored — inferred memories degrade gracefully
|
|
13
|
+
*
|
|
14
|
+
* Storage: <runsRoot>/_martin/memory.jsonl
|
|
15
|
+
*/
|
|
16
|
+
export type MemoryKind = "consent" | "preference" | "pattern" | "feedback" | "budget" | "model";
|
|
17
|
+
export interface MemoryEntry {
|
|
18
|
+
/** ISO-8601 timestamp */
|
|
19
|
+
timestamp: string;
|
|
20
|
+
/** What kind of memory this is */
|
|
21
|
+
kind: MemoryKind;
|
|
22
|
+
/** Dot-notated key, e.g. "model.direct.claude", "budget.auth.usd", "gate.bypassed" */
|
|
23
|
+
key: string;
|
|
24
|
+
/** The memory payload */
|
|
25
|
+
value: unknown;
|
|
26
|
+
/** Did the user say it explicitly, or did Martin observe it? */
|
|
27
|
+
source: "explicit" | "inferred";
|
|
28
|
+
/** Confidence score 0-1 — inferred memories should be < 0.8 */
|
|
29
|
+
confidence: number;
|
|
30
|
+
}
|
|
31
|
+
export interface MemorySummary {
|
|
32
|
+
totalEntries: number;
|
|
33
|
+
byKind: Partial<Record<MemoryKind, MemoryEntry[]>>;
|
|
34
|
+
recentPreferences: MemoryEntry[];
|
|
35
|
+
recentConsents: MemoryEntry[];
|
|
36
|
+
recentFeedback: MemoryEntry[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Append a memory entry. Never overwrites — always appends.
|
|
40
|
+
*/
|
|
41
|
+
export declare function appendMemory(runsRoot: string, entry: MemoryEntry): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Read all memory entries in order (oldest first).
|
|
44
|
+
*/
|
|
45
|
+
export declare function readMemoryEntries(runsRoot: string): Promise<MemoryEntry[]>;
|
|
46
|
+
/**
|
|
47
|
+
* Get the most recent memory entry for a specific key.
|
|
48
|
+
* Returns undefined if no entry exists for that key.
|
|
49
|
+
*/
|
|
50
|
+
export declare function getPreference(runsRoot: string, key: string): Promise<MemoryEntry | undefined>;
|
|
51
|
+
/**
|
|
52
|
+
* Build a summary of memory entries for MCP resource and MartinLoop context injection.
|
|
53
|
+
* Returns the top 20 most recent entries per kind.
|
|
54
|
+
*/
|
|
55
|
+
export declare function buildMemorySummary(entries: MemoryEntry[]): MemorySummary;
|
|
56
|
+
/**
|
|
57
|
+
* Record a preference from user input.
|
|
58
|
+
*/
|
|
59
|
+
export declare function recordPreference(runsRoot: string, key: string, value: unknown, source?: "explicit" | "inferred"): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Record a consent signal (user approved or denied a governance action).
|
|
62
|
+
*/
|
|
63
|
+
export declare function recordConsent(runsRoot: string, action: string, approved: boolean, context?: Record<string, unknown>): Promise<void>;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MartinLoop Memory Store
|
|
3
|
+
*
|
|
4
|
+
* Append-only, never-overwriting persistent memory. Records user consent
|
|
5
|
+
* signals, preferences, behavioral patterns, and learning outcomes so
|
|
6
|
+
* MartinLoop gets smarter with every session.
|
|
7
|
+
*
|
|
8
|
+
* Design principles:
|
|
9
|
+
* - Append-only JSONL — entries are never deleted or overwritten
|
|
10
|
+
* - Structured by kind — each memory entry has a type and a key
|
|
11
|
+
* - Source-aware — distinguishes explicit user input from inferred patterns
|
|
12
|
+
* - Confidence-scored — inferred memories degrade gracefully
|
|
13
|
+
*
|
|
14
|
+
* Storage: <runsRoot>/_martin/memory.jsonl
|
|
15
|
+
*/
|
|
16
|
+
import { appendFile, mkdir, readFile } from "node:fs/promises";
|
|
17
|
+
import { join, resolve } from "node:path";
|
|
18
|
+
const MEMORY_DIRECTORY = "_martin";
|
|
19
|
+
const MEMORY_FILENAME = "memory.jsonl";
|
|
20
|
+
function resolveMemoryPath(runsRoot) {
|
|
21
|
+
return join(resolve(runsRoot), MEMORY_DIRECTORY, MEMORY_FILENAME);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Append a memory entry. Never overwrites — always appends.
|
|
25
|
+
*/
|
|
26
|
+
export async function appendMemory(runsRoot, entry) {
|
|
27
|
+
const memPath = resolveMemoryPath(runsRoot);
|
|
28
|
+
await mkdir(join(resolve(runsRoot), MEMORY_DIRECTORY), { recursive: true });
|
|
29
|
+
await appendFile(memPath, JSON.stringify(entry) + "\n", "utf8");
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Read all memory entries in order (oldest first).
|
|
33
|
+
*/
|
|
34
|
+
export async function readMemoryEntries(runsRoot) {
|
|
35
|
+
const memPath = resolveMemoryPath(runsRoot);
|
|
36
|
+
try {
|
|
37
|
+
const raw = await readFile(memPath, "utf8");
|
|
38
|
+
return raw
|
|
39
|
+
.split("\n")
|
|
40
|
+
.filter((line) => line.trim().length > 0)
|
|
41
|
+
.map((line) => {
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(line);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
.filter((e) => e !== null);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Get the most recent memory entry for a specific key.
|
|
57
|
+
* Returns undefined if no entry exists for that key.
|
|
58
|
+
*/
|
|
59
|
+
export async function getPreference(runsRoot, key) {
|
|
60
|
+
const entries = await readMemoryEntries(runsRoot);
|
|
61
|
+
// Return the most recent entry for this key (last in file)
|
|
62
|
+
const matches = entries.filter((e) => e.key === key);
|
|
63
|
+
return matches[matches.length - 1];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Build a summary of memory entries for MCP resource and MartinLoop context injection.
|
|
67
|
+
* Returns the top 20 most recent entries per kind.
|
|
68
|
+
*/
|
|
69
|
+
export function buildMemorySummary(entries) {
|
|
70
|
+
const byKind = {};
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
if (!byKind[entry.kind]) {
|
|
73
|
+
byKind[entry.kind] = [];
|
|
74
|
+
}
|
|
75
|
+
byKind[entry.kind].push(entry);
|
|
76
|
+
}
|
|
77
|
+
// Keep only the 20 most recent per kind
|
|
78
|
+
for (const kind of Object.keys(byKind)) {
|
|
79
|
+
byKind[kind] = byKind[kind].slice(-20);
|
|
80
|
+
}
|
|
81
|
+
const recent = entries.slice(-50);
|
|
82
|
+
return {
|
|
83
|
+
totalEntries: entries.length,
|
|
84
|
+
byKind,
|
|
85
|
+
recentPreferences: recent.filter((e) => e.kind === "preference"),
|
|
86
|
+
recentConsents: recent.filter((e) => e.kind === "consent"),
|
|
87
|
+
recentFeedback: recent.filter((e) => e.kind === "feedback")
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Record a preference from user input.
|
|
92
|
+
*/
|
|
93
|
+
export async function recordPreference(runsRoot, key, value, source = "explicit") {
|
|
94
|
+
await appendMemory(runsRoot, {
|
|
95
|
+
timestamp: new Date().toISOString(),
|
|
96
|
+
kind: "preference",
|
|
97
|
+
key,
|
|
98
|
+
value,
|
|
99
|
+
source,
|
|
100
|
+
confidence: source === "explicit" ? 1.0 : 0.6
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Record a consent signal (user approved or denied a governance action).
|
|
105
|
+
*/
|
|
106
|
+
export async function recordConsent(runsRoot, action, approved, context) {
|
|
107
|
+
await appendMemory(runsRoot, {
|
|
108
|
+
timestamp: new Date().toISOString(),
|
|
109
|
+
kind: "consent",
|
|
110
|
+
key: `consent.${action}`,
|
|
111
|
+
value: { approved, context },
|
|
112
|
+
source: "explicit",
|
|
113
|
+
confidence: 1.0
|
|
114
|
+
});
|
|
115
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads completed loop records from ~/.martin/runs/ for analysis.
|
|
3
|
+
* Used by the Trust Calibration Engine and other offline analytics.
|
|
4
|
+
*
|
|
5
|
+
* Supports both storage layouts:
|
|
6
|
+
* - legacy root JSONL files: <runsRoot>/*.jsonl
|
|
7
|
+
* - canonical run trees: <runsRoot>/<loopId>/loop-record.json
|
|
8
|
+
*/
|
|
9
|
+
export interface LoopAttemptRecord {
|
|
10
|
+
index: number;
|
|
11
|
+
model?: string;
|
|
12
|
+
adapterId?: string;
|
|
13
|
+
failureClass?: string;
|
|
14
|
+
intervention?: string;
|
|
15
|
+
startedAt?: string;
|
|
16
|
+
completedAt?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface LoopRunRecord {
|
|
19
|
+
loopId: string;
|
|
20
|
+
status: string;
|
|
21
|
+
lifecycleState: string;
|
|
22
|
+
createdAt: string;
|
|
23
|
+
updatedAt: string;
|
|
24
|
+
budget: {
|
|
25
|
+
maxUsd: number;
|
|
26
|
+
softLimitUsd: number;
|
|
27
|
+
maxIterations: number;
|
|
28
|
+
maxTokens: number;
|
|
29
|
+
};
|
|
30
|
+
cost: {
|
|
31
|
+
actualUsd: number;
|
|
32
|
+
tokensIn: number;
|
|
33
|
+
tokensOut: number;
|
|
34
|
+
avoidedUsd?: number;
|
|
35
|
+
};
|
|
36
|
+
attempts: LoopAttemptRecord[];
|
|
37
|
+
task: {
|
|
38
|
+
title: string;
|
|
39
|
+
objective: string;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export interface LoopRecordsRollup {
|
|
43
|
+
generatedAt: string;
|
|
44
|
+
totalRuns: number;
|
|
45
|
+
statusBreakdown: Record<string, number>;
|
|
46
|
+
lifecycleBreakdown: Record<string, number>;
|
|
47
|
+
latestByLoopId: Record<string, {
|
|
48
|
+
status: string;
|
|
49
|
+
lifecycleState: string;
|
|
50
|
+
updatedAt: string;
|
|
51
|
+
costUsd: number;
|
|
52
|
+
attempts: number;
|
|
53
|
+
}>;
|
|
54
|
+
}
|
|
55
|
+
export declare function readLoopRecordsFromFile(file: string): Promise<LoopRunRecord[]>;
|
|
56
|
+
export declare function readLatestLoopRecordFromFile(file: string): Promise<LoopRunRecord | null>;
|
|
57
|
+
/**
|
|
58
|
+
* Reads all loop records from the given directory (default: ~/.martin/runs/).
|
|
59
|
+
* Returns an empty array if the directory doesn't exist or has no records.
|
|
60
|
+
*/
|
|
61
|
+
export declare function readAllLoopRecords(runsDir?: string): Promise<LoopRunRecord[]>;
|
|
62
|
+
/**
|
|
63
|
+
* Returns the most recently updated loop record, or null if none exist.
|
|
64
|
+
*/
|
|
65
|
+
export declare function readLatestLoopRecord(runsDir?: string): Promise<LoopRunRecord | null>;
|
|
66
|
+
export declare function buildLoopRecordsRollup(records: LoopRunRecord[]): LoopRecordsRollup;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads completed loop records from ~/.martin/runs/ for analysis.
|
|
3
|
+
* Used by the Trust Calibration Engine and other offline analytics.
|
|
4
|
+
*
|
|
5
|
+
* Supports both storage layouts:
|
|
6
|
+
* - legacy root JSONL files: <runsRoot>/*.jsonl
|
|
7
|
+
* - canonical run trees: <runsRoot>/<loopId>/loop-record.json
|
|
8
|
+
*/
|
|
9
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { extname, join } from "node:path";
|
|
12
|
+
export async function readLoopRecordsFromFile(file) {
|
|
13
|
+
const text = await readFile(file, "utf8");
|
|
14
|
+
const extension = extname(file).toLowerCase();
|
|
15
|
+
if (extension === ".jsonl") {
|
|
16
|
+
return text
|
|
17
|
+
.split(/\r?\n/u)
|
|
18
|
+
.map((line) => line.trim())
|
|
19
|
+
.filter(Boolean)
|
|
20
|
+
.map((line) => JSON.parse(line));
|
|
21
|
+
}
|
|
22
|
+
const parsed = JSON.parse(text);
|
|
23
|
+
return Array.isArray(parsed) ? parsed : [parsed];
|
|
24
|
+
}
|
|
25
|
+
export async function readLatestLoopRecordFromFile(file) {
|
|
26
|
+
const records = await readLoopRecordsFromFile(file);
|
|
27
|
+
if (records.length === 0)
|
|
28
|
+
return null;
|
|
29
|
+
return records.reduce((latest, record) => {
|
|
30
|
+
const currentTimestamp = new Date(record.updatedAt ?? record.createdAt).getTime();
|
|
31
|
+
const latestTimestamp = new Date(latest.updatedAt ?? latest.createdAt).getTime();
|
|
32
|
+
return currentTimestamp > latestTimestamp ? record : latest;
|
|
33
|
+
}, records[0]);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Reads all loop records from the given directory (default: ~/.martin/runs/).
|
|
37
|
+
* Returns an empty array if the directory doesn't exist or has no records.
|
|
38
|
+
*/
|
|
39
|
+
export async function readAllLoopRecords(runsDir) {
|
|
40
|
+
const dir = runsDir ?? join(homedir(), ".martin", "runs");
|
|
41
|
+
let entries;
|
|
42
|
+
try {
|
|
43
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
const recordsByLoopId = new Map();
|
|
49
|
+
const jsonlFiles = entries
|
|
50
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl"))
|
|
51
|
+
.map((entry) => entry.name);
|
|
52
|
+
for (const file of jsonlFiles) {
|
|
53
|
+
try {
|
|
54
|
+
const fromFile = await readLoopRecordsFromFile(join(dir, file));
|
|
55
|
+
for (const record of fromFile) {
|
|
56
|
+
ingestRecord(recordsByLoopId, record, "legacy_jsonl");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// skip malformed files or lines
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const runDirectories = entries.filter((entry) => entry.isDirectory());
|
|
64
|
+
for (const entry of runDirectories) {
|
|
65
|
+
try {
|
|
66
|
+
const canonical = await readLoopRecordsFromFile(join(dir, entry.name, "loop-record.json"));
|
|
67
|
+
for (const record of canonical) {
|
|
68
|
+
ingestRecord(recordsByLoopId, record, "canonical_tree");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// skip missing or malformed canonical records
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return [...recordsByLoopId.values()].map((entry) => entry.record);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Returns the most recently updated loop record, or null if none exist.
|
|
79
|
+
*/
|
|
80
|
+
export async function readLatestLoopRecord(runsDir) {
|
|
81
|
+
const records = await readAllLoopRecords(runsDir);
|
|
82
|
+
if (records.length === 0)
|
|
83
|
+
return null;
|
|
84
|
+
return records.reduce((latest, r) => {
|
|
85
|
+
const a = new Date(r.updatedAt ?? r.createdAt).getTime();
|
|
86
|
+
const b = new Date(latest.updatedAt ?? latest.createdAt).getTime();
|
|
87
|
+
return a > b ? r : latest;
|
|
88
|
+
}, records[0]);
|
|
89
|
+
}
|
|
90
|
+
export function buildLoopRecordsRollup(records) {
|
|
91
|
+
const statusBreakdown = {};
|
|
92
|
+
const lifecycleBreakdown = {};
|
|
93
|
+
const latestByLoopId = {};
|
|
94
|
+
for (const record of records) {
|
|
95
|
+
statusBreakdown[record.status] = (statusBreakdown[record.status] ?? 0) + 1;
|
|
96
|
+
lifecycleBreakdown[record.lifecycleState] = (lifecycleBreakdown[record.lifecycleState] ?? 0) + 1;
|
|
97
|
+
latestByLoopId[record.loopId] = {
|
|
98
|
+
status: record.status,
|
|
99
|
+
lifecycleState: record.lifecycleState,
|
|
100
|
+
updatedAt: record.updatedAt,
|
|
101
|
+
costUsd: record.cost.actualUsd,
|
|
102
|
+
attempts: record.attempts.length
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
generatedAt: new Date().toISOString(),
|
|
107
|
+
totalRuns: records.length,
|
|
108
|
+
statusBreakdown,
|
|
109
|
+
lifecycleBreakdown,
|
|
110
|
+
latestByLoopId
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function ingestRecord(recordsByLoopId, record, source) {
|
|
114
|
+
const existing = recordsByLoopId.get(record.loopId);
|
|
115
|
+
if (!existing) {
|
|
116
|
+
recordsByLoopId.set(record.loopId, { record, source });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const candidateTimestamp = resolveRecordTimestamp(record);
|
|
120
|
+
const existingTimestamp = resolveRecordTimestamp(existing.record);
|
|
121
|
+
if (candidateTimestamp > existingTimestamp) {
|
|
122
|
+
recordsByLoopId.set(record.loopId, { record, source });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (candidateTimestamp === existingTimestamp &&
|
|
126
|
+
sourcePrecedence(source) > sourcePrecedence(existing.source)) {
|
|
127
|
+
recordsByLoopId.set(record.loopId, { record, source });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function resolveRecordTimestamp(record) {
|
|
131
|
+
const updated = Date.parse(record.updatedAt ?? "");
|
|
132
|
+
if (Number.isFinite(updated)) {
|
|
133
|
+
return updated;
|
|
134
|
+
}
|
|
135
|
+
const created = Date.parse(record.createdAt ?? "");
|
|
136
|
+
return Number.isFinite(created) ? created : 0;
|
|
137
|
+
}
|
|
138
|
+
function sourcePrecedence(source) {
|
|
139
|
+
return source === "canonical_tree" ? 2 : 1;
|
|
140
|
+
}
|