fix: seal P1 acceptance process boundaries
This commit is contained in:
+3
-3
@@ -5,7 +5,7 @@
|
||||
|
||||
## P1 configuration-process automated integration — PASS 2026-08-09
|
||||
|
||||
- Retained evidence: `.artifacts/p1-integration/p1-70727c7802050c76978d5007a634ede1/report.md`
|
||||
- Retained evidence: `.artifacts/p1-integration/p1-57d5f3b1e420f348f849943f9ee6227c/report.md`
|
||||
- automated integration: PASS
|
||||
- manual acceptance: PENDING
|
||||
- The contextual negatives use a separate `invalid-context` branch, remote, checkout, data,
|
||||
@@ -20,8 +20,8 @@
|
||||
(156 objects, 48 blobs) with zero findings; the frozen final filesystem and virtual-report scan
|
||||
also found no fixture canaries outside the excluded secret fixture directory.
|
||||
- Final report hashes: `report.json`
|
||||
`61d767ea90ad1055a1f6fdadec551752b36f44ca173959c33f470c57a4c706a2`;
|
||||
`report.md` `dbbb37f47f5d686bde3a3516ff7fb3d06c8835aa3b72167f1984436c330446ef`.
|
||||
`dfe4b2f9d7bf0b9cce114ca93239e2b87e0b87f5719504d74ed2c437763c817c`;
|
||||
`report.md` `4f45aaedfc4f6399ab5ab6ebab5764055a243daae16227a29ce16d32b8ee0fa7`.
|
||||
|
||||
## Internal Qdrant + Ollama semantic infrastructure — LIVE 2026-08-08
|
||||
|
||||
|
||||
+281
-109
@@ -23,6 +23,7 @@ const mutableWorkerThreads = require("node:worker_threads");
|
||||
let commandEventSink;
|
||||
let activeCommandCheckId;
|
||||
let activeExecutablePolicy;
|
||||
let activePolicyRejectionSink;
|
||||
let integrationOwner;
|
||||
let productionSurfaceOwner;
|
||||
const RUN_ID = /^p1-[0-9a-f]{32}$/;
|
||||
@@ -227,41 +228,142 @@ function resolveTrustedSystemExecutableSync(name) {
|
||||
throw new Error(`cannot resolve trusted system executable: ${name}`);
|
||||
}
|
||||
|
||||
export async function resolveProductionExecutables({ repositoryRoot, thtBin } = {}) {
|
||||
export async function resolveProductionExecutables({ repositoryRoot } = {}) {
|
||||
const repo = canonicalRoot(repositoryRoot);
|
||||
const gitPath = resolveTrustedSystemExecutableSync("git");
|
||||
const pythonPath = resolveTrustedSystemExecutableSync("python3");
|
||||
const expectedThtRoot = join(repo, "harness", ".venv");
|
||||
const candidateTht = thtBin ?? join(expectedThtRoot, "bin", "tht");
|
||||
if (!isAbsolute(candidateTht)) throw new Error("THT executable must be absolute");
|
||||
const thtPath = realpathSync(candidateTht);
|
||||
const thtRelative = relative(expectedThtRoot, thtPath);
|
||||
if (thtRelative.startsWith("..") || isAbsolute(thtRelative)) throw new Error("THT executable leaves the repository virtual environment");
|
||||
await access(thtPath, fsConstants.X_OK);
|
||||
return { gitPath, pythonPath, thtPath };
|
||||
const thtPath = join(repo, "harness", ".venv", "bin", "tht");
|
||||
let entry;
|
||||
try { entry = lstatSync(thtPath); } catch { throw new Error("trusted THT executable is unavailable"); }
|
||||
if (!entry.isFile() || entry.isSymbolicLink() || realpathSync(thtPath) !== thtPath) throw new Error("trusted THT entrypoint identity is invalid");
|
||||
accessSync(thtPath, fsConstants.X_OK);
|
||||
const entrypointBytes = await readFile(thtPath, "utf8");
|
||||
const lines = entrypointBytes.replaceAll("\r\n", "\n").split("\n");
|
||||
const shebang = lines.shift() ?? "";
|
||||
const pythonLexical = shebang.startsWith("#!") ? shebang.slice(2) : "";
|
||||
const expectedBody = [
|
||||
"import sys", "from tht.cli import app", "if __name__ == '__main__':",
|
||||
" if sys.argv[0].endswith('.exe'):", " sys.argv[0] = sys.argv[0][:-4]",
|
||||
" sys.exit(app())", "",
|
||||
].join("\n");
|
||||
const venvBin = join(repo, "harness", ".venv", "bin");
|
||||
if (dirname(pythonLexical) !== venvBin || !/^python3(?:\.\d+)?$/.test(basename(pythonLexical))
|
||||
|| realpathSync(pythonLexical) !== realpathSync(join(venvBin, "python"))
|
||||
|| lines.join("\n") !== expectedBody) throw new Error("trusted THT generated entrypoint is invalid");
|
||||
const sourceRoot = join(repo, "harness", "tht");
|
||||
const pyproject = await readFile(join(repo, "harness", "pyproject.toml"), "utf8");
|
||||
if (!/^tht\s*=\s*["']tht\.cli:app["']$/m.test(pyproject)) throw new Error("trusted THT console-script declaration is invalid");
|
||||
const status = await runCommand({
|
||||
executable: gitPath,
|
||||
argv: ["-C", repo, "status", "--porcelain=v1", "--untracked-files=all", "--", "harness/tht", "harness/pyproject.toml"],
|
||||
env: {
|
||||
PATH: dirname(gitPath), HOME: "/nonexistent", GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null",
|
||||
GIT_CONFIG_COUNT: "1", GIT_CONFIG_KEY_0: "core.fsmonitor", GIT_CONFIG_VALUE_0: "false",
|
||||
},
|
||||
});
|
||||
if (status.stdout !== "") throw new Error("trusted THT source is not tracked and clean");
|
||||
const pythonVersion = basename(pythonLexical);
|
||||
const sitePackages = join(repo, "harness", ".venv", "lib", pythonVersion, "site-packages");
|
||||
const siteEntries = await readdir(sitePackages);
|
||||
const allPthFiles = siteEntries.filter((name) => name.endsWith(".pth"));
|
||||
const pthFiles = allPthFiles.filter((name) => /^__editable__\.tht-.*\.pth$/.test(name));
|
||||
const finderFiles = siteEntries.filter((name) => /^__editable___tht_.*_finder\.py$/.test(name));
|
||||
if (pthFiles.length !== 1 || allPthFiles.length !== 1 || finderFiles.length !== 1
|
||||
|| siteEntries.some((name) => /^(?:sitecustomize|usercustomize|tht)\.py$/.test(name) || name === "tht")) {
|
||||
throw new Error("trusted THT editable binding is ambiguous");
|
||||
}
|
||||
const pth = await readFile(join(sitePackages, pthFiles[0]), "utf8");
|
||||
const finder = await readFile(join(sitePackages, finderFiles[0]), "utf8");
|
||||
const finderModule = finderFiles[0].slice(0, -3);
|
||||
if (pth.trim() !== `import ${finderModule}; ${finderModule}.install()`
|
||||
|| !finder.includes(`MAPPING: dict[str, str] = {'tht': '${sourceRoot}'}`)
|
||||
|| /\b(?:subprocess|socket|requests|httpx|urllib|multiprocessing)\b|\bos\.system\b|\bPopen\b/.test(finder)) {
|
||||
throw new Error("trusted THT editable binding is invalid");
|
||||
}
|
||||
return {
|
||||
gitPath, pythonPath, thtPath,
|
||||
thtIdentity: {
|
||||
entrypoint: "generated-console-script", entrypointSha256: sha256(entrypointBytes),
|
||||
pythonPath: pythonLexical, pythonCanonicalPath: realpathSync(pythonLexical),
|
||||
sourceRoot, sourceStatus: "tracked-clean", editableFinderSha256: sha256(finder),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const GIT_VERBS = new Set([
|
||||
"--version", "add", "cat-file", "checkout", "clean", "clone", "commit", "config", "fetch", "for-each-ref",
|
||||
"init", "ls-tree", "merge", "merge-base", "push", "remote", "reset", "rev-list", "rev-parse", "show",
|
||||
"show-ref", "status", "symbolic-ref", "write-tree",
|
||||
const FIXTURE_GIT_CONFIG = new Map([
|
||||
["user.name", new Set(["P1 Fixture Curator", "P1 Context Curator"])],
|
||||
["user.email", new Set(["p1-curator@example.invalid", "p1-context@example.invalid"])],
|
||||
]);
|
||||
function gitVerb(argv) {
|
||||
if (argv[0] === "--version") return "--version";
|
||||
let index = 0;
|
||||
while (index < argv.length) {
|
||||
if (["-C", "--git-dir", "--work-tree", "-c"].includes(argv[index])) { index += 2; continue; }
|
||||
if (argv[index].startsWith("--git-dir=") || argv[index].startsWith("--work-tree=")) { index += 1; continue; }
|
||||
return argv[index];
|
||||
}
|
||||
return undefined;
|
||||
function ownedGitPath(value, runRoot) {
|
||||
if (typeof value !== "string" || !isAbsolute(value) || value.includes("\0")) return false;
|
||||
if (!runRoot) return true;
|
||||
const lexical = resolve(value); const rel = relative(runRoot, lexical);
|
||||
if (rel.startsWith("..") || isAbsolute(rel)) return false;
|
||||
try { validateNoSymlinkAncestors(runRoot, lexical); } catch { return false; }
|
||||
return true;
|
||||
}
|
||||
function validateGitInvocation(argv) {
|
||||
const verb = gitVerb(argv);
|
||||
if (!verb || !GIT_VERBS.has(verb)) throw new Error("Git command is prohibited");
|
||||
if (argv.some((value) => /^[a-z][a-z0-9+.-]*:\/\//i.test(value) || /^[^/\s]+@[^:\s]+:/.test(value))) {
|
||||
throw new Error("Git network URL is prohibited");
|
||||
function ownedEmptyHooksPath(value, runRoot) {
|
||||
if (!ownedGitPath(value, runRoot) || !/(?:^|\/)registry\/locks\/empty-hooks$/.test(value)) return false;
|
||||
try {
|
||||
const entry = lstatSync(value);
|
||||
return entry.isDirectory() && !entry.isSymbolicLink() && realpathSync(value) === value;
|
||||
} catch { return false; }
|
||||
}
|
||||
function safeGitOperand(value) { return typeof value === "string" && value.length > 0 && !value.startsWith("-") && !/[\0\n\r]/.test(value); }
|
||||
function exactArray(value, expected) { return value.length === expected.length && value.every((item, index) => item === expected[index]); }
|
||||
export function validateGitInvocation(argv, { runRoot } = {}) {
|
||||
if (!Array.isArray(argv) || argv.some((value) => typeof value !== "string")) throw new Error("Git command is prohibited");
|
||||
if (exactArray(argv, ["--version"])) return "--version";
|
||||
let index = 0; let prefix;
|
||||
if (["-C", "--git-dir"].includes(argv[0])) {
|
||||
if (!ownedGitPath(argv[1], runRoot)) throw new Error("Git command is prohibited");
|
||||
prefix = argv[0]; index = 2;
|
||||
} else if (argv[0] === "-c") {
|
||||
if (typeof argv[1] !== "string" || !argv[1].startsWith("core.hooksPath=")) throw new Error("Git command is prohibited");
|
||||
const hooksPath = argv[1].slice("core.hooksPath=".length);
|
||||
if (!ownedEmptyHooksPath(hooksPath, runRoot)) throw new Error("Git command is prohibited");
|
||||
prefix = "hooks"; index = 2;
|
||||
} else if (argv[0]?.startsWith("-")) throw new Error("Git command is prohibited");
|
||||
const verb = argv[index]; const args = argv.slice(index + 1);
|
||||
const branch = (value) => /^(?:main|invalid-context)$/.test(value ?? "");
|
||||
const object = (value) => safeGitOperand(value) && !/^[a-z][a-z0-9+.-]*:\/\//i.test(value) && !/^[^/\s]+@[^:\s]+:/.test(value);
|
||||
const ownedPair = (values) => values.length === 2 && values.every((value) => ownedGitPath(value, runRoot));
|
||||
let valid = false;
|
||||
switch (verb) {
|
||||
case "init": valid = !prefix && args.length === 3 && args[0] === "--bare" && args[1] === "--initial-branch=main" && ownedGitPath(args[2], runRoot); break;
|
||||
case "clone": valid = (!prefix && ownedPair(args))
|
||||
|| (!prefix && args[0] === "--bare" && ownedPair(args.slice(1)))
|
||||
|| (prefix === "hooks" && args.length === 6 && args[0] === "--branch" && branch(args[1]) && args[2] === "--single-branch" && args[3] === "--" && ownedPair(args.slice(4))); break;
|
||||
case "config": valid = !prefix && args.length === 2 && FIXTURE_GIT_CONFIG.get(args[0])?.has(args[1]) === true; break;
|
||||
case "add": valid = (!prefix || prefix === "hooks") && ((args.length === 1 && /^(?:workspace-content|workspace-content\/p1-filesystem\/evidence\/guide\.md)$/.test(args[0]))
|
||||
|| (args.length === 2 && args[0] === "-A" && args[1] === "workspace-content/p1-filesystem/evidence")
|
||||
|| (args[0] === "--" && args.length >= 2 && args.slice(1).every((value) => /^(?:workspaces|workspace-docs)\/[A-Za-z0-9./-]+$/.test(value)))); break;
|
||||
case "commit": valid = (!prefix || prefix === "hooks") && args.length === 2 && args[0] === "-m" && /^(?:Bootstrap curated P1 content|Update curated Evidence only|Invalid contextual Evidence state|Publish workspace p1-(?:filesystem|http|s3))$/.test(args[1]); break;
|
||||
case "push": valid = (!prefix || prefix === "hooks") && (exactArray(args, ["origin", "main"]) || exactArray(args, ["origin", "invalid-context"])
|
||||
|| exactArray(args, ["-u", "origin", "invalid-context"]) || exactArray(args, ["origin", "HEAD:main"])); break;
|
||||
case "checkout": valid = !prefix && exactArray(args, ["-b", "invalid-context"]); break;
|
||||
case "fetch": valid = (!prefix || prefix === "hooks") && (exactArray(args, ["origin", "main"]) || (args.length === 3 && args[0] === "--no-tags" && args[1] === "origin" && branch(args[2]))); break;
|
||||
case "remote": valid = prefix === "hooks" && args.length === 4 && exactArray(args.slice(0, 3), ["set-url", "origin", "--"]) && ownedGitPath(args[3], runRoot); break;
|
||||
case "merge": valid = prefix === "hooks" && exactArray(args, ["--ff-only", "FETCH_HEAD"]); break;
|
||||
case "merge-base": valid = prefix === "hooks" && exactArray(args, ["HEAD", "FETCH_HEAD"]); break;
|
||||
case "reset": valid = (!prefix || prefix === "hooks") && args.length === 2 && args[0] === "--hard" && /^(?:origin\/main|refs\/remotes\/origin\/(?:main|invalid-context))$/.test(args[1]); break;
|
||||
case "clean": valid = prefix === "hooks" && exactArray(args, ["-fd", "--", "workspaces", "workspace-docs"]); break;
|
||||
case "status": valid = (!prefix && (exactArray(args, ["--porcelain=v1"]) || exactArray(args, ["--porcelain"])))
|
||||
|| (prefix === "hooks" && exactArray(args, ["--porcelain"]))
|
||||
|| (prefix === "-C" && exactArray(args, ["--porcelain=v1", "--untracked-files=all", "--", "harness/tht", "harness/pyproject.toml"])); break;
|
||||
case "write-tree": valid = !prefix && args.length === 0; break;
|
||||
case "show-ref": valid = (!prefix || prefix === "--git-dir" || prefix === "hooks") && args.length === 0; break;
|
||||
case "symbolic-ref": valid = (!prefix || prefix === "hooks") && exactArray(args, ["--short", "HEAD"]); break;
|
||||
case "rev-list": valid = ((prefix === "--git-dir" || prefix === "-C") && exactArray(args, ["--objects", "--all"]))
|
||||
|| (prefix === "hooks" && exactArray(args, ["--left-right", "--count", "HEAD...@{upstream}"])); break;
|
||||
case "ls-tree": valid = prefix === "hooks" && exactArray(args, ["-r", "--name-only", "HEAD", "--", "workspaces"]); break;
|
||||
case "cat-file": valid = (!prefix || prefix === "--git-dir" || prefix === "-C" || prefix === "hooks") && args.length === 2
|
||||
&& ((args[0] === "-e" || args[0] === "-t" || args[0] === "blob") && object(args[1])); break;
|
||||
case "show": valid = prefix === "hooks" && args.length === 1 && object(args[0]); break;
|
||||
case "rev-parse": valid = (!prefix || prefix === "--git-dir" || prefix === "hooks") && args.length === 1 && object(args[0]); break;
|
||||
default: valid = false;
|
||||
}
|
||||
if (!valid) throw new Error("Git command is prohibited");
|
||||
return verb;
|
||||
}
|
||||
function boundedChildEnvironment(value, expected) {
|
||||
@@ -278,19 +380,50 @@ function boundedChildEnvironment(value, expected) {
|
||||
for (const [key, value] of Object.entries(expected)) if (environment[key] !== value) throw new Error("child environment changed acceptance bounds");
|
||||
return environment;
|
||||
}
|
||||
function sanitizedArgvLabels(argv = []) {
|
||||
return Array.isArray(argv) ? argv.filter((value) => typeof value === "string").map((value) =>
|
||||
isAbsolute(value) || value.includes(sep) ? "[path]" : /^[a-z]+:\/\//i.test(value) ? "[url]" : value.length > 80 ? "[value]" : value) : [];
|
||||
}
|
||||
function safeChildEvent(events, { surface = "child_process", api, executable, argv = [], outcome, detail, bounds }) {
|
||||
events.push({
|
||||
surface, api, executable: executable ? basename(executable) : undefined,
|
||||
argvLabels: argv.map((value) => isAbsolute(value) || value.includes(sep) ? "[path]" : /^[a-z]+:\/\//i.test(value) ? "[url]" : value.length > 80 ? "[value]" : value),
|
||||
argvLabels: sanitizedArgvLabels(argv),
|
||||
outcome, ...(activeCommandCheckId ? { checkId: activeCommandCheckId } : {}), ...(detail ? { detail } : {}), ...(bounds ? { bounds } : {}),
|
||||
});
|
||||
}
|
||||
function recordPolicyRejection({ executable, argv, api = "validation", detail = "policy" } = {}) {
|
||||
const event = {
|
||||
surface: "child_process", api, executable: typeof executable === "string" ? basename(executable) : undefined,
|
||||
argvLabels: sanitizedArgvLabels(argv), outcome: "REJECTED", detail,
|
||||
...(activeCommandCheckId ? { checkId: activeCommandCheckId } : {}),
|
||||
};
|
||||
if (activePolicyRejectionSink) activePolicyRejectionSink.push(event);
|
||||
else if (commandEventSink) commandEventSink.push(event);
|
||||
return event;
|
||||
}
|
||||
function policyError(message, details) { recordPolicyRejection(details); throw new Error(message); }
|
||||
function validateThtInvocation(argv, { thtPath, runRoot, cwd } = {}) {
|
||||
const configPath = Array.isArray(argv) ? argv[3] : undefined;
|
||||
let configEntry;
|
||||
try { configEntry = typeof configPath === "string" ? lstatSync(configPath) : undefined; } catch { configEntry = undefined; }
|
||||
if (!configEntry || !exactArray(argv, ["config", "check", "-c", configPath]) || !ownedGitPath(configPath, runRoot)
|
||||
|| !/\.ya?ml$/.test(configPath) || !configEntry.isFile() || configEntry.isSymbolicLink()
|
||||
|| realpathSync(configPath) !== configPath || cwd !== dirname(dirname(dirname(thtPath)))) {
|
||||
throw new Error("THT command is prohibited");
|
||||
}
|
||||
return "config-check";
|
||||
}
|
||||
|
||||
export function installProductionSurfaceGuard({ gitPath, pythonPath, thtPath, runRoot, environment, originalFetch = globalThis.fetch }) {
|
||||
export function installProductionSurfaceGuard({
|
||||
gitPath, pythonPath, thtPath, thtIdentity, runRoot, environment,
|
||||
originalFetch = globalThis.fetch, failPatchAt,
|
||||
}) {
|
||||
if (productionSurfaceOwner) throw new Error("production surface guard is already active");
|
||||
for (const value of [gitPath, pythonPath, thtPath, runRoot]) if (!isAbsolute(value)) throw new Error("production guard paths must be absolute");
|
||||
if (typeof originalFetch !== "function") throw new Error("global fetch is unavailable");
|
||||
if (!thtIdentity || thtIdentity.sourceStatus !== "tracked-clean") throw new Error("trusted THT identity is absent");
|
||||
if (failPatchAt !== undefined && (!Number.isInteger(failPatchAt) || failPatchAt < 1 || failPatchAt > 12)) throw new Error("invalid production patch failure probe");
|
||||
const token = Symbol("p1-production-surface");
|
||||
productionSurfaceOwner = token;
|
||||
const events = [];
|
||||
const originals = {
|
||||
execFile: mutableChildProcess.execFile, spawn: mutableChildProcess.spawn,
|
||||
@@ -299,12 +432,29 @@ export function installProductionSurfaceGuard({ gitPath, pythonPath, thtPath, ru
|
||||
createSocket: mutableDgram.createSocket, Worker: mutableWorkerThreads.Worker,
|
||||
dns: new Map(), dnsPromises: new Map(), dlopen: process.dlopen,
|
||||
};
|
||||
const resolveChild = (executable, argv) => {
|
||||
for (const [name, value] of Object.entries(originals)) {
|
||||
if (!["dns", "dnsPromises"].includes(name) && typeof value !== "function") throw new Error("production patch prerequisite is unavailable");
|
||||
}
|
||||
const validateOptions = (options, allowed, api) => {
|
||||
if (!options || typeof options !== "object" || Array.isArray(options)) throw new Error(`${api} options are invalid`);
|
||||
for (const key of Object.keys(options)) if (!allowed.has(key)) throw new Error(`${api} option is prohibited`);
|
||||
if ("timeout" in options && (!Number.isSafeInteger(options.timeout) || options.timeout < 1 || options.timeout > 300_000)) throw new Error("command bounds are invalid");
|
||||
if ("maxBuffer" in options && (!Number.isSafeInteger(options.maxBuffer) || options.maxBuffer < 1 || options.maxBuffer > MAX_OUTPUT)) throw new Error("command bounds are invalid");
|
||||
if ("shell" in options && options.shell !== false) throw new Error(`${api} shell is prohibited`);
|
||||
};
|
||||
const resolveChild = (executable, argv, options, api) => {
|
||||
const canonical = executable === "git" ? gitPath : executable === "python3" ? pythonPath : executable;
|
||||
if (canonical === gitPath) return { executable: gitPath, kind: "git", verb: validateGitInvocation(argv) };
|
||||
if (canonical === thtPath) return { executable: thtPath, kind: "tht" };
|
||||
if (canonical === gitPath) {
|
||||
validateGitInvocation(argv, { runRoot });
|
||||
if (options.cwd !== undefined && !ownedGitPath(options.cwd, runRoot)) throw new Error("Git working directory is prohibited");
|
||||
return { executable: gitPath, kind: "git" };
|
||||
}
|
||||
if (canonical === thtPath) {
|
||||
validateThtInvocation(argv, { thtPath, runRoot, cwd: options.cwd });
|
||||
return { executable: thtPath, kind: "tht" };
|
||||
}
|
||||
if (canonical === pythonPath) {
|
||||
if (argv.length !== 3 || argv[0] !== "-c" || argv[1] !== PYTHON_LOCK_HOLDER_PROGRAM
|
||||
if (api !== "spawn" || argv.length !== 3 || argv[0] !== "-c" || argv[1] !== PYTHON_LOCK_HOLDER_PROGRAM
|
||||
|| !isAbsolute(argv[2]) || relative(runRoot, argv[2]).startsWith("..") || basename(argv[2]) !== "repository.lock") {
|
||||
throw new Error("child command is prohibited");
|
||||
}
|
||||
@@ -312,18 +462,21 @@ export function installProductionSurfaceGuard({ gitPath, pythonPath, thtPath, ru
|
||||
}
|
||||
throw new Error("child command is prohibited");
|
||||
};
|
||||
const rejectChild = (api, args) => {
|
||||
safeChildEvent(events, { api, executable: typeof args[0] === "string" ? args[0] : undefined, outcome: "REJECTED" });
|
||||
throw new Error("child command is prohibited");
|
||||
const rejectChild = (api, args, message = "child command is prohibited") => {
|
||||
safeChildEvent(events, { api, executable: typeof args[0] === "string" ? args[0] : undefined, argv: Array.isArray(args[1]) ? args[1] : [], outcome: "REJECTED" });
|
||||
throw new Error(message);
|
||||
};
|
||||
const guardedExecFile = function guardedExecFile(executable, argv, options, callback) {
|
||||
if (!Array.isArray(argv)) return rejectChild("execFile", [executable]);
|
||||
if (typeof options === "function") { callback = options; options = {}; }
|
||||
options ??= {};
|
||||
let resolved;
|
||||
try { resolved = resolveChild(executable, argv); boundedChildEnvironment(options.env, environment); }
|
||||
catch (error) { safeChildEvent(events, { api: "execFile", executable, argv, outcome: "REJECTED" }); throw error; }
|
||||
const bounded = { ...options, env: options.env ?? environment, timeout: Math.min(options.timeout ?? 30_000, 300_000), maxBuffer: Math.min(options.maxBuffer ?? MAX_OUTPUT, MAX_OUTPUT), shell: false };
|
||||
try {
|
||||
validateOptions(options, new Set(["cwd", "env", "timeout", "maxBuffer", "encoding", "shell"]), "execFile");
|
||||
resolved = resolveChild(executable, argv, options, "execFile");
|
||||
boundedChildEnvironment(options.env, environment);
|
||||
} catch (error) { safeChildEvent(events, { api: "execFile", executable, argv, outcome: "REJECTED" }); throw error; }
|
||||
const bounded = { ...options, env: options.env ?? environment, timeout: options.timeout ?? 30_000, maxBuffer: options.maxBuffer ?? MAX_OUTPUT, shell: false };
|
||||
safeChildEvent(events, { api: "execFile", executable: resolved.executable, argv, outcome: "STARTED", detail: resolved.kind,
|
||||
bounds: { timeoutMs: bounded.timeout, maxOutputBytes: bounded.maxBuffer, environment: "owned" } });
|
||||
return originals.execFile(resolved.executable, argv, bounded, (error, stdout, stderr) => {
|
||||
@@ -337,8 +490,12 @@ export function installProductionSurfaceGuard({ gitPath, pythonPath, thtPath, ru
|
||||
const guardedSpawn = function guardedSpawn(executable, argv, options = {}) {
|
||||
if (!Array.isArray(argv)) return rejectChild("spawn", [executable]);
|
||||
let resolved;
|
||||
try { resolved = resolveChild(executable, argv); boundedChildEnvironment(options.env, environment); }
|
||||
catch (error) { safeChildEvent(events, { api: "spawn", executable, argv, outcome: "REJECTED" }); throw error; }
|
||||
try {
|
||||
validateOptions(options, new Set(["cwd", "env", "stdio", "shell"]), "spawn");
|
||||
if ("stdio" in options && JSON.stringify(options.stdio) !== JSON.stringify(["pipe", "pipe", "pipe"])) throw new Error("spawn stdio is prohibited");
|
||||
resolved = resolveChild(executable, argv, options, "spawn");
|
||||
boundedChildEnvironment(options.env, environment);
|
||||
} catch (error) { safeChildEvent(events, { api: "spawn", executable, argv, outcome: "REJECTED" }); throw error; }
|
||||
const bounded = { ...options, env: options.env ?? environment, shell: false };
|
||||
safeChildEvent(events, { api: "spawn", executable: resolved.executable, argv, outcome: "STARTED", detail: resolved.kind,
|
||||
bounds: { timeoutMs: 300_000, maxOutputBytes: MAX_OUTPUT, environment: "owned" } });
|
||||
@@ -351,100 +508,104 @@ export function installProductionSurfaceGuard({ gitPath, pythonPath, thtPath, ru
|
||||
child.once("error", () => { clearTimeout(timer); });
|
||||
return child;
|
||||
};
|
||||
const owned = { execFile: guardedExecFile, spawn: guardedSpawn, child: new Map(), dns: new Map(), dnsPromises: new Map() };
|
||||
mutableChildProcess.execFile = guardedExecFile;
|
||||
mutableChildProcess.spawn = guardedSpawn;
|
||||
for (const api of ["exec", "execSync", "execFileSync", "spawnSync", "fork"]) {
|
||||
const wrapper = (...args) => rejectChild(api, args); owned.child.set(api, wrapper); mutableChildProcess[api] = wrapper;
|
||||
}
|
||||
const childWrappers = new Map();
|
||||
for (const api of ["exec", "execSync", "execFileSync", "spawnSync", "fork"]) childWrappers.set(api, (...args) => rejectChild(api, args));
|
||||
const guardedCreateSocket = (..._args) => { safeChildEvent(events, { surface: "dgram", api: "createSocket", outcome: "REJECTED" }); throw new Error("prohibited production surface: dgram"); };
|
||||
class ProhibitedWorker { constructor() { safeChildEvent(events, { surface: "worker_threads", api: "Worker", outcome: "REJECTED" }); throw new Error("prohibited production surface: worker_threads"); } }
|
||||
mutableDgram.createSocket = guardedCreateSocket;
|
||||
mutableWorkerThreads.Worker = ProhibitedWorker;
|
||||
const dnsWrappers = new Map(); const dnsPromiseWrappers = new Map();
|
||||
for (const name of ["lookup", "resolve", "resolve4", "resolve6", "resolveAny", "resolveCaa", "resolveCname", "resolveMx", "resolveNaptr", "resolveNs", "resolvePtr", "resolveSoa", "resolveSrv", "resolveTxt", "reverse", "Resolver"]) {
|
||||
if (typeof mutableDns[name] !== "function") continue;
|
||||
originals.dns.set(name, mutableDns[name]);
|
||||
const original = originals.dns.get(name);
|
||||
const wrapper = (...args) => {
|
||||
const original = mutableDns[name]; originals.dns.set(name, original);
|
||||
dnsWrappers.set(name, (...args) => {
|
||||
if (name === "lookup" && ["127.0.0.1", "::1"].includes(args[0])) {
|
||||
safeChildEvent(events, { surface: "dns", api: name, outcome: "PASS", detail: "owned-loopback-literal" });
|
||||
return original(...args);
|
||||
safeChildEvent(events, { surface: "dns", api: name, outcome: "PASS", detail: "owned-loopback-literal" }); return original(...args);
|
||||
}
|
||||
safeChildEvent(events, { surface: "dns", api: name, outcome: "REJECTED" });
|
||||
throw new Error("prohibited production surface: dns");
|
||||
};
|
||||
owned.dns.set(name, wrapper); mutableDns[name] = wrapper;
|
||||
safeChildEvent(events, { surface: "dns", api: name, outcome: "REJECTED" }); throw new Error("prohibited production surface: dns");
|
||||
});
|
||||
}
|
||||
for (const [name, value] of Object.entries(mutableDns.promises ?? {})) if (typeof value === "function") {
|
||||
originals.dnsPromises.set(name, value);
|
||||
const original = originals.dnsPromises.get(name);
|
||||
const wrapper = async (...args) => {
|
||||
dnsPromiseWrappers.set(name, async (...args) => {
|
||||
if (name === "lookup" && ["127.0.0.1", "::1"].includes(args[0])) {
|
||||
safeChildEvent(events, { surface: "dns", api: `promises.${name}`, outcome: "PASS", detail: "owned-loopback-literal" });
|
||||
return await original(...args);
|
||||
safeChildEvent(events, { surface: "dns", api: `promises.${name}`, outcome: "PASS", detail: "owned-loopback-literal" }); return await value(...args);
|
||||
}
|
||||
safeChildEvent(events, { surface: "dns", api: `promises.${name}`, outcome: "REJECTED" });
|
||||
throw new Error("prohibited production surface: dns");
|
||||
};
|
||||
owned.dnsPromises.set(name, wrapper); mutableDns.promises[name] = wrapper;
|
||||
safeChildEvent(events, { surface: "dns", api: `promises.${name}`, outcome: "REJECTED" }); throw new Error("prohibited production surface: dns");
|
||||
});
|
||||
}
|
||||
const guardedDlopen = (..._args) => { safeChildEvent(events, { surface: "native_addon", api: "dlopen", outcome: "REJECTED" }); throw new Error("prohibited production surface: native addon"); };
|
||||
process.dlopen = guardedDlopen;
|
||||
syncBuiltinESMExports();
|
||||
const network = installNetworkGuard(originalFetch);
|
||||
activeExecutablePolicy = { gitPath, pythonPath, thtPath };
|
||||
let restored = false;
|
||||
const changes = []; let network; let restored = false; let patchStep = 0;
|
||||
const assign = (target, key, value) => { const original = target[key]; target[key] = value; changes.push({ target, key, value, original }); };
|
||||
const checkpoint = () => { patchStep += 1; if (failPatchAt === patchStep) throw new Error("injected production patch failure"); };
|
||||
const rollback = () => {
|
||||
const errors = [];
|
||||
if (network) { try { network.restore(); } catch (error) { errors.push(error); } network = undefined; }
|
||||
for (const { target, key, original } of [...changes].reverse()) { try { target[key] = original; } catch (error) { errors.push(error); } }
|
||||
try { syncBuiltinESMExports(); } catch (error) { errors.push(error); }
|
||||
if (productionSurfaceOwner === token) productionSurfaceOwner = undefined;
|
||||
if (activePolicyRejectionSink === events) activePolicyRejectionSink = undefined;
|
||||
if (activeExecutablePolicy?.owner === token) activeExecutablePolicy = undefined;
|
||||
return errors;
|
||||
};
|
||||
try {
|
||||
productionSurfaceOwner = token; checkpoint();
|
||||
assign(mutableChildProcess, "execFile", guardedExecFile); checkpoint();
|
||||
assign(mutableChildProcess, "spawn", guardedSpawn); checkpoint();
|
||||
for (const [api, wrapper] of childWrappers) assign(mutableChildProcess, api, wrapper); checkpoint();
|
||||
assign(mutableDgram, "createSocket", guardedCreateSocket); checkpoint();
|
||||
assign(mutableWorkerThreads, "Worker", ProhibitedWorker); checkpoint();
|
||||
for (const [name, wrapper] of dnsWrappers) assign(mutableDns, name, wrapper); checkpoint();
|
||||
for (const [name, wrapper] of dnsPromiseWrappers) assign(mutableDns.promises, name, wrapper); checkpoint();
|
||||
assign(process, "dlopen", guardedDlopen); checkpoint();
|
||||
syncBuiltinESMExports(); checkpoint();
|
||||
network = installNetworkGuard(originalFetch); checkpoint();
|
||||
activePolicyRejectionSink = events;
|
||||
activeExecutablePolicy = { gitPath, pythonPath, thtPath, thtIdentity, runRoot, owner: token }; checkpoint();
|
||||
} catch (error) {
|
||||
const rollbackErrors = rollback();
|
||||
if (rollbackErrors.length) throw new Error("production surface guard installation rollback failed", { cause: error });
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
events, externalAttempts: network.externalAttempts,
|
||||
addOwnedOrigin: network.addOwnedOrigin, hasOwnedOrigin: network.hasOwnedOrigin,
|
||||
restore() {
|
||||
if (restored) throw new Error("production surface guard restored twice");
|
||||
restored = true;
|
||||
let tampered = productionSurfaceOwner !== token;
|
||||
const ownsToken = productionSurfaceOwner === token;
|
||||
const restoreOwned = (target, key, wrapper, original) => {
|
||||
if (target[key] !== wrapper) tampered = true;
|
||||
if (ownsToken) target[key] = original;
|
||||
};
|
||||
const errors = [];
|
||||
try { network.restore(); } catch (error) { errors.push(error); }
|
||||
restoreOwned(mutableChildProcess, "execFile", guardedExecFile, originals.execFile);
|
||||
restoreOwned(mutableChildProcess, "spawn", guardedSpawn, originals.spawn);
|
||||
for (const [api, wrapper] of owned.child) restoreOwned(mutableChildProcess, api, wrapper, originals[api]);
|
||||
restoreOwned(mutableDgram, "createSocket", guardedCreateSocket, originals.createSocket);
|
||||
restoreOwned(mutableWorkerThreads, "Worker", ProhibitedWorker, originals.Worker);
|
||||
for (const [name, wrapper] of owned.dns) restoreOwned(mutableDns, name, wrapper, originals.dns.get(name));
|
||||
for (const [name, wrapper] of owned.dnsPromises) restoreOwned(mutableDns.promises, name, wrapper, originals.dnsPromises.get(name));
|
||||
restoreOwned(process, "dlopen", guardedDlopen, originals.dlopen); syncBuiltinESMExports();
|
||||
if (productionSurfaceOwner === token) productionSurfaceOwner = undefined;
|
||||
if (activeExecutablePolicy?.gitPath === gitPath) activeExecutablePolicy = undefined;
|
||||
let tampered = productionSurfaceOwner !== token || activePolicyRejectionSink !== events || activeExecutablePolicy?.owner !== token;
|
||||
for (const { target, key, value } of changes) if (target[key] !== value) tampered = true;
|
||||
const errors = rollback();
|
||||
if (tampered || errors.length) throw new Error("production surface guard ownership restoration failed");
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function runCommand(options) {
|
||||
if (!options || typeof options !== "object" || Array.isArray(options)) throw new Error("command requires an options object");
|
||||
const details = () => ({ executable: options && typeof options === "object" ? options.executable : undefined,
|
||||
argv: options && typeof options === "object" ? options.argv : undefined, api: "runCommand", detail: "policy" });
|
||||
if (!options || typeof options !== "object" || Array.isArray(options)) policyError("command requires an options object", details());
|
||||
const allowed = new Set(["executable", "argv", "cwd", "env", "timeoutMs", "stdin", "maxOutputBytes"]);
|
||||
for (const key of Object.keys(options)) if (!allowed.has(key)) throw new Error(`unsupported command option ${key}`);
|
||||
for (const key of Object.keys(options)) if (!allowed.has(key)) policyError(`unsupported command option ${key}`, details());
|
||||
const { executable, argv, cwd, env, timeoutMs = 30_000, stdin, maxOutputBytes = MAX_OUTPUT } = options;
|
||||
if (typeof executable !== "string" || !isAbsolute(executable) || /[;&|`$><\n\r]/.test(executable)) throw new Error("command executable is invalid");
|
||||
if (typeof executable !== "string" || !isAbsolute(executable) || /[;&|`$><\n\r]/.test(executable)) policyError("command executable is invalid", details());
|
||||
let canonical;
|
||||
try { canonical = realpathSync(executable); } catch { throw new Error("command executable is not allowlisted"); }
|
||||
try { canonical = realpathSync(executable); } catch { policyError("command executable is not allowlisted", details()); }
|
||||
const allowedGit = activeExecutablePolicy?.gitPath ?? resolveTrustedSystemExecutableSync("git");
|
||||
const allowedTht = activeExecutablePolicy?.thtPath;
|
||||
if (canonical !== allowedGit && canonical !== allowedTht) throw new Error("command executable is not allowlisted");
|
||||
if (!Array.isArray(argv) || argv.some((value) => typeof value !== "string")) throw new Error("command argv must be a string array");
|
||||
if (canonical === allowedGit) validateGitInvocation(argv);
|
||||
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 300_000 || !Number.isSafeInteger(maxOutputBytes) || maxOutputBytes < 1 || maxOutputBytes > MAX_OUTPUT) throw new Error("command bounds are invalid");
|
||||
if (canonical !== allowedGit && canonical !== allowedTht) policyError("command executable is not allowlisted", details());
|
||||
if (!Array.isArray(argv) || argv.some((value) => typeof value !== "string")) policyError("command argv must be a string array", details());
|
||||
try {
|
||||
if (canonical === allowedGit) validateGitInvocation(argv, { runRoot: activeExecutablePolicy?.runRoot });
|
||||
if (canonical === allowedTht) validateThtInvocation(argv, { thtPath: allowedTht, runRoot: activeExecutablePolicy.runRoot, cwd });
|
||||
} catch (error) { policyError(error.message, details()); }
|
||||
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 300_000 || !Number.isSafeInteger(maxOutputBytes) || maxOutputBytes < 1 || maxOutputBytes > MAX_OUTPUT) {
|
||||
policyError("command bounds are invalid", details());
|
||||
}
|
||||
return await new Promise((resolvePromise, reject) => {
|
||||
const child = mutableChildProcess.execFile(canonical, argv, { cwd, env, timeout: timeoutMs, maxBuffer: maxOutputBytes, encoding: "utf8", shell: false }, (error, stdout, stderr) => {
|
||||
const code = error && typeof error.code === "number" ? error.code : error ? 1 : 0;
|
||||
const result = { code, stdout: stdout ?? "", stderr: stderr ?? "" };
|
||||
if (commandEventSink) commandEventSink.push({
|
||||
executable: basename(canonical),
|
||||
argvLabels: argv.map((value) => isAbsolute(value) || value.includes(sep) ? "[path]" : /^[a-z]+:\/\//i.test(value) ? "[url]" : value.length > 80 ? "[value]" : value),
|
||||
outcome: error ? "FAIL" : "PASS",
|
||||
executable: basename(canonical), argvLabels: sanitizedArgvLabels(argv), outcome: error ? "FAIL" : "PASS",
|
||||
...(activeCommandCheckId ? { checkId: activeCommandCheckId } : {}),
|
||||
});
|
||||
if (error) Object.assign(error, { result });
|
||||
@@ -618,8 +779,15 @@ export function installNetworkGuard(fetchImplementation = globalThis.fetch) {
|
||||
}
|
||||
return originalConnect.apply(this, args);
|
||||
};
|
||||
globalThis.fetch = guardedFetch;
|
||||
Socket.prototype.connect = guardedSocketConnect;
|
||||
let fetchChanged = false; let socketChanged = false;
|
||||
try {
|
||||
globalThis.fetch = guardedFetch; fetchChanged = true;
|
||||
Socket.prototype.connect = guardedSocketConnect; socketChanged = true;
|
||||
} catch (error) {
|
||||
if (socketChanged) Socket.prototype.connect = originalConnect;
|
||||
if (fetchChanged) globalThis.fetch = originalFetch;
|
||||
throw error;
|
||||
}
|
||||
let restored = false;
|
||||
return {
|
||||
externalAttempts,
|
||||
@@ -893,9 +1061,7 @@ export function buildSafeEnvironment({ ambient = process.env, fixture = {} } = {
|
||||
}
|
||||
|
||||
async function setupContext(run, repositoryRoot, env, ctx = {}) {
|
||||
const executables = await resolveProductionExecutables({
|
||||
repositoryRoot, thtBin: env.THT_BIN ?? join(repositoryRoot, "harness", ".venv", "bin", "tht"),
|
||||
});
|
||||
const executables = await resolveProductionExecutables({ repositoryRoot });
|
||||
const harnessDir = realpathSync(join(repositoryRoot, "harness"));
|
||||
const gitTracePath = join(run.root, "logs", "production-git-trace.jsonl");
|
||||
const ownedHome = join(run.root, "installation", "runtime", "acceptance-home");
|
||||
@@ -907,9 +1073,10 @@ async function setupContext(run, repositoryRoot, env, ctx = {}) {
|
||||
PATH: executablePath, HOME: ownedHome, TMPDIR: ownedTmp,
|
||||
GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null", GIT_TERMINAL_PROMPT: "0",
|
||||
GIT_ASKPASS: "/bin/false", SSH_ASKPASS: "/bin/false", GIT_ALLOW_PROTOCOL: "file", GIT_PROTOCOL_FROM_USER: "0",
|
||||
GIT_CONFIG_COUNT: "3", GIT_CONFIG_KEY_0: "commit.gpgSign", GIT_CONFIG_VALUE_0: "false",
|
||||
GIT_CONFIG_COUNT: "4", GIT_CONFIG_KEY_0: "commit.gpgSign", GIT_CONFIG_VALUE_0: "false",
|
||||
GIT_CONFIG_KEY_1: "tag.gpgSign", GIT_CONFIG_VALUE_1: "false",
|
||||
GIT_CONFIG_KEY_2: "credential.helper", GIT_CONFIG_VALUE_2: "",
|
||||
GIT_CONFIG_KEY_3: "core.fsmonitor", GIT_CONFIG_VALUE_3: "false", GIT_PAGER: "/bin/cat",
|
||||
HOST: "127.0.0.1", PORT: "0", AUTH_MODE: "none", THT_BIN: executables.thtPath,
|
||||
THT_HARNESS_DIR: harnessDir, THT_DATA_ROOT: join(run.root, "installation", "data"),
|
||||
SETTINGS_FILE: join(run.root, "installation", "data", "settings.json"),
|
||||
@@ -1397,7 +1564,10 @@ function productionChecks(ctx) {
|
||||
const gitTraceProof = await assertProductionGitTrace(ctx);
|
||||
assert(present.length === 0 && prohibitedRoutesCalled.length === 0 && prohibitedCommands.length === 0, "prohibited P1 scope operation observed");
|
||||
assert(ctx.networkGuard.externalAttempts.length === 0, "external network connection attempted");
|
||||
const rejectedSurfaces = ctx.networkGuard.events.filter(({ outcome }) => outcome === "REJECTED");
|
||||
const rejectedSurfaces = [
|
||||
...ctx.networkGuard.events,
|
||||
...(commandEventSink ?? []).filter((event) => event.outcome === "REJECTED" && !ctx.networkGuard.events.includes(event)),
|
||||
].filter(({ outcome }) => outcome === "REJECTED");
|
||||
const rejectedChildren = rejectedSurfaces.filter(({ surface }) => surface === "child_process");
|
||||
const childKinds = new Set(ctx.networkGuard.events.filter(({ surface }) => surface === "child_process").map(({ detail }) => detail).filter(Boolean));
|
||||
assert(rejectedSurfaces.length === 0 && rejectedChildren.length === 0 && ["git", "python-lock-holder", "tht"].every((kind) => childKinds.has(kind)),
|
||||
@@ -1589,9 +1759,11 @@ export async function runIntegration({
|
||||
attachResultArtifact(results, "preflight", commandArtifact);
|
||||
if (ctx.networkGuard) {
|
||||
const executablePolicy = {};
|
||||
for (const [name, executablePath] of Object.entries(ctx.executables)) {
|
||||
for (const name of ["gitPath", "pythonPath", "thtPath"]) {
|
||||
const executablePath = ctx.executables[name];
|
||||
executablePolicy[name] = { path: executablePath, sha256: sha256(await readFile(executablePath)) };
|
||||
}
|
||||
executablePolicy.thtIdentity = ctx.executables.thtIdentity;
|
||||
const childArtifact = await evidence(run, "logs/production-child-events.json", {
|
||||
executablePolicy, environmentPolicy: {
|
||||
PATH: ctx.env.PATH, HOME: ctx.env.HOME, TMPDIR: ctx.env.TMPDIR,
|
||||
|
||||
@@ -280,15 +280,13 @@ test("command helper accepts only executable plus separate argv", async () => {
|
||||
await assert.rejects(runCommand({ executable: "git status; rm -rf /", argv: [] }));
|
||||
await assert.rejects(runCommand({ executable: "/tmp/git", argv: ["--version"] }), /command executable is not allowlisted/);
|
||||
await assert.rejects(runCommand({ executable: "tht", argv: ["config", "check"] }), /command executable is invalid/);
|
||||
const repositoryRoot = await fakeRepository();
|
||||
const executable = join(repositoryRoot, "executable with spaces");
|
||||
const scratchRoot = await fakeRepository();
|
||||
const executable = join(scratchRoot, "executable with spaces");
|
||||
await writeFile(executable, "#!/bin/sh\nprintf '%s' \"$1\"\n", { mode: 0o700 });
|
||||
await chmod(executable, 0o700);
|
||||
await assert.rejects(runCommand({ executable, argv: ["literal;not-a-shell"] }), /command executable is not allowlisted/);
|
||||
const tht = join(repositoryRoot, "harness", ".venv", "bin", "tht");
|
||||
await mkdir(dirname(tht), { recursive: true });
|
||||
await writeFile(tht, "#!/bin/sh\nexit 0\n", { mode: 0o700 });
|
||||
const { gitPath } = await resolveProductionExecutables({ repositoryRoot, thtBin: tht, ambientPath: process.env.PATH });
|
||||
const repositoryRoot = await realpath(join(dirname(fileURLToPath(import.meta.url)), "..", ".."));
|
||||
const { gitPath } = await resolveProductionExecutables({ repositoryRoot });
|
||||
const result = await runCommand({ executable: gitPath, argv: ["--version"] });
|
||||
assert.match(result.stdout, /^git version /);
|
||||
assert.equal(result.code, 0);
|
||||
@@ -350,7 +348,7 @@ test("announce callback observes PASS and manual pending before non-keep cleanup
|
||||
|
||||
test("public wrapper replaces ambient environment before invoking the runner", async () => {
|
||||
const wrapper = await readFile(join(dirname(fileURLToPath(import.meta.url)), "..", "..", "scripts", "p1-acceptance.sh"), "utf8");
|
||||
assert.match(wrapper, /safe_env=\(env -i/);
|
||||
assert.match(wrapper, /safe_env=\(\/usr\/bin\/env -i/);
|
||||
assert.match(wrapper, /P1_ACCEPTANCE_FAIL_AT/);
|
||||
assert.doesNotMatch(wrapper, /export THT_BIN/);
|
||||
});
|
||||
@@ -473,14 +471,11 @@ test("runIntegration fails closed when a later duplicate overwrites stale artifa
|
||||
});
|
||||
|
||||
test("production surface guard rejects and records UDP, Worker, git ls-remote, and unexpected python", async () => {
|
||||
const repositoryRoot = await fakeRepository();
|
||||
const thtPath = join(repositoryRoot, "harness", ".venv", "bin", "tht");
|
||||
await mkdir(dirname(thtPath), { recursive: true });
|
||||
await writeFile(thtPath, "#!/bin/sh\nexit 0\n", { mode: 0o700 });
|
||||
await chmod(thtPath, 0o700);
|
||||
const executables = await resolveProductionExecutables({ repositoryRoot, thtBin: thtPath, ambientPath: process.env.PATH });
|
||||
const runRoot = await fakeRepository();
|
||||
const repositoryRoot = await realpath(join(dirname(fileURLToPath(import.meta.url)), "..", ".."));
|
||||
const executables = await resolveProductionExecutables({ repositoryRoot });
|
||||
const guard = installProductionSurfaceGuard({
|
||||
...executables, runRoot: repositoryRoot, environment: { ...process.env }, originalFetch: globalThis.fetch,
|
||||
...executables, runRoot, environment: { ...process.env }, originalFetch: globalThis.fetch,
|
||||
});
|
||||
try {
|
||||
assert.throws(() => dgram.createSocket("udp4"), /prohibited production surface/);
|
||||
@@ -576,16 +571,140 @@ test("environment tampering fails the audit and restores the caller environment"
|
||||
});
|
||||
|
||||
test("production guard detects global tampering and restores without stranding patches", async () => {
|
||||
const repositoryRoot = await fakeRepository();
|
||||
const thtPath = join(repositoryRoot, "harness", ".venv", "bin", "tht");
|
||||
await mkdir(dirname(thtPath), { recursive: true });
|
||||
await writeFile(thtPath, "#!/bin/sh\nexit 0\n", { mode: 0o700 });
|
||||
const executables = await resolveProductionExecutables({ repositoryRoot, thtBin: thtPath });
|
||||
const runRoot = await fakeRepository();
|
||||
const repositoryRoot = await realpath(join(dirname(fileURLToPath(import.meta.url)), "..", ".."));
|
||||
const executables = await resolveProductionExecutables({ repositoryRoot });
|
||||
const originalFetch = globalThis.fetch;
|
||||
const guard = installProductionSurfaceGuard({ ...executables, runRoot: repositoryRoot, environment: { ...process.env }, originalFetch });
|
||||
const guard = installProductionSurfaceGuard({ ...executables, runRoot, environment: { ...process.env }, originalFetch });
|
||||
globalThis.fetch = originalFetch;
|
||||
assert.throws(() => guard.restore(), /ownership restoration failed/);
|
||||
assert.equal(globalThis.fetch, originalFetch);
|
||||
const childProcess = await import("node:child_process");
|
||||
assert.doesNotThrow(() => childProcess.spawn);
|
||||
});
|
||||
|
||||
|
||||
test("Git grammar rejects helper, config, alias, and network-capable spellings with one event each", async () => {
|
||||
const repositoryRoot = await realpath(join(dirname(fileURLToPath(import.meta.url)), "..", ".."));
|
||||
const runRoot = await fakeRepository();
|
||||
const executables = await resolveProductionExecutables({ repositoryRoot, thtBin: "/tmp/hostile-tht" });
|
||||
const guard = installProductionSurfaceGuard({ ...executables, runRoot, environment: { ...process.env } });
|
||||
const source = join(runRoot, "source.git");
|
||||
const destination = join(runRoot, "destination");
|
||||
const marker = join(runRoot, "helper-ran");
|
||||
await execFileAsync(executables.gitPath, ["init", "--bare", source]);
|
||||
const helper = join(runRoot, "upload-helper");
|
||||
await writeFile(helper, `#!/bin/sh\nprintf ran > "${marker}"\nexit 99\n`, { mode: 0o700 });
|
||||
const prohibited = [
|
||||
["clone", `--upload-pack=${helper}`, source, destination],
|
||||
["clone", "--receive-pack=/tmp/helper", source, destination],
|
||||
["--exec-path=/tmp", "status"],
|
||||
["-c", "alias.status=!touch /tmp/pwn", "status"],
|
||||
["-c", "core.hooksPath=/tmp/hooks", "status"],
|
||||
["-c", "diff.external=/tmp/helper", "status"],
|
||||
["config", "filter.bad.clean", "/tmp/helper"],
|
||||
["ls-remote", "https://example.com/repo.git"],
|
||||
];
|
||||
try {
|
||||
for (const argv of prohibited) {
|
||||
const before = guard.events.length;
|
||||
await assert.rejects(runCommand({ executable: executables.gitPath, argv }), /Git command is prohibited/);
|
||||
assert.equal(guard.events.length - before, 1);
|
||||
assert.equal(guard.events.at(-1).outcome, "REJECTED");
|
||||
}
|
||||
await assert.rejects(lstat(marker));
|
||||
} finally { guard.restore(); }
|
||||
});
|
||||
|
||||
test("production executables ignore ambient THT and bind the generated tht entrypoint to reviewed source", async () => {
|
||||
const repositoryRoot = await realpath(join(dirname(fileURLToPath(import.meta.url)), "..", ".."));
|
||||
const hostile = join(await fakeRepository(), "tht");
|
||||
await writeFile(hostile, "#!/bin/sh\nexit 0\n", { mode: 0o700 });
|
||||
const executables = await resolveProductionExecutables({ repositoryRoot, thtBin: hostile });
|
||||
assert.equal(executables.thtPath, join(repositoryRoot, "harness", ".venv", "bin", "tht"));
|
||||
assert.equal(executables.thtIdentity.sourceRoot, join(repositoryRoot, "harness", "tht"));
|
||||
assert.equal(executables.thtIdentity.sourceStatus, "tracked-clean");
|
||||
assert.equal(executables.thtIdentity.entrypoint, "generated-console-script");
|
||||
assert.match(executables.thtIdentity.pythonPath, /python3(?:\.\d+)?$/);
|
||||
});
|
||||
|
||||
test("tht accepts only config check for one owned rendered yaml", async () => {
|
||||
const repositoryRoot = await realpath(join(dirname(fileURLToPath(import.meta.url)), "..", ".."));
|
||||
const runRoot = await fakeRepository();
|
||||
const rendered = join(runRoot, "rendered", "workspace.yaml");
|
||||
await mkdir(dirname(rendered), { recursive: true });
|
||||
await writeFile(rendered, "profile: acceptance\n");
|
||||
const executables = await resolveProductionExecutables({ repositoryRoot });
|
||||
const guard = installProductionSurfaceGuard({ ...executables, runRoot, environment: { ...process.env } });
|
||||
const childProcess = await import("node:child_process");
|
||||
try {
|
||||
for (const argv of [
|
||||
["config", "check"], ["config", "check", "-c", "/tmp/unowned.yaml"],
|
||||
["doctor"], ["config", "check", "-c", rendered, "--extra"],
|
||||
]) {
|
||||
const before = guard.events.length;
|
||||
assert.throws(() => childProcess.execFile(executables.thtPath, argv), /THT command is prohibited/);
|
||||
assert.equal(guard.events.length - before, 1);
|
||||
}
|
||||
} finally { guard.restore(); }
|
||||
});
|
||||
|
||||
test("production guard installation rolls back every patch and owner on every injected patch failure", async () => {
|
||||
const repositoryRoot = await realpath(join(dirname(fileURLToPath(import.meta.url)), "..", ".."));
|
||||
const runRoot = await fakeRepository();
|
||||
const executables = await resolveProductionExecutables({ repositoryRoot });
|
||||
const childProcess = await import("node:child_process");
|
||||
const originalSpawn = childProcess.spawn;
|
||||
const originalDgram = dgram.createSocket;
|
||||
const originalFetch = globalThis.fetch;
|
||||
for (let failPatchAt = 1; failPatchAt <= 12; failPatchAt += 1) {
|
||||
assert.throws(() => installProductionSurfaceGuard({
|
||||
...executables, runRoot, environment: { ...process.env }, failPatchAt,
|
||||
}), /injected production patch failure/);
|
||||
assert.equal(childProcess.spawn, originalSpawn);
|
||||
assert.equal(dgram.createSocket, originalDgram);
|
||||
assert.equal(globalThis.fetch, originalFetch);
|
||||
const reacquired = installProductionSurfaceGuard({ ...executables, runRoot, environment: { ...process.env } });
|
||||
reacquired.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("command bounds reject zero, negative, fractional, and nonnumeric timeouts with one sanitized event", async () => {
|
||||
const repositoryRoot = await realpath(join(dirname(fileURLToPath(import.meta.url)), "..", ".."));
|
||||
const runRoot = await fakeRepository();
|
||||
const executables = await resolveProductionExecutables({ repositoryRoot });
|
||||
const guard = installProductionSurfaceGuard({ ...executables, runRoot, environment: { ...process.env } });
|
||||
try {
|
||||
for (const timeoutMs of [0, -1, 1.5, NaN]) {
|
||||
const before = guard.events.length;
|
||||
await assert.rejects(runCommand({ executable: executables.gitPath, argv: ["--version"], timeoutMs }), /command bounds are invalid/);
|
||||
assert.equal(guard.events.length - before, 1);
|
||||
}
|
||||
} finally { guard.restore(); }
|
||||
});
|
||||
|
||||
test("public wrapper has no ambient command resolution and isolates the build and runner", async () => {
|
||||
const wrapper = await readFile(join(dirname(fileURLToPath(import.meta.url)), "..", "..", "scripts", "p1-acceptance.sh"), "utf8");
|
||||
assert.doesNotMatch(wrapper, /command\s+-v/);
|
||||
assert.doesNotMatch(wrapper, /\b(?:node|npm)\s+--prefix/);
|
||||
assert.match(wrapper, /env -i/);
|
||||
assert.match(wrapper, /npm-cli\.js/);
|
||||
assert.match(wrapper, /"\$node_path" "\$npm_path"/);
|
||||
});
|
||||
|
||||
|
||||
test("hostile PATH Node npm and THT substitutes never execute before a real wrapper integration", async () => {
|
||||
const hostileRoot = await fakeRepository();
|
||||
const marker = join(hostileRoot, "ambient-tool-ran");
|
||||
for (const name of ["node", "npm", "tht"]) {
|
||||
const path = join(hostileRoot, name);
|
||||
await writeFile(path, `#!/bin/sh\nprintf '%s' '${name}' >> '${marker}'\nexit 97\n`, { mode: 0o700 });
|
||||
await chmod(path, 0o700);
|
||||
}
|
||||
const wrapper = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "scripts", "p1-acceptance.sh");
|
||||
const { stdout } = await execFileAsync(wrapper, ["integration"], {
|
||||
env: { ...process.env, PATH: hostileRoot, THT_BIN: join(hostileRoot, "tht") }, timeout: 120_000, maxBuffer: 4 * 1024 * 1024,
|
||||
});
|
||||
assert.match(stdout, /automated integration: PASS/);
|
||||
await assert.rejects(lstat(marker));
|
||||
});
|
||||
|
||||
+57
-10
@@ -1,23 +1,70 @@
|
||||
#!/usr/bin/env bash
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
|
||||
script_path=${BASH_SOURCE[0]}
|
||||
script_dir=${script_path%/*}
|
||||
[[ "$script_dir" != "$script_path" ]] || script_dir=.
|
||||
repo_root="$(cd -P -- "$script_dir/.." && pwd)"
|
||||
if [[ $# -lt 1 || "$1" != "integration" || $# -gt 2 || ( $# -eq 2 && "$2" != "--keep" ) ]]; then
|
||||
printf 'usage: %s integration [--keep]\n' "$0" >&2
|
||||
exit 2
|
||||
fi
|
||||
for command in node npm git python3; do command -v "$command" >/dev/null || { printf 'missing command: %s\n' "$command" >&2; exit 127; }; done
|
||||
node_command="$(command -v node)"
|
||||
node_bin="$(cd "$(dirname "$node_command")" && pwd -P)/$(basename "$node_command")"
|
||||
THT_BIN="${THT_BIN:-$repo_root/harness/.venv/bin/tht}"
|
||||
[[ "$THT_BIN" = /* && -x "$THT_BIN" ]] || { printf 'THT_BIN must be an absolute executable path\n' >&2; exit 127; }
|
||||
npm --prefix "$repo_root/backend" run build
|
||||
safe_env=(env -i "PATH=/usr/bin:/bin" "HOME=/nonexistent" "TMPDIR=/tmp" "LANG=${LANG:-C}" "THT_BIN=$THT_BIN")
|
||||
|
||||
canonical_file() {
|
||||
local path=$1 target parent leaf
|
||||
[[ "$path" = /* ]] || return 1
|
||||
while [[ -L "$path" ]]; do
|
||||
target=$(/usr/bin/readlink "$path") || return 1
|
||||
if [[ "$target" = /* ]]; then path=$target; else path="${path%/*}/$target"; fi
|
||||
done
|
||||
parent=${path%/*}; leaf=${path##*/}
|
||||
parent=$(cd -P -- "$parent" && pwd) || return 1
|
||||
printf '%s/%s\n' "$parent" "$leaf"
|
||||
}
|
||||
|
||||
node_path= npm_path= toolchain_prefix=
|
||||
for pair in \
|
||||
"/usr/bin/node|/usr/bin/npm|/usr" \
|
||||
"/opt/homebrew/bin/node|/opt/homebrew/bin/npm|/opt/homebrew" \
|
||||
"/usr/local/bin/node|/usr/local/bin/npm|/usr/local"; do
|
||||
node_candidate=${pair%%|*}; remainder=${pair#*|}; npm_candidate=${remainder%%|*}; prefix=${remainder##*|}
|
||||
[[ -e "$node_candidate" && -e "$npm_candidate" ]] || continue
|
||||
resolved_node=$(canonical_file "$node_candidate") || continue
|
||||
resolved_npm=$(canonical_file "$npm_candidate") || continue
|
||||
[[ -f "$resolved_node" && ! -L "$resolved_node" && -x "$resolved_node" ]] || continue
|
||||
[[ -f "$resolved_npm" && ! -L "$resolved_npm" ]] || continue
|
||||
[[ "${resolved_npm##*/}" = "npm-cli.js" ]] || continue
|
||||
case "$prefix|$resolved_node|$resolved_npm" in
|
||||
"/usr|/usr/bin/node|/usr/"*"/npm/bin/npm-cli.js"| "/opt/homebrew|/opt/homebrew/Cellar/node/"*"/bin/node|/opt/homebrew/lib/node_modules/npm/bin/npm-cli.js"| "/usr/local|/usr/local/"*"node"*"|/usr/local/lib/node_modules/npm/bin/npm-cli.js") ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
{ IFS= read -r npm_header; IFS= read -r npm_bootstrap; } < "$resolved_npm"
|
||||
[[ "$npm_header" = '#!/usr/bin/env node' && "$npm_bootstrap" = "require('../lib/cli.js')(process)" ]] || continue
|
||||
node_path=$resolved_node; npm_path=$resolved_npm; toolchain_prefix=$prefix
|
||||
break
|
||||
done
|
||||
[[ -n "$node_path" && -n "$npm_path" && -n "$toolchain_prefix" ]] || {
|
||||
printf 'trusted fixed Node/npm toolchain is unavailable\n' >&2
|
||||
exit 127
|
||||
}
|
||||
|
||||
wrapper_root=$(/usr/bin/mktemp -d /tmp/thoth-p1-wrapper.XXXXXXXX)
|
||||
trap '/bin/rm -rf -- "$wrapper_root"' EXIT HUP INT TERM
|
||||
/bin/mkdir -m 700 "$wrapper_root/home" "$wrapper_root/tmp"
|
||||
owned_path="${node_path%/*}:/usr/bin:/bin"
|
||||
build_env=(/usr/bin/env -i "PATH=$owned_path" "HOME=$wrapper_root/home" "TMPDIR=$wrapper_root/tmp" "LANG=${LANG:-C}")
|
||||
for name in LC_ALL TZ; do
|
||||
[[ -n "${!name:-}" ]] && build_env+=("$name=${!name}")
|
||||
done
|
||||
"${build_env[@]}" "$node_path" "$npm_path" --prefix "$repo_root/backend" run build
|
||||
|
||||
safe_env=(/usr/bin/env -i "PATH=$owned_path" "HOME=$wrapper_root/home" "TMPDIR=$wrapper_root/tmp" "LANG=${LANG:-C}"
|
||||
"THT_BIN=$repo_root/harness/.venv/bin/tht" "P1_ACCEPTANCE_NODE_PATH=$node_path" "P1_ACCEPTANCE_NPM_PATH=$npm_path")
|
||||
for name in LC_ALL TZ; do
|
||||
[[ -n "${!name:-}" ]] && safe_env+=("$name=${!name}")
|
||||
done
|
||||
[[ -n "${P1_ACCEPTANCE_FAIL_AT:-}" ]] && safe_env+=("P1_ACCEPTANCE_FAIL_AT=$P1_ACCEPTANCE_FAIL_AT")
|
||||
set +e
|
||||
"${safe_env[@]}" "$node_bin" "$repo_root/backend/scripts/p1-acceptance.mjs" "$@"
|
||||
"${safe_env[@]}" "$node_path" "$repo_root/backend/scripts/p1-acceptance.mjs" "$@"
|
||||
status=$?
|
||||
set -e
|
||||
exit "$status"
|
||||
|
||||
Reference in New Issue
Block a user