Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
139 commits
Select commit Hold shift + click to select a range
68236c5
test(actions): bind runner evidence to current run attempt
seonghobae Aug 24, 2026
22db494
fix(actions): fail closed on unscoped rerun evidence
seonghobae Aug 24, 2026
14e914b
test(actions): cover rerun fail-closed boundary
seonghobae Aug 24, 2026
9d5ce57
test(actions): reject mismatched workflow run identity
seonghobae Aug 24, 2026
7233e25
fix(actions): bind fetched workflow run to selection
seonghobae Aug 24, 2026
34cbf4c
test(actions): require explicit workflow run attempt
seonghobae Aug 24, 2026
1c33680
fix(actions): require explicit workflow run attempt
seonghobae Aug 24, 2026
6313023
test(actions): preserve workflow run attempt fixtures
seonghobae Aug 24, 2026
628a9b1
test(actions): scope job reads to exact workflow attempt
seonghobae Aug 24, 2026
c0518e2
fix(actions): read exact workflow-attempt jobs
seonghobae Aug 24, 2026
1e359f4
test(actions): use exact-attempt evidence fixture
seonghobae Aug 24, 2026
dea0759
test(actions): bind delegated-token fixture to run attempt
seonghobae Aug 24, 2026
b625aaf
docs(actions): describe exact-attempt job authority
seonghobae Aug 24, 2026
1ada155
test(actions): cover invalid attempt adapter authority
seonghobae Aug 24, 2026
544ea8a
test(actions): retain workflow attempt in assignment evidence
seonghobae Aug 24, 2026
e0895b4
fix(actions): retain workflow attempt in assignment evidence
seonghobae Aug 24, 2026
0820a44
test(actions): reject symlinked delegated-token parents
seonghobae Aug 24, 2026
6ff79c7
fix(actions): reject symlinked delegated-token parents
seonghobae Aug 24, 2026
f006dd2
refactor(actions): keep parent-symlink guard coverage-reachable
seonghobae Aug 24, 2026
ff76f97
test(actions): cover unverifiable delegated-token parents
seonghobae Aug 24, 2026
2dd3dc5
test(actions): align capability failures with parent hardening
seonghobae Aug 24, 2026
1ff8cd7
test(actions): reject delegated token path replacement during read
seonghobae Aug 24, 2026
5e74c63
fix(actions): bind delegated token bytes to capability pathname
seonghobae Aug 24, 2026
c79cd97
test(actions): reject non-canonical token capability path
seonghobae Aug 24, 2026
502ebaf
fix(actions): preserve exact token capability path authority
seonghobae Aug 24, 2026
972c0b1
test(actions): require attempt identity in evaluator
seonghobae Aug 24, 2026
9f93892
fix(actions): fail closed on invalid attempt evidence
seonghobae Aug 24, 2026
2c8c887
test(docs): require attempt-scoped runner audit contract
seonghobae Aug 24, 2026
da8b548
docs(actions): align runner audit with attempt-scoped authority
seonghobae Aug 24, 2026
e6a4edb
test(actions): bind runner fixtures to current attempt
seonghobae Aug 24, 2026
ff57ac6
test(actions): bind timestamp fixture to current attempt
seonghobae Aug 24, 2026
8673cce
test(actions): reject coerced token capability paths
seonghobae Aug 24, 2026
11cd8fc
fix(actions): reject coerced token capability paths
seonghobae Aug 24, 2026
ed9b381
fix(actions): preserve absent token-path contract
seonghobae Aug 24, 2026
94fb0ec
test(actions): reject hard-linked delegated token capability
seonghobae Aug 24, 2026
bd9e5fb
fix(actions): reject hard-linked delegated token capability
seonghobae Aug 24, 2026
150ae06
fix(actions): preserve replacement-race failure classification
seonghobae Aug 24, 2026
6e51424
test(actions): accept attempt-scoped adapters independent of function…
seonghobae Aug 24, 2026
4d6ab68
fix(actions): stop treating function arity as attempt authority
seonghobae Aug 24, 2026
4fdf778
test(actions): retire function-arity reader authority
seonghobae Aug 24, 2026
9b134b7
test(actions): reject whitespace-bearing delegated tokens
seonghobae Aug 24, 2026
68e8a0b
fix(actions): require canonical delegated bearer bytes
seonghobae Aug 24, 2026
143241b
test(actions): reject normalized capability paths at CLI boundary
seonghobae Aug 24, 2026
4e57a16
fix(actions): preserve canonical capability path authority
seonghobae Aug 24, 2026
4af65f5
test(workflow-registry): preserve capability path authority
seonghobae Aug 25, 2026
f94eb8e
fix(workflow-registry): preserve capability path authority
seonghobae Aug 25, 2026
54a776a
fix(workflow-registry): keep disablement capability path exact
seonghobae Aug 25, 2026
be6bf07
fix(governance): preserve maintainer capability path bytes
seonghobae Aug 25, 2026
2406acf
test(maintainer-app): require exact capability path authority
seonghobae Aug 25, 2026
125c1b0
fix(maintainer-app): preserve delegated capability path bytes
seonghobae Aug 25, 2026
4e534e5
test(production-env): require exact capability path authority
seonghobae Aug 25, 2026
cd19e6e
fix(production-env): preserve delegated capability path bytes
seonghobae Aug 25, 2026
6379362
test(governance): recognize shared capability-path ingress
seonghobae Aug 25, 2026
2e86fcc
test(governance): prove canonical delegated capability path
seonghobae Aug 25, 2026
ad016b4
fix(governance): adopt canonical delegated token authority
seonghobae Aug 25, 2026
a562d50
test(governance): preserve canonical token capability invariants
seonghobae Aug 25, 2026
699d45e
test(governance): classify delegated token path failures precisely
seonghobae Aug 25, 2026
86f6857
test(governance): reject aliased delegated token paths
seonghobae Aug 25, 2026
893bac3
fix(governance): require canonical delegated token pathname
seonghobae Aug 25, 2026
5173b8e
test(actions): bind runner audit to raw path and exact attempt
seonghobae Aug 25, 2026
fb2a987
fix(actions): bind runner evidence to exact attempt
seonghobae Aug 25, 2026
9376533
test(actions): restore exact-attempt runner regressions
seonghobae Aug 25, 2026
109dd98
test(actions): reject post-open report parent symlink
seonghobae Aug 25, 2026
73f7bde
fix(actions): revalidate report parent before write
seonghobae Aug 25, 2026
6966b82
test(acquisition): reject post-open parent substitution
seonghobae Aug 25, 2026
47b2397
fix(acquisition): revalidate output parents around leaf opens
seonghobae Aug 25, 2026
e9abf22
test(actions): reject mismatched job attempt evidence
seonghobae Aug 25, 2026
94e451f
test(actions): bind job evidence to exact run attempt
seonghobae Aug 25, 2026
158a73d
fix(actions): bind job evidence to exact run attempt
seonghobae Aug 25, 2026
4f50992
fix(actions): reject cross-attempt runner identity
seonghobae Aug 25, 2026
e83201e
test(actions): carry current attempt through runner fixtures
seonghobae Aug 25, 2026
ee30ab4
test(actions): retain attempt identity in timestamp fixtures
seonghobae Aug 25, 2026
beb3f1b
test(actions): bind report fixture to current attempt
seonghobae Aug 25, 2026
1bf92eb
test(actions): bind evidence fixture to current attempt
seonghobae Aug 25, 2026
aede781
test(actions): bind capability shim to current attempt
seonghobae Aug 25, 2026
a28920e
test(actions): bind CLI fixtures to current attempt
seonghobae Aug 25, 2026
087c135
test(actions): isolate current-attempt stall evidence
seonghobae Aug 25, 2026
93552b4
docs(actions): bind runner evidence to job attempt identity
seonghobae Aug 25, 2026
578d9a5
test(docs): bind runner doctoring to job attempt identity
seonghobae Aug 25, 2026
ab4e950
docs(actions): retain byte-canonical capability authority
seonghobae Aug 25, 2026
e7db983
test(runner): preserve explicit undefined attempt evidence
seonghobae Aug 25, 2026
7d7b31d
test(acquisition): cover unauthenticated output cleanup
seonghobae Aug 25, 2026
9613c5b
test(operations): reject staged runner report replacement
seonghobae Aug 25, 2026
3c1ac67
fix(operations): bind runner report staging identity
seonghobae Aug 25, 2026
d03ce0c
test(operations): exercise hardened runner report writer
seonghobae Aug 25, 2026
72c606f
test(operations): exercise canonical private report output
seonghobae Aug 25, 2026
9b8d317
test(acquisition): preserve existing report metadata on failed replac…
seonghobae Aug 25, 2026
36c588b
fix(acquisition): keep prior report metadata immutable until replacement
seonghobae Aug 25, 2026
aaad0e6
test(acquisition): replace safe read-only retained evidence atomically
seonghobae Aug 25, 2026
01d650c
fix(acquisition): verify existing evidence read-only before replacement
seonghobae Aug 25, 2026
fc559a9
test(acquisition): model read-only verification of existing evidence
seonghobae Aug 25, 2026
8acb666
test(acquisition): reject same-inode replacement races
seonghobae Aug 25, 2026
1715736
fix(acquisition): reject concurrent retained-evidence mutation
seonghobae Aug 25, 2026
e06351a
test(acquisition): model read-only no-follow support
seonghobae Aug 25, 2026
29965d0
test(acquisition): include read-only filesystem capability
seonghobae Aug 25, 2026
1aa2117
test(runner): include read-only no-follow capability
seonghobae Aug 25, 2026
1c22751
test(acquisition): reject staged same-inode version drift
seonghobae Aug 25, 2026
0aa6927
fix(acquisition): bind staged evidence version before rename
seonghobae Aug 25, 2026
6d5b4f7
test(acquisition): reject new-output version drift
seonghobae Aug 25, 2026
0b77efa
fix(acquisition): bind new evidence pathname version
seonghobae Aug 25, 2026
8cae8b6
test(actions): reject control-only runner identity evidence
seonghobae Aug 25, 2026
ba18c81
fix(actions): reject control-only runner identity evidence
seonghobae Aug 25, 2026
64f36e1
test(actions): reject format-only runner identity
seonghobae Aug 26, 2026
c876141
fix(actions): ignore format-only runner identity
seonghobae Aug 26, 2026
23d8cb7
test(actions): reject normalized runner identity authority
seonghobae Aug 26, 2026
933b8be
fix(actions): reject non-canonical runner name authority
seonghobae Aug 26, 2026
350bdaf
test(reviewer): require non-OpenSSL sandbox substrate
seonghobae Aug 26, 2026
b205ba4
fix(reviewer): use non-OpenSSL sandbox substrate
seonghobae Aug 26, 2026
46d8401
fix(reviewer): scan non-OpenSSL sandbox image
seonghobae Aug 26, 2026
f1360cc
fix(reviewer): converge sandbox substrate contracts
seonghobae Aug 26, 2026
4a492fc
test(reviewer): preserve hard vulnerability filtering
seonghobae Aug 26, 2026
373e853
test(reviewer): require Trivy fail-closed exit
seonghobae Aug 26, 2026
3b774f2
test(acquisition): reject post-rename evidence mutation
seonghobae Aug 26, 2026
bd6b7d1
fix(acquisition): bind final replacement version
seonghobae Aug 26, 2026
07b3769
fix(acquisition): allow rename ctime while binding final evidence
seonghobae Aug 26, 2026
ca19dd3
test(acquisition): reject dot-segment output aliases
seonghobae Aug 27, 2026
32d8ea4
fix(acquisition): reject lexical output path aliases
seonghobae Aug 27, 2026
c0803c0
test(auth): reject BOM-normalized delegated token
seonghobae Aug 28, 2026
1fc9d5d
fix(auth): preserve BOM bytes before bearer validation
seonghobae Aug 28, 2026
3b64a07
test(governance): reject BOM-normalized live audit JSON
seonghobae Aug 28, 2026
ae3f26f
fix(governance): preserve live audit JSON BOM bytes
seonghobae Aug 28, 2026
266b63d
fix(governance): stop normalizing CLI JSON whitespace
seonghobae Aug 28, 2026
b243971
test(governance): classify BOM as invalid CLI JSON
seonghobae Aug 28, 2026
c4631de
fix(governance): classify malformed CLI JSON consistently
seonghobae Aug 28, 2026
6a2c5aa
Merge remote-tracking branch 'origin/main' into work-pr503
claude Aug 30, 2026
ecfeb11
test(acquisition): clean unaccepted output after close failure
seonghobae Aug 30, 2026
1438dc0
fix(acquisition): clean failed new-file close paths
seonghobae Aug 30, 2026
00c433f
test(governance): reproduce runner rerun evidence race
seonghobae Aug 30, 2026
2217922
fix(governance): revalidate workflow run after job collection
seonghobae Aug 30, 2026
f85c798
fix(acquisition): preserve close-error precedence outside finally
seonghobae Aug 30, 2026
a86215f
test(reviewer): bind Trivy hardening flags to scan run block
seonghobae Aug 30, 2026
6bdc9b6
test(acquisition): reject concurrent stale evidence replacement
seonghobae Aug 30, 2026
fd2a4ff
fix(acquisition): serialize same-target evidence writers
seonghobae Aug 30, 2026
fadca60
test(governance): pin invalid-JSON diagnostic prefix
seonghobae Aug 30, 2026
2c07186
test(acquisition): reproduce writer-lock acquisition poisoning
seonghobae Aug 30, 2026
a565add
fix(acquisition): recover failed writer-lock acquisition
seonghobae Aug 30, 2026
e2ab96b
test(acquisition): cover replacement lock identity refusal
seonghobae Aug 30, 2026
e9897ff
test(acquisition): cover vanished lock recovery branch
seonghobae Aug 30, 2026
e49d37e
Merge main (#500 OpenSSL 3.5.8 patch-validator fix) into fix/workflow…
claude Aug 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 19 additions & 17 deletions docs/doctoring/actions-runner-assignment-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,25 @@ This control is deliberately narrower than CI or merge readiness. It does not cr

## Evidence model

The operator supplies an explicit bounded set of GitHub Actions workflow-run IDs plus the exact expected pull-request source-head SHA. The collector retrieves each selected workflow run and **all** job pages for that run, then the evaluator classifies runner assignment separately from the later workflow conclusion.
The operator supplies an explicit bounded set of GitHub Actions workflow-run IDs plus the exact expected pull-request source-head SHA. For each selected run, the collector first validates the run's exact positive `run_attempt`, then retrieves **all** job pages only from `actions/runs/{run_id}/attempts/{run_attempt}/jobs?per_page=100`. Every retained workflow job must independently carry that same exact positive `run_attempt`. The evaluator then classifies runner assignment separately from the later workflow conclusion.

Required invariants are:

1. the repository is exactly `ContextualWisdomLab/noema`;
2. the production CLI reads its short-lived GitHub transport authority from an owner-only delegated token capability file named by `NOEMA_MAINTAINER_TOKEN_PATH`; the bearer value is never read from ambient `GH_TOKEN` and is never retained in the report;
3. the expected source head is one canonical lowercase 40-character commit SHA;
4. one to twenty unique positive run IDs are selected explicitly;
5. selected runs must be `pull_request` runs bound to that exact source head;
6. job pages are fully paginated with `per_page=100` and `filter=all`, bounded to at most 2,000 retained jobs;
7. runner assignment is observed only from a positive `runner_id` or a non-empty `runner_name`; `started_at` is not runner-assignment authority because GitHub may populate it while a job is still queued without runner identity;
8. a `waiting`, `pending`, or `requested` job remains non-passing `PENDING` because those states do not by themselves isolate runner allocation;
9. a queued job in a run where another job has already received a runner remains non-passing `PENDING`, because the queued job may be waiting on an explicit `jobs.<job_id>.needs` dependency rather than runner capacity;
10. the bounded grace may produce `runner_assignment_stalled` only when the workflow run itself remains `queued`, the job remains `queued`, and no job in that selected run has assignment evidence;
11. an assigned job may produce runner-assignment `PASS` even if its later workflow/test conclusion is `failure`, because those are separate evidence classes.
5. selected runs must be `pull_request` runs bound to that exact source head and must carry a positive safe-integer `run_attempt`;
6. job pages are fully paginated from `actions/runs/{run_id}/attempts/{run_attempt}/jobs?per_page=100`, bounded to at most 2,000 retained jobs, and every retained workflow job must carry the same exact positive `run_attempt` as its selected run; the run-wide `filter=all` endpoint can include predecessor attempts and is therefore non-authoritative for current-attempt runner identity;
7. runner assignment is observed only from a positive `runner_id` or a non-empty `runner_name` on a job whose `run_attempt` matches its selected run; `started_at` is not runner-assignment authority because GitHub may populate it while a job is still queued without runner identity;
8. a `waiting`, `pending`, or `requested` current-attempt job remains non-passing `PENDING` because those states do not by themselves isolate runner allocation;
9. a queued current-attempt job in a run where another **current-attempt** job has already received a runner remains non-passing `PENDING`, because the queued job may be waiting on an explicit `jobs.<job_id>.needs` dependency rather than runner capacity; predecessor-attempt assignment must not suppress current-attempt stall classification;
10. the bounded grace may produce `runner_assignment_stalled` only when the workflow run itself remains `queued`, the current-attempt job remains `queued`, and no job in that selected **current attempt** has assignment evidence;
11. an assigned current-attempt job may produce runner-assignment `PASS` even if its later workflow/test conclusion is `failure`, because those are separate evidence classes.

The default runner-allocation grace is five minutes and may be bounded by `NOEMA_ACTIONS_AUDIT_QUEUE_GRACE_MILLISECONDS`; the evaluator rejects values above thirty minutes rather than allowing a true isolated queue condition to remain indefinitely pending.

This classifier is intentionally conservative because the GitHub workflow-job REST representation does not expose a durable repository-consumable timestamp meaning “this job became eligible for runner allocation.” A workflow run's `created_at` is therefore not a trustworthy age for every downstream job, and a job's `started_at` is not runner-assignment authority. The evaluator uses run age only after the selected evidence isolates the top-level queued runner-allocation boundary described above.
This classifier is intentionally conservative because the GitHub workflow-job REST representation does not expose a durable repository-consumable timestamp meaning “this job became eligible for runner allocation.” A workflow run's `created_at` is therefore not a trustworthy age for every downstream job, and a job's `started_at` is not runner-assignment authority. The evaluator uses run age only after the selected current-attempt evidence isolates the top-level queued runner-allocation boundary described above.

### Pre-run waits are not runner stalls

Expand All @@ -36,7 +36,7 @@ Those states remain operationally non-passing, but they are not evidence that Gi

## Operator contract

The production command consumes an owner-only delegated token capability file. The path itself is non-secret; the token bytes must be materialized by an authorized caller and removed promptly after the audit.
The production command consumes an owner-only delegated token capability file. The path itself is non-secret; the token bytes must be materialized by an authorized caller and removed promptly after the audit. The configured capability pathname is **byte-canonical** authority: it must already be the exact absolute lexical path accepted by the hardened reader, without trimming, coercion, relative-path resolution, or dot-segment aliasing.

Example:

Expand Down Expand Up @@ -70,17 +70,17 @@ artifacts/operations/actions-runner-assignment-audit.json

The report records repository, expected head, selected run IDs, observation time, queue grace, deterministic checks/failures, and explicit false authority flags for required-check success, review, merge, release, and deployment. Temporary report bytes are created owner-only and atomically renamed onto the fixed report path.

`PASS` exits zero. `PENDING` and `FAIL` both exit nonzero. A malformed source identity, cross-repository request, missing/unsafe capability file, malformed API JSON, GitHub CLI failure, pagination-shape failure, excessive evidence, or head mismatch fails closed.
`PASS` exits zero. `PENDING` and `FAIL` both exit nonzero. A malformed source identity, cross-repository request, missing/unsafe capability file, malformed API JSON, GitHub CLI failure, pagination-shape failure, excessive evidence, run-attempt mismatch, or head mismatch fails closed.

## RCA interpretation

A `runner_assignment_stalled` result supports the narrow hypothesis **“the selected current-head workflow run and job remained at an isolated queued boundary without observable runner assignment beyond the configured grace interval.”** It does not by itself identify why. Possible causes remain materially distinct and require separate evidence, including GitHub-hosted runner capacity, repository/organization Actions policy, runner-group restrictions, billing/spending controls, concurrency saturation, enterprise policy, or a GitHub service incident.
A `runner_assignment_stalled` result supports the narrow hypothesis **“the selected current-head workflow run and current-attempt job remained at an isolated queued boundary without observable current-attempt runner assignment beyond the configured grace interval.”** It does not by itself identify why. Possible causes remain materially distinct and require separate evidence, including GitHub-hosted runner capacity, repository/organization Actions policy, runner-group restrictions, billing/spending controls, concurrency saturation, enterprise policy, or a GitHub service incident.

A `PENDING` result for environment protection, `needs` dependency waiting, or other pre-run uncertainty means only that runner allocation has **not been isolated as the failing boundary**. It is not a health PASS and cannot satisfy a required Check.

Conversely, an observed runner assignment falsifies the hypothesis that the specific selected job is still blocked at runner allocation. A later failing step must be investigated at that later boundary rather than described as a runner-assignment incident.
Conversely, an observed current-attempt runner assignment falsifies the hypothesis that the specific selected job is still blocked at runner allocation. A later failing step must be investigated at that later boundary rather than described as a runner-assignment incident. Assignment from a predecessor attempt is not evidence about the current attempt.

This separation matters for issue #30 because historical Noema runs exhibited queued jobs without logs, while later runs demonstrably received GitHub-hosted runners. Repository evidence therefore needs to preserve **assignment state** independently from **job conclusion**, **dependency/protection waiting**, and any organization-level causal claim.
This separation matters for issue #30 because historical Noema runs exhibited queued jobs without logs, while later runs demonstrably received GitHub-hosted runners. Repository evidence therefore needs to preserve **attempt identity and assignment state** independently from **job conclusion**, **dependency/protection waiting**, and any organization-level causal claim.

## Security and privacy

Expand All @@ -94,12 +94,14 @@ The audit is diagnostic evidence. A passing assignment audit cannot satisfy bran

The repository-owned slice is acceptable when:

- realistic tests reproduce an isolated runner stall, a fresh queue, deployment/environment waiting, downstream dependency waiting, assigned-but-failed jobs, head mismatch, malformed evidence, pagination, and bounded selection;
- queued `started_at` timestamps without runner identity remain unassigned, while a positive `runner_id` or a non-empty `runner_name` is assignment evidence;
- realistic tests reproduce an isolated current-attempt runner stall, a fresh queue, deployment/environment waiting, downstream dependency waiting, assigned-but-failed jobs, predecessor-attempt contamination, head mismatch, malformed evidence, pagination, and bounded selection;
- every selected run and retained workflow job carries the same exact positive `run_attempt`, and predecessor-attempt jobs cannot contribute runner identity or suppress current-attempt stall classification;
- queued `started_at` timestamps without runner identity remain unassigned, while a positive `runner_id` or a non-empty `runner_name` on a matching current-attempt job is assignment evidence;
- the production entrypoint succeeds with an owner-only delegated token capability file and fails closed when only ambient `GH_TOKEN` is present;
- environment-protected and dependency-blocked jobs remain nonzero `PENDING` and are not mislabeled as runner-allocation stalls;
- the pure evaluator and bounded source collector are GREEN;
- the operator adapter performs only the two documented read families and fully paginates jobs;
- the operator adapter performs only the two documented read families, binds job reads to the exact `run_attempt`, and fully paginates jobs;
- the run-wide `filter=all` endpoint is treated as non-authoritative because it can include predecessor attempts;
- the `gh` subprocess inherits only the minimal read-authority environment documented above;
- `PENDING` remains nonzero;
- report output is credential-free and authority-separated;
Expand Down
34 changes: 32 additions & 2 deletions reviewer/tests/test_codegraph_sandbox_image_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,30 @@
CENTRAL_WORKFLOW = REPOSITORY_ROOT / ".github" / "workflows" / "central-review.yml"


def _yaml_run_blocks(workflow: str) -> list[str]:
"""Return literal YAML run-block bodies without matching neighboring steps."""
lines = workflow.splitlines()
blocks: list[str] = []
index = 0
while index < len(lines):
line = lines[index]
if line.lstrip() != "run: |":
index += 1
continue
run_indent = len(line) - len(line.lstrip())
body: list[str] = []
index += 1
while index < len(lines):
candidate = lines[index]
candidate_indent = len(candidate) - len(candidate.lstrip())
if candidate.strip() and candidate_indent <= run_indent:
break
body.append(candidate)
index += 1
blocks.append("\n".join(body))
return blocks


def test_codegraph_sandbox_uses_scanned_non_openssl_runtime_substrate() -> None:
"""Reviewer workflows must scan java-base and invoke the bundled Node explicitly."""
expected_source = "gcr.io/distroless/java-base-debian13:nonroot"
Expand All @@ -20,8 +44,14 @@ def test_codegraph_sandbox_uses_scanned_non_openssl_runtime_substrate() -> None:
assert f"NOEMA_CODEGRAPH_SANDBOX_SOURCE_IMAGE: {expected_source}" in workflow
assert f"{expected_repository}@sha256:" in workflow
assert "gcr.io/distroless/nodejs24-debian13" not in workflow
assert "trivy image" in workflow
assert "--severity MEDIUM,HIGH,CRITICAL" in workflow
trivy_run_blocks = [
block for block in _yaml_run_blocks(workflow) if "trivy image" in block
]
assert len(trivy_run_blocks) == 1
trivy_run_block = trivy_run_blocks[0]
assert "--exit-code 1" in trivy_run_block
assert "--ignore-unfixed" in trivy_run_block
assert "--severity MEDIUM,HIGH,CRITICAL" in trivy_run_block

assert sandbox.TRUSTED_CODEGRAPH_IMAGE_REPOSITORY == expected_repository
source = (REPOSITORY_ROOT / "reviewer" / "noema_reviewer" / "sandbox.py").read_text(
Expand Down
74 changes: 38 additions & 36 deletions scripts/actions-runner-assignment-audit.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

import {
closeSync,
constants,
fchmodSync,
fstatSync,
ftruncateSync,
lstatSync,
mkdirSync,
openSync,
Expand All @@ -11,7 +15,6 @@ import {
} from "node:fs";
import { dirname, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { randomUUID } from "node:crypto";
import { spawnSync } from "node:child_process";
import {
DEFAULT_RUNNER_QUEUE_GRACE_MILLISECONDS,
Expand All @@ -23,7 +26,10 @@ import {
parseSelectedRunIds,
} from "./lib/actions-runner-assignment-source.mjs";
import { readDelegatedGithubToken } from "./lib/delegated-github-token.mjs";
import { assertAcquisitionPrivatePathParents } from "./lib/acquisition-private-output.mjs";
import {
assertAcquisitionPrivatePathParents,
writeAcquisitionPrivateFile,
} from "./lib/acquisition-private-output.mjs";
import { hasDuplicateJsonObjectKeys } from "./normalize-commercial-readiness-evidence.mjs";

const AUDITED_REPOSITORY = "ContextualWisdomLab/noema";
Expand All @@ -41,14 +47,17 @@ const defaultGhRuntime = {
};

const defaultWriteIo = {
closeSync,
constants,
fchmodSync,
fstatSync,
ftruncateSync,
lstatSync,
mkdirSync,
openSync,
writeFileSync,
closeSync,
renameSync,
unlinkSync,
randomUUID,
writeFileSync,
};

function boundedErrorText(value) {
Expand Down Expand Up @@ -211,6 +220,10 @@ export function ghApi(path, options = {}, runtime = defaultGhRuntime) {
/**
* Create read-only GitHub Actions REST adapters for the operator audit.
*
* Job reads are bound to the exact positive workflow attempt returned by the
* selected run resource. GitHub's run-wide `filter=all` endpoint can include
* predecessor attempts and is therefore not authoritative for runner identity.
*
* @param {{repository: string, gh_api: Function}} input Repository and API reader.
* @returns {{fetch_run: Function, fetch_job_pages: Function}} Bounded read adapters.
*/
Expand All @@ -227,11 +240,15 @@ export function createGhReadAdapters(input) {
input.gh_api(`repos/${AUDITED_REPOSITORY}/actions/runs/${runId}`, {
paginate: false,
}),
fetch_job_pages: async (runId) =>
input.gh_api(
`repos/${AUDITED_REPOSITORY}/actions/runs/${runId}/jobs?filter=all&per_page=100`,
fetch_job_pages: async (runId, runAttempt) => {
if (!Number.isSafeInteger(runAttempt) || runAttempt <= 0) {
throw new Error("Workflow run_attempt must be a positive integer before reading jobs.");
}
return input.gh_api(
`repos/${AUDITED_REPOSITORY}/actions/runs/${runId}/attempts/${runAttempt}/jobs?per_page=100`,
{ paginate: true },
),
);
},
};
}

Expand All @@ -255,13 +272,15 @@ function parseQueueGrace(value) {
}

/**
* Write the fixed audit report atomically with owner-only temporary permissions.
* Write the fixed audit report through Noema's canonical private-output boundary.
*
* The optional I/O seam permits deterministic failure testing without changing
* the production report path, file mode, atomic rename, or cleanup semantics.
* The optional I/O seam permits deterministic failure testing while preserving
* parent-directory validation, owner-only permissions, no-follow leaf opens,
* single-link regular-file authority, descriptor/path identity checks, complete
* staged writes, identity-bounded cleanup, and atomic replacement.
*
* @param {unknown} report Bounded report value.
* @param {object} io File-system and UUID operations.
* @param {object} io File-system operations used by the private-output boundary.
* @returns {string} Absolute report path.
*/
export function writeReportAtomically(report, io = defaultWriteIo) {
Expand All @@ -270,28 +289,11 @@ export function writeReportAtomically(report, io = defaultWriteIo) {
assertAcquisitionPrivatePathParents(reportPath, io);
io.mkdirSync(reportDirectory, { recursive: true, mode: 0o700 });
assertAcquisitionPrivatePathParents(reportPath, io);
const temporaryPath = `${reportPath}.tmp-${process.pid}-${io.randomUUID()}`;
let descriptor;
try {
descriptor = io.openSync(temporaryPath, "wx", 0o600);
io.writeFileSync(descriptor, `${JSON.stringify(report, null, 2)}\n`, "utf8");
io.closeSync(descriptor);
descriptor = undefined;
io.renameSync(temporaryPath, reportPath);
} finally {
if (descriptor !== undefined) {
try {
io.closeSync(descriptor);
} catch {
// Cleanup failures must not replace the original report-write failure.
}
}
try {
io.unlinkSync(temporaryPath);
} catch {
// Cleanup failures must not replace the original report-write result.
}
}
writeAcquisitionPrivateFile(
reportPath,
`${JSON.stringify(report, null, 2)}\n`,
io,
);
return reportPath;
}

Expand Down Expand Up @@ -396,7 +398,7 @@ export async function main(options = {}) {
let githubApi = options.gh_api;

if (githubApi === undefined) {
const tokenPath = String(sourceEnvironment.NOEMA_MAINTAINER_TOKEN_PATH ?? "").trim();
const tokenPath = sourceEnvironment.NOEMA_MAINTAINER_TOKEN_PATH;
const delegatedToken = readDelegatedGithubToken(tokenPath);
const subprocessEnvironment = {
PATH: sourceEnvironment.PATH,
Expand Down
Loading
Loading