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.
Files changed (223) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +260 -0
  3. package/bin/mcp.mjs +61 -0
  4. package/bin/swarm.mjs +45 -0
  5. package/dist/agents/aider.d.ts +12 -0
  6. package/dist/agents/aider.js +192 -0
  7. package/dist/agents/aider.js.map +1 -0
  8. package/dist/agents/claude-code.d.ts +9 -0
  9. package/dist/agents/claude-code.js +236 -0
  10. package/dist/agents/claude-code.js.map +1 -0
  11. package/dist/agents/codex.d.ts +14 -0
  12. package/dist/agents/codex.js +203 -0
  13. package/dist/agents/codex.js.map +1 -0
  14. package/dist/agents/direct-llm.d.ts +22 -0
  15. package/dist/agents/direct-llm.js +254 -0
  16. package/dist/agents/direct-llm.js.map +1 -0
  17. package/dist/agents/mock.d.ts +10 -0
  18. package/dist/agents/mock.js +170 -0
  19. package/dist/agents/mock.js.map +1 -0
  20. package/dist/agents/opencode.d.ts +23 -0
  21. package/dist/agents/opencode.js +631 -0
  22. package/dist/agents/opencode.js.map +1 -0
  23. package/dist/agents/provider.d.ts +11 -0
  24. package/dist/agents/provider.js +31 -0
  25. package/dist/agents/provider.js.map +1 -0
  26. package/dist/compression/compressor.d.ts +28 -0
  27. package/dist/compression/compressor.js +268 -0
  28. package/dist/compression/compressor.js.map +1 -0
  29. package/dist/config.d.ts +43 -0
  30. package/dist/config.js +172 -0
  31. package/dist/config.js.map +1 -0
  32. package/dist/contextos/loader.d.ts +46 -0
  33. package/dist/contextos/loader.js +120 -0
  34. package/dist/contextos/loader.js.map +1 -0
  35. package/dist/contextos/selector.d.ts +46 -0
  36. package/dist/contextos/selector.js +80 -0
  37. package/dist/contextos/selector.js.map +1 -0
  38. package/dist/core/action-dispatcher.d.ts +15 -0
  39. package/dist/core/action-dispatcher.js +28 -0
  40. package/dist/core/action-dispatcher.js.map +1 -0
  41. package/dist/core/action-schema.d.ts +72 -0
  42. package/dist/core/action-schema.js +86 -0
  43. package/dist/core/action-schema.js.map +1 -0
  44. package/dist/core/repl-interface.d.ts +48 -0
  45. package/dist/core/repl-interface.js +5 -0
  46. package/dist/core/repl-interface.js.map +1 -0
  47. package/dist/core/repl.d.ts +59 -0
  48. package/dist/core/repl.js +353 -0
  49. package/dist/core/repl.js.map +1 -0
  50. package/dist/core/rlm.d.ts +65 -0
  51. package/dist/core/rlm.js +564 -0
  52. package/dist/core/rlm.js.map +1 -0
  53. package/dist/core/runtime.py +470 -0
  54. package/dist/core/types.d.ts +265 -0
  55. package/dist/core/types.js +19 -0
  56. package/dist/core/types.js.map +1 -0
  57. package/dist/env.d.ts +10 -0
  58. package/dist/env.js +75 -0
  59. package/dist/env.js.map +1 -0
  60. package/dist/hooks/runner.d.ts +43 -0
  61. package/dist/hooks/runner.js +173 -0
  62. package/dist/hooks/runner.js.map +1 -0
  63. package/dist/labs/cli.d.ts +15 -0
  64. package/dist/labs/cli.js +295 -0
  65. package/dist/labs/cli.js.map +1 -0
  66. package/dist/labs/episodic-memory.d.ts +132 -0
  67. package/dist/labs/episodic-memory.js +391 -0
  68. package/dist/labs/episodic-memory.js.map +1 -0
  69. package/dist/labs/interactive-swarm.d.ts +20 -0
  70. package/dist/labs/interactive-swarm.js +1173 -0
  71. package/dist/labs/interactive-swarm.js.map +1 -0
  72. package/dist/labs/interactive.d.ts +10 -0
  73. package/dist/labs/interactive.js +2099 -0
  74. package/dist/labs/interactive.js.map +1 -0
  75. package/dist/labs/ts-analyzer.d.ts +74 -0
  76. package/dist/labs/ts-analyzer.js +379 -0
  77. package/dist/labs/ts-analyzer.js.map +1 -0
  78. package/dist/labs/viewer.d.ts +12 -0
  79. package/dist/labs/viewer.js +1284 -0
  80. package/dist/labs/viewer.js.map +1 -0
  81. package/dist/main.d.ts +15 -0
  82. package/dist/main.js +247 -0
  83. package/dist/main.js.map +1 -0
  84. package/dist/mcp/server.d.ts +16 -0
  85. package/dist/mcp/server.js +192775 -0
  86. package/dist/mcp/server.js.map +1 -0
  87. package/dist/mcp/session.d.ts +117 -0
  88. package/dist/mcp/session.js +450 -0
  89. package/dist/mcp/session.js.map +1 -0
  90. package/dist/mcp/state.d.ts +42 -0
  91. package/dist/mcp/state.js +537 -0
  92. package/dist/mcp/state.js.map +1 -0
  93. package/dist/mcp/tools/contextos.d.ts +17 -0
  94. package/dist/mcp/tools/contextos.js +583 -0
  95. package/dist/mcp/tools/contextos.js.map +1 -0
  96. package/dist/mcp/tools.d.ts +15 -0
  97. package/dist/mcp/tools.js +393 -0
  98. package/dist/mcp/tools.js.map +1 -0
  99. package/dist/orchestration/reviewer-gate.d.ts +43 -0
  100. package/dist/orchestration/reviewer-gate.js +280 -0
  101. package/dist/orchestration/reviewer-gate.js.map +1 -0
  102. package/dist/orchestration/reviewer-prompt.d.ts +18 -0
  103. package/dist/orchestration/reviewer-prompt.js +67 -0
  104. package/dist/orchestration/reviewer-prompt.js.map +1 -0
  105. package/dist/orchestration/verification-runner.d.ts +53 -0
  106. package/dist/orchestration/verification-runner.js +352 -0
  107. package/dist/orchestration/verification-runner.js.map +1 -0
  108. package/dist/plugins/adapter.d.ts +11 -0
  109. package/dist/plugins/adapter.js +11 -0
  110. package/dist/plugins/adapter.js.map +1 -0
  111. package/dist/plugins/core/archive.d.ts +9 -0
  112. package/dist/plugins/core/archive.js +128 -0
  113. package/dist/plugins/core/archive.js.map +1 -0
  114. package/dist/plugins/core/digest.d.ts +11 -0
  115. package/dist/plugins/core/digest.js +70 -0
  116. package/dist/plugins/core/digest.js.map +1 -0
  117. package/dist/plugins/core/grants.d.ts +22 -0
  118. package/dist/plugins/core/grants.js +90 -0
  119. package/dist/plugins/core/grants.js.map +1 -0
  120. package/dist/plugins/core/index.d.ts +10 -0
  121. package/dist/plugins/core/index.js +11 -0
  122. package/dist/plugins/core/index.js.map +1 -0
  123. package/dist/plugins/core/pinning.d.ts +15 -0
  124. package/dist/plugins/core/pinning.js +86 -0
  125. package/dist/plugins/core/pinning.js.map +1 -0
  126. package/dist/plugins/core/types.d.ts +105 -0
  127. package/dist/plugins/core/types.js +22 -0
  128. package/dist/plugins/core/types.js.map +1 -0
  129. package/dist/plugins/core/updater.d.ts +16 -0
  130. package/dist/plugins/core/updater.js +64 -0
  131. package/dist/plugins/core/updater.js.map +1 -0
  132. package/dist/plugins/loader.d.ts +65 -0
  133. package/dist/plugins/loader.js +110 -0
  134. package/dist/plugins/loader.js.map +1 -0
  135. package/dist/prompts/orchestrator.d.ts +6 -0
  136. package/dist/prompts/orchestrator.js +114 -0
  137. package/dist/prompts/orchestrator.js.map +1 -0
  138. package/dist/routing/model-resolver.d.ts +27 -0
  139. package/dist/routing/model-resolver.js +265 -0
  140. package/dist/routing/model-resolver.js.map +1 -0
  141. package/dist/routing/model-router.d.ts +130 -0
  142. package/dist/routing/model-router.js +515 -0
  143. package/dist/routing/model-router.js.map +1 -0
  144. package/dist/runtime/attestations.cjs +260 -0
  145. package/dist/runtime/reviewer-pipeline.cjs +319 -0
  146. package/dist/runtime/sandbox.cjs +468 -0
  147. package/dist/runtime/state-machine.cjs +885 -0
  148. package/dist/runtime/thread-store.cjs +175 -0
  149. package/dist/runtime/transactional-git.cjs +315 -0
  150. package/dist/runtime/verification-pipeline.cjs +435 -0
  151. package/dist/sandbox/execution.d.ts +165 -0
  152. package/dist/sandbox/execution.js +497 -0
  153. package/dist/sandbox/execution.js.map +1 -0
  154. package/dist/security/file-policy.d.ts +22 -0
  155. package/dist/security/file-policy.js +53 -0
  156. package/dist/security/file-policy.js.map +1 -0
  157. package/dist/security/repository-boundary.d.ts +21 -0
  158. package/dist/security/repository-boundary.js +119 -0
  159. package/dist/security/repository-boundary.js.map +1 -0
  160. package/dist/security/secret-filter.d.ts +36 -0
  161. package/dist/security/secret-filter.js +149 -0
  162. package/dist/security/secret-filter.js.map +1 -0
  163. package/dist/swarm.d.ts +14 -0
  164. package/dist/swarm.js +624 -0
  165. package/dist/swarm.js.map +1 -0
  166. package/dist/threads/cache.d.ts +64 -0
  167. package/dist/threads/cache.js +241 -0
  168. package/dist/threads/cache.js.map +1 -0
  169. package/dist/threads/manager.d.ts +94 -0
  170. package/dist/threads/manager.js +914 -0
  171. package/dist/threads/manager.js.map +1 -0
  172. package/dist/ui/banner.d.ts +14 -0
  173. package/dist/ui/banner.js +42 -0
  174. package/dist/ui/banner.js.map +1 -0
  175. package/dist/ui/dashboard.d.ts +33 -0
  176. package/dist/ui/dashboard.js +135 -0
  177. package/dist/ui/dashboard.js.map +1 -0
  178. package/dist/ui/index.d.ts +11 -0
  179. package/dist/ui/index.js +12 -0
  180. package/dist/ui/index.js.map +1 -0
  181. package/dist/ui/log.d.ts +39 -0
  182. package/dist/ui/log.js +126 -0
  183. package/dist/ui/log.js.map +1 -0
  184. package/dist/ui/onboarding.d.ts +14 -0
  185. package/dist/ui/onboarding.js +755 -0
  186. package/dist/ui/onboarding.js.map +1 -0
  187. package/dist/ui/run-log.d.ts +50 -0
  188. package/dist/ui/run-log.js +60 -0
  189. package/dist/ui/run-log.js.map +1 -0
  190. package/dist/ui/spinner.d.ts +41 -0
  191. package/dist/ui/spinner.js +172 -0
  192. package/dist/ui/spinner.js.map +1 -0
  193. package/dist/ui/streaming-feed.d.ts +72 -0
  194. package/dist/ui/streaming-feed.js +386 -0
  195. package/dist/ui/streaming-feed.js.map +1 -0
  196. package/dist/ui/summary.d.ts +18 -0
  197. package/dist/ui/summary.js +113 -0
  198. package/dist/ui/summary.js.map +1 -0
  199. package/dist/ui/text-input.d.ts +19 -0
  200. package/dist/ui/text-input.js +266 -0
  201. package/dist/ui/text-input.js.map +1 -0
  202. package/dist/ui/theme.d.ts +63 -0
  203. package/dist/ui/theme.js +97 -0
  204. package/dist/ui/theme.js.map +1 -0
  205. package/dist/utils/command-exists.d.ts +11 -0
  206. package/dist/utils/command-exists.js +41 -0
  207. package/dist/utils/command-exists.js.map +1 -0
  208. package/dist/utils/process-security.d.ts +19 -0
  209. package/dist/utils/process-security.js +90 -0
  210. package/dist/utils/process-security.js.map +1 -0
  211. package/dist/utils/rolling-buffer.d.ts +16 -0
  212. package/dist/utils/rolling-buffer.js +30 -0
  213. package/dist/utils/rolling-buffer.js.map +1 -0
  214. package/dist/utils/safe-spawn.d.ts +13 -0
  215. package/dist/utils/safe-spawn.js +49 -0
  216. package/dist/utils/safe-spawn.js.map +1 -0
  217. package/dist/worktree/manager.d.ts +69 -0
  218. package/dist/worktree/manager.js +593 -0
  219. package/dist/worktree/manager.js.map +1 -0
  220. package/dist/worktree/merge.d.ts +58 -0
  221. package/dist/worktree/merge.js +389 -0
  222. package/dist/worktree/merge.js.map +1 -0
  223. package/package.json +63 -0
@@ -0,0 +1,435 @@
1
+ /**
2
+ * .agents/runtime/verification-pipeline.cjs
3
+ * ContextOS — Secure Verification Pipeline & Immutable Candidate Execution
4
+ *
5
+ * Implements Section 16 of CONTEXTOS_IMPLEMENTATION_PLAN.md:
6
+ * - Structured VerificationSpec parsing and command trust enforcement
7
+ * - Host-mode restrictions (bans npx, enforces argv array)
8
+ * - Cross-platform bounded process tree termination (Windows taskkill, Unix process groups)
9
+ * - Ephemeral minimal environment and sensitive token redaction
10
+ * - Immutable candidate validation (detects post-test candidate mutation and dirty trees)
11
+ * - Generates machine-verifiable VerificationAttestation with SHA-256 output digest
12
+ */
13
+
14
+ "use strict";
15
+
16
+ const { spawn, execFileSync } = require("node:child_process");
17
+ const crypto = require("node:crypto");
18
+ const _path = require("node:path");
19
+ const { sha256, createVerificationAttestation } = require("./attestations.cjs");
20
+
21
+ const TRUSTED_TOOLS = new Set(["npm", "pnpm", "yarn", "pytest", "cargo", "go"]);
22
+ const FORBIDDEN_EVAL_ARGS = new Set(["-e", "-c", "--eval", "--print", "-p", "--input-type"]);
23
+
24
+ const SANITIZED_ENV_ALLOWLIST = new Set([
25
+ "PATH",
26
+ "HOME",
27
+ "USERPROFILE",
28
+ "SYSTEMROOT",
29
+ "WINDIR",
30
+ "TEMP",
31
+ "TMP",
32
+ "NODE_ENV",
33
+ "LANG",
34
+ "LC_ALL",
35
+ "CI",
36
+ ]);
37
+
38
+ /**
39
+ * Parses a string command or object into a canonical VerificationSpec.
40
+ *
41
+ * @param {string|Object} input
42
+ * @returns {Object} VerificationSpec
43
+ */
44
+ function parseVerificationSpec(input) {
45
+ if (!input) {
46
+ throw new Error("Verification specification cannot be empty");
47
+ }
48
+
49
+ if (typeof input === "object" && input.executable && Array.isArray(input.args)) {
50
+ return {
51
+ executable: input.executable,
52
+ args: input.args,
53
+ timeoutMs: input.timeoutMs || 60000,
54
+ required: input.required !== false,
55
+ network: input.network || "deny",
56
+ allowedOutputPaths: input.allowedOutputPaths || [],
57
+ };
58
+ }
59
+
60
+ if (typeof input === "string") {
61
+ const trimmed = input.trim();
62
+ if (!trimmed) {
63
+ throw new Error("Verification command cannot be empty string");
64
+ }
65
+
66
+ // Ban shell metacharacters and chaining
67
+ if (/[&|;`$><%]/.test(trimmed)) {
68
+ const err = new Error(
69
+ "Security error: Shell metacharacters and command chaining are prohibited in verification commands",
70
+ );
71
+ err.code = "CTX_COMMAND_CHAINING_PROHIBITED";
72
+ throw err;
73
+ }
74
+
75
+ const parts = trimmed.split(/\s+/);
76
+ const rawExe = parts[0].toLowerCase().replace(/\.(cmd|bat|exe)$/i, "");
77
+ const args = parts.slice(1);
78
+
79
+ return {
80
+ executable: rawExe,
81
+ args,
82
+ timeoutMs: 60000,
83
+ required: true,
84
+ network: "deny",
85
+ isLegacyString: true,
86
+ };
87
+ }
88
+
89
+ throw new Error("Invalid verification specification format: expected VerificationSpec object or command string");
90
+ }
91
+
92
+ /**
93
+ * Enforces command trust policy.
94
+ *
95
+ * @param {Object} spec - VerificationSpec
96
+ * @param {Object} [options] - { runnerMode, trusted }
97
+ */
98
+ function validateCommandTrust(spec, options = {}) {
99
+ const runnerMode = options.runnerMode || "host-unsafe";
100
+
101
+ // Rule 1: npx is forbidden in host mode (Section 16.2: "npx запрещён в host mode")
102
+ if (spec.executable === "npx" && runnerMode === "host-unsafe") {
103
+ const err = new Error(
104
+ 'Security error: "npx" execution is strictly forbidden in host mode. Use OCI sandbox or local package runner.',
105
+ );
106
+ err.code = "CTX_COMMAND_UNTRUSTED";
107
+ throw err;
108
+ }
109
+
110
+ // Rule 2: Unlisted tools require OCI runner or explicit local trust
111
+ if (!TRUSTED_TOOLS.has(spec.executable)) {
112
+ if (runnerMode === "host-unsafe" && !options.trusted) {
113
+ const err = new Error(
114
+ `Security error: Command "${spec.executable}" is not in the trusted tools whitelist (${Array.from(TRUSTED_TOOLS).join(", ")}). Arbitrary commands require OCI sandbox or explicit user trust.`,
115
+ );
116
+ err.code = "CTX_COMMAND_UNTRUSTED";
117
+ throw err;
118
+ }
119
+ }
120
+
121
+ // Rule 3: Inline evaluation arguments prohibited
122
+ for (const arg of spec.args) {
123
+ if (FORBIDDEN_EVAL_ARGS.has(arg.toLowerCase())) {
124
+ const err = new Error(
125
+ `Security error: Inline code evaluation argument "${arg}" is prohibited in verification spec.`,
126
+ );
127
+ err.code = "CTX_FORBIDDEN_ARGUMENT";
128
+ throw err;
129
+ }
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Builds a sanitized, minimal environment stripped of secrets and tokens.
135
+ */
136
+ function getSanitizedEnvironment(customEnv = {}) {
137
+ const env = {
138
+ CI: "true",
139
+ NODE_ENV: "test",
140
+ };
141
+
142
+ for (const key of SANITIZED_ENV_ALLOWLIST) {
143
+ if (process.env[key] !== undefined) {
144
+ env[key] = process.env[key];
145
+ }
146
+ }
147
+
148
+ // Merge any explicitly permitted custom env variables
149
+ for (const [k, v] of Object.entries(customEnv)) {
150
+ // Prevent accidental leak of token/secret/password variables
151
+ if (!/TOKEN|SECRET|KEY|PASSWORD|AUTH|CREDENTIAL/i.test(k)) {
152
+ env[k] = v;
153
+ }
154
+ }
155
+
156
+ return env;
157
+ }
158
+
159
+ /**
160
+ * Redacts API keys, bearer tokens, and secret strings from output preview.
161
+ */
162
+ function redactSensitiveOutput(text = "") {
163
+ if (!text) return "";
164
+ return (
165
+ text
166
+ // Secret tokens and API keys
167
+ .replace(/(?:sk-[a-zA-Z0-9]{20,}|gh[pousr]-[a-zA-Z0-9]{36}|AIza[0-9A-Za-z-_]{35})/g, "[REDACTED_API_KEY]")
168
+ // Bearer / Authorization headers
169
+ .replace(/(?:Authorization:\s*)?(Bearer\s+)[^\s\n]+/gi, "$1[REDACTED_TOKEN]")
170
+ // Password fields
171
+ .replace(/(password[\s:=]+)[^\s\n&]+/gi, "$1[REDACTED_PASSWORD]")
172
+ // Generic high-entropy hex/base64 tokens (40+ chars)
173
+ .replace(
174
+ /(["']?[a-zA-Z0-9_-]*(?:token|secret|password|key)["']?\s*[:=]\s*["']?)[a-zA-Z0-9_.+/=-]{32,}(["']?)/gi,
175
+ "$1[REDACTED]$2",
176
+ )
177
+ );
178
+ }
179
+
180
+ /**
181
+ * Bounded, cross-platform process tree killer (Section 16.4).
182
+ * Kills children and grandchildren upon timeout or cancellation.
183
+ */
184
+ function killProcessTree(pid) {
185
+ if (!pid) return;
186
+
187
+ if (process.platform === "win32") {
188
+ try {
189
+ execFileSync("taskkill", ["/PID", String(pid), "/T", "/F"], {
190
+ stdio: "ignore",
191
+ windowsHide: true,
192
+ });
193
+ } catch {
194
+ // Process might have already exited
195
+ }
196
+ } else {
197
+ try {
198
+ // Kill entire process group
199
+ process.kill(-pid, "SIGKILL");
200
+ } catch {
201
+ try {
202
+ process.kill(pid, "SIGKILL");
203
+ } catch {
204
+ // already exited
205
+ }
206
+ }
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Checks that the working tree and candidate commit remained immutable (Section 16.3).
212
+ *
213
+ * @param {string} worktreePath
214
+ * @param {string} expectedHeadSha
215
+ * @returns {{ clean: boolean, mutatedHead?: boolean, dirtyFiles?: string[] }}
216
+ */
217
+ function verifyCandidateImmutability(worktreePath, expectedHeadSha) {
218
+ try {
219
+ const currentHead = execFileSync("git", ["rev-parse", "HEAD"], {
220
+ cwd: worktreePath,
221
+ encoding: "utf8",
222
+ stdio: ["ignore", "pipe", "ignore"],
223
+ windowsHide: true,
224
+ }).trim();
225
+
226
+ if (expectedHeadSha && currentHead !== expectedHeadSha) {
227
+ return { clean: false, mutatedHead: true };
228
+ }
229
+
230
+ const statusOutput = execFileSync("git", ["status", "--porcelain"], {
231
+ cwd: worktreePath,
232
+ encoding: "utf8",
233
+ stdio: ["ignore", "pipe", "ignore"],
234
+ windowsHide: true,
235
+ }).trim();
236
+
237
+ if (statusOutput.length > 0) {
238
+ const dirtyFiles = statusOutput.split("\n").map((l) => l.trim().slice(3));
239
+ return { clean: false, mutatedHead: false, dirtyFiles };
240
+ }
241
+
242
+ return { clean: true };
243
+ } catch {
244
+ // If not a git worktree or command fails, treat as non-violating for plain test dirs
245
+ return { clean: true };
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Executes a verification run in a worktree and generates a VerificationAttestation.
251
+ *
252
+ * @param {string} worktreePath
253
+ * @param {Object|string} specInput
254
+ * @param {Object} subjectInfo - { repositoryFingerprint, baseSha, headSha, diffSha256, scopeSha256 }
255
+ * @param {Object} [options] - { runnerMode, trusted }
256
+ * @returns {Promise<Object>} VerificationAttestation
257
+ */
258
+ async function executeVerification(worktreePath, specInput, subjectInfo = {}, options = {}) {
259
+ const startedAt = Date.now();
260
+ const spec = parseVerificationSpec(specInput);
261
+ const runnerMode = options.runnerMode || "host-unsafe";
262
+
263
+ // 1. Validate Command Trust Policy
264
+ validateCommandTrust(spec, { runnerMode, trusted: options.trusted });
265
+
266
+ // 2. Pre-Verification Immutability Snapshot
267
+ const preCheck = verifyCandidateImmutability(worktreePath, subjectInfo.headSha);
268
+ if (!preCheck.clean && preCheck.mutatedHead) {
269
+ throw new Error("Cannot execute verification: worktree HEAD does not match subject headSha");
270
+ }
271
+
272
+ // 3. Spawn verification subprocess
273
+ const hasShell = process.platform === "win32" && /\.(cmd|bat)$/i.test(spec.executable);
274
+
275
+ return new Promise((resolve) => {
276
+ let child;
277
+ let timedOut = false;
278
+ let stdoutData = "";
279
+ let stderrData = "";
280
+ const hash = crypto.createHash("sha256");
281
+
282
+ const timeoutTimer = setTimeout(() => {
283
+ timedOut = true;
284
+ if (child?.pid) {
285
+ killProcessTree(child.pid);
286
+ }
287
+ }, spec.timeoutMs);
288
+
289
+ try {
290
+ child = spawn(spec.executable, spec.args, {
291
+ cwd: worktreePath,
292
+ env: getSanitizedEnvironment(options.env),
293
+ shell: hasShell,
294
+ detached: process.platform !== "win32",
295
+ stdio: ["ignore", "pipe", "pipe"],
296
+ windowsHide: true,
297
+ });
298
+ } catch (spawnErr) {
299
+ clearTimeout(timeoutTimer);
300
+ const completedAt = Date.now();
301
+ return resolve(
302
+ createVerificationAttestation({
303
+ status: "ERROR",
304
+ subject: subjectInfo,
305
+ command: { executable: spec.executable, args: spec.args, cwd: worktreePath, timeoutMs: spec.timeoutMs },
306
+ runnerMode,
307
+ startedAt,
308
+ completedAt,
309
+ reasonCode: "SPAWN_FAILED",
310
+ evidence: {
311
+ exitCode: 127,
312
+ outputSha256: sha256(spawnErr.message),
313
+ redactedPreview: spawnErr.message,
314
+ },
315
+ }),
316
+ );
317
+ }
318
+
319
+ child.stdout?.on("data", (chunk) => {
320
+ hash.update(chunk);
321
+ if (stdoutData.length < 50000) {
322
+ stdoutData += chunk.toString();
323
+ }
324
+ });
325
+
326
+ child.stderr?.on("data", (chunk) => {
327
+ hash.update(chunk);
328
+ if (stderrData.length < 50000) {
329
+ stderrData += chunk.toString();
330
+ }
331
+ });
332
+
333
+ child.on("error", (err) => {
334
+ clearTimeout(timeoutTimer);
335
+ const completedAt = Date.now();
336
+ resolve(
337
+ createVerificationAttestation({
338
+ status: "ERROR",
339
+ subject: subjectInfo,
340
+ command: { executable: spec.executable, args: spec.args, cwd: worktreePath, timeoutMs: spec.timeoutMs },
341
+ runnerMode,
342
+ startedAt,
343
+ completedAt,
344
+ reasonCode: err.code === "ENOENT" ? "SPAWN_FAILED" : "EXECUTION_ERROR",
345
+ evidence: {
346
+ exitCode: 1,
347
+ outputSha256: sha256(err.message),
348
+ redactedPreview: err.message,
349
+ },
350
+ }),
351
+ );
352
+ });
353
+
354
+ child.on("close", (exitCode, signal) => {
355
+ clearTimeout(timeoutTimer);
356
+ const completedAt = Date.now();
357
+ const outputSha256 = hash.digest("hex");
358
+ const combinedOutput = `${stdoutData}\n${stderrData}`.trim();
359
+ const redactedPreview = redactSensitiveOutput(combinedOutput.slice(0, 2000));
360
+
361
+ // Check timeout
362
+ if (timedOut) {
363
+ return resolve(
364
+ createVerificationAttestation({
365
+ status: "TIMEOUT",
366
+ subject: subjectInfo,
367
+ command: { executable: spec.executable, args: spec.args, cwd: worktreePath, timeoutMs: spec.timeoutMs },
368
+ runnerMode,
369
+ startedAt,
370
+ completedAt,
371
+ reasonCode: "EXECUTION_TIMED_OUT",
372
+ evidence: {
373
+ exitCode: null,
374
+ signal: "SIGKILL",
375
+ outputSha256,
376
+ redactedPreview: `${redactedPreview}\n[VERIFICATION_TIMEOUT after ${spec.timeoutMs}ms]`,
377
+ },
378
+ }),
379
+ );
380
+ }
381
+
382
+ // 4. Post-Verification Immutability Check (Section 16.3)
383
+ const postCheck = verifyCandidateImmutability(worktreePath, subjectInfo.headSha);
384
+ if (!postCheck.clean) {
385
+ // Test script modified HEAD or left dirty files -> invalidates verification
386
+ return resolve(
387
+ createVerificationAttestation({
388
+ status: "FAIL",
389
+ subject: subjectInfo,
390
+ command: { executable: spec.executable, args: spec.args, cwd: worktreePath, timeoutMs: spec.timeoutMs },
391
+ runnerMode,
392
+ startedAt,
393
+ completedAt,
394
+ reasonCode: postCheck.mutatedHead ? "TEST_MUTATED_HEAD" : "TEST_DIRTY_WORKTREE",
395
+ evidence: {
396
+ exitCode: 1,
397
+ outputSha256,
398
+ redactedPreview: `Verification failed: test script modified worktree state (${(postCheck.dirtyFiles || []).join(", ")})`,
399
+ },
400
+ }),
401
+ );
402
+ }
403
+
404
+ // 5. Normal Outcome
405
+ const isSuccess = exitCode === 0;
406
+ resolve(
407
+ createVerificationAttestation({
408
+ status: isSuccess ? "PASS" : "FAIL",
409
+ subject: subjectInfo,
410
+ command: { executable: spec.executable, args: spec.args, cwd: worktreePath, timeoutMs: spec.timeoutMs },
411
+ runnerMode,
412
+ startedAt,
413
+ completedAt,
414
+ evidence: {
415
+ exitCode: exitCode ?? (isSuccess ? 0 : 1),
416
+ signal: signal || null,
417
+ outputSha256,
418
+ redactedPreview,
419
+ },
420
+ }),
421
+ );
422
+ });
423
+ });
424
+ }
425
+
426
+ module.exports = {
427
+ TRUSTED_TOOLS,
428
+ parseVerificationSpec,
429
+ validateCommandTrust,
430
+ getSanitizedEnvironment,
431
+ redactSensitiveOutput,
432
+ killProcessTree,
433
+ verifyCandidateImmutability,
434
+ executeVerification,
435
+ };
@@ -0,0 +1,165 @@
1
+ /**
2
+ * ContextOS — Real Execution Sandbox & OCI Container Profile Engine
3
+ *
4
+ * Implements Section 19 of CONTEXTOS_IMPLEMENTATION_PLAN.md & Milestones W5.3 and W5.4:
5
+ * - Execution modes: oci-required, oci-preferred, host-unsafe
6
+ * - Fail-closed fallback: zero silent fallbacks from OCI to host
7
+ * - Auto-merge governance: auto-merge permanently blocked by default in host-unsafe mode
8
+ * - Repository config is prohibited from enabling unsafe auto-merge override
9
+ * - Hardened container profile (read-only root, cap-drop ALL, no-new-privileges, non-root user 1000:1000)
10
+ * - Network isolation: default network=none
11
+ * - Ephemeral isolated TMPFS mounts (no real HOME mounted)
12
+ * - Direct OCI engine execution (Docker/Podman/mock) without shell
13
+ * - Engine/image digest verification and runner evidence recording
14
+ * - Adversarial attack validation (traversal, git hooks, secret staging, config tampering)
15
+ */
16
+ export type SandboxMode = "oci-required" | "oci-preferred" | "host-unsafe";
17
+ export type ContainerEngine = "docker" | "podman" | "mock";
18
+ export type NetworkMode = "deny" | "allow";
19
+ export interface SandboxLimits {
20
+ cpus?: string;
21
+ memoryMb?: number;
22
+ pidsLimit?: number;
23
+ timeoutMs?: number;
24
+ maxOutputBytes?: number;
25
+ }
26
+ export interface SandboxOptions {
27
+ mode?: SandboxMode;
28
+ containerEngine?: ContainerEngine | null;
29
+ image?: string;
30
+ expectedImageDigest?: string;
31
+ network?: NetworkMode;
32
+ limits?: SandboxLimits;
33
+ mockExecutor?: (engine: string, args: string[], options: {
34
+ cwd: string;
35
+ timeoutMs?: number;
36
+ signal?: AbortSignal;
37
+ }) => Promise<{
38
+ exitCode: number;
39
+ stdout: string;
40
+ stderr: string;
41
+ }>;
42
+ }
43
+ export interface AutoMergeDecision {
44
+ allowed: boolean;
45
+ reasonCode: string;
46
+ reason: string;
47
+ }
48
+ export interface AdversarialCheckResult {
49
+ safe: boolean;
50
+ rule?: string;
51
+ violation?: string;
52
+ }
53
+ export interface PlanResult {
54
+ runner: "oci" | "host-unsafe";
55
+ autoMergeBlocked: boolean;
56
+ warning?: string;
57
+ }
58
+ export interface SandboxExecutionEvidence {
59
+ engine: string;
60
+ image: string;
61
+ imageDigest?: string;
62
+ exitCode: number | null;
63
+ durationMs: number;
64
+ outputSha256: string;
65
+ runnerMode: "oci" | "host-unsafe";
66
+ network: NetworkMode;
67
+ timestamp: number;
68
+ }
69
+ export interface SandboxExecutionResult {
70
+ success: boolean;
71
+ exitCode: number | null;
72
+ signal: string | null;
73
+ durationMs: number;
74
+ stdout: string;
75
+ stderr: string;
76
+ outputSha256: string;
77
+ redactedOutput: string;
78
+ runnerMode: "oci" | "host-unsafe";
79
+ containerEngine: string;
80
+ image: string;
81
+ imageDigest?: string;
82
+ autoMergeBlocked: boolean;
83
+ evidence: SandboxExecutionEvidence;
84
+ warning?: string;
85
+ }
86
+ export interface SandboxExecuteOptions {
87
+ userOverride?: boolean;
88
+ isFromRepoConfig?: boolean;
89
+ signal?: AbortSignal;
90
+ timeoutMs?: number;
91
+ network?: NetworkMode;
92
+ }
93
+ export declare class ExecutionSandbox {
94
+ mode: SandboxMode;
95
+ containerEngine: ContainerEngine | null;
96
+ image: string;
97
+ expectedImageDigest?: string;
98
+ network: NetworkMode;
99
+ limits: {
100
+ cpus: string;
101
+ memoryMb: number;
102
+ pidsLimit: number;
103
+ timeoutMs: number;
104
+ maxOutputBytes: number;
105
+ };
106
+ mockExecutor?: (engine: string, args: string[], options: {
107
+ cwd: string;
108
+ timeoutMs?: number;
109
+ signal?: AbortSignal;
110
+ }) => Promise<{
111
+ exitCode: number;
112
+ stdout: string;
113
+ stderr: string;
114
+ }>;
115
+ constructor(options?: SandboxOptions);
116
+ /**
117
+ * Evaluates if auto-merge is permitted based on execution outcome and mode.
118
+ * Section 19.1 & W5.4:
119
+ * - oci-required: permitted after gates pass
120
+ * - oci-preferred: permitted only if OCI container was actually used
121
+ * - host-unsafe: blocked by default; requires explicit user-local safety override
122
+ * - Repository config is strictly forbidden from enabling unsafe override
123
+ */
124
+ canAutoMerge(executionRecord?: {
125
+ runnerMode?: string;
126
+ userOverride?: boolean;
127
+ isFromRepoConfig?: boolean;
128
+ }): AutoMergeDecision;
129
+ /**
130
+ * Generates hardened OCI container CLI arguments adhering strictly to Section 19.2.
131
+ */
132
+ buildContainerArgs(workspacePath: string, commandArgs: string[], overrides?: Partial<SandboxOptions>): string[];
133
+ /**
134
+ * Scans a proposed action or file write for adversarial attacks (Section 19.6).
135
+ */
136
+ validateAdversarialAttempt(action?: {
137
+ targetPath?: string;
138
+ content?: string;
139
+ commandArgs?: string[];
140
+ }): AdversarialCheckResult;
141
+ /**
142
+ * Plans execution runner and validates against fail-closed policy.
143
+ */
144
+ planExecution(isEngineAvailable: boolean): PlanResult;
145
+ /**
146
+ * Detects whether a functional container engine (docker/podman/mock) is available.
147
+ */
148
+ detectContainerEngine(): ContainerEngine | null;
149
+ /**
150
+ * Resolves image digest for evidence and pin verification.
151
+ */
152
+ getImageDigest(engine: ContainerEngine, image: string): string | null;
153
+ /**
154
+ * Executes a command inside the hardened sandbox or host fallback according to policy.
155
+ */
156
+ execute(workspacePath: string, commandArgs: string[], options?: SandboxExecuteOptions): Promise<SandboxExecutionResult>;
157
+ /**
158
+ * Executes the command directly inside an isolated OCI container without shell.
159
+ */
160
+ private executeInContainer;
161
+ /**
162
+ * Executes on host in host-unsafe fallback mode with full process isolation and secret redaction.
163
+ */
164
+ private executeInHost;
165
+ }