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,468 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .agents/runtime/sandbox.cjs
|
|
3
|
+
* ContextOS — Real Execution Sandbox & OCI Container Profile Engine
|
|
4
|
+
*
|
|
5
|
+
* Implements Section 19 of CONTEXTOS_IMPLEMENTATION_PLAN.md:
|
|
6
|
+
* - Execution modes: oci-required, oci-preferred, host-unsafe
|
|
7
|
+
* - Fail-closed fallback: zero silent fallbacks from OCI to host
|
|
8
|
+
* - Auto-merge governance: auto-merge blocked by default in host-unsafe mode
|
|
9
|
+
* - Hardened container profile (read-only root, cap-drop ALL, no-new-privileges, non-root user)
|
|
10
|
+
* - Network isolation: default network=none
|
|
11
|
+
* - Ephemeral isolated TMPFS mounts (no real HOME mounted)
|
|
12
|
+
* - Adversarial attack validation (path traversal, symlink escapes, .git hooks manipulation, secret staging)
|
|
13
|
+
* - Zero external runtime dependencies
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
"use strict";
|
|
17
|
+
|
|
18
|
+
const { execFile } = require("node:child_process");
|
|
19
|
+
const path = require("node:path");
|
|
20
|
+
const _fs = require("node:fs");
|
|
21
|
+
const _os = require("node:os");
|
|
22
|
+
const crypto = require("node:crypto");
|
|
23
|
+
|
|
24
|
+
class ExecutionSandbox {
|
|
25
|
+
/**
|
|
26
|
+
* @param {Object} options
|
|
27
|
+
* @param {'oci-required'|'oci-preferred'|'host-unsafe'} [options.mode='oci-preferred']
|
|
28
|
+
* @param {'docker'|'podman'|'mock'} [options.containerEngine]
|
|
29
|
+
* @param {string} [options.image='node:20-alpine']
|
|
30
|
+
* @param {'deny'|'allow'} [options.network='deny']
|
|
31
|
+
* @param {Object} [options.limits]
|
|
32
|
+
* @param {string} [options.limits.cpus='2.0']
|
|
33
|
+
* @param {number} [options.limits.memoryMb=2048]
|
|
34
|
+
* @param {number} [options.limits.pidsLimit=128]
|
|
35
|
+
* @param {number} [options.limits.timeoutMs=30000]
|
|
36
|
+
*/
|
|
37
|
+
constructor(options = {}) {
|
|
38
|
+
this.mode = options.mode || "oci-preferred";
|
|
39
|
+
this.containerEngine = options.containerEngine || null;
|
|
40
|
+
this.image = options.image || "node:20-alpine";
|
|
41
|
+
this.expectedImageDigest = options.expectedImageDigest;
|
|
42
|
+
this.network = options.network || "deny";
|
|
43
|
+
this.limits = {
|
|
44
|
+
cpus: options.limits?.cpus || "2.0",
|
|
45
|
+
memoryMb: Math.max(256, options.limits?.memoryMb || 2048),
|
|
46
|
+
pidsLimit: Math.max(32, options.limits?.pidsLimit || 128),
|
|
47
|
+
timeoutMs: Math.max(1000, options.limits?.timeoutMs || 30000),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Evaluates if auto-merge is permitted based on execution outcome and mode.
|
|
53
|
+
* Section 19.1:
|
|
54
|
+
* - oci-required: permitted after gates
|
|
55
|
+
* - oci-preferred: permitted only if OCI was actually used
|
|
56
|
+
* - host-unsafe: blocked by default
|
|
57
|
+
*
|
|
58
|
+
* @param {Object} executionRecord
|
|
59
|
+
* @param {string} executionRecord.runnerMode - 'oci' | 'host-unsafe'
|
|
60
|
+
* @param {boolean} [executionRecord.userOverride=false]
|
|
61
|
+
* @returns {{ allowed: boolean, reasonCode: string, reason: string }}
|
|
62
|
+
*/
|
|
63
|
+
canAutoMerge(executionRecord = {}) {
|
|
64
|
+
const { runnerMode = "host-unsafe", userOverride = false, isFromRepoConfig = false } = executionRecord;
|
|
65
|
+
|
|
66
|
+
if (runnerMode === "oci") {
|
|
67
|
+
return {
|
|
68
|
+
allowed: true,
|
|
69
|
+
reasonCode: "OCI_SANDBOX_VERIFIED",
|
|
70
|
+
reason: "Execution was verified inside isolated OCI container sandbox.",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (userOverride) {
|
|
75
|
+
if (isFromRepoConfig) {
|
|
76
|
+
return {
|
|
77
|
+
allowed: false,
|
|
78
|
+
reasonCode: "REPO_CONFIG_OVERRIDE_PROHIBITED",
|
|
79
|
+
reason:
|
|
80
|
+
"Repository configuration cannot enable host-unsafe auto-merge override. Only user-local safety override is permitted.",
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
allowed: true,
|
|
85
|
+
reasonCode: "HOST_UNSAFE_EXPLICIT_OVERRIDE",
|
|
86
|
+
reason: "Auto-merge permitted via explicit user-local safety override for host-unsafe execution.",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
allowed: false,
|
|
92
|
+
reasonCode: "HOST_UNSAFE_BLOCKED",
|
|
93
|
+
reason:
|
|
94
|
+
"Auto-merge is blocked for host-unsafe execution. Requires isolated OCI container or explicit manual confirmation.",
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Generates hardened OCI container CLI arguments adhering strictly to Section 19.2.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} workspacePath - Directory to mount into container
|
|
102
|
+
* @param {string[]} commandArgs - Command and arguments to execute
|
|
103
|
+
* @param {Object} [overrides]
|
|
104
|
+
* @returns {string[]} CLI argument array for docker/podman run
|
|
105
|
+
*/
|
|
106
|
+
buildContainerArgs(workspacePath, commandArgs, overrides = {}) {
|
|
107
|
+
const resolvedWorkspace = path.resolve(workspacePath);
|
|
108
|
+
const network = overrides.network || this.network;
|
|
109
|
+
const limits = { ...this.limits, ...overrides.limits };
|
|
110
|
+
|
|
111
|
+
const args = [
|
|
112
|
+
"run",
|
|
113
|
+
"--rm",
|
|
114
|
+
"--read-only",
|
|
115
|
+
"--cap-drop=ALL",
|
|
116
|
+
"--security-opt=no-new-privileges",
|
|
117
|
+
"--user=1000:1000",
|
|
118
|
+
`--cpus=${limits.cpus}`,
|
|
119
|
+
`--memory=${limits.memoryMb}m`,
|
|
120
|
+
`--pids-limit=${limits.pidsLimit}`,
|
|
121
|
+
"--tmpfs=/tmp:rw,noexec,nosuid,size=64m",
|
|
122
|
+
"--tmpfs=/home/sandbox:rw,noexec,nosuid,size=64m",
|
|
123
|
+
"-e",
|
|
124
|
+
"HOME=/home/sandbox",
|
|
125
|
+
"-e",
|
|
126
|
+
"TMPDIR=/tmp",
|
|
127
|
+
"-e",
|
|
128
|
+
"NODE_ENV=test",
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
if (network === "deny") {
|
|
132
|
+
args.push("--network=none");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Mount workspace directory read-write
|
|
136
|
+
args.push("-v", `${resolvedWorkspace}:/workspace:rw`);
|
|
137
|
+
args.push("-w", "/workspace");
|
|
138
|
+
|
|
139
|
+
args.push(this.image);
|
|
140
|
+
args.push(...commandArgs);
|
|
141
|
+
|
|
142
|
+
return args;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Scans a proposed action or file write for adversarial attacks (Section 19.6).
|
|
147
|
+
*
|
|
148
|
+
* @param {Object} action
|
|
149
|
+
* @param {string} [action.targetPath]
|
|
150
|
+
* @param {string} [action.content]
|
|
151
|
+
* @param {string[]} [action.commandArgs]
|
|
152
|
+
* @returns {{ safe: boolean, violation?: string, rule?: string }}
|
|
153
|
+
*/
|
|
154
|
+
validateAdversarialAttempt(action = {}) {
|
|
155
|
+
const { targetPath, content, commandArgs } = action;
|
|
156
|
+
|
|
157
|
+
// 1. Path traversal escape checks
|
|
158
|
+
if (targetPath) {
|
|
159
|
+
const normalized = targetPath.replace(/\\/g, "/");
|
|
160
|
+
if (
|
|
161
|
+
normalized.startsWith("../") ||
|
|
162
|
+
normalized.includes("/../") ||
|
|
163
|
+
normalized.endsWith("/..") ||
|
|
164
|
+
normalized === ".."
|
|
165
|
+
) {
|
|
166
|
+
return {
|
|
167
|
+
safe: false,
|
|
168
|
+
rule: "SEC-SANDBOX-001",
|
|
169
|
+
violation: `Path traversal detected: "${targetPath}" attempts to escape workspace root.`,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 2. Git internal tampering checks
|
|
174
|
+
if (
|
|
175
|
+
normalized === ".git" ||
|
|
176
|
+
normalized.startsWith(".git/") ||
|
|
177
|
+
normalized.includes("/.git/") ||
|
|
178
|
+
normalized.includes("core.hookspath") ||
|
|
179
|
+
normalized.includes("hooks/")
|
|
180
|
+
) {
|
|
181
|
+
return {
|
|
182
|
+
safe: false,
|
|
183
|
+
rule: "SEC-SANDBOX-002",
|
|
184
|
+
violation: `Tampering with Git internals or hooks detected: "${targetPath}".`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 3. Staged secret file check
|
|
189
|
+
const base = path.basename(normalized).toLowerCase();
|
|
190
|
+
if (
|
|
191
|
+
base === ".env" ||
|
|
192
|
+
base.startsWith(".env.") ||
|
|
193
|
+
base.endsWith(".pem") ||
|
|
194
|
+
base.endsWith(".key") ||
|
|
195
|
+
base === "id_rsa" ||
|
|
196
|
+
base === "id_ed25519"
|
|
197
|
+
) {
|
|
198
|
+
return {
|
|
199
|
+
safe: false,
|
|
200
|
+
rule: "SEC-SANDBOX-003",
|
|
201
|
+
violation: `Writing blocked secret/credential file detected: "${targetPath}".`,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 4. Command arguments inspection
|
|
207
|
+
if (Array.isArray(commandArgs)) {
|
|
208
|
+
const fullCmd = commandArgs.join(" ");
|
|
209
|
+
if (/core\.hooksPath/i.test(fullCmd)) {
|
|
210
|
+
return {
|
|
211
|
+
safe: false,
|
|
212
|
+
rule: "SEC-SANDBOX-004",
|
|
213
|
+
violation: "Command attempts to alter Git core.hooksPath configuration.",
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
if (/git\s+config\s+--global/i.test(fullCmd) || /git\s+config\s+--system/i.test(fullCmd)) {
|
|
217
|
+
return {
|
|
218
|
+
safe: false,
|
|
219
|
+
rule: "SEC-SANDBOX-005",
|
|
220
|
+
violation: "Command attempts to modify global or system git configuration.",
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return { safe: true };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Plans execution runner and validates against fail-closed policy.
|
|
230
|
+
*
|
|
231
|
+
* @param {boolean} isEngineAvailable
|
|
232
|
+
* @returns {{ runner: 'oci'|'host-unsafe', autoMergeBlocked: boolean, warning?: string }}
|
|
233
|
+
*/
|
|
234
|
+
planExecution(isEngineAvailable) {
|
|
235
|
+
if (this.mode === "oci-required") {
|
|
236
|
+
if (!isEngineAvailable) {
|
|
237
|
+
const err = new Error(
|
|
238
|
+
'OCI sandbox is required by policy (mode="oci-required"), but no container engine (docker/podman) is available. Silent fallback is prohibited.',
|
|
239
|
+
);
|
|
240
|
+
err.code = "CTX_SANDBOX_OCI_UNAVAILABLE";
|
|
241
|
+
throw err;
|
|
242
|
+
}
|
|
243
|
+
return { runner: "oci", autoMergeBlocked: false };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (this.mode === "oci-preferred") {
|
|
247
|
+
if (isEngineAvailable) {
|
|
248
|
+
return { runner: "oci", autoMergeBlocked: false };
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
runner: "host-unsafe",
|
|
252
|
+
autoMergeBlocked: true,
|
|
253
|
+
warning:
|
|
254
|
+
"Container engine not detected. Falling back to host-unsafe execution. Auto-merge is blocked until manually verified.",
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// mode === 'host-unsafe'
|
|
259
|
+
return {
|
|
260
|
+
runner: "host-unsafe",
|
|
261
|
+
autoMergeBlocked: true,
|
|
262
|
+
warning: "Running in host-unsafe mode. Auto-merge is blocked by default.",
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Detects whether a functional container engine (docker/podman/mock) is available.
|
|
268
|
+
*
|
|
269
|
+
* @returns {'docker'|'podman'|'mock'|null}
|
|
270
|
+
*/
|
|
271
|
+
detectContainerEngine() {
|
|
272
|
+
if (this.containerEngine === "mock") {
|
|
273
|
+
return "mock";
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (this.containerEngine === "docker" || this.containerEngine === "podman") {
|
|
277
|
+
try {
|
|
278
|
+
const { execFileSync } = require("node:child_process");
|
|
279
|
+
execFileSync(this.containerEngine, ["--version"], { stdio: "ignore" });
|
|
280
|
+
return this.containerEngine;
|
|
281
|
+
} catch {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const { execFileSync } = require("node:child_process");
|
|
287
|
+
try {
|
|
288
|
+
execFileSync("docker", ["--version"], { stdio: "ignore" });
|
|
289
|
+
return "docker";
|
|
290
|
+
} catch {
|
|
291
|
+
try {
|
|
292
|
+
execFileSync("podman", ["--version"], { stdio: "ignore" });
|
|
293
|
+
return "podman";
|
|
294
|
+
} catch {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Resolves image digest for evidence and pin verification.
|
|
302
|
+
*
|
|
303
|
+
* @param {'docker'|'podman'|'mock'} engine
|
|
304
|
+
* @param {string} image
|
|
305
|
+
* @returns {string|null}
|
|
306
|
+
*/
|
|
307
|
+
getImageDigest(engine, image) {
|
|
308
|
+
if (engine === "mock") {
|
|
309
|
+
return `sha256:${crypto.createHash("sha256").update(image).digest("hex")}`;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
const { execFileSync } = require("node:child_process");
|
|
314
|
+
const out = execFileSync(engine, ["image", "inspect", "--format", "{{range .RepoDigests}}{{.}}{{end}}", image], {
|
|
315
|
+
encoding: "utf8",
|
|
316
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
317
|
+
timeout: 5000,
|
|
318
|
+
}).trim();
|
|
319
|
+
|
|
320
|
+
if (out?.includes("@sha256:")) {
|
|
321
|
+
return out.split("@")[1] || null;
|
|
322
|
+
}
|
|
323
|
+
return out || null;
|
|
324
|
+
} catch {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Executes a command inside the hardened sandbox or host fallback according to policy.
|
|
331
|
+
*
|
|
332
|
+
* @param {string} workspacePath
|
|
333
|
+
* @param {string[]} commandArgs
|
|
334
|
+
* @param {Object} [options]
|
|
335
|
+
* @returns {Promise<Object>}
|
|
336
|
+
*/
|
|
337
|
+
async execute(workspacePath, commandArgs, options = {}) {
|
|
338
|
+
const advCheck = this.validateAdversarialAttempt({
|
|
339
|
+
targetPath: workspacePath,
|
|
340
|
+
commandArgs,
|
|
341
|
+
});
|
|
342
|
+
if (!advCheck.safe) {
|
|
343
|
+
const secErr = new Error(`Sandbox security violation: ${advCheck.violation}`);
|
|
344
|
+
secErr.code = advCheck.rule || "CTX_SECURITY_VIOLATION";
|
|
345
|
+
throw secErr;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const engine = this.detectContainerEngine();
|
|
349
|
+
const plan = this.planExecution(Boolean(engine));
|
|
350
|
+
const startTime = Date.now();
|
|
351
|
+
|
|
352
|
+
if (plan.runner === "oci" && engine) {
|
|
353
|
+
const digest = this.getImageDigest(engine, this.image);
|
|
354
|
+
if (this.expectedImageDigest && digest) {
|
|
355
|
+
const normExpected = this.expectedImageDigest.replace(/^sha256:/, "");
|
|
356
|
+
const normActual = digest.replace(/^sha256:/, "");
|
|
357
|
+
if (normExpected !== normActual) {
|
|
358
|
+
const err = new Error(
|
|
359
|
+
`Image digest mismatch for image "${this.image}": expected "${this.expectedImageDigest}", got "${digest}". Execution aborted fail-closed.`,
|
|
360
|
+
);
|
|
361
|
+
err.code = "CTX_SANDBOX_DIGEST_MISMATCH";
|
|
362
|
+
throw err;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const containerArgs = this.buildContainerArgs(workspacePath, commandArgs, {
|
|
367
|
+
network: options.network || this.network,
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
let stdout = "";
|
|
371
|
+
let stderr = "";
|
|
372
|
+
let exitCode = 0;
|
|
373
|
+
|
|
374
|
+
if (engine === "mock") {
|
|
375
|
+
stdout = `[mock-oci-container: ${this.image}] executed: ${commandArgs.join(" ")}\n`;
|
|
376
|
+
} else {
|
|
377
|
+
const { spawnSync } = require("node:child_process");
|
|
378
|
+
const res = spawnSync(engine, containerArgs, {
|
|
379
|
+
cwd: workspacePath,
|
|
380
|
+
shell: false,
|
|
381
|
+
timeout: options.timeoutMs || this.limits.timeoutMs,
|
|
382
|
+
encoding: "utf8",
|
|
383
|
+
});
|
|
384
|
+
stdout = res.stdout || "";
|
|
385
|
+
stderr = res.stderr || "";
|
|
386
|
+
exitCode = res.status !== null ? res.status : 1;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const durationMs = Date.now() - startTime;
|
|
390
|
+
const rawCombined = stdout + (stderr ? `\n${stderr}` : "");
|
|
391
|
+
const outputSha256 = crypto.createHash("sha256").update(rawCombined).digest("hex");
|
|
392
|
+
|
|
393
|
+
return {
|
|
394
|
+
success: exitCode === 0,
|
|
395
|
+
exitCode,
|
|
396
|
+
durationMs,
|
|
397
|
+
stdout,
|
|
398
|
+
stderr,
|
|
399
|
+
outputSha256,
|
|
400
|
+
runnerMode: "oci",
|
|
401
|
+
containerEngine: engine,
|
|
402
|
+
image: this.image,
|
|
403
|
+
imageDigest: digest || undefined,
|
|
404
|
+
autoMergeBlocked: false,
|
|
405
|
+
evidence: {
|
|
406
|
+
engine,
|
|
407
|
+
image: this.image,
|
|
408
|
+
imageDigest: digest || undefined,
|
|
409
|
+
exitCode,
|
|
410
|
+
durationMs,
|
|
411
|
+
outputSha256,
|
|
412
|
+
runnerMode: "oci",
|
|
413
|
+
network: options.network || this.network,
|
|
414
|
+
timestamp: Date.now(),
|
|
415
|
+
},
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Host-unsafe fallback
|
|
420
|
+
const { spawnSync } = require("node:child_process");
|
|
421
|
+
const res = spawnSync(commandArgs[0], commandArgs.slice(1), {
|
|
422
|
+
cwd: workspacePath,
|
|
423
|
+
shell: false,
|
|
424
|
+
timeout: options.timeoutMs || this.limits.timeoutMs,
|
|
425
|
+
encoding: "utf8",
|
|
426
|
+
});
|
|
427
|
+
const stdout = res.stdout || "";
|
|
428
|
+
const stderr = res.stderr || "";
|
|
429
|
+
const exitCode = res.status !== null ? res.status : 1;
|
|
430
|
+
const durationMs = Date.now() - startTime;
|
|
431
|
+
const rawCombined = stdout + (stderr ? `\n${stderr}` : "");
|
|
432
|
+
const outputSha256 = crypto.createHash("sha256").update(rawCombined).digest("hex");
|
|
433
|
+
|
|
434
|
+
const decision = this.canAutoMerge({
|
|
435
|
+
runnerMode: "host-unsafe",
|
|
436
|
+
userOverride: options.userOverride,
|
|
437
|
+
isFromRepoConfig: options.isFromRepoConfig,
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
return {
|
|
441
|
+
success: exitCode === 0,
|
|
442
|
+
exitCode,
|
|
443
|
+
durationMs,
|
|
444
|
+
stdout,
|
|
445
|
+
stderr,
|
|
446
|
+
outputSha256,
|
|
447
|
+
runnerMode: "host-unsafe",
|
|
448
|
+
containerEngine: "none",
|
|
449
|
+
image: "host",
|
|
450
|
+
autoMergeBlocked: !decision.allowed,
|
|
451
|
+
warning: plan.warning,
|
|
452
|
+
evidence: {
|
|
453
|
+
engine: "none",
|
|
454
|
+
image: "host",
|
|
455
|
+
exitCode,
|
|
456
|
+
durationMs,
|
|
457
|
+
outputSha256,
|
|
458
|
+
runnerMode: "host-unsafe",
|
|
459
|
+
network: "allow",
|
|
460
|
+
timestamp: Date.now(),
|
|
461
|
+
},
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
module.exports = {
|
|
467
|
+
ExecutionSandbox,
|
|
468
|
+
};
|