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,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
|
+
}
|