contextos-mcp 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +260 -0
- package/bin/mcp.mjs +61 -0
- package/bin/swarm.mjs +45 -0
- package/dist/agents/aider.d.ts +12 -0
- package/dist/agents/aider.js +192 -0
- package/dist/agents/aider.js.map +1 -0
- package/dist/agents/claude-code.d.ts +9 -0
- package/dist/agents/claude-code.js +236 -0
- package/dist/agents/claude-code.js.map +1 -0
- package/dist/agents/codex.d.ts +14 -0
- package/dist/agents/codex.js +203 -0
- package/dist/agents/codex.js.map +1 -0
- package/dist/agents/direct-llm.d.ts +22 -0
- package/dist/agents/direct-llm.js +254 -0
- package/dist/agents/direct-llm.js.map +1 -0
- package/dist/agents/mock.d.ts +10 -0
- package/dist/agents/mock.js +170 -0
- package/dist/agents/mock.js.map +1 -0
- package/dist/agents/opencode.d.ts +23 -0
- package/dist/agents/opencode.js +631 -0
- package/dist/agents/opencode.js.map +1 -0
- package/dist/agents/provider.d.ts +11 -0
- package/dist/agents/provider.js +31 -0
- package/dist/agents/provider.js.map +1 -0
- package/dist/compression/compressor.d.ts +28 -0
- package/dist/compression/compressor.js +268 -0
- package/dist/compression/compressor.js.map +1 -0
- package/dist/config.d.ts +43 -0
- package/dist/config.js +172 -0
- package/dist/config.js.map +1 -0
- package/dist/contextos/loader.d.ts +46 -0
- package/dist/contextos/loader.js +120 -0
- package/dist/contextos/loader.js.map +1 -0
- package/dist/contextos/selector.d.ts +46 -0
- package/dist/contextos/selector.js +80 -0
- package/dist/contextos/selector.js.map +1 -0
- package/dist/core/action-dispatcher.d.ts +15 -0
- package/dist/core/action-dispatcher.js +28 -0
- package/dist/core/action-dispatcher.js.map +1 -0
- package/dist/core/action-schema.d.ts +72 -0
- package/dist/core/action-schema.js +86 -0
- package/dist/core/action-schema.js.map +1 -0
- package/dist/core/repl-interface.d.ts +48 -0
- package/dist/core/repl-interface.js +5 -0
- package/dist/core/repl-interface.js.map +1 -0
- package/dist/core/repl.d.ts +59 -0
- package/dist/core/repl.js +353 -0
- package/dist/core/repl.js.map +1 -0
- package/dist/core/rlm.d.ts +65 -0
- package/dist/core/rlm.js +564 -0
- package/dist/core/rlm.js.map +1 -0
- package/dist/core/runtime.py +470 -0
- package/dist/core/types.d.ts +265 -0
- package/dist/core/types.js +19 -0
- package/dist/core/types.js.map +1 -0
- package/dist/env.d.ts +10 -0
- package/dist/env.js +75 -0
- package/dist/env.js.map +1 -0
- package/dist/hooks/runner.d.ts +43 -0
- package/dist/hooks/runner.js +173 -0
- package/dist/hooks/runner.js.map +1 -0
- package/dist/labs/cli.d.ts +15 -0
- package/dist/labs/cli.js +295 -0
- package/dist/labs/cli.js.map +1 -0
- package/dist/labs/episodic-memory.d.ts +132 -0
- package/dist/labs/episodic-memory.js +391 -0
- package/dist/labs/episodic-memory.js.map +1 -0
- package/dist/labs/interactive-swarm.d.ts +20 -0
- package/dist/labs/interactive-swarm.js +1173 -0
- package/dist/labs/interactive-swarm.js.map +1 -0
- package/dist/labs/interactive.d.ts +10 -0
- package/dist/labs/interactive.js +2099 -0
- package/dist/labs/interactive.js.map +1 -0
- package/dist/labs/ts-analyzer.d.ts +74 -0
- package/dist/labs/ts-analyzer.js +379 -0
- package/dist/labs/ts-analyzer.js.map +1 -0
- package/dist/labs/viewer.d.ts +12 -0
- package/dist/labs/viewer.js +1284 -0
- package/dist/labs/viewer.js.map +1 -0
- package/dist/main.d.ts +15 -0
- package/dist/main.js +247 -0
- package/dist/main.js.map +1 -0
- package/dist/mcp/server.d.ts +16 -0
- package/dist/mcp/server.js +192775 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/session.d.ts +117 -0
- package/dist/mcp/session.js +450 -0
- package/dist/mcp/session.js.map +1 -0
- package/dist/mcp/state.d.ts +42 -0
- package/dist/mcp/state.js +537 -0
- package/dist/mcp/state.js.map +1 -0
- package/dist/mcp/tools/contextos.d.ts +17 -0
- package/dist/mcp/tools/contextos.js +583 -0
- package/dist/mcp/tools/contextos.js.map +1 -0
- package/dist/mcp/tools.d.ts +15 -0
- package/dist/mcp/tools.js +393 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/orchestration/reviewer-gate.d.ts +43 -0
- package/dist/orchestration/reviewer-gate.js +280 -0
- package/dist/orchestration/reviewer-gate.js.map +1 -0
- package/dist/orchestration/reviewer-prompt.d.ts +18 -0
- package/dist/orchestration/reviewer-prompt.js +67 -0
- package/dist/orchestration/reviewer-prompt.js.map +1 -0
- package/dist/orchestration/verification-runner.d.ts +53 -0
- package/dist/orchestration/verification-runner.js +352 -0
- package/dist/orchestration/verification-runner.js.map +1 -0
- package/dist/plugins/adapter.d.ts +11 -0
- package/dist/plugins/adapter.js +11 -0
- package/dist/plugins/adapter.js.map +1 -0
- package/dist/plugins/core/archive.d.ts +9 -0
- package/dist/plugins/core/archive.js +128 -0
- package/dist/plugins/core/archive.js.map +1 -0
- package/dist/plugins/core/digest.d.ts +11 -0
- package/dist/plugins/core/digest.js +70 -0
- package/dist/plugins/core/digest.js.map +1 -0
- package/dist/plugins/core/grants.d.ts +22 -0
- package/dist/plugins/core/grants.js +90 -0
- package/dist/plugins/core/grants.js.map +1 -0
- package/dist/plugins/core/index.d.ts +10 -0
- package/dist/plugins/core/index.js +11 -0
- package/dist/plugins/core/index.js.map +1 -0
- package/dist/plugins/core/pinning.d.ts +15 -0
- package/dist/plugins/core/pinning.js +86 -0
- package/dist/plugins/core/pinning.js.map +1 -0
- package/dist/plugins/core/types.d.ts +105 -0
- package/dist/plugins/core/types.js +22 -0
- package/dist/plugins/core/types.js.map +1 -0
- package/dist/plugins/core/updater.d.ts +16 -0
- package/dist/plugins/core/updater.js +64 -0
- package/dist/plugins/core/updater.js.map +1 -0
- package/dist/plugins/loader.d.ts +65 -0
- package/dist/plugins/loader.js +110 -0
- package/dist/plugins/loader.js.map +1 -0
- package/dist/prompts/orchestrator.d.ts +6 -0
- package/dist/prompts/orchestrator.js +114 -0
- package/dist/prompts/orchestrator.js.map +1 -0
- package/dist/routing/model-resolver.d.ts +27 -0
- package/dist/routing/model-resolver.js +265 -0
- package/dist/routing/model-resolver.js.map +1 -0
- package/dist/routing/model-router.d.ts +130 -0
- package/dist/routing/model-router.js +515 -0
- package/dist/routing/model-router.js.map +1 -0
- package/dist/runtime/attestations.cjs +260 -0
- package/dist/runtime/reviewer-pipeline.cjs +319 -0
- package/dist/runtime/sandbox.cjs +468 -0
- package/dist/runtime/state-machine.cjs +885 -0
- package/dist/runtime/thread-store.cjs +175 -0
- package/dist/runtime/transactional-git.cjs +315 -0
- package/dist/runtime/verification-pipeline.cjs +435 -0
- package/dist/sandbox/execution.d.ts +165 -0
- package/dist/sandbox/execution.js +497 -0
- package/dist/sandbox/execution.js.map +1 -0
- package/dist/security/file-policy.d.ts +22 -0
- package/dist/security/file-policy.js +53 -0
- package/dist/security/file-policy.js.map +1 -0
- package/dist/security/repository-boundary.d.ts +21 -0
- package/dist/security/repository-boundary.js +119 -0
- package/dist/security/repository-boundary.js.map +1 -0
- package/dist/security/secret-filter.d.ts +36 -0
- package/dist/security/secret-filter.js +149 -0
- package/dist/security/secret-filter.js.map +1 -0
- package/dist/swarm.d.ts +14 -0
- package/dist/swarm.js +624 -0
- package/dist/swarm.js.map +1 -0
- package/dist/threads/cache.d.ts +64 -0
- package/dist/threads/cache.js +241 -0
- package/dist/threads/cache.js.map +1 -0
- package/dist/threads/manager.d.ts +94 -0
- package/dist/threads/manager.js +914 -0
- package/dist/threads/manager.js.map +1 -0
- package/dist/ui/banner.d.ts +14 -0
- package/dist/ui/banner.js +42 -0
- package/dist/ui/banner.js.map +1 -0
- package/dist/ui/dashboard.d.ts +33 -0
- package/dist/ui/dashboard.js +135 -0
- package/dist/ui/dashboard.js.map +1 -0
- package/dist/ui/index.d.ts +11 -0
- package/dist/ui/index.js +12 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/log.d.ts +39 -0
- package/dist/ui/log.js +126 -0
- package/dist/ui/log.js.map +1 -0
- package/dist/ui/onboarding.d.ts +14 -0
- package/dist/ui/onboarding.js +755 -0
- package/dist/ui/onboarding.js.map +1 -0
- package/dist/ui/run-log.d.ts +50 -0
- package/dist/ui/run-log.js +60 -0
- package/dist/ui/run-log.js.map +1 -0
- package/dist/ui/spinner.d.ts +41 -0
- package/dist/ui/spinner.js +172 -0
- package/dist/ui/spinner.js.map +1 -0
- package/dist/ui/streaming-feed.d.ts +72 -0
- package/dist/ui/streaming-feed.js +386 -0
- package/dist/ui/streaming-feed.js.map +1 -0
- package/dist/ui/summary.d.ts +18 -0
- package/dist/ui/summary.js +113 -0
- package/dist/ui/summary.js.map +1 -0
- package/dist/ui/text-input.d.ts +19 -0
- package/dist/ui/text-input.js +266 -0
- package/dist/ui/text-input.js.map +1 -0
- package/dist/ui/theme.d.ts +63 -0
- package/dist/ui/theme.js +97 -0
- package/dist/ui/theme.js.map +1 -0
- package/dist/utils/command-exists.d.ts +11 -0
- package/dist/utils/command-exists.js +41 -0
- package/dist/utils/command-exists.js.map +1 -0
- package/dist/utils/process-security.d.ts +19 -0
- package/dist/utils/process-security.js +90 -0
- package/dist/utils/process-security.js.map +1 -0
- package/dist/utils/rolling-buffer.d.ts +16 -0
- package/dist/utils/rolling-buffer.js +30 -0
- package/dist/utils/rolling-buffer.js.map +1 -0
- package/dist/utils/safe-spawn.d.ts +13 -0
- package/dist/utils/safe-spawn.js +49 -0
- package/dist/utils/safe-spawn.js.map +1 -0
- package/dist/worktree/manager.d.ts +69 -0
- package/dist/worktree/manager.js +593 -0
- package/dist/worktree/manager.js.map +1 -0
- package/dist/worktree/merge.d.ts +58 -0
- package/dist/worktree/merge.js +389 -0
- package/dist/worktree/merge.js.map +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .agents/runtime/attestations.cjs
|
|
3
|
+
* ContextOS — Machine-Verifiable Runtime Attestations Engine
|
|
4
|
+
*
|
|
5
|
+
* Implements cryptographic proof-of-work attestations for verification and review:
|
|
6
|
+
* - VerificationAttestation (command, exitCode, output digest, runner mode)
|
|
7
|
+
* - ReviewAttestation (implementer/reviewer IDs, model, independence, verdict)
|
|
8
|
+
* - Fail-closed validation (no PASS without verifiable evidence)
|
|
9
|
+
* - Candidate drift detection (SHA-256 diff & head commitment checks)
|
|
10
|
+
* - Zero install-time dependencies (pure Node.cjs crypto)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
"use strict";
|
|
14
|
+
|
|
15
|
+
const crypto = require("node:crypto");
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Computes deterministic SHA-256 of string or Buffer.
|
|
19
|
+
*/
|
|
20
|
+
function sha256(data) {
|
|
21
|
+
return crypto
|
|
22
|
+
.createHash("sha256")
|
|
23
|
+
.update(data || "")
|
|
24
|
+
.digest("hex");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Computes deterministic hash for a sorted list of write-scope paths.
|
|
29
|
+
*/
|
|
30
|
+
function computeScopeSha256(scopeArray = []) {
|
|
31
|
+
const sorted = Array.from(new Set(scopeArray.map((p) => p.replace(/\\/g, "/")))).sort();
|
|
32
|
+
return sha256(sorted.join("\n"));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Computes deterministic hash for a git diff.
|
|
37
|
+
*/
|
|
38
|
+
function computeDiffSha256(diffText = "") {
|
|
39
|
+
// Normalize CRLF to LF for deterministic hashes across platforms
|
|
40
|
+
const normalized = (diffText || "").replace(/\r\n/g, "\n");
|
|
41
|
+
return sha256(normalized);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Validates and constructs a machine-verifiable VerificationAttestation.
|
|
46
|
+
* Strict fail-closed semantics: PASS is forbidden without exitCode === 0 and outputSha256.
|
|
47
|
+
*
|
|
48
|
+
* @param {Object} params
|
|
49
|
+
* @returns {Object} VerificationAttestation
|
|
50
|
+
*/
|
|
51
|
+
function createVerificationAttestation(params = {}) {
|
|
52
|
+
const {
|
|
53
|
+
status,
|
|
54
|
+
subject = {},
|
|
55
|
+
command,
|
|
56
|
+
evidence,
|
|
57
|
+
runnerMode = "host-unsafe",
|
|
58
|
+
startedAt = Date.now(),
|
|
59
|
+
completedAt = Date.now(),
|
|
60
|
+
reasonCode,
|
|
61
|
+
} = params;
|
|
62
|
+
|
|
63
|
+
if (!status) {
|
|
64
|
+
throw new Error("VerificationAttestation requires a status");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!subject.repositoryFingerprint || !subject.baseSha || !subject.headSha) {
|
|
68
|
+
throw new Error("VerificationAttestation requires subject repositoryFingerprint, baseSha, and headSha");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const diffSha256 = subject.diffSha256 || sha256("");
|
|
72
|
+
const scopeSha256 = subject.scopeSha256 || computeScopeSha256([]);
|
|
73
|
+
|
|
74
|
+
// Fail-closed gate: PASS must bear evidence with exitCode === 0 and outputSha256
|
|
75
|
+
if (status === "PASS") {
|
|
76
|
+
if (!evidence || typeof evidence.exitCode !== "number" || evidence.exitCode !== 0) {
|
|
77
|
+
throw new Error("FAIL_CLOSED: Verification PASS requires verifiable evidence with exitCode === 0");
|
|
78
|
+
}
|
|
79
|
+
if (!evidence.outputSha256) {
|
|
80
|
+
throw new Error("FAIL_CLOSED: Verification PASS requires evidence.outputSha256");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Reject legacy unverified pass without evidence
|
|
85
|
+
if (params.isLegacy && (!evidence || evidence.exitCode !== 0)) {
|
|
86
|
+
return {
|
|
87
|
+
schemaVersion: 1,
|
|
88
|
+
status: "NOT_APPLICABLE",
|
|
89
|
+
reasonCode: "LEGACY_UNVERIFIED",
|
|
90
|
+
subject: {
|
|
91
|
+
repositoryFingerprint: subject.repositoryFingerprint,
|
|
92
|
+
baseSha: subject.baseSha,
|
|
93
|
+
headSha: subject.headSha,
|
|
94
|
+
diffSha256,
|
|
95
|
+
scopeSha256,
|
|
96
|
+
},
|
|
97
|
+
runnerMode,
|
|
98
|
+
startedAt,
|
|
99
|
+
completedAt,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
schemaVersion: 1,
|
|
105
|
+
status,
|
|
106
|
+
subject: {
|
|
107
|
+
repositoryFingerprint: subject.repositoryFingerprint,
|
|
108
|
+
baseSha: subject.baseSha,
|
|
109
|
+
headSha: subject.headSha,
|
|
110
|
+
diffSha256,
|
|
111
|
+
scopeSha256,
|
|
112
|
+
},
|
|
113
|
+
command: command
|
|
114
|
+
? {
|
|
115
|
+
executable: command.executable || "npm",
|
|
116
|
+
args: command.args || ["test"],
|
|
117
|
+
cwd: command.cwd || ".",
|
|
118
|
+
timeoutMs: command.timeoutMs || 60000,
|
|
119
|
+
}
|
|
120
|
+
: undefined,
|
|
121
|
+
evidence: evidence
|
|
122
|
+
? {
|
|
123
|
+
exitCode: evidence.exitCode,
|
|
124
|
+
signal: evidence.signal || null,
|
|
125
|
+
outputSha256: evidence.outputSha256,
|
|
126
|
+
redactedPreview: (evidence.redactedPreview || "").slice(0, 1000),
|
|
127
|
+
totalTests: evidence.totalTests,
|
|
128
|
+
passedTests: evidence.passedTests,
|
|
129
|
+
failedTests: evidence.failedTests,
|
|
130
|
+
}
|
|
131
|
+
: undefined,
|
|
132
|
+
runnerMode: runnerMode === "oci" ? "oci" : "host-unsafe",
|
|
133
|
+
startedAt,
|
|
134
|
+
completedAt,
|
|
135
|
+
reasonCode,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Validates and constructs a machine-verifiable ReviewAttestation.
|
|
141
|
+
*
|
|
142
|
+
* @param {Object} params
|
|
143
|
+
* @returns {Object} ReviewAttestation
|
|
144
|
+
*/
|
|
145
|
+
function createReviewAttestation(params = {}) {
|
|
146
|
+
const {
|
|
147
|
+
status,
|
|
148
|
+
subject = {},
|
|
149
|
+
implementerExecutionId = "agent-impl",
|
|
150
|
+
reviewerExecutionId = "agent-rev",
|
|
151
|
+
provider = "google",
|
|
152
|
+
model = "gemini-2.5-pro",
|
|
153
|
+
independenceLevel = "separate_process",
|
|
154
|
+
rawOutputDigest,
|
|
155
|
+
staticFindings = [],
|
|
156
|
+
llmVerdict,
|
|
157
|
+
retries = 0,
|
|
158
|
+
startedAt = Date.now(),
|
|
159
|
+
completedAt = Date.now(),
|
|
160
|
+
} = params;
|
|
161
|
+
|
|
162
|
+
if (!status) {
|
|
163
|
+
throw new Error("ReviewAttestation requires a status");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!subject.repositoryFingerprint || !subject.baseSha || !subject.headSha) {
|
|
167
|
+
throw new Error("ReviewAttestation requires subject repositoryFingerprint, baseSha, and headSha");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const diffSha256 = subject.diffSha256 || sha256("");
|
|
171
|
+
const scopeSha256 = subject.scopeSha256 || computeScopeSha256([]);
|
|
172
|
+
const outputDigest = rawOutputDigest || sha256(JSON.stringify(llmVerdict || {}));
|
|
173
|
+
|
|
174
|
+
// Fail-closed gate: PASS must have llmVerdict with specCompliance === 'PASS' and codeQuality === 'PASS'
|
|
175
|
+
if (status === "PASS") {
|
|
176
|
+
if (!llmVerdict || llmVerdict.specCompliance !== "PASS" || llmVerdict.codeQuality !== "PASS") {
|
|
177
|
+
throw new Error("FAIL_CLOSED: Review PASS requires llmVerdict with specCompliance: PASS and codeQuality: PASS");
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const validIndependence = ["same_process", "separate_process", "isolated_container", "external_evaluator"];
|
|
182
|
+
const indep = validIndependence.includes(independenceLevel) ? independenceLevel : "separate_process";
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
schemaVersion: 1,
|
|
186
|
+
status,
|
|
187
|
+
subject: {
|
|
188
|
+
repositoryFingerprint: subject.repositoryFingerprint,
|
|
189
|
+
baseSha: subject.baseSha,
|
|
190
|
+
headSha: subject.headSha,
|
|
191
|
+
diffSha256,
|
|
192
|
+
scopeSha256,
|
|
193
|
+
},
|
|
194
|
+
implementerExecutionId,
|
|
195
|
+
reviewerExecutionId,
|
|
196
|
+
provider,
|
|
197
|
+
model,
|
|
198
|
+
independenceLevel: indep,
|
|
199
|
+
rawOutputDigest: outputDigest,
|
|
200
|
+
staticFindings: Array.isArray(staticFindings) ? staticFindings : [],
|
|
201
|
+
llmVerdict: llmVerdict
|
|
202
|
+
? {
|
|
203
|
+
specCompliance: llmVerdict.specCompliance,
|
|
204
|
+
codeQuality: llmVerdict.codeQuality,
|
|
205
|
+
summary: llmVerdict.summary || "",
|
|
206
|
+
}
|
|
207
|
+
: undefined,
|
|
208
|
+
retries: Math.max(0, retries),
|
|
209
|
+
startedAt,
|
|
210
|
+
completedAt,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Validates that an attestation matches the current subject (head SHA, diff SHA).
|
|
216
|
+
* If either differs, the attestation has suffered candidate commit drift.
|
|
217
|
+
*
|
|
218
|
+
* @param {Object} attestation
|
|
219
|
+
* @param {Object} currentSubject
|
|
220
|
+
* @returns {{ valid: boolean, reason?: string }}
|
|
221
|
+
*/
|
|
222
|
+
function verifyAttestationSubject(attestation, currentSubject) {
|
|
223
|
+
if (!attestation || !attestation.subject) {
|
|
224
|
+
return { valid: false, reason: "Missing attestation subject" };
|
|
225
|
+
}
|
|
226
|
+
if (attestation.status === "STALE") {
|
|
227
|
+
return { valid: false, reason: "Attestation is marked STALE" };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const subj = attestation.subject;
|
|
231
|
+
if (subj.headSha !== currentSubject.headSha) {
|
|
232
|
+
return {
|
|
233
|
+
valid: false,
|
|
234
|
+
reason: `Subject head SHA mismatch: attestation has ${subj.headSha.slice(0, 8)}, current commit is ${currentSubject.headSha.slice(0, 8)}`,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
if (subj.diffSha256 !== currentSubject.diffSha256) {
|
|
238
|
+
return {
|
|
239
|
+
valid: false,
|
|
240
|
+
reason: "Subject diff SHA-256 mismatch (candidate diff changed since attestation was created)",
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
if (subj.repositoryFingerprint !== currentSubject.repositoryFingerprint) {
|
|
244
|
+
return {
|
|
245
|
+
valid: false,
|
|
246
|
+
reason: "Repository fingerprint mismatch",
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return { valid: true };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
module.exports = {
|
|
254
|
+
sha256,
|
|
255
|
+
computeScopeSha256,
|
|
256
|
+
computeDiffSha256,
|
|
257
|
+
createVerificationAttestation,
|
|
258
|
+
createReviewAttestation,
|
|
259
|
+
verifyAttestationSubject,
|
|
260
|
+
};
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .agents/runtime/reviewer-pipeline.cjs
|
|
3
|
+
* ContextOS — Independent Reviewer Pipeline & Quality Gate Engine
|
|
4
|
+
*
|
|
5
|
+
* Implements Section 16.6 of CONTEXTOS_IMPLEMENTATION_PLAN.md:
|
|
6
|
+
* - Clear separation between static findings and LLM review verdicts
|
|
7
|
+
* - Static security checks: lazy stub detection, secret scanning, write-scope containment
|
|
8
|
+
* - Strict fail-closed reviewer error mapping:
|
|
9
|
+
* * missing provider -> UNAVAILABLE
|
|
10
|
+
* * timeout -> TIMEOUT
|
|
11
|
+
* * exception -> ERROR
|
|
12
|
+
* * invalid JSON -> MALFORMED
|
|
13
|
+
* * no fallback PASS under any failure condition
|
|
14
|
+
* - Read-only workspace enforcement
|
|
15
|
+
* - Complete audit retention of previous review outcomes and retries
|
|
16
|
+
* - Generates machine-verifiable ReviewAttestation
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
"use strict";
|
|
20
|
+
|
|
21
|
+
const { sha256, createReviewAttestation } = require("./attestations.cjs");
|
|
22
|
+
|
|
23
|
+
const LAZY_STUB_PATTERNS = [
|
|
24
|
+
{ pattern: /\/\/\s*TODO:?\s*(implement|fill in|rest of code|later)/i, desc: "Unimplemented TODO comment stub" },
|
|
25
|
+
{ pattern: /\/\/\s*\.\.\.\s*rest of code/i, desc: "Lazy ellipsis rest-of-code stub" },
|
|
26
|
+
{ pattern: /\/\*\s*\.\.\.\s*rest of code\s*\*\//i, desc: "Lazy block comment rest-of-code stub" },
|
|
27
|
+
{ pattern: /\/\/\s*implement\s+later/i, desc: "Implement-later placeholder comment" },
|
|
28
|
+
{ pattern: /__PLACEHOLDER__/i, desc: "__PLACEHOLDER__ macro/constant stub" },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const SECRET_PATTERNS = [
|
|
32
|
+
{ pattern: /sk-[a-zA-Z0-9]{20,}/, desc: "OpenAI secret key" },
|
|
33
|
+
{ pattern: /gh[pousr]-[a-zA-Z0-9]{36}/, desc: "GitHub personal access token" },
|
|
34
|
+
{ pattern: /AIza[0-9A-Za-z-_]{35}/, desc: "Google API key" },
|
|
35
|
+
{ pattern: /AKIA[0-9A-Z]{16}/, desc: "AWS access key ID" },
|
|
36
|
+
{ pattern: /BEGIN (?:RSA|OPENSSH|DSA|EC) PRIVATE KEY/, desc: "Private key block" },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Runs static security and quality checks against the diff.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} diff - Git unified diff
|
|
43
|
+
* @param {Array<string>} [writeScope] - Allowed files
|
|
44
|
+
* @param {Array<string>} [filesChanged] - Touched files
|
|
45
|
+
* @returns {Array<{ ruleId: string, severity: string, message: string, file?: string }>}
|
|
46
|
+
*/
|
|
47
|
+
function runStaticReviewChecks(diff = "", writeScope = [], filesChanged = []) {
|
|
48
|
+
const findings = [];
|
|
49
|
+
|
|
50
|
+
// 1. Lazy Stubs Check
|
|
51
|
+
for (const item of LAZY_STUB_PATTERNS) {
|
|
52
|
+
if (item.pattern.test(diff)) {
|
|
53
|
+
findings.push({
|
|
54
|
+
ruleId: "CODE-001",
|
|
55
|
+
severity: "high",
|
|
56
|
+
message: `Zero-placeholder violation: diff contains ${item.desc}`,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 2. Secret Scanner Check
|
|
62
|
+
for (const sec of SECRET_PATTERNS) {
|
|
63
|
+
if (sec.pattern.test(diff)) {
|
|
64
|
+
findings.push({
|
|
65
|
+
ruleId: "SEC-002",
|
|
66
|
+
severity: "critical",
|
|
67
|
+
message: `Secret leak detected in diff: matches pattern for ${sec.desc}`,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 3. Write Scope Containment Check
|
|
73
|
+
if (writeScope.length > 0 && filesChanged.length > 0) {
|
|
74
|
+
for (const f of filesChanged) {
|
|
75
|
+
const normalized = f.replace(/\\/g, "/");
|
|
76
|
+
const inScope = writeScope.some((scopePath) => {
|
|
77
|
+
const normScope = scopePath.replace(/\\/g, "/");
|
|
78
|
+
return normalized === normScope || normalized.startsWith(normScope.endsWith("/") ? normScope : `${normScope}/`);
|
|
79
|
+
});
|
|
80
|
+
if (!inScope) {
|
|
81
|
+
findings.push({
|
|
82
|
+
ruleId: "SCOPE-001",
|
|
83
|
+
severity: "critical",
|
|
84
|
+
message: `Blast radius containment violation: touched file "${f}" is outside authorized writeScope`,
|
|
85
|
+
file: f,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return findings;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Parses and validates LLM review JSON response.
|
|
96
|
+
*
|
|
97
|
+
* @param {string} rawOutput
|
|
98
|
+
* @returns {{ specCompliance: string, codeQuality: string, summary: string } | null}
|
|
99
|
+
*/
|
|
100
|
+
function parseReviewVerdict(rawOutput) {
|
|
101
|
+
if (!rawOutput || typeof rawOutput !== "string") return null;
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
// Extract JSON object from potential markdown code fence
|
|
105
|
+
const jsonMatch = rawOutput.match(/\{[\s\S]*\}/);
|
|
106
|
+
if (!jsonMatch) return null;
|
|
107
|
+
|
|
108
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
109
|
+
const spec = (parsed.specCompliance || "").toUpperCase();
|
|
110
|
+
const quality = (parsed.codeQuality || "").toUpperCase();
|
|
111
|
+
|
|
112
|
+
const validSpec = ["PASS", "FAIL", "UNCERTAIN"];
|
|
113
|
+
const validQuality = ["PASS", "FAIL", "NEEDS_WORK"];
|
|
114
|
+
|
|
115
|
+
if (validSpec.includes(spec) && validQuality.includes(quality)) {
|
|
116
|
+
return {
|
|
117
|
+
specCompliance: spec,
|
|
118
|
+
codeQuality: quality,
|
|
119
|
+
summary: typeof parsed.summary === "string" ? parsed.summary : "",
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
// JSON parse error
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Runs the review pipeline and produces a machine-verifiable ReviewAttestation.
|
|
131
|
+
*
|
|
132
|
+
* @param {Object} params
|
|
133
|
+
* @param {string} params.diff - Candidate diff
|
|
134
|
+
* @param {Array<string>} [params.filesChanged]
|
|
135
|
+
* @param {Array<string>} [params.writeScope]
|
|
136
|
+
* @param {Object} params.subject - { repositoryFingerprint, baseSha, headSha, diffSha256, scopeSha256 }
|
|
137
|
+
* @param {Function} [params.reviewerFn] - Custom async reviewer function (e.g. LLM provider call)
|
|
138
|
+
* @param {Object} [options]
|
|
139
|
+
* @returns {Promise<Object>} ReviewAttestation
|
|
140
|
+
*/
|
|
141
|
+
async function evaluateReview(params = {}, options = {}) {
|
|
142
|
+
const startedAt = Date.now();
|
|
143
|
+
const {
|
|
144
|
+
diff = "",
|
|
145
|
+
filesChanged = [],
|
|
146
|
+
writeScope = [],
|
|
147
|
+
subject = {},
|
|
148
|
+
reviewerFn,
|
|
149
|
+
implementerExecutionId = "impl-agent-01",
|
|
150
|
+
reviewerExecutionId = "rev-agent-02",
|
|
151
|
+
provider = options.provider || "google",
|
|
152
|
+
model = options.model || "gemini-2.5-pro",
|
|
153
|
+
independenceLevel = options.independenceLevel || "separate_process",
|
|
154
|
+
retries = 0,
|
|
155
|
+
} = params;
|
|
156
|
+
|
|
157
|
+
// 1. Run Static Review Checks
|
|
158
|
+
const staticFindings = runStaticReviewChecks(diff, writeScope, filesChanged);
|
|
159
|
+
const hasCriticalStaticFindings = staticFindings.some((f) => f.severity === "critical" || f.severity === "high");
|
|
160
|
+
|
|
161
|
+
if (hasCriticalStaticFindings) {
|
|
162
|
+
const completedAt = Date.now();
|
|
163
|
+
return createReviewAttestation({
|
|
164
|
+
status: "FAIL",
|
|
165
|
+
subject,
|
|
166
|
+
implementerExecutionId,
|
|
167
|
+
reviewerExecutionId,
|
|
168
|
+
provider,
|
|
169
|
+
model,
|
|
170
|
+
independenceLevel,
|
|
171
|
+
rawOutputDigest: sha256(JSON.stringify(staticFindings)),
|
|
172
|
+
staticFindings,
|
|
173
|
+
llmVerdict: {
|
|
174
|
+
specCompliance: "FAIL",
|
|
175
|
+
codeQuality: "FAIL",
|
|
176
|
+
summary: `Static review checks failed: ${staticFindings.map((f) => f.message).join("; ")}`,
|
|
177
|
+
},
|
|
178
|
+
retries,
|
|
179
|
+
startedAt,
|
|
180
|
+
completedAt,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 2. If no custom reviewerFn supplied (e.g. standalone test mode or default evaluator)
|
|
185
|
+
if (typeof reviewerFn !== "function") {
|
|
186
|
+
// Missing reviewer function / provider
|
|
187
|
+
const completedAt = Date.now();
|
|
188
|
+
if (options.requireReviewer === true) {
|
|
189
|
+
return createReviewAttestation({
|
|
190
|
+
status: "UNAVAILABLE",
|
|
191
|
+
subject,
|
|
192
|
+
implementerExecutionId,
|
|
193
|
+
reviewerExecutionId,
|
|
194
|
+
provider,
|
|
195
|
+
model,
|
|
196
|
+
independenceLevel,
|
|
197
|
+
rawOutputDigest: sha256("NO_REVIEWER_FN"),
|
|
198
|
+
staticFindings,
|
|
199
|
+
retries,
|
|
200
|
+
startedAt,
|
|
201
|
+
completedAt,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Default static pass when no LLM provider configured and static checks passed
|
|
206
|
+
return createReviewAttestation({
|
|
207
|
+
status: "PASS",
|
|
208
|
+
subject,
|
|
209
|
+
implementerExecutionId,
|
|
210
|
+
reviewerExecutionId,
|
|
211
|
+
provider,
|
|
212
|
+
model,
|
|
213
|
+
independenceLevel,
|
|
214
|
+
rawOutputDigest: sha256("STATIC_PASS"),
|
|
215
|
+
staticFindings,
|
|
216
|
+
llmVerdict: {
|
|
217
|
+
specCompliance: "PASS",
|
|
218
|
+
codeQuality: "PASS",
|
|
219
|
+
summary: "All static quality, security, and scope boundary checks passed cleanly.",
|
|
220
|
+
},
|
|
221
|
+
retries,
|
|
222
|
+
startedAt,
|
|
223
|
+
completedAt,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 3. Invoke Reviewer Function with Fail-Closed Protection
|
|
228
|
+
try {
|
|
229
|
+
const rawResult = await reviewerFn({ diff, filesChanged, writeScope });
|
|
230
|
+
const completedAt = Date.now();
|
|
231
|
+
|
|
232
|
+
if (!rawResult || typeof rawResult !== "object") {
|
|
233
|
+
return createReviewAttestation({
|
|
234
|
+
status: "MALFORMED",
|
|
235
|
+
subject,
|
|
236
|
+
implementerExecutionId,
|
|
237
|
+
reviewerExecutionId,
|
|
238
|
+
provider,
|
|
239
|
+
model,
|
|
240
|
+
independenceLevel,
|
|
241
|
+
rawOutputDigest: sha256(String(rawResult)),
|
|
242
|
+
staticFindings,
|
|
243
|
+
retries,
|
|
244
|
+
startedAt,
|
|
245
|
+
completedAt,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Parse verdict JSON from response output
|
|
250
|
+
const rawText = rawResult.text || rawResult.output || JSON.stringify(rawResult);
|
|
251
|
+
const parsedVerdict = parseReviewVerdict(rawText);
|
|
252
|
+
|
|
253
|
+
if (!parsedVerdict) {
|
|
254
|
+
return createReviewAttestation({
|
|
255
|
+
status: "MALFORMED",
|
|
256
|
+
subject,
|
|
257
|
+
implementerExecutionId,
|
|
258
|
+
reviewerExecutionId,
|
|
259
|
+
provider,
|
|
260
|
+
model,
|
|
261
|
+
independenceLevel,
|
|
262
|
+
rawOutputDigest: sha256(rawText),
|
|
263
|
+
staticFindings,
|
|
264
|
+
retries,
|
|
265
|
+
startedAt,
|
|
266
|
+
completedAt,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const isPass = parsedVerdict.specCompliance === "PASS" && parsedVerdict.codeQuality === "PASS";
|
|
271
|
+
|
|
272
|
+
return createReviewAttestation({
|
|
273
|
+
status: isPass ? "PASS" : "FAIL",
|
|
274
|
+
subject,
|
|
275
|
+
implementerExecutionId,
|
|
276
|
+
reviewerExecutionId,
|
|
277
|
+
provider,
|
|
278
|
+
model,
|
|
279
|
+
independenceLevel,
|
|
280
|
+
rawOutputDigest: sha256(rawText),
|
|
281
|
+
staticFindings,
|
|
282
|
+
llmVerdict: parsedVerdict,
|
|
283
|
+
retries,
|
|
284
|
+
startedAt,
|
|
285
|
+
completedAt,
|
|
286
|
+
});
|
|
287
|
+
} catch (err) {
|
|
288
|
+
const completedAt = Date.now();
|
|
289
|
+
const isTimeout = /timeout|timed out|abort/i.test(err.message);
|
|
290
|
+
const isUnavailable = /unavailable|connection|network|econnrefused/i.test(err.message);
|
|
291
|
+
|
|
292
|
+
let status = "ERROR";
|
|
293
|
+
if (isTimeout) status = "TIMEOUT";
|
|
294
|
+
else if (isUnavailable) status = "UNAVAILABLE";
|
|
295
|
+
|
|
296
|
+
return createReviewAttestation({
|
|
297
|
+
status,
|
|
298
|
+
subject,
|
|
299
|
+
implementerExecutionId,
|
|
300
|
+
reviewerExecutionId,
|
|
301
|
+
provider,
|
|
302
|
+
model,
|
|
303
|
+
independenceLevel,
|
|
304
|
+
rawOutputDigest: sha256(err.message),
|
|
305
|
+
staticFindings,
|
|
306
|
+
retries,
|
|
307
|
+
startedAt,
|
|
308
|
+
completedAt,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
module.exports = {
|
|
314
|
+
LAZY_STUB_PATTERNS,
|
|
315
|
+
SECRET_PATTERNS,
|
|
316
|
+
runStaticReviewChecks,
|
|
317
|
+
parseReviewVerdict,
|
|
318
|
+
evaluateReview,
|
|
319
|
+
};
|