fix(pi-workflow): allow offline verification command

This commit is contained in:
jb
2026-08-25 20:27:54 -04:00
parent 8086247f26
commit 3c5168aa3c
3 changed files with 85 additions and 15 deletions
+61
View File
@@ -278,6 +278,11 @@ test("Git probe and approved host commands enforce identity and ownership", asyn
"import bytecode_module\nassert bytecode_module.VALUE == 1\n",
"utf8",
);
await writeFile(
path.join(directory, "check-offline.cjs"),
'process.exit(process.env.PI_OFFLINE === "1" ? 0 : 1)',
"utf8",
);
await execFileAsync("git", ["-C", directory, "add", "."]);
await execFileAsync("git", ["-C", directory, "commit", "-qm", "initial"]);
const probe = new GitStateProbe();
@@ -303,6 +308,62 @@ test("Git probe and approved host commands enforce identity and ownership", asyn
access(path.join(directory, "src", "__pycache__")),
/ENOENT/,
);
const offline = await executor.run({
worktreeRoot: directory,
ownedPaths: ["src"],
executable: "env",
args: ["PI_OFFLINE=1", process.execPath, "check-offline.cjs"],
});
assert.equal(offline.result.code, 0);
await assert.rejects(
executor.run({
worktreeRoot: directory,
ownedPaths: ["src"],
executable: "env",
args: ["OTHER=value", process.execPath, "check-offline.cjs"],
}),
/cannot invoke a command wrapper or shell/,
);
const blockedOfflineCommands: Array<{
args: string[];
expected: RegExp;
}> = [
{
args: ["PI_OFFLINE=1", "OTHER=value", process.execPath],
expected: /cannot invoke a command wrapper or shell/,
},
{
args: ["PI_OFFLINE=1", "sh", "-c", "touch src/bypass"],
expected: /cannot invoke a command wrapper or shell/,
},
{
args: ["PI_OFFLINE=1", "git", "clean", "-fd"],
expected: /Git host commands are not allowed/,
},
{
args: ["PI_OFFLINE=1", process.execPath, "-e", "process.exit(0)"],
expected: /cannot execute inline interpreter code/,
},
{
args: ["PI_OFFLINE=1", "env", "PI_OFFLINE=1", process.execPath],
expected: /cannot invoke a command wrapper or shell/,
},
{
args: ["PI_OFFLINE=1", "npm", "exec", "git"],
expected: /cannot invoke package-manager executable wrappers/,
},
];
for (const blocked of blockedOfflineCommands) {
await assert.rejects(
executor.run({
worktreeRoot: directory,
ownedPaths: ["src"],
executable: "env",
args: blocked.args,
}),
blocked.expected,
);
}
await assert.rejects(
executor.run({
worktreeRoot: directory,
+23 -14
View File
@@ -489,6 +489,7 @@ const SHELL_EXECUTABLES = new Set([
]);
const COMMAND_WRAPPERS = new Set(["env", "find", "npx", "xargs"]);
const ENVIRONMENT_ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
const INLINE_EVALUATION_FLAGS = new Map<string, Set<string>>([
["node", new Set(["-e", "--eval", "-p", "--print"])],
["python", new Set(["-c"])],
@@ -524,7 +525,23 @@ export class ApprovedHostCommandExecutor {
if (!options.executable || options.args.some((argument) => argument.includes("\0"))) {
throw new Error("Host commands require a fixed executable and argument array");
}
const executableName = path.basename(options.executable).toLowerCase();
let executable = options.executable;
let args = [...options.args];
const commandEnv: NodeJS.ProcessEnv = {
...process.env,
PYTHONDONTWRITEBYTECODE: "1",
};
if (
path.basename(executable).toLowerCase() === "env" &&
args[0] === "PI_OFFLINE=1" &&
args[1] &&
!ENVIRONMENT_ASSIGNMENT.test(args[1])
) {
commandEnv.PI_OFFLINE = "1";
executable = args[1];
args = args.slice(2);
}
const executableName = path.basename(executable).toLowerCase();
if (
SHELL_EXECUTABLES.has(executableName) ||
COMMAND_WRAPPERS.has(executableName)
@@ -532,11 +549,11 @@ export class ApprovedHostCommandExecutor {
throw new Error("Host commands cannot invoke a command wrapper or shell");
}
const evaluationFlags = INLINE_EVALUATION_FLAGS.get(executableName);
if (evaluationFlags && options.args.some((argument) => evaluationFlags.has(argument))) {
if (evaluationFlags && args.some((argument) => evaluationFlags.has(argument))) {
throw new Error("Host commands cannot execute inline interpreter code");
}
const packageExecCommands = PACKAGE_EXEC_SUBCOMMANDS.get(executableName);
if (packageExecCommands?.has(options.args[0] ?? "")) {
if (packageExecCommands?.has(args[0] ?? "")) {
throw new Error("Host commands cannot invoke package-manager executable wrappers");
}
if (executableName === "git") {
@@ -551,17 +568,9 @@ export class ApprovedHostCommandExecutor {
: undefined;
const beforeRepository = await this.probe.captureRepository(root);
const before = beforeRepository.get(root) ?? (await this.probe.capture(root));
const result = await this.runner.run(
root,
options.executable,
[...options.args],
{
env: {
...process.env,
PYTHONDONTWRITEBYTECODE: "1",
},
},
);
const result = await this.runner.run(root, executable, args, {
env: commandEnv,
});
try {
const afterRepository = await this.probe.captureRepository(root);
const after = afterRepository.get(root) ?? (await this.probe.capture(root));