@martinloop/mcp 0.3.2 → 0.3.4
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 +112 -107
- package/dist/discovery-support.js +2 -2
- package/dist/prompts.d.ts +2 -1
- package/dist/resources.d.ts +2 -1
- package/dist/resources.js +147 -147
- package/dist/server-validation.js +5 -2
- package/dist/server.js +5 -4
- package/dist/tools/eval.js +29 -5
- package/dist/tools/execution-policy.d.ts +11 -0
- package/dist/tools/execution-policy.js +47 -0
- package/dist/tools/run-dossier.js +2 -2
- package/dist/tools/run-loop.d.ts +2 -1
- package/dist/tools/run-loop.js +15 -2
- package/dist/tools/run-store.js +166 -6
- package/dist/tools/tool-support.d.ts +3 -2
- package/dist/tools/tool-support.js +112 -18
- package/dist/tools/triage-runs.js +31 -6
- package/dist/vendor/adapters/claude-cli.d.ts +1 -1
- package/dist/vendor/adapters/claude-cli.js +71 -20
- package/dist/vendor/adapters/cli-bridge.d.ts +6 -0
- package/dist/vendor/adapters/cli-bridge.js +91 -7
- package/dist/vendor/adapters/codex-launcher.js +65 -8
- package/dist/vendor/adapters/openai-compatible.d.ts +1 -1
- package/dist/vendor/adapters/openai-compatible.js +7 -7
- package/dist/vendor/adapters/verifier-only.js +1 -1
- package/dist/vendor/contracts/adapter-capabilities.d.ts +17 -0
- package/dist/vendor/contracts/adapter-capabilities.js +35 -0
- package/dist/vendor/contracts/context-graph.d.ts +48 -0
- package/dist/vendor/contracts/context-graph.js +26 -0
- package/dist/vendor/contracts/execution-policy.d.ts +62 -0
- package/dist/vendor/contracts/execution-policy.js +34 -0
- package/dist/vendor/contracts/governance.d.ts +6 -3
- package/dist/vendor/contracts/identity.d.ts +29 -0
- package/dist/vendor/contracts/identity.js +30 -0
- package/dist/vendor/contracts/index.d.ts +13 -2
- package/dist/vendor/contracts/index.js +4 -0
- package/dist/vendor/contracts/observation.d.ts +21 -0
- package/dist/vendor/contracts/observation.js +27 -0
- package/dist/vendor/contracts/trajectory.d.ts +26 -0
- package/dist/vendor/contracts/trajectory.js +27 -0
- package/dist/vendor/contracts/verification.d.ts +28 -0
- package/dist/vendor/contracts/verification.js +7 -0
- package/dist/vendor/core/context-graph.d.ts +3 -0
- package/dist/vendor/core/context-graph.js +371 -0
- package/dist/vendor/core/context-integrity.js +1 -1
- package/dist/vendor/core/grounding.js +1 -1
- package/dist/vendor/core/identity.d.ts +20 -0
- package/dist/vendor/core/identity.js +86 -0
- package/dist/vendor/core/index.d.ts +7 -0
- package/dist/vendor/core/index.js +63 -9
- package/dist/vendor/core/leash.js +1 -1
- package/dist/vendor/core/persistence/integrity.d.ts +1 -0
- package/dist/vendor/core/persistence/integrity.js +7 -3
- package/dist/vendor/core/persistence/runs-reader.d.ts +14 -0
- package/dist/vendor/core/persistence/runs-reader.js +61 -4
- package/dist/vendor/core/policy-compiler.d.ts +2 -0
- package/dist/vendor/core/policy-compiler.js +195 -0
- package/dist/vendor/core/policy.d.ts +2 -0
- package/dist/vendor/core/policy.js +71 -4
- package/dist/vendor/core/rollback.js +32 -12
- package/dist/vendor/core/trajectory.d.ts +18 -0
- package/dist/vendor/core/trajectory.js +295 -0
- package/package.json +76 -75
- package/server.json +21 -21
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { extname, join, posix, relative, resolve } from "node:path";
|
|
3
|
+
import { cloneContextGraphSnapshot } from "../contracts/index.js";
|
|
4
|
+
const TEXT_EXTENSIONS = new Set([
|
|
5
|
+
".ts",
|
|
6
|
+
".tsx",
|
|
7
|
+
".js",
|
|
8
|
+
".jsx",
|
|
9
|
+
".mjs",
|
|
10
|
+
".cjs",
|
|
11
|
+
".json",
|
|
12
|
+
".md",
|
|
13
|
+
".yaml",
|
|
14
|
+
".yml",
|
|
15
|
+
".txt",
|
|
16
|
+
".py",
|
|
17
|
+
".go",
|
|
18
|
+
".rs",
|
|
19
|
+
".java",
|
|
20
|
+
".sh"
|
|
21
|
+
]);
|
|
22
|
+
const IGNORED_DIRS = new Set([
|
|
23
|
+
".git",
|
|
24
|
+
"node_modules",
|
|
25
|
+
"dist",
|
|
26
|
+
"build",
|
|
27
|
+
".next",
|
|
28
|
+
".turbo",
|
|
29
|
+
"coverage",
|
|
30
|
+
".pnpm-store",
|
|
31
|
+
".npm-cache"
|
|
32
|
+
]);
|
|
33
|
+
export async function buildContextGraphSnapshot(repoRoot, options = {}) {
|
|
34
|
+
const normalizedRoot = resolve(repoRoot);
|
|
35
|
+
const maxFiles = options.maxFiles ?? 600;
|
|
36
|
+
const maxFileBytes = options.maxFileBytes ?? 96_000;
|
|
37
|
+
const nodes = [];
|
|
38
|
+
const state = { count: 0, truncated: false };
|
|
39
|
+
await walkContextGraph(normalizedRoot, normalizedRoot, nodes, state, { maxFiles, maxFileBytes });
|
|
40
|
+
const edges = buildContextGraphEdges(nodes);
|
|
41
|
+
return cloneContextGraphSnapshot({
|
|
42
|
+
schemaVersion: "martin.context-graph.v1",
|
|
43
|
+
repoRoot: normalizedRoot,
|
|
44
|
+
createdAt: new Date().toISOString(),
|
|
45
|
+
nodeCount: nodes.length,
|
|
46
|
+
edgeCount: edges.length,
|
|
47
|
+
truncated: state.truncated,
|
|
48
|
+
nodes,
|
|
49
|
+
edges
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
export function queryContextGraph(snapshot, query) {
|
|
53
|
+
const terms = normalizeQueryTerms(query);
|
|
54
|
+
const pathPrefix = query.pathPrefix?.replace(/\\/gu, "/").replace(/^\.\/+/u, "");
|
|
55
|
+
const limit = query.limit ?? 8;
|
|
56
|
+
return snapshot.nodes
|
|
57
|
+
.filter((node) => {
|
|
58
|
+
if (query.kinds && !query.kinds.includes(node.kind)) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (pathPrefix && !node.path.startsWith(pathPrefix)) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
})
|
|
66
|
+
.map((node) => scoreContextNode(node, terms))
|
|
67
|
+
.filter((hit) => hit !== undefined)
|
|
68
|
+
.sort((left, right) => right.score - left.score || left.path.localeCompare(right.path))
|
|
69
|
+
.slice(0, limit);
|
|
70
|
+
}
|
|
71
|
+
async function walkContextGraph(repoRoot, currentDir, nodes, state, options) {
|
|
72
|
+
if (state.count >= options.maxFiles) {
|
|
73
|
+
state.truncated = true;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
if (state.count >= options.maxFiles) {
|
|
79
|
+
state.truncated = true;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
const absPath = join(currentDir, entry.name);
|
|
83
|
+
if (entry.isDirectory()) {
|
|
84
|
+
if (!IGNORED_DIRS.has(entry.name)) {
|
|
85
|
+
await walkContextGraph(repoRoot, absPath, nodes, state, options);
|
|
86
|
+
}
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!entry.isFile()) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const extension = extname(entry.name).toLowerCase();
|
|
93
|
+
if (!TEXT_EXTENSIONS.has(extension)) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const content = await readFile(absPath, "utf8").catch(() => undefined);
|
|
97
|
+
if (!content || content.length > options.maxFileBytes) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const relPath = relative(repoRoot, absPath).replace(/\\/gu, "/");
|
|
101
|
+
nodes.push(createContextNode(relPath, content));
|
|
102
|
+
state.count += 1;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function createContextNode(path, content) {
|
|
106
|
+
const kind = classifyContextNodeKind(path);
|
|
107
|
+
return {
|
|
108
|
+
nodeId: path,
|
|
109
|
+
path,
|
|
110
|
+
kind,
|
|
111
|
+
...(inferLanguage(path) ? { language: inferLanguage(path) } : {}),
|
|
112
|
+
sizeBytes: Buffer.byteLength(content, "utf8"),
|
|
113
|
+
headings: extractHeadings(path, content),
|
|
114
|
+
symbols: extractSymbols(content),
|
|
115
|
+
keywords: extractKeywords(path, content),
|
|
116
|
+
imports: extractImports(path, content)
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function buildContextGraphEdges(nodes) {
|
|
120
|
+
const paths = new Set(nodes.map((node) => node.path));
|
|
121
|
+
const edges = [];
|
|
122
|
+
for (const node of nodes) {
|
|
123
|
+
for (const importTarget of node.imports) {
|
|
124
|
+
const resolvedTarget = resolveContextImport(node.path, importTarget, paths);
|
|
125
|
+
if (resolvedTarget) {
|
|
126
|
+
edges.push({
|
|
127
|
+
fromNodeId: node.nodeId,
|
|
128
|
+
toNodeId: resolvedTarget,
|
|
129
|
+
kind: "imports"
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (node.kind === "document") {
|
|
134
|
+
const documentTargets = node.keywords
|
|
135
|
+
.map((keyword) => nodes.find((candidate) => candidate.path !== node.path && candidate.path.includes(keyword)))
|
|
136
|
+
.filter((candidate) => candidate !== undefined)
|
|
137
|
+
.slice(0, 3);
|
|
138
|
+
for (const target of documentTargets) {
|
|
139
|
+
edges.push({
|
|
140
|
+
fromNodeId: node.nodeId,
|
|
141
|
+
toNodeId: target.nodeId,
|
|
142
|
+
kind: "documents"
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return dedupeEdges(edges);
|
|
148
|
+
}
|
|
149
|
+
function scoreContextNode(node, terms) {
|
|
150
|
+
if (terms.length === 0) {
|
|
151
|
+
return {
|
|
152
|
+
nodeId: node.nodeId,
|
|
153
|
+
path: node.path,
|
|
154
|
+
kind: node.kind,
|
|
155
|
+
score: 1,
|
|
156
|
+
matchedTerms: [],
|
|
157
|
+
reasons: ["default_match"]
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
let score = 0;
|
|
161
|
+
const matchedTerms = new Set();
|
|
162
|
+
const reasons = new Set();
|
|
163
|
+
for (const term of terms) {
|
|
164
|
+
if (node.path.toLowerCase().includes(term)) {
|
|
165
|
+
score += 6;
|
|
166
|
+
matchedTerms.add(term);
|
|
167
|
+
reasons.add("path");
|
|
168
|
+
}
|
|
169
|
+
if (node.keywords.includes(term)) {
|
|
170
|
+
score += 3;
|
|
171
|
+
matchedTerms.add(term);
|
|
172
|
+
reasons.add("keyword");
|
|
173
|
+
}
|
|
174
|
+
if (node.symbols.some((symbol) => symbol.toLowerCase().includes(term))) {
|
|
175
|
+
score += 5;
|
|
176
|
+
matchedTerms.add(term);
|
|
177
|
+
reasons.add("symbol");
|
|
178
|
+
}
|
|
179
|
+
if (node.headings.some((heading) => heading.toLowerCase().includes(term))) {
|
|
180
|
+
score += 4;
|
|
181
|
+
matchedTerms.add(term);
|
|
182
|
+
reasons.add("heading");
|
|
183
|
+
}
|
|
184
|
+
if (node.imports.some((importPath) => importPath.toLowerCase().includes(term))) {
|
|
185
|
+
score += 2;
|
|
186
|
+
matchedTerms.add(term);
|
|
187
|
+
reasons.add("import");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (score === 0) {
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
nodeId: node.nodeId,
|
|
195
|
+
path: node.path,
|
|
196
|
+
kind: node.kind,
|
|
197
|
+
score,
|
|
198
|
+
matchedTerms: [...matchedTerms],
|
|
199
|
+
reasons: [...reasons]
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function normalizeQueryTerms(query) {
|
|
203
|
+
const rawTerms = [
|
|
204
|
+
...(query.text ? tokenize(query.text) : []),
|
|
205
|
+
...((query.terms ?? []).flatMap((term) => tokenize(term)))
|
|
206
|
+
];
|
|
207
|
+
return [...new Set(rawTerms)];
|
|
208
|
+
}
|
|
209
|
+
function classifyContextNodeKind(path) {
|
|
210
|
+
const extension = extname(path).toLowerCase();
|
|
211
|
+
if (path.endsWith("package.json") || path.endsWith("tsconfig.json")) {
|
|
212
|
+
return "manifest";
|
|
213
|
+
}
|
|
214
|
+
if (extension === ".md" || extension === ".txt") {
|
|
215
|
+
return "document";
|
|
216
|
+
}
|
|
217
|
+
if (extension === ".sh") {
|
|
218
|
+
return "script";
|
|
219
|
+
}
|
|
220
|
+
if (extension === ".json" || extension === ".yaml" || extension === ".yml") {
|
|
221
|
+
return "config";
|
|
222
|
+
}
|
|
223
|
+
return "source_file";
|
|
224
|
+
}
|
|
225
|
+
function inferLanguage(path) {
|
|
226
|
+
const extension = extname(path).toLowerCase();
|
|
227
|
+
switch (extension) {
|
|
228
|
+
case ".ts":
|
|
229
|
+
case ".tsx":
|
|
230
|
+
return "typescript";
|
|
231
|
+
case ".js":
|
|
232
|
+
case ".jsx":
|
|
233
|
+
case ".mjs":
|
|
234
|
+
case ".cjs":
|
|
235
|
+
return "javascript";
|
|
236
|
+
case ".md":
|
|
237
|
+
return "markdown";
|
|
238
|
+
case ".json":
|
|
239
|
+
return "json";
|
|
240
|
+
case ".yaml":
|
|
241
|
+
case ".yml":
|
|
242
|
+
return "yaml";
|
|
243
|
+
case ".py":
|
|
244
|
+
return "python";
|
|
245
|
+
case ".sh":
|
|
246
|
+
return "shell";
|
|
247
|
+
default:
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function extractHeadings(path, content) {
|
|
252
|
+
if (extname(path).toLowerCase() !== ".md") {
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
return content
|
|
256
|
+
.split(/\r?\n/gu)
|
|
257
|
+
.map((line) => line.trim())
|
|
258
|
+
.filter((line) => line.startsWith("#"))
|
|
259
|
+
.map((line) => line.replace(/^#+\s*/u, "").trim())
|
|
260
|
+
.filter(Boolean)
|
|
261
|
+
.slice(0, 12);
|
|
262
|
+
}
|
|
263
|
+
function extractSymbols(content) {
|
|
264
|
+
const out = new Set();
|
|
265
|
+
const patterns = [
|
|
266
|
+
/export\s+(?:async\s+)?function\s+([A-Za-z0-9_]+)/gu,
|
|
267
|
+
/export\s+class\s+([A-Za-z0-9_]+)/gu,
|
|
268
|
+
/export\s+const\s+([A-Za-z0-9_]+)/gu,
|
|
269
|
+
/function\s+([A-Za-z0-9_]+)/gu,
|
|
270
|
+
/class\s+([A-Za-z0-9_]+)/gu,
|
|
271
|
+
/interface\s+([A-Za-z0-9_]+)/gu,
|
|
272
|
+
/type\s+([A-Za-z0-9_]+)/gu
|
|
273
|
+
];
|
|
274
|
+
for (const pattern of patterns) {
|
|
275
|
+
for (const match of content.matchAll(pattern)) {
|
|
276
|
+
if (match[1]) {
|
|
277
|
+
out.add(match[1]);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return [...out].slice(0, 16);
|
|
282
|
+
}
|
|
283
|
+
function extractKeywords(path, content) {
|
|
284
|
+
const out = new Set();
|
|
285
|
+
for (const part of path.split(/[\/._-]+/u)) {
|
|
286
|
+
if (part.length >= 3) {
|
|
287
|
+
out.add(part.toLowerCase());
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
for (const token of tokenize(content.split(/\r?\n/gu).slice(0, 16).join(" "))) {
|
|
291
|
+
out.add(token);
|
|
292
|
+
}
|
|
293
|
+
return [...out].slice(0, 24);
|
|
294
|
+
}
|
|
295
|
+
function extractImports(path, content) {
|
|
296
|
+
if (![".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"].includes(extname(path).toLowerCase())) {
|
|
297
|
+
return [];
|
|
298
|
+
}
|
|
299
|
+
const imports = new Set();
|
|
300
|
+
const patterns = [
|
|
301
|
+
/from\s+["']([^"']+)["']/gu,
|
|
302
|
+
/require\(\s*["']([^"']+)["']\s*\)/gu,
|
|
303
|
+
/import\(\s*["']([^"']+)["']\s*\)/gu
|
|
304
|
+
];
|
|
305
|
+
for (const pattern of patterns) {
|
|
306
|
+
for (const match of content.matchAll(pattern)) {
|
|
307
|
+
const candidate = match[1]?.trim();
|
|
308
|
+
if (candidate) {
|
|
309
|
+
imports.add(candidate);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return [...imports].slice(0, 24);
|
|
314
|
+
}
|
|
315
|
+
function resolveContextImport(fromPath, importPath, paths) {
|
|
316
|
+
if (!importPath.startsWith(".")) {
|
|
317
|
+
return undefined;
|
|
318
|
+
}
|
|
319
|
+
const fromDirectory = fromPath.includes("/") ? fromPath.slice(0, fromPath.lastIndexOf("/")) : "";
|
|
320
|
+
const base = posix.normalize(posix.join(fromDirectory, importPath)).replace(/^\.\//u, "");
|
|
321
|
+
const extensionlessBase = extname(base) ? base.slice(0, -extname(base).length) : base;
|
|
322
|
+
const candidates = [
|
|
323
|
+
base,
|
|
324
|
+
extensionlessBase,
|
|
325
|
+
`${extensionlessBase}.ts`,
|
|
326
|
+
`${extensionlessBase}.tsx`,
|
|
327
|
+
`${extensionlessBase}.js`,
|
|
328
|
+
`${extensionlessBase}.jsx`,
|
|
329
|
+
`${extensionlessBase}.md`,
|
|
330
|
+
`${base}.ts`,
|
|
331
|
+
`${base}.tsx`,
|
|
332
|
+
`${base}.js`,
|
|
333
|
+
`${base}.jsx`,
|
|
334
|
+
`${base}.md`,
|
|
335
|
+
`${base}/index.ts`,
|
|
336
|
+
`${base}/index.tsx`,
|
|
337
|
+
`${base}/index.js`
|
|
338
|
+
];
|
|
339
|
+
return candidates.find((candidate) => paths.has(candidate));
|
|
340
|
+
}
|
|
341
|
+
function dedupeEdges(edges) {
|
|
342
|
+
const seen = new Set();
|
|
343
|
+
return edges.filter((edge) => {
|
|
344
|
+
const key = `${edge.kind}:${edge.fromNodeId}:${edge.toNodeId}`;
|
|
345
|
+
if (seen.has(key)) {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
seen.add(key);
|
|
349
|
+
return true;
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
function tokenize(input) {
|
|
353
|
+
const normalized = input.replace(/([a-z0-9])([A-Z])/gu, "$1 $2").toLowerCase();
|
|
354
|
+
const stopWords = new Set([
|
|
355
|
+
"the",
|
|
356
|
+
"and",
|
|
357
|
+
"for",
|
|
358
|
+
"with",
|
|
359
|
+
"from",
|
|
360
|
+
"that",
|
|
361
|
+
"this",
|
|
362
|
+
"into",
|
|
363
|
+
"only",
|
|
364
|
+
"keep",
|
|
365
|
+
"make",
|
|
366
|
+
"loop",
|
|
367
|
+
"then",
|
|
368
|
+
"when"
|
|
369
|
+
]);
|
|
370
|
+
return [...new Set(normalized.match(/[a-z0-9_]{3,}/gu) ?? [])].filter((term) => !stopWords.has(term));
|
|
371
|
+
}
|
|
@@ -21,7 +21,7 @@ const POISON_PATTERNS = [
|
|
|
21
21
|
*/
|
|
22
22
|
const IDENTITY_REDEFINITION_PATTERNS = [
|
|
23
23
|
/\byou(?:'re|\s+are)\s+now\s+(?:a|an|the)\b(?!\s+(?:martin\s+loop|ai\s+coding\s+agent))/i,
|
|
24
|
-
/\byou(?:'re|\s+are)\s+no\s+longer\s+(
|
|
24
|
+
/\byou(?:'re|\s+are)\s+no\s+longer\s+(?!(?:martin\s+loop|an?\s+ai)\b)/i,
|
|
25
25
|
/\bforget\s+(?:that\s+)?you(?:'re|\s+are)\s+martin\s+loop\b/i,
|
|
26
26
|
/\b(?:pretend|imagine)\s+(?:that\s+)?you(?:'re|\s+are)\b/i,
|
|
27
27
|
/\bact\s+as\s+(?:if\s+you(?:'re|\s+are)\s+)?(?:a|an)\s+(?:different|new|unrestricted|jailbroken)\b/i,
|
|
@@ -13,7 +13,7 @@ const IGNORED_DIRS = new Set([
|
|
|
13
13
|
const MAX_FILE_BYTES = 64_000;
|
|
14
14
|
const MAX_FILES = 500;
|
|
15
15
|
export function resolveGroundingRoot(env = process.env) {
|
|
16
|
-
return env["MARTIN_GROUNDING_DIR"]?.trim()
|
|
16
|
+
return env["MARTIN_GROUNDING_DIR"]?.trim() ||
|
|
17
17
|
join(homedir(), ".martin", "grounding");
|
|
18
18
|
}
|
|
19
19
|
export async function loadOrBuildRepoGroundingIndex(repoRoot) {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type IdentityAttestation, type IdentityClaims, type IdentityToken } from "../contracts/index.js";
|
|
2
|
+
export interface LocalIdentityAuthority {
|
|
3
|
+
keyId: string;
|
|
4
|
+
algorithm: "ed25519";
|
|
5
|
+
privateKeyPem: string;
|
|
6
|
+
publicKeyPem: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function createLocalIdentityAuthority(input?: {
|
|
9
|
+
keyId?: string;
|
|
10
|
+
}): LocalIdentityAuthority;
|
|
11
|
+
export declare function issueIdentityToken(input: {
|
|
12
|
+
authority: Pick<LocalIdentityAuthority, "keyId" | "algorithm" | "privateKeyPem">;
|
|
13
|
+
claims: IdentityClaims;
|
|
14
|
+
}): IdentityToken;
|
|
15
|
+
export declare function verifyIdentityToken(input: {
|
|
16
|
+
token: IdentityToken;
|
|
17
|
+
publicKeyPem: string;
|
|
18
|
+
now?: string;
|
|
19
|
+
}): IdentityAttestation;
|
|
20
|
+
export declare function hasIdentityScope(token: IdentityToken, requiredScope: string): boolean;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { createPrivateKey, createPublicKey, generateKeyPairSync, sign, verify } from "node:crypto";
|
|
2
|
+
import { cloneIdentityAttestation, cloneIdentityClaims, cloneIdentityToken } from "../contracts/index.js";
|
|
3
|
+
export function createLocalIdentityAuthority(input = {}) {
|
|
4
|
+
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
|
|
5
|
+
return {
|
|
6
|
+
keyId: input.keyId ?? `martin-local-${Date.now().toString(36)}`,
|
|
7
|
+
algorithm: "ed25519",
|
|
8
|
+
privateKeyPem: privateKey.export({ format: "pem", type: "pkcs8" }).toString(),
|
|
9
|
+
publicKeyPem: publicKey.export({ format: "pem", type: "spki" }).toString()
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export function issueIdentityToken(input) {
|
|
13
|
+
const claims = cloneIdentityClaims(input.claims);
|
|
14
|
+
const signer = createPrivateKey(input.authority.privateKeyPem);
|
|
15
|
+
const payload = serializeIdentityClaims(claims);
|
|
16
|
+
const signature = sign(null, payload, signer).toString("base64");
|
|
17
|
+
return cloneIdentityToken({
|
|
18
|
+
schemaVersion: "martin.identity.v1",
|
|
19
|
+
algorithm: input.authority.algorithm,
|
|
20
|
+
keyId: input.authority.keyId,
|
|
21
|
+
claims,
|
|
22
|
+
signature
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
export function verifyIdentityToken(input) {
|
|
26
|
+
const token = cloneIdentityToken(input.token);
|
|
27
|
+
const verifier = createPublicKey(input.publicKeyPem);
|
|
28
|
+
const payload = serializeIdentityClaims(token.claims);
|
|
29
|
+
const verifiedAt = input.now ?? new Date().toISOString();
|
|
30
|
+
if (token.schemaVersion !== "martin.identity.v1") {
|
|
31
|
+
return cloneIdentityAttestation({
|
|
32
|
+
valid: false,
|
|
33
|
+
verifiedAt,
|
|
34
|
+
reason: `Unsupported identity schema version '${token.schemaVersion}'.`,
|
|
35
|
+
token
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
const signatureValid = verify(null, payload, verifier, Buffer.from(token.signature, "base64"));
|
|
39
|
+
if (!signatureValid) {
|
|
40
|
+
return cloneIdentityAttestation({
|
|
41
|
+
valid: false,
|
|
42
|
+
verifiedAt,
|
|
43
|
+
reason: "Identity signature verification failed.",
|
|
44
|
+
token
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const expiresAt = token.claims.expiresAt ? Date.parse(token.claims.expiresAt) : undefined;
|
|
48
|
+
if (expiresAt !== undefined && Number.isFinite(expiresAt) && expiresAt <= Date.parse(verifiedAt)) {
|
|
49
|
+
return cloneIdentityAttestation({
|
|
50
|
+
valid: false,
|
|
51
|
+
verifiedAt,
|
|
52
|
+
reason: "Identity token is expired.",
|
|
53
|
+
token
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return cloneIdentityAttestation({
|
|
57
|
+
valid: true,
|
|
58
|
+
verifiedAt,
|
|
59
|
+
reason: `Identity token for role '${token.claims.role}' verified successfully.`,
|
|
60
|
+
token
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
export function hasIdentityScope(token, requiredScope) {
|
|
64
|
+
const required = requiredScope.trim().toLowerCase();
|
|
65
|
+
if (required.length === 0) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
return token.claims.scopes.some((scope) => scope.trim().toLowerCase() === required);
|
|
69
|
+
}
|
|
70
|
+
function serializeIdentityClaims(claims) {
|
|
71
|
+
return Buffer.from(JSON.stringify(claims, sortKeys), "utf8");
|
|
72
|
+
}
|
|
73
|
+
function sortKeys(_, value) {
|
|
74
|
+
if (Array.isArray(value)) {
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
if (value && typeof value === "object") {
|
|
78
|
+
return Object.keys(value)
|
|
79
|
+
.sort()
|
|
80
|
+
.reduce((sorted, key) => {
|
|
81
|
+
sorted[key] = value[key];
|
|
82
|
+
return sorted;
|
|
83
|
+
}, {});
|
|
84
|
+
}
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
@@ -9,6 +9,11 @@ export { classifyFailure, computeEvidenceVector, evaluatePatchDecision, evaluate
|
|
|
9
9
|
export type { BudgetPreflightDecision, BudgetPreflightInput, CostGovernorState, EvidenceVectorInput, EvaluatedPatchDecision, ExitDecision, FailureAssessment, PatchDecisionInput, RecoveryDecision, RecoveryRecipe } from "./policy.js";
|
|
10
10
|
export type { ResolvedExecutionProfile, SafetyLeashDecision, SafetyViolation } from "./leash.js";
|
|
11
11
|
export type { GroundingScanResult, GroundingViolation, GroundingViolationKind, RepoGroundingHit, RepoGroundingIndex } from "./grounding.js";
|
|
12
|
+
export { buildContextGraphSnapshot, queryContextGraph } from "./context-graph.js";
|
|
13
|
+
export { createLocalIdentityAuthority, hasIdentityScope, issueIdentityToken, verifyIdentityToken } from "./identity.js";
|
|
14
|
+
export type { LocalIdentityAuthority } from "./identity.js";
|
|
15
|
+
export { compileExecutionPolicy } from "./policy-compiler.js";
|
|
16
|
+
export { assessTrajectory, decideCircuitBreak } from "./trajectory.js";
|
|
12
17
|
export { runContextIntegrityPrecheck } from "./context-integrity.js";
|
|
13
18
|
export type { ContextIntegrityPrecheck, ContextIntegrityVerdict } from "./context-integrity.js";
|
|
14
19
|
export { compilePromptPacket } from "./compiler.js";
|
|
@@ -132,6 +137,8 @@ export interface AttemptPolicyDecision {
|
|
|
132
137
|
allowed: boolean;
|
|
133
138
|
reason: string;
|
|
134
139
|
recommendedIntervention?: InterventionType;
|
|
140
|
+
trajectoryAssessment?: import("../contracts/index.js").TrajectoryAssessment;
|
|
141
|
+
circuitBreakDecision?: import("../contracts/index.js").CircuitBreakDecision;
|
|
135
142
|
}
|
|
136
143
|
/**
|
|
137
144
|
* Admission gate — must pass before any attempt is executed.
|
|
@@ -7,7 +7,12 @@ import { captureRollbackBoundary, restoreRollbackBoundary } from "./rollback.js"
|
|
|
7
7
|
import { compilePromptPacket } from "./compiler.js";
|
|
8
8
|
import { makeLedgerEvent, resolveRunsRoot, runDir } from "./persistence/index.js";
|
|
9
9
|
import { runContextIntegrityPrecheck } from "./context-integrity.js";
|
|
10
|
+
import { decideCircuitBreak } from "./trajectory.js";
|
|
10
11
|
export { classifyFailure, computeEvidenceVector, evaluatePatchDecision, evaluateCostGovernor, evaluateBudgetPreflight, inferExit, nextPolicyPhase, policyPhaseToLifecycleState, scorePatchDecision, selectRecoveryRecipe, evaluateVerificationLeash, evaluateFilesystemLeash, evaluateChangeApprovalLeash, evaluateSecretLeash, resolveExecutionProfile, redactSecretsFromText, buildRepoGroundingIndex, loadOrBuildRepoGroundingIndex, queryRepoGroundingIndex, scanPatchForGroundingViolations, captureRollbackBoundary, restoreRollbackBoundary };
|
|
12
|
+
export { buildContextGraphSnapshot, queryContextGraph } from "./context-graph.js";
|
|
13
|
+
export { createLocalIdentityAuthority, hasIdentityScope, issueIdentityToken, verifyIdentityToken } from "./identity.js";
|
|
14
|
+
export { compileExecutionPolicy } from "./policy-compiler.js";
|
|
15
|
+
export { assessTrajectory, decideCircuitBreak } from "./trajectory.js";
|
|
11
16
|
// ─── Context Integrity Pre-gate ──────────────────────────────────────────────
|
|
12
17
|
export { runContextIntegrityPrecheck } from "./context-integrity.js";
|
|
13
18
|
// ─── Prompt packet compiler ──────────────────────────────────────────────────
|
|
@@ -22,12 +27,35 @@ export { compileAndPersistContext } from "./persistence/index.js";
|
|
|
22
27
|
*/
|
|
23
28
|
export function evaluateAttemptPolicy(input) {
|
|
24
29
|
const { request, projectedUsd } = input;
|
|
30
|
+
const trajectoryAttempts = request.previousAttempts.map((attempt) => ({
|
|
31
|
+
index: attempt.index,
|
|
32
|
+
summary: attempt.summary,
|
|
33
|
+
failureClass: attempt.failureClass
|
|
34
|
+
}));
|
|
35
|
+
const circuitBreakDecision = decideCircuitBreak({
|
|
36
|
+
objective: request.context.objective,
|
|
37
|
+
verificationPlan: request.context.verificationPlan,
|
|
38
|
+
attempts: trajectoryAttempts,
|
|
39
|
+
remainingIterations: request.context.remainingIterations
|
|
40
|
+
});
|
|
41
|
+
const trajectoryAssessment = circuitBreakDecision.assessment;
|
|
42
|
+
if (circuitBreakDecision.shouldStop) {
|
|
43
|
+
return {
|
|
44
|
+
allowed: false,
|
|
45
|
+
reason: circuitBreakDecision.reason,
|
|
46
|
+
recommendedIntervention: circuitBreakDecision.recommendedIntervention,
|
|
47
|
+
trajectoryAssessment,
|
|
48
|
+
circuitBreakDecision
|
|
49
|
+
};
|
|
50
|
+
}
|
|
25
51
|
// Budget gate: reject if projected cost exceeds remaining
|
|
26
52
|
if (projectedUsd > request.context.remainingBudgetUsd) {
|
|
27
53
|
return {
|
|
28
54
|
allowed: false,
|
|
29
55
|
reason: `Projected cost $${projectedUsd} exceeds remaining budget $${request.context.remainingBudgetUsd}.`,
|
|
30
|
-
recommendedIntervention: "stop_loop"
|
|
56
|
+
recommendedIntervention: "stop_loop",
|
|
57
|
+
trajectoryAssessment,
|
|
58
|
+
circuitBreakDecision
|
|
31
59
|
};
|
|
32
60
|
}
|
|
33
61
|
// Iteration gate
|
|
@@ -35,7 +63,9 @@ export function evaluateAttemptPolicy(input) {
|
|
|
35
63
|
return {
|
|
36
64
|
allowed: false,
|
|
37
65
|
reason: "No remaining iterations in the budget.",
|
|
38
|
-
recommendedIntervention: "stop_loop"
|
|
66
|
+
recommendedIntervention: "stop_loop",
|
|
67
|
+
trajectoryAssessment,
|
|
68
|
+
circuitBreakDecision
|
|
39
69
|
};
|
|
40
70
|
}
|
|
41
71
|
// Oscillation detection: A/B/A pattern in failure classes
|
|
@@ -49,7 +79,9 @@ export function evaluateAttemptPolicy(input) {
|
|
|
49
79
|
return {
|
|
50
80
|
allowed: false,
|
|
51
81
|
reason: "Oscillating failure pattern detected. Escalating to human.",
|
|
52
|
-
recommendedIntervention: "escalate_human"
|
|
82
|
+
recommendedIntervention: "escalate_human",
|
|
83
|
+
trajectoryAssessment,
|
|
84
|
+
circuitBreakDecision
|
|
53
85
|
};
|
|
54
86
|
}
|
|
55
87
|
}
|
|
@@ -70,14 +102,18 @@ export function evaluateAttemptPolicy(input) {
|
|
|
70
102
|
return {
|
|
71
103
|
allowed: false,
|
|
72
104
|
reason: "Materially repetitive attempts detected. Escalating to human.",
|
|
73
|
-
recommendedIntervention: "escalate_human"
|
|
105
|
+
recommendedIntervention: "escalate_human",
|
|
106
|
+
trajectoryAssessment,
|
|
107
|
+
circuitBreakDecision
|
|
74
108
|
};
|
|
75
109
|
}
|
|
76
110
|
}
|
|
77
111
|
}
|
|
78
112
|
return {
|
|
79
113
|
allowed: true,
|
|
80
|
-
reason: "Attempt admitted."
|
|
114
|
+
reason: "Attempt admitted.",
|
|
115
|
+
trajectoryAssessment,
|
|
116
|
+
circuitBreakDecision
|
|
81
117
|
};
|
|
82
118
|
}
|
|
83
119
|
export function distillContext(loop, options = {}) {
|
|
@@ -278,7 +314,7 @@ export async function runMartin(input) {
|
|
|
278
314
|
// re-enters subsequent prompts, matching the documented "tool output / test output" scope.
|
|
279
315
|
const priorVerifierOutput = loop.events
|
|
280
316
|
.filter((event) => event.type === "verification.completed")
|
|
281
|
-
.flatMap((event) => event.payload
|
|
317
|
+
.flatMap((event) => event.payload?.steps ?? [])
|
|
282
318
|
.map((step) => step.detail)
|
|
283
319
|
.filter((detail) => Boolean(detail))
|
|
284
320
|
.join("\n---\n");
|
|
@@ -453,7 +489,8 @@ export async function runMartin(input) {
|
|
|
453
489
|
...(failure?.failureClass ? { failureClass: failure.failureClass } : {}),
|
|
454
490
|
...(failure?.recommendedIntervention
|
|
455
491
|
? { intervention: failure.recommendedIntervention }
|
|
456
|
-
: {})
|
|
492
|
+
: {}),
|
|
493
|
+
...(failure?.diagnosticHint ? { diagnosticHint: failure.diagnosticHint } : {})
|
|
457
494
|
};
|
|
458
495
|
loop = {
|
|
459
496
|
...loop,
|
|
@@ -468,7 +505,7 @@ export async function runMartin(input) {
|
|
|
468
505
|
estimatedUsd: roundUsd((loop.cost.estimatedUsd ?? loop.cost.actualUsd) + result.usage.estimatedUsd)
|
|
469
506
|
}
|
|
470
507
|
: {}),
|
|
471
|
-
provenance: getUsageProvenance(result.usage),
|
|
508
|
+
provenance: mergeCostProvenance(loop.cost.provenance, getUsageProvenance(result.usage)),
|
|
472
509
|
...(result.usage.providerSettlement
|
|
473
510
|
? { providerSettlement: result.usage.providerSettlement }
|
|
474
511
|
: {})
|
|
@@ -552,7 +589,7 @@ export async function runMartin(input) {
|
|
|
552
589
|
actualUsd: loop.cost.actualUsd,
|
|
553
590
|
remainingBudgetUsd: costState.remainingBudgetUsd,
|
|
554
591
|
pressure: costState.pressure,
|
|
555
|
-
provenance:
|
|
592
|
+
provenance: loop.cost.provenance
|
|
556
593
|
}
|
|
557
594
|
}, { now: now(), idFactory });
|
|
558
595
|
if (input.store) {
|
|
@@ -1136,6 +1173,23 @@ function getUsageProvenance(usage) {
|
|
|
1136
1173
|
}
|
|
1137
1174
|
return "actual";
|
|
1138
1175
|
}
|
|
1176
|
+
const COST_PROVENANCE_RANK = {
|
|
1177
|
+
unavailable: 0,
|
|
1178
|
+
estimated: 1,
|
|
1179
|
+
actual: 2
|
|
1180
|
+
};
|
|
1181
|
+
/**
|
|
1182
|
+
* Aggregates cost provenance across attempts. The cumulative loop provenance
|
|
1183
|
+
* can only be as trustworthy as its weakest attempt: if any attempt's cost was
|
|
1184
|
+
* estimated or unavailable, the cumulative total must reflect that, even if a
|
|
1185
|
+
* later attempt reports an authoritative actual cost.
|
|
1186
|
+
*/
|
|
1187
|
+
function mergeCostProvenance(previous, current) {
|
|
1188
|
+
if (previous === undefined) {
|
|
1189
|
+
return current;
|
|
1190
|
+
}
|
|
1191
|
+
return COST_PROVENANCE_RANK[current] < COST_PROVENANCE_RANK[previous] ? current : previous;
|
|
1192
|
+
}
|
|
1139
1193
|
function resolveChangedFiles(result, repoRoot) {
|
|
1140
1194
|
if (result.execution?.changedFiles !== undefined) {
|
|
1141
1195
|
return result.execution.changedFiles;
|
|
@@ -39,7 +39,7 @@ const BLOCKED_PATTERNS = [
|
|
|
39
39
|
*/
|
|
40
40
|
function commandContainsDestructiveRemoval(command) {
|
|
41
41
|
const normalized = command.replace(/\$\{?IFS\}?/giu, " ").toLowerCase();
|
|
42
|
-
const rmInvocation = /(?:^|[\s;&|`(])(
|
|
42
|
+
const rmInvocation = /(?:^|[\s;&|`(])(?:[^\s;&|`(]+\/)?rm\s+([^\n;|`]+)/giu;
|
|
43
43
|
let match;
|
|
44
44
|
while ((match = rmInvocation.exec(normalized)) !== null) {
|
|
45
45
|
const args = match[1] ?? "";
|
|
@@ -21,6 +21,7 @@ export interface StoredReceiptIntegrityMaterial {
|
|
|
21
21
|
chain: ReceiptIntegrityChainEntry[];
|
|
22
22
|
signatureHmacSha256: string;
|
|
23
23
|
}
|
|
24
|
+
export declare function resolveReceiptIntegrityRoot(env?: NodeJS.ProcessEnv): string;
|
|
24
25
|
export declare function writeReceiptIntegrityMaterial(input: {
|
|
25
26
|
runId: string;
|
|
26
27
|
runsRoot: string;
|