@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,19 @@
|
|
|
1
|
+
export declare const MARTIN_ERROR_CATEGORIES: readonly ["invalid_input", "environment", "auth", "not_found", "store_unreadable", "verification_failed", "policy_blocked", "budget_exit", "transient"];
|
|
2
|
+
export type MartinErrorCategory = (typeof MARTIN_ERROR_CATEGORIES)[number];
|
|
3
|
+
export type MartinOutputMode = "human" | "json" | "quiet";
|
|
4
|
+
export interface MartinRunSelector {
|
|
5
|
+
runsDir?: string;
|
|
6
|
+
file?: string;
|
|
7
|
+
loopId?: string;
|
|
8
|
+
latest?: boolean;
|
|
9
|
+
attemptIndex?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface MartinRunListFilters {
|
|
12
|
+
runsDir?: string;
|
|
13
|
+
limit?: number;
|
|
14
|
+
status?: string;
|
|
15
|
+
lifecycleState?: string;
|
|
16
|
+
adapterId?: string;
|
|
17
|
+
model?: string;
|
|
18
|
+
updatedAfter?: string;
|
|
19
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { InterventionType } from "./index.js";
|
|
2
|
+
export type TrajectoryStatus = "healthy" | "watch" | "drifting" | "stalled";
|
|
3
|
+
export type TrajectoryConfidence = "low" | "medium" | "high";
|
|
4
|
+
export type TrajectorySignalCode = "failure_oscillation" | "attempt_repetition" | "verification_stall" | "objective_drift";
|
|
5
|
+
export interface TrajectorySignal {
|
|
6
|
+
code: TrajectorySignalCode;
|
|
7
|
+
severity: "low" | "medium" | "high";
|
|
8
|
+
summary: string;
|
|
9
|
+
attemptIndexes: number[];
|
|
10
|
+
matchedTerms?: string[];
|
|
11
|
+
}
|
|
12
|
+
export interface TrajectoryAssessment {
|
|
13
|
+
status: TrajectoryStatus;
|
|
14
|
+
confidence: TrajectoryConfidence;
|
|
15
|
+
summary: string;
|
|
16
|
+
signals: TrajectorySignal[];
|
|
17
|
+
recommendedIntervention?: InterventionType;
|
|
18
|
+
}
|
|
19
|
+
export interface CircuitBreakDecision {
|
|
20
|
+
shouldStop: boolean;
|
|
21
|
+
reason: string;
|
|
22
|
+
assessment: TrajectoryAssessment;
|
|
23
|
+
recommendedIntervention?: InterventionType;
|
|
24
|
+
}
|
|
25
|
+
export declare function cloneTrajectoryAssessment(assessment: TrajectoryAssessment): TrajectoryAssessment;
|
|
26
|
+
export declare function cloneCircuitBreakDecision(decision: CircuitBreakDecision): CircuitBreakDecision;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function cloneTrajectoryAssessment(assessment) {
|
|
2
|
+
return {
|
|
3
|
+
status: assessment.status,
|
|
4
|
+
confidence: assessment.confidence,
|
|
5
|
+
summary: assessment.summary,
|
|
6
|
+
...(assessment.recommendedIntervention
|
|
7
|
+
? { recommendedIntervention: assessment.recommendedIntervention }
|
|
8
|
+
: {}),
|
|
9
|
+
signals: assessment.signals.map((signal) => ({
|
|
10
|
+
code: signal.code,
|
|
11
|
+
severity: signal.severity,
|
|
12
|
+
summary: signal.summary,
|
|
13
|
+
attemptIndexes: [...signal.attemptIndexes],
|
|
14
|
+
...(signal.matchedTerms ? { matchedTerms: [...signal.matchedTerms] } : {})
|
|
15
|
+
}))
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function cloneCircuitBreakDecision(decision) {
|
|
19
|
+
return {
|
|
20
|
+
shouldStop: decision.shouldStop,
|
|
21
|
+
reason: decision.reason,
|
|
22
|
+
assessment: cloneTrajectoryAssessment(decision.assessment),
|
|
23
|
+
...(decision.recommendedIntervention
|
|
24
|
+
? { recommendedIntervention: decision.recommendedIntervention }
|
|
25
|
+
: {})
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { LoopAttempt, LoopTask } from "../../../contracts/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* The subset of MartinAdapterRequest fields needed by the prompt packet
|
|
4
|
+
* compiler. Defined here (not in index.ts) to avoid circular imports:
|
|
5
|
+
* index.ts → ./persistence/index.js → ./persistence/compiler.js → ./compiler.js (no cycle)
|
|
6
|
+
*/
|
|
7
|
+
export interface CompilerAdapterRequest {
|
|
8
|
+
loopId: string;
|
|
9
|
+
attemptId: string;
|
|
10
|
+
context: {
|
|
11
|
+
taskTitle: string;
|
|
12
|
+
objective: string;
|
|
13
|
+
verificationPlan: string[];
|
|
14
|
+
verificationStack?: LoopTask["verificationStack"];
|
|
15
|
+
mutationMode?: LoopTask["mutationMode"];
|
|
16
|
+
repoRoot?: string;
|
|
17
|
+
allowedPaths?: string[];
|
|
18
|
+
deniedPaths?: string[];
|
|
19
|
+
acceptanceCriteria?: string[];
|
|
20
|
+
focus: string;
|
|
21
|
+
remainingBudgetUsd: number;
|
|
22
|
+
remainingIterations: number;
|
|
23
|
+
remainingTokens: number;
|
|
24
|
+
};
|
|
25
|
+
previousAttempts: LoopAttempt[];
|
|
26
|
+
}
|
|
27
|
+
export interface PromptPacket {
|
|
28
|
+
loopId: string;
|
|
29
|
+
attemptNumber: number;
|
|
30
|
+
contract: {
|
|
31
|
+
objective: string;
|
|
32
|
+
verificationPlan: string[];
|
|
33
|
+
mutationMode?: LoopTask["mutationMode"];
|
|
34
|
+
allowedPaths?: string[];
|
|
35
|
+
deniedPaths?: string[];
|
|
36
|
+
acceptanceCriteria?: string[];
|
|
37
|
+
};
|
|
38
|
+
/** Prior failure/intervention pairs as "failureClass:intervention" strings. */
|
|
39
|
+
priorFailurePatterns: string[];
|
|
40
|
+
guidance: string;
|
|
41
|
+
budgetEnvelope: {
|
|
42
|
+
remainingBudgetUsd: number;
|
|
43
|
+
remainingIterations: number;
|
|
44
|
+
remainingTokens: number;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Compiles a deterministic PromptPacket from a MartinAdapterRequest.
|
|
49
|
+
* This is the context compiler — takes structured request state and produces
|
|
50
|
+
* a reconstructable packet (no chat history required).
|
|
51
|
+
*/
|
|
52
|
+
export declare function compilePromptPacket(request: CompilerAdapterRequest): PromptPacket;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { redactSecretsFromText } from "./leash.js";
|
|
2
|
+
/**
|
|
3
|
+
* Compiles a deterministic PromptPacket from a MartinAdapterRequest.
|
|
4
|
+
* This is the context compiler — takes structured request state and produces
|
|
5
|
+
* a reconstructable packet (no chat history required).
|
|
6
|
+
*/
|
|
7
|
+
export function compilePromptPacket(request) {
|
|
8
|
+
const priorFailurePatterns = request.previousAttempts
|
|
9
|
+
.filter((a) => a.failureClass && a.intervention)
|
|
10
|
+
.map((a) => `${a.failureClass}:${a.intervention}`);
|
|
11
|
+
const guidanceParts = request.context.mutationMode === "verify_only"
|
|
12
|
+
? [
|
|
13
|
+
"Do not modify files.",
|
|
14
|
+
"Run the verifier only and report whether it passed."
|
|
15
|
+
]
|
|
16
|
+
: [
|
|
17
|
+
"Only modify files directly required to satisfy the contract.",
|
|
18
|
+
"Do not touch files outside the allowed paths."
|
|
19
|
+
];
|
|
20
|
+
if (request.context.allowedPaths && request.context.allowedPaths.length > 0) {
|
|
21
|
+
guidanceParts.push(`Allowed paths: ${request.context.allowedPaths.join(", ")}.`);
|
|
22
|
+
}
|
|
23
|
+
if (request.context.deniedPaths && request.context.deniedPaths.length > 0) {
|
|
24
|
+
guidanceParts.push(`Denied paths (never touch): ${request.context.deniedPaths.join(", ")}.`);
|
|
25
|
+
}
|
|
26
|
+
if (priorFailurePatterns.length > 0) {
|
|
27
|
+
guidanceParts.push(`Prior failure patterns: ${priorFailurePatterns.join(", ")}. Adjust strategy accordingly.`);
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
loopId: request.loopId,
|
|
31
|
+
attemptNumber: request.previousAttempts.length + 1,
|
|
32
|
+
contract: {
|
|
33
|
+
objective: redactSecretsFromText(request.context.objective),
|
|
34
|
+
verificationPlan: request.context.verificationPlan,
|
|
35
|
+
...(request.context.mutationMode ? { mutationMode: request.context.mutationMode } : {}),
|
|
36
|
+
...(request.context.allowedPaths ? { allowedPaths: request.context.allowedPaths } : {}),
|
|
37
|
+
...(request.context.deniedPaths ? { deniedPaths: request.context.deniedPaths } : {}),
|
|
38
|
+
...(request.context.acceptanceCriteria
|
|
39
|
+
? {
|
|
40
|
+
acceptanceCriteria: request.context.acceptanceCriteria.map((criterion) => redactSecretsFromText(criterion))
|
|
41
|
+
}
|
|
42
|
+
: {})
|
|
43
|
+
},
|
|
44
|
+
priorFailurePatterns,
|
|
45
|
+
guidance: guidanceParts.join(" "),
|
|
46
|
+
budgetEnvelope: {
|
|
47
|
+
remainingBudgetUsd: request.context.remainingBudgetUsd,
|
|
48
|
+
remainingIterations: request.context.remainingIterations,
|
|
49
|
+
remainingTokens: request.context.remainingTokens
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type ContextGraphBuildOptions, type ContextGraphHit, type ContextGraphSnapshot, type ContextQuery } from "../../../contracts/index.js";
|
|
2
|
+
export declare function buildContextGraphSnapshot(repoRoot: string, options?: ContextGraphBuildOptions): Promise<ContextGraphSnapshot>;
|
|
3
|
+
export declare function queryContextGraph(snapshot: ContextGraphSnapshot, query: ContextQuery): ContextGraphHit[];
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type ContextIntegrityVerdict = "clean" | "context_poisoning_warning" | "context_poisoning_block";
|
|
2
|
+
export interface ContextIntegrityPrecheck {
|
|
3
|
+
runId: string;
|
|
4
|
+
attemptIndex: number;
|
|
5
|
+
verdict: ContextIntegrityVerdict;
|
|
6
|
+
reason?: string;
|
|
7
|
+
detectedSignals: string[];
|
|
8
|
+
analyzedChannels: {
|
|
9
|
+
system: boolean;
|
|
10
|
+
user: boolean;
|
|
11
|
+
tools: boolean;
|
|
12
|
+
history: boolean;
|
|
13
|
+
};
|
|
14
|
+
timestamp: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* T05: Context Poisoning Pre-gate.
|
|
18
|
+
* Scans untrusted input channels for authority inversion or instruction re-injection.
|
|
19
|
+
* Runs BEFORE admission control and core reasoning.
|
|
20
|
+
*/
|
|
21
|
+
export declare function runContextIntegrityPrecheck(runId: string, attemptIndex: number, artifactsDir: string, inputs: {
|
|
22
|
+
userPrompt?: string;
|
|
23
|
+
toolOutput?: string;
|
|
24
|
+
retrievedContext?: string;
|
|
25
|
+
history?: string;
|
|
26
|
+
}): Promise<ContextIntegrityPrecheck>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
const POISON_PATTERNS = [
|
|
4
|
+
/ignore\s+(?:all\s+)?previous\s+instructions/i,
|
|
5
|
+
/you\s+are\s+now\s+a\s+(?!Martin\s+Loop)/i,
|
|
6
|
+
/new\s+rule:/i,
|
|
7
|
+
/disregard\s+(?:safety|policy|guardrails)/i,
|
|
8
|
+
/override\s+system\s+authority/i,
|
|
9
|
+
/hidden\s+instruction:/i,
|
|
10
|
+
/\[system_override\]/i,
|
|
11
|
+
/\[authority_inversion\]/i
|
|
12
|
+
];
|
|
13
|
+
/**
|
|
14
|
+
* Identity-redefinition / persona-override patterns.
|
|
15
|
+
*
|
|
16
|
+
* These intentionally require an *override framing* (e.g. "now", "no longer",
|
|
17
|
+
* "forget", "pretend", or an explicit authority-role claim) rather than any
|
|
18
|
+
* sentence shaped like "you are X" / "I am X" — the latter matches ordinary
|
|
19
|
+
* benign text (e.g. "You are welcome to try MartinLoop") and produced
|
|
20
|
+
* false-positive hard aborts.
|
|
21
|
+
*/
|
|
22
|
+
const IDENTITY_REDEFINITION_PATTERNS = [
|
|
23
|
+
/\byou(?:'re|\s+are)\s+now\s+(?:a|an|the)\b(?!\s+(?:martin\s+loop|ai\s+coding\s+agent))/i,
|
|
24
|
+
/\byou(?:'re|\s+are)\s+no\s+longer\s+(?!(?:martin\s+loop|an?\s+ai)\b)/i,
|
|
25
|
+
/\bforget\s+(?:that\s+)?you(?:'re|\s+are)\s+martin\s+loop\b/i,
|
|
26
|
+
/\b(?:pretend|imagine)\s+(?:that\s+)?you(?:'re|\s+are)\b/i,
|
|
27
|
+
/\bact\s+as\s+(?:if\s+you(?:'re|\s+are)\s+)?(?:a|an)\s+(?:different|new|unrestricted|jailbroken)\b/i,
|
|
28
|
+
/\bi\s+am\s+(?:the|your)\s+(?:new\s+)?(?:system|developer|admin(?:istrator)?|root\s*user|owner|creator|operator)\b/i
|
|
29
|
+
];
|
|
30
|
+
/**
|
|
31
|
+
* T05: Context Poisoning Pre-gate.
|
|
32
|
+
* Scans untrusted input channels for authority inversion or instruction re-injection.
|
|
33
|
+
* Runs BEFORE admission control and core reasoning.
|
|
34
|
+
*/
|
|
35
|
+
export async function runContextIntegrityPrecheck(runId, attemptIndex, artifactsDir, inputs) {
|
|
36
|
+
const signals = [];
|
|
37
|
+
const analyzedChannels = {
|
|
38
|
+
system: true,
|
|
39
|
+
user: Boolean(inputs.userPrompt),
|
|
40
|
+
tools: Boolean(inputs.toolOutput),
|
|
41
|
+
history: Boolean(inputs.history)
|
|
42
|
+
};
|
|
43
|
+
const untrustedBuffer = [
|
|
44
|
+
inputs.userPrompt,
|
|
45
|
+
inputs.toolOutput,
|
|
46
|
+
inputs.retrievedContext,
|
|
47
|
+
inputs.history
|
|
48
|
+
]
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
.join("\n---\n");
|
|
51
|
+
for (const pattern of POISON_PATTERNS) {
|
|
52
|
+
if (pattern.test(untrustedBuffer)) {
|
|
53
|
+
signals.push(`Detected poison pattern: ${pattern.toString()}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
for (const pattern of IDENTITY_REDEFINITION_PATTERNS) {
|
|
57
|
+
if (pattern.test(untrustedBuffer)) {
|
|
58
|
+
signals.push("Identity redefinition attempt detected.");
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const verdict = signals.length > 0 ? "context_poisoning_block" : "clean";
|
|
63
|
+
const precheck = {
|
|
64
|
+
runId,
|
|
65
|
+
attemptIndex,
|
|
66
|
+
verdict,
|
|
67
|
+
reason: signals.length > 0 ? `Detected ${signals.length} poisoning signal(s).` : undefined,
|
|
68
|
+
detectedSignals: signals,
|
|
69
|
+
analyzedChannels,
|
|
70
|
+
timestamp: new Date().toISOString()
|
|
71
|
+
};
|
|
72
|
+
try {
|
|
73
|
+
await mkdir(artifactsDir, { recursive: true });
|
|
74
|
+
await writeFile(join(artifactsDir, "context-integrity-precheck.json"), JSON.stringify(precheck, null, 2), "utf8");
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// non-fatal — artifact persistence is best-effort
|
|
78
|
+
}
|
|
79
|
+
return precheck;
|
|
80
|
+
}
|