fix(pi-workflow): suppress verification bytecode
This commit is contained in:
@@ -402,6 +402,7 @@ Mutating Assignments run in the Workflow Run's worktree.
|
|||||||
- All mutating Assignments are serialized in the first version. Concurrency remains available for read-only Assignments.
|
- All mutating Assignments are serialized in the first version. Concurrency remains available for read-only Assignments.
|
||||||
- Mutating child sessions do not receive `bash` or other process-execution tools. Assignment-scoped file operations reject symlink traversal and require canonical containment beneath both the worktree root and the Assignment's owned path roots.
|
- Mutating child sessions do not receive `bash` or other process-execution tools. Assignment-scoped file operations reject symlink traversal and require canonical containment beneath both the worktree root and the Assignment's owned path roots.
|
||||||
- Plan-approved verification and generation commands run separately through a host executor using fixed executable/argument arrays, never agent-authored shell strings.
|
- Plan-approved verification and generation commands run separately through a host executor using fixed executable/argument arrays, never agent-authored shell strings.
|
||||||
|
- The host executor sets `PYTHONDONTWRITEBYTECODE=1` so Python-backed checks do not create out-of-scope `__pycache__` artifacts.
|
||||||
- The engine records branch, HEAD, worktree identity, and Git diff state before and after every mutating Assignment and host command.
|
- The engine records branch, HEAD, worktree identity, and Git diff state before and after every mutating Assignment and host command.
|
||||||
- Out-of-scope changes, branch movement, commits, or worktree metadata changes invalidate the Assignment and open a Recovery Gate before more mutation starts.
|
- Out-of-scope changes, branch movement, commits, or worktree metadata changes invalidate the Assignment and open a Recovery Gate before more mutation starts.
|
||||||
- The scheduler reserves capacity synchronously before launching children.
|
- The scheduler reserves capacity synchronously before launching children.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { execFile } from "node:child_process";
|
import { execFile } from "node:child_process";
|
||||||
import {
|
import {
|
||||||
|
access,
|
||||||
mkdir,
|
mkdir,
|
||||||
mkdtemp,
|
mkdtemp,
|
||||||
readFile,
|
readFile,
|
||||||
@@ -267,6 +268,16 @@ test("Git probe and approved host commands enforce identity and ownership", asyn
|
|||||||
'require("node:fs").writeFileSync(process.argv[2], "after\\n")',
|
'require("node:fs").writeFileSync(process.argv[2], "after\\n")',
|
||||||
"utf8",
|
"utf8",
|
||||||
);
|
);
|
||||||
|
await writeFile(
|
||||||
|
path.join(directory, "src", "bytecode_module.py"),
|
||||||
|
"VALUE = 1\n",
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
await writeFile(
|
||||||
|
path.join(directory, "src", "check_bytecode.py"),
|
||||||
|
"import bytecode_module\nassert bytecode_module.VALUE == 1\n",
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
await execFileAsync("git", ["-C", directory, "add", "."]);
|
await execFileAsync("git", ["-C", directory, "add", "."]);
|
||||||
await execFileAsync("git", ["-C", directory, "commit", "-qm", "initial"]);
|
await execFileAsync("git", ["-C", directory, "commit", "-qm", "initial"]);
|
||||||
const probe = new GitStateProbe();
|
const probe = new GitStateProbe();
|
||||||
@@ -281,6 +292,17 @@ test("Git probe and approved host commands enforce identity and ownership", asyn
|
|||||||
});
|
});
|
||||||
assert.equal(allowed.result.code, 0);
|
assert.equal(allowed.result.code, 0);
|
||||||
assert.deepEqual(allowed.after.changedPaths, ["src/file.ts"]);
|
assert.deepEqual(allowed.after.changedPaths, ["src/file.ts"]);
|
||||||
|
const bytecodeDisabled = await executor.run({
|
||||||
|
worktreeRoot: directory,
|
||||||
|
ownedPaths: ["src"],
|
||||||
|
executable: "python3",
|
||||||
|
args: ["src/check_bytecode.py"],
|
||||||
|
});
|
||||||
|
assert.equal(bytecodeDisabled.result.code, 0);
|
||||||
|
await assert.rejects(
|
||||||
|
access(path.join(directory, "src", "__pycache__")),
|
||||||
|
/ENOENT/,
|
||||||
|
);
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
executor.run({
|
executor.run({
|
||||||
worktreeRoot: directory,
|
worktreeRoot: directory,
|
||||||
|
|||||||
@@ -23,8 +23,17 @@ export interface CommandResult {
|
|||||||
stderr: string;
|
stderr: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CommandRunOptions {
|
||||||
|
env?: NodeJS.ProcessEnv;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CommandRunner {
|
export interface CommandRunner {
|
||||||
run(cwd: string, executable: string, args: string[]): Promise<CommandResult>;
|
run(
|
||||||
|
cwd: string,
|
||||||
|
executable: string,
|
||||||
|
args: string[],
|
||||||
|
options?: CommandRunOptions,
|
||||||
|
): Promise<CommandResult>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WorkspaceSafetyError extends Error {
|
export class WorkspaceSafetyError extends Error {
|
||||||
@@ -546,6 +555,12 @@ export class ApprovedHostCommandExecutor {
|
|||||||
root,
|
root,
|
||||||
options.executable,
|
options.executable,
|
||||||
[...options.args],
|
[...options.args],
|
||||||
|
{
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
PYTHONDONTWRITEBYTECODE: "1",
|
||||||
|
},
|
||||||
|
},
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
const afterRepository = await this.probe.captureRepository(root);
|
const afterRepository = await this.probe.captureRepository(root);
|
||||||
@@ -790,12 +805,18 @@ export class WorkspaceValidationError extends Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ExecFileCommandRunner implements CommandRunner {
|
export class ExecFileCommandRunner implements CommandRunner {
|
||||||
async run(cwd: string, executable: string, args: string[]) {
|
async run(
|
||||||
|
cwd: string,
|
||||||
|
executable: string,
|
||||||
|
args: string[],
|
||||||
|
options: CommandRunOptions = {},
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const result = await execFileAsync(executable, args, {
|
const result = await execFileAsync(executable, args, {
|
||||||
cwd,
|
cwd,
|
||||||
encoding: "utf8",
|
encoding: "utf8",
|
||||||
maxBuffer: 4 * 1024 * 1024,
|
maxBuffer: 4 * 1024 * 1024,
|
||||||
|
env: options.env,
|
||||||
});
|
});
|
||||||
return { code: 0, stdout: result.stdout, stderr: result.stderr };
|
return { code: 0, stdout: result.stdout, stderr: result.stderr };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user