From 8086247f26ea0e37ac6b6496870cae2e01133800 Mon Sep 17 00:00:00 2001 From: JB Date: Tue, 25 Aug 2026 20:19:53 -0400 Subject: [PATCH] fix(pi-workflow): suppress verification bytecode --- docs/pi-workflow/design.md | 1 + extensions/pi-workflow/workspace.test.ts | 22 +++++++++++++++++++++ extensions/pi-workflow/workspace.ts | 25 ++++++++++++++++++++++-- 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/docs/pi-workflow/design.md b/docs/pi-workflow/design.md index e8a7e07..19ed55c 100644 --- a/docs/pi-workflow/design.md +++ b/docs/pi-workflow/design.md @@ -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. - 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. +- 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. - 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. diff --git a/extensions/pi-workflow/workspace.test.ts b/extensions/pi-workflow/workspace.test.ts index c59586c..226df57 100644 --- a/extensions/pi-workflow/workspace.test.ts +++ b/extensions/pi-workflow/workspace.test.ts @@ -1,6 +1,7 @@ import assert from "node:assert/strict"; import { execFile } from "node:child_process"; import { + access, mkdir, mkdtemp, 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")', "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, "commit", "-qm", "initial"]); 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.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( executor.run({ worktreeRoot: directory, diff --git a/extensions/pi-workflow/workspace.ts b/extensions/pi-workflow/workspace.ts index ae95c0f..468c167 100644 --- a/extensions/pi-workflow/workspace.ts +++ b/extensions/pi-workflow/workspace.ts @@ -23,8 +23,17 @@ export interface CommandResult { stderr: string; } +export interface CommandRunOptions { + env?: NodeJS.ProcessEnv; +} + export interface CommandRunner { - run(cwd: string, executable: string, args: string[]): Promise; + run( + cwd: string, + executable: string, + args: string[], + options?: CommandRunOptions, + ): Promise; } export class WorkspaceSafetyError extends Error { @@ -546,6 +555,12 @@ export class ApprovedHostCommandExecutor { root, options.executable, [...options.args], + { + env: { + ...process.env, + PYTHONDONTWRITEBYTECODE: "1", + }, + }, ); try { const afterRepository = await this.probe.captureRepository(root); @@ -790,12 +805,18 @@ export class WorkspaceValidationError extends Error { } export class ExecFileCommandRunner implements CommandRunner { - async run(cwd: string, executable: string, args: string[]) { + async run( + cwd: string, + executable: string, + args: string[], + options: CommandRunOptions = {}, + ) { try { const result = await execFileAsync(executable, args, { cwd, encoding: "utf8", maxBuffer: 4 * 1024 * 1024, + env: options.env, }); return { code: 0, stdout: result.stdout, stderr: result.stderr }; } catch (error) {