refactor: adapter 구현중..
This commit is contained in:
@@ -10,6 +10,39 @@ checking, the typed contract checker, and this workflow drift check. The Gitea
|
||||
adapter runs each gate as an independent matrix check with full fan-out and no
|
||||
soft-fail wiring.
|
||||
|
||||
Contract loading validates every registered command entry against the
|
||||
authoritative root package-script graph and lifecycle/install policy before the
|
||||
runner enters its execution callback. Foreign cwd/workspace script dispatch,
|
||||
argument-sensitive dispatchers, and ineffective lifecycle suppression therefore
|
||||
fail preflight even when the later `check:ci` command would never run.
|
||||
For npm, that root-only boundary also parses options following an explicit
|
||||
`run`/`run-script` target or implicit `test`/`start`/`stop`/`restart` target:
|
||||
`--workspace`, `-w`, `--workspaces`, and `--prefix` are foreign manifest scope
|
||||
and are rejected before execution. The first literal `--` ends npm option
|
||||
parsing, so later tokens are ordinary script arguments. pnpm and Yarn differ:
|
||||
their options following the selected script name are forwarded to that script;
|
||||
their cwd/workspace selectors are rejected only where the manager consumes
|
||||
them before script selection.
|
||||
|
||||
The npm graph includes existing root-manifest `pre<script>` and `post<script>`
|
||||
hooks around every explicit `run`/`run-script` dependency and implicit
|
||||
`start`/`stop`/`restart`/`test` dependency. Hooks are omitted only when ordered
|
||||
npm options establish an unambiguous effective `--ignore-scripts` value before
|
||||
the first literal `--`; bare and explicitly true forms omit hooks, while false,
|
||||
negative, contradictory, malformed, and post-delimiter forms keep traversal or
|
||||
fail closed. Case-insensitive `npm_config_workspace`, `npm_config_workspaces`,
|
||||
`npm_config_prefix`, `npm_config_userconfig`, and `npm_config_globalconfig`
|
||||
assignments are rejected whenever the tokenized command invokes npm. Direct
|
||||
assignments, static paths whose basename is `env`, exact `command`/`exec`
|
||||
prefix chains, prior exports, and `set -a` assignments share one prefix grammar.
|
||||
Only modeled non-scope `env -i`, `env -u`, `env --unset`, and `env --` forms are
|
||||
allowed; cwd-changing or unknown options fail closed. Dynamic assignment names,
|
||||
unmodeled environment mutation, npm `--userconfig`/`--globalconfig`, and
|
||||
unquoted pathname expansion before the npm argument delimiter are rejected.
|
||||
The gate runner checks the same inherited environment names before loading the
|
||||
contract or entering the execution callback, so they cannot reach a child gate
|
||||
process.
|
||||
|
||||
The dependency graph is:
|
||||
|
||||
```text
|
||||
@@ -52,19 +85,98 @@ digest가 일치할 때만 report를 업로드한다. Promotion은 같은 archiv
|
||||
별도 경로로 내려받고 SHA/member 검증을 마친 뒤 격리된 root에 추출하여 local
|
||||
evidence를 read-only로 다시 계산하고 Ed25519 signature/digest를 확인한다.
|
||||
Promotion job에는 build/rebuild command가 없으며 검증한 archive 자체를 변경 없이
|
||||
그대로 승격한다.
|
||||
그대로 승격한다. Local evidence 관계 검증에 필요한 policy와 verifier source도
|
||||
archive member로 고정되며, checkout 밖의 격리된 cwd에서도 archive path와 기대
|
||||
digest만으로 candidate-internal check를 재계산하고 archived secret-scan
|
||||
policy/rule/SARIF/zero-finding/digest 관계를 검증한다. Archive에 없는 checkout
|
||||
source를 다시 scan했다고 주장하지 않는다.
|
||||
|
||||
Provider baseline은 Gitea 1.26.4 이상과 Gitea Runner 1.0.0 이상이다. 이
|
||||
workflow의 provider job은 Linux runner에서 실행 권한이 있는
|
||||
`/usr/bin/bwrap`를 필수로 요구하며, trusted `process.execPath`를 sandbox 안의
|
||||
`/tmp/node`에 read-only bind한다. Provider command는 bubblewrap 안에서
|
||||
`/bin/sh -eu -c`로 비대화식 실행되고 30분 안에 종료되어야 한다. Sandbox는
|
||||
workspace를 read-only로 bind하고 `.git`을 가리며, 별도의 `untrusted`
|
||||
raw-evidence 하위 디렉터리만 writable로 노출한다. 따라서 command는 전달된
|
||||
candidate/environment 값을 읽고 지정된 raw report 하나만 기록해야 하며,
|
||||
workspace 수정, host home/toolcache 접근, sealed evidence 직접 기록에 의존하면
|
||||
안 된다. Supervisor는 provider 종류에 해당하는 credential prefix와 제한된
|
||||
환경만 전달하고, sandbox 또는 출력 경계를 만들 수 없으면 fail closed한다.
|
||||
Provider baseline은 Gitea 1.26.4 이상과 Gitea Runner 1.0.0 이상이다. Linux
|
||||
runner에는 실행 가능한 `/usr/bin/bwrap`, `/usr/bin/prlimit`,
|
||||
`/usr/bin/systemd-run`, `/usr/bin/systemctl`, `bwrap --size` 지원, active user
|
||||
bus, systemd user manager 254 이상, unified cgroup v2와 delegated
|
||||
memory/pids/CPU controller가 모두 필요하다. 실행 파일, user manager/version,
|
||||
trust/archive/path 같은 host-side preflight 실패는 raw report 생성 전에 차단된다.
|
||||
`bwrap --size` 수용 여부와 실제 delegated controller/limit 값은 owned raw inode를
|
||||
만든 뒤 scope 안에서만 확정할 수 있으며, 이 단계의 실패는 해당 inode를
|
||||
identity-bound cleanup하고 fail closed한다.
|
||||
|
||||
각 provider는 고유한 collected user scope에서 실행된다. Supervisor는 실행 전에
|
||||
실제 cgroup membership과 `memory.max=1073741824`, `memory.swap.max=0`,
|
||||
`pids.max=64`, `cpu.max="100000 100000"`을 확인한다. 내부 process에는 core 0,
|
||||
file-size 8,388,607 bytes, open FD 64, CPU 1,200초 상한도 적용된다.
|
||||
같은 UID 전체에 합산되는 `RLIMIT_NPROC`로 provider별 32개를 보장한다고 주장하지
|
||||
않으며 aggregate PID authority는 cgroup `TasksMax=64`다.
|
||||
Bubblewrap는 network namespace를 분리해 완전 offline으로 실행하고 workspace,
|
||||
verified candidate, `.git` mask, `/tmp`, `/etc`, `/proc`, `/dev`를 read-only로
|
||||
유지한다. Archive 검증·추출과 sandbox/trust preflight가 끝난 뒤 supervisor가
|
||||
생성하고 inode를 고정한 정확한 raw report 파일 하나만 read-write bind된다.
|
||||
주변 `untrusted` directory 전체는 writable이 아니다. 실행 전 또는 provider
|
||||
실패 시, 그리고 성공적으로 sealed evidence를 게시한 뒤에도 supervisor가 소유한
|
||||
inode만 atomic quarantine을 거쳐 제거하므로 빈 stale report 없이 재시도할 수 있다.
|
||||
|
||||
Provider command와 provider-prefixed environment는 bounded length-prefixed bwrap vector로
|
||||
`systemd-run` stdin에 전달되어 supervisor/systemd/bwrap wrapper argv나 unit
|
||||
metadata에 노출되지 않는다. 단, 최종 provider executable의 일반 argv는 같은
|
||||
UID의 process inspection에 보일 수 있으므로 command 문자열과 인자에 token,
|
||||
password, private-key material을 넣으면 안 된다. Credential은 반드시 해당 종류의
|
||||
`VULNERABILITY_PROVIDER_*` 또는 `PROVENANCE_PROVIDER_*` environment로만 전달하고
|
||||
`*_COMMAND`에는 넣지 않는다. 정상 provider 종료까지 같은 stdin을 parent-liveness
|
||||
pipe로 열어 두며 supervisor hard death의 EOF를 받은 in-scope wrapper는 provider
|
||||
process group 전체를 종료하고 dev/inode가 일치하는 raw report만 정리한다.
|
||||
|
||||
별도의 trusted guardian child는 provider scope 밖에서 filesystem transaction 전체를
|
||||
소유한다. Client는 spawn 전에 canonical raw/evidence directory를
|
||||
`O_DIRECTORY|O_NOFOLLOW`로 열고 identity를 확인한 뒤 provider kind와 nonce에서
|
||||
canonical raw/final 및 nonce-private raw-staging/sealed-temp exact leaf를 확정한다.
|
||||
Client가 두 private file을 `O_RDWR|O_CREAT|O_EXCL|O_NOFOLLOW` mode `0600`으로
|
||||
미리 할당하고 dev/inode를 기록한다. Directory descriptor는 guardian fd 3/4,
|
||||
private file descriptor는 fd 5/6으로 상속되며 guardian argv에는 Node와 helper
|
||||
경로만 있다. 최초 canonical v2 frame에는 provider kind, absolute deadline,
|
||||
32-byte nonce만 전달한다.
|
||||
|
||||
Guardian bootstrap은 procfs link를 읽기 전에 fd 5/6을 fstat한다. Procfs pathname은
|
||||
canonical directory의 exact direct-child grammar를 만족하고 descriptor-relative
|
||||
lstat이 이미 확보한 fd identity/type/mode/size/link count와 일치할 때만 해당
|
||||
identity의 cleanup alias가 된다. Guard frame 검증 뒤 raw staging을 fixed raw leaf에
|
||||
no-overwrite hard link하고 두 alias의 link count 2를 확인한 다음 private alias를
|
||||
제거하고 raw directory를 sync한다. Canonical raw가 같은 identity와 link count 1로
|
||||
남은 뒤에만 READY를 응답한다. READY 전 종료 시 client는 pre-spawn raw identity로
|
||||
raw staging/canonical을, sealed identity로 temp/final을 각각 확인해 일치하는 alias만
|
||||
정리한다. 현재 canonical pathname을 새 ownership identity로 승격하지 않으므로 외부
|
||||
file과 concurrent same-kind winner를 보존하면서 같은 workspace를 즉시 재시도할 수
|
||||
있다. Supervisor는 READY identity와 canonical target도 정확히 확인한다.
|
||||
|
||||
검증된 JSON bytes는 pinned temp inode에만 기록하고 `0400` 적용과 file sync를 마친다.
|
||||
Guardian은 authenticated publish의 size/hash/identity를 재검증하고 같은 directory에서
|
||||
no-replace `link(temp, final)`, temp unlink, directory sync를 수행한 뒤 PUBLISHED를
|
||||
응답한다. `GITHUB_OUTPUT` append 이후 commit은 raw inode를 제거하고
|
||||
`commitPending`으로만 전이하며, 깨끗한 control EOF까지 확인해야 PASS가 된다. 그 전의
|
||||
EOF, deadline, 잘못된 frame/nonce, commit 뒤 추가 byte 또는 guardian 조기 종료는
|
||||
identity가 일치하는 raw/temp/final을 모두 정리하고 fail closed한다. Provider wall
|
||||
timeout 30분에 post-processing 10분을 더한 guardian lease 상한은 40분이다.
|
||||
|
||||
Client가 private allocation 뒤 guardian spawn 전에 hard stop되면 empty mode-`0600`
|
||||
nonce-private leaf만 남을 수 있다. 기록되지 않은 pathname은 ownership 근거가 아니므로
|
||||
자동 sweeping하지 않는다. 이 private leaf는 fixed raw/final name을 점유하지 않아
|
||||
same-kind retry를 막지 않는다.
|
||||
|
||||
`GITHUB_OUTPUT`은 runner가 소유한 regular file이라는 실행기 계약을 전제로 한다.
|
||||
Sealing/output I/O의 OS-level cancellation을 보장하지 않는다. Provider scope가 이미
|
||||
수집된 뒤 guardian이 종료되면 scope-active latch는 늦은 kill을 시작하지 않고 lifecycle
|
||||
error만 기록한다. 이후 publish/commit의 awaited failure가 identity가 고정된
|
||||
raw/temp/final fallback을 모두 정리하고 fail closed한다.
|
||||
|
||||
Provider stdout/stderr는 credential을 포함할 수 있는 untrusted bytes이므로 CI log로
|
||||
재전송하거나 보관하지 않고, byte 수만 합산해 1 MiB 상한을 적용한다. Guardian의
|
||||
stderr/control fd가 닫혀 진단 출력이 `EPIPE`/`EBADF`가 되어도 cleanup 뒤 nonzero
|
||||
종료는 생략되지 않는다. Provider wall-clock 상한은 30분이다. Wall
|
||||
timeout, aggregate output 초과, parent-liveness loss, 실행 중 guardian loss는 provider process group을
|
||||
명시적으로 SIGKILL한다. 일반 command 실패와 FD/CPU RLIMIT 종료는 실제 exit/signal로
|
||||
systemd completion을 거치며, 모든 경로에서 wrapper 종료와 systemd unit/cgroup
|
||||
collection을 확인한다. Adapter는 사전에 배치된 offline data와 supervisor candidate binding만
|
||||
읽어 정확한 report inode에 기록해야 한다. Scope/cgroup limit drift, residual unit,
|
||||
workspace·host home/toolcache·sealed evidence 접근 의존성은 모두 blocking failure다.
|
||||
|
||||
Workflow가 실행하는 action은 `scripts/contracts/ci-gates.ts`의 단일 typed,
|
||||
runtime-frozen registry에서만 resolve된다. `uses:`에는 repository 별칭, tag,
|
||||
@@ -192,6 +304,9 @@ Repository variables required by higher tiers:
|
||||
`PROVENANCE_PUBLIC_KEY_PATH`, and `PROVENANCE_KEY_ID` for separately managed
|
||||
trusted Ed25519 verification material
|
||||
|
||||
두 provider role은 서로 다른 key ID뿐 아니라 canonical DER-SPKI public-key bytes도
|
||||
사용해야 한다. 동일 key를 서로 다른 ID로 재등록한 구성도 finalizer가 거절한다.
|
||||
|
||||
If any external provider command, report, trust path, or key ID is absent,
|
||||
promotion remains unavailable with `FAIL_UNVERIFIED`; there is no local
|
||||
generator/restore fallback.
|
||||
@@ -215,20 +330,51 @@ consumer도 artifact service나 transfer action을 신뢰 경계 밖으로 보
|
||||
manifest와 signed provider evidence에 바인딩된 digest를 다운로드 후 다시
|
||||
검증해야 한다. 현재 producer-side adjacency 자체는 consumer-side digest
|
||||
revalidation을 대신하지 않는다.
|
||||
Promotion job에는 job-level `if`가 없다. 기본 `needs` 성공 의미론으로 immutable
|
||||
build, vulnerability provider, provenance provider 세 job이 모두 성공해야 하며,
|
||||
`always()`나 `cancelled()`로 cancellation을 덮어쓰지 않는다. Bare `always()`는
|
||||
step cleanup에만 사용된다. 다만 cancellation 시 cleanup 실행 여부는 workflow
|
||||
정적 타입이나 단위 테스트로 증명하지 않았으며 runner/native smoke에서 확인해야
|
||||
하는 신뢰 경계다.
|
||||
Finalizer output은 `RUNNER_TEMP` 아래 random private directory이며 exact-five
|
||||
upload는 `${{ steps.finalize.outputs.staging_root }}`만 사용한다. 바로 다음
|
||||
`always()` cleanup은 finalizer의 token과 runner-temp device/inode를 모두
|
||||
요구한다. stable `.release/promoted-staging` directory를 만들거나 재사용하지
|
||||
않는다. exact five는 captured archive/report 두 개와 process 안에서 생성한
|
||||
provider/promotion verification v3 두 개이며 promotion record는 provider record,
|
||||
local assessment, report hashes와 run/source/candidate/nonces/key identities/
|
||||
trust-policy hash를 함께 bind한다. 이 descriptor-relative 정리는 ancestor 교체와 symlink leaf를
|
||||
fail-closed로 처리하지만 upload action의 same-UID pathname reopen 또는 atomic
|
||||
upload는 `${{ steps.finalize.outputs.staging_root }}` 아래 다음 다섯 canonical
|
||||
pathname만 사용한다: `release-candidate.tar.gz`, `vulnerability-report.json`,
|
||||
`provenance-attestation.json`, `provider-verification.json`,
|
||||
`promotion-verification.json`. 바로 다음
|
||||
`always()` cleanup은 staging path/token, runner-temp device/inode와 staging-leaf
|
||||
device/inode 여섯 output을 모두 요구한다. cleanup은 pin한 leaf descriptor에서
|
||||
exact-five name만 unlink하고 non-recursive `rmdir`만 사용하므로 교체된 directory나
|
||||
canary tree를 recursive 삭제하지 않는다. stable `.release/promoted-staging`
|
||||
directory를 만들거나 재사용하지 않는다. exact five는 captured archive 한 개,
|
||||
captured report 두 개와 process 안에서 생성한 provider/promotion verification v3
|
||||
두 개이며 promotion
|
||||
record는 provider record, local assessment, report hashes와 run/source/candidate/
|
||||
nonces/key identities/trust-policy hash 및 signed `secretScanAttestation`을 함께
|
||||
bind한다. 이 attestation은 PASS와 local-assessment/source-set/policy/SARIF/
|
||||
scan-input digest를 포함한다. Supervisor/finalizer는 captured archive에서 기대
|
||||
tuple을 유도해 exact equality를 확인하지만, 실제로 같은 source-set 전체를
|
||||
독립 스캔하고 forged empty SARIF에 서명하지 않을 책임은 trusted vulnerability
|
||||
provider에 있다. Staging은 restrictive
|
||||
umask와 무관하게 directory `0700`, file `0400`을 강제하고, 모든 write 뒤 live
|
||||
time으로 exact-five signature/freshness를 다시 확인한 뒤에만 output을 공개한다.
|
||||
Descriptor-relative 정리는 ancestor/leaf 교체와 symlink를 fail-closed로 처리하지만
|
||||
upload action의 same-UID pathname reopen 또는 atomic
|
||||
`renameat2` handoff를 보장하지 않는다. staging Gitea smoke/native adapter 확인
|
||||
전에는 그 경계를 닫았다고 보고하지 않는다. 실제 smoke는 exact-five
|
||||
upload-download와 success, validation failure, upload failure, cancellation 각각의
|
||||
cleanup을 관찰해야 한다. 현재 repository에는 native uploader나 `renameat2`
|
||||
보장이 없다.
|
||||
또한 portable Node의 `mkdir`와 최초 pathname `lstat`는 atomic하지 않다. 구현은
|
||||
mkdir 직후 metadata를 저장하고 이후 `O_DIRECTORY|O_NOFOLLOW` descriptor의
|
||||
device/inode와 비교한 뒤에만 permission을 바꾸지만, 최초 lstat보다 앞서 성공한
|
||||
malicious same-UID 교체는 native/privilege 경계로 남는다. 따라서 `RUNNER_TEMP`
|
||||
private `0700` ancestor와 exclusive single-tenant runner가 필수다.
|
||||
실패 cleanup도 created device/inode와 opened descriptor가 일치한 뒤에만 활성화된다.
|
||||
불일치 descriptor는 close만 수행하며 현재 visible replacement pathname은 unlink나
|
||||
`rmdir`하지 않는다. 공격자가 original directory를 다른 이름이나 parent 밖으로
|
||||
이동한 경우 portable Node parent scan으로 안전하게 회수할 수 없으므로, 공격자를
|
||||
배제한 trusted runner/native cleanup 또는 격리된 test fixture가 잔여 directory를
|
||||
후처리해야 한다.
|
||||
|
||||
Branch protection must mark each `FE-GATE-* / <name>` check required for its
|
||||
declared tier. This repository cannot configure server-side protection by
|
||||
|
||||
+175
-33
@@ -2,7 +2,17 @@
|
||||
|
||||
## Local blocking controls
|
||||
|
||||
- `pnpm install --frozen-lockfile` and a real manifest/lock mismatch fixture
|
||||
- `pnpm install --frozen-lockfile --ignore-scripts` and a real manifest/lock
|
||||
mismatch fixture; contract loading applies the root-only graph and lifecycle
|
||||
policy to every registered command before the gate runner can spawn one, and
|
||||
rejects nested installs without effective `--ignore-scripts`; npm script
|
||||
traversal includes existing pre/post hooks unless an ordered bare or explicit
|
||||
true `--ignore-scripts` suppresses them. Workspace/prefix and indirect
|
||||
user/global config authority are rejected in npm options, direct or dynamic
|
||||
assignments, exact `command`/`exec`/`env` prefix chains, cross-segment shell
|
||||
state, and the inherited runner environment. Unknown or cwd-changing `env`
|
||||
options and unquoted pre-delimiter pathname expansion fail closed, while the
|
||||
explicitly modeled non-scope `env` options remain usable
|
||||
- all direct and transitive lockfile rows with package SHA-512 integrity
|
||||
- production/development, direct/transitive and platform-optional classification
|
||||
- package-manifest license allow/deny policy
|
||||
@@ -39,7 +49,8 @@ Ed25519 signatures verified with separately configured trusted public keys and
|
||||
key IDs (`VULNERABILITY_PUBLIC_KEY_PATH`, `VULNERABILITY_KEY_ID`,
|
||||
`PROVENANCE_PUBLIC_KEY_PATH`, and `PROVENANCE_KEY_ID`). Keys of another curve,
|
||||
including Ed448, are rejected even if a document labels its algorithm
|
||||
`Ed25519`.
|
||||
`Ed25519`. The two roles must use different key IDs and different canonical
|
||||
DER-SPKI key bytes; giving the same key two IDs is rejected.
|
||||
|
||||
`immutable_build` archives the raw `pnpm-lock.yaml`, `dist` (including hidden
|
||||
`.vite` files), the build manifest, module inventory, release verification,
|
||||
@@ -58,15 +69,21 @@ with its strict schema, and binds its dist and lockfile digests before upload.
|
||||
|
||||
If either provider input is absent, local verification remains meaningful but
|
||||
`artifacts/security/supply-chain-verification.json` records
|
||||
`promotionStatus: FAIL_UNVERIFIED`. `verify:provider-evidence` and
|
||||
`verify:promotion` then exit non-zero. Promotion recomputes the candidate file
|
||||
set and digests, then read-only revalidates the archived executable schemas,
|
||||
raw lockfile, module inventory, build outputs, release coherence, SBOM,
|
||||
provenance, security scan and supply-chain coherence. It never rebuilds or
|
||||
rewrites candidate evidence. Promotion uploads the already verified archive
|
||||
itself with the two provider reports and verification records; it does not
|
||||
create a replacement archive from extracted files. Scanner or signing outages
|
||||
are not converted to an empty PASS.
|
||||
`promotionStatus: FAIL_UNVERIFIED`. The finalizer and downstream
|
||||
`verify:promotion` exact-five validator then exit non-zero. Promotion derives
|
||||
the candidate file set and digests only from the captured tar bytes, then
|
||||
read-only revalidates archived executable schemas, archived policy/verifier
|
||||
source bytes, raw lockfile, module inventory, build outputs, release coherence,
|
||||
SBOM, provenance and supply-chain coherence. Candidate-internal checks are
|
||||
recomputed; for the checkout-dependent secret scan, promotion independently
|
||||
checks the archived policy, exact rule set, strict SARIF, zero findings and all
|
||||
manifest/assessment digest bindings. It does not claim to rescan source bytes
|
||||
that are not candidate members. It never rebuilds or rewrites candidate
|
||||
evidence and never falls back to the checkout tree.
|
||||
Promotion uploads the already verified archive itself with the two provider
|
||||
reports and generated verification records; it does not create a replacement
|
||||
archive from extracted files. Scanner or signing outages are not converted to
|
||||
an empty PASS.
|
||||
|
||||
The generated workflow is also a supply-chain control. `config/ci/gates.json`
|
||||
is its sole typed authority. Run `corepack pnpm generate:ci-workflow` after a
|
||||
@@ -94,17 +111,107 @@ A real end-to-end provider smoke on the staging Gitea instance remains
|
||||
mandatory before any generated job becomes a required check.
|
||||
|
||||
External provider supervision is fail-closed and requires a Linux runner with
|
||||
an executable `/usr/bin/bwrap`. Bubblewrap mounts the repository workspace
|
||||
read-only, hides `.git`, and read-only binds the trusted `process.execPath`
|
||||
inside the sandbox at `/tmp/node`. It exposes only the sibling `untrusted` raw-evidence
|
||||
directory as writable. Provider commands run non-interactively through
|
||||
`/bin/sh -eu -c`, receive a minimized environment plus only their own
|
||||
provider-prefixed credentials, and have a 30-minute limit. They must consume
|
||||
the supplied candidate paths and digests, write exactly the configured raw
|
||||
report, and must not depend on workspace mutation, host home/toolcache access,
|
||||
or direct access to the sealed evidence path. Missing sandbox support, stale or
|
||||
misplaced outputs, command failure/timeout, and post-command candidate drift
|
||||
all stop publication.
|
||||
executable `/usr/bin/bwrap`, `/usr/bin/prlimit`, `/usr/bin/systemd-run`, and
|
||||
`/usr/bin/systemctl`, bubblewrap support for `--size`, an active user bus, a
|
||||
systemd user manager version 254 or newer, unified cgroup v2, and delegated
|
||||
memory, pids, and CPU controllers. Executable access, user-manager/version,
|
||||
trust, archive, and path failures are rejected before raw creation. Bubblewrap
|
||||
`--size` acceptance and effective delegated controller values can only be
|
||||
verified after the owned raw inode exists inside a new scope; failures there
|
||||
remove that inode by identity and fail closed. Each invocation runs in a unique collected user scope and verifies
|
||||
its effective cgroup membership and limits before bubblewrap starts: memory is
|
||||
exactly 1 GiB, swap is zero, `TasksMax` is 64, and CPU quota is 100% per 100 ms.
|
||||
The inner process also has zero core size, a 8,388,607-byte file-size limit,
|
||||
64 open files and at most 1,200 CPU seconds. `TasksMax=64` is the authoritative
|
||||
aggregate PID boundary; no per-provider `RLIMIT_NPROC=32` claim is made because
|
||||
that limit is counted across the runner's same-UID process population.
|
||||
|
||||
Bubblewrap uses a private network namespace (`--unshare-net`), mounts the
|
||||
workspace and verified candidate read-only, hides `.git`, and read-only binds
|
||||
the trusted `process.execPath` at `/tmp/node`. The supervisor creates and pins
|
||||
the exact configured raw-report inode only after sandbox/trust/archive
|
||||
preflight; that inode is the only provider evidence path mounted read-write.
|
||||
The provider cannot write the surrounding `untrusted` directory, workspace,
|
||||
candidate, host home/toolcache, sealed evidence path, or general temporary
|
||||
filesystem. Pre-execution and provider failures remove only the supervisor-
|
||||
owned raw inode so the same job can retry without a stale empty report.
|
||||
|
||||
The complete bwrap argument/environment vector, including the provider command
|
||||
and provider-prefixed environment, is carried in a bounded length-prefixed frame over
|
||||
`systemd-run` stdin rather
|
||||
than placed in the supervisor, systemd, or bwrap wrapper argv. This prevents
|
||||
credentials from entering unit metadata and wrapper command lines. The final
|
||||
provider executable and its ordinary arguments remain visible to same-UID
|
||||
process inspection, so commands must never contain tokens or secrets. The same
|
||||
stdin remains open as a parent-liveness channel until normal provider exit; EOF
|
||||
caused by supervisor death makes the in-scope wrapper kill the provider process
|
||||
group and remove only the dev/inode-matched raw report. Supply
|
||||
credentials only through the provider-kind prefix
|
||||
(`VULNERABILITY_PROVIDER_*` or `PROVENANCE_PROVIDER_*`, excluding `*_COMMAND`).
|
||||
|
||||
A separate trusted guardian starts outside the provider scope and owns the
|
||||
filesystem transaction. Before spawn, the client opens and identity-checks the
|
||||
canonical raw and evidence directories with `O_DIRECTORY|O_NOFOLLOW`, derives
|
||||
the exact canonical and nonce-private leaves, and exclusively allocates empty
|
||||
mode-`0600` raw-staging and sealed-temp files. It records both dev/inode pairs
|
||||
before spawn and inherits the directory descriptors as guardian fd 3/fd 4 and
|
||||
the private file descriptors as fd 5/fd 6. Its argv contains only the trusted
|
||||
Node and helper paths. A bounded canonical v2 request carries only provider
|
||||
kind, an absolute deadline, and a random 32-byte control nonce.
|
||||
|
||||
At bootstrap the guardian fstats fd 5/fd 6 before reading their procfs links.
|
||||
Each procfs target is accepted only as a direct-child alias whose exact grammar,
|
||||
descriptor-relative lstat, type, mode, size, link count, and dev/inode match the
|
||||
already-recorded descriptor identity. The guardian publishes raw staging to the
|
||||
fixed raw leaf with a no-overwrite hard link, verifies both aliases at link
|
||||
count two, removes the private raw alias, syncs the raw directory, and verifies
|
||||
the canonical raw alias at link count one before authenticated READY. If startup
|
||||
ends before READY is accepted, the client cleans raw staging/canonical only when
|
||||
they match its pre-spawn raw identity and sealed temp/final only when they match
|
||||
its pre-spawn sealed identity. It never derives cleanup ownership by opening a
|
||||
current canonical pathname, so an external file or a concurrent same-kind
|
||||
winner is preserved. Provider execution starts only after the supervisor
|
||||
confirms that the returned identities and canonical targets match exactly.
|
||||
|
||||
After evidence validation, the supervisor writes schema-validated bytes to the
|
||||
pinned temp inode, changes it to `0400`, fsyncs it, and sends authenticated
|
||||
size/hash/identity metadata. The guardian verifies the held descriptor and
|
||||
pathname, publishes without replacement using same-directory `link`, removes
|
||||
the temp name, fsyncs the directory, and returns authenticated PUBLISHED. Only
|
||||
after successful `GITHUB_OUTPUT` append does the supervisor send commit. Commit
|
||||
removes the raw inode and enters `commitPending`; clean control EOF is the sole
|
||||
success terminal and preserves the sealed final. EOF without that terminal,
|
||||
deadline expiry, malformed/trailing control data, a wrong nonce, or premature
|
||||
guardian exit cleans every matching raw/temp/final identity and fails closed.
|
||||
Guardian loss while the provider scope is active also triggers whole-scope kill
|
||||
and collection.
|
||||
|
||||
`GITHUB_OUTPUT` is assumed to be a runner-owned regular file. This protocol
|
||||
does not claim OS-level cancellation of sealing or output I/O. If the guardian
|
||||
exits after scope collection, the scope-active latch records the lifecycle error
|
||||
without starting a late kill. Publication or terminal commit observes the
|
||||
nonzero exit and the client removes every identity-pinned raw/temp/final
|
||||
fallback before failing closed. The lease is bounded by the 30-minute provider
|
||||
wall limit plus a fixed ten-minute post-processing allowance.
|
||||
|
||||
A client hard stop after private allocation but before guardian spawn can leave
|
||||
only empty mode-`0600` nonce-private leaves. Automatic sweeping is intentionally
|
||||
omitted because an unrecorded pathname does not prove ownership; these private
|
||||
leaves cannot occupy the fixed raw or final names and do not block a retry.
|
||||
|
||||
Provider stdout and stderr are untrusted secret-bearing bytes. The supervisor
|
||||
does not retain or forward them to CI logs; it counts them only to enforce one
|
||||
1 MiB aggregate limit. Guardian diagnostics are best effort, so closed stderr
|
||||
or control descriptors cannot bypass cleanup or the required nonzero exit. The
|
||||
provider wall-clock limit is 30 minutes. Wall timeout, output overflow, parent-liveness loss,
|
||||
and active-scope guardian loss explicitly SIGKILL the whole provider process group. Ordinary command and
|
||||
RLIMIT failures complete through systemd with their concrete exit/signal; every
|
||||
path still waits for wrapper closure and requires the systemd unit/cgroup to be
|
||||
collected before returning. Provider adapters must therefore operate entirely from pre-populated
|
||||
offline data, consume the supplied candidate bindings, and write exactly the
|
||||
pinned report inode. Missing prerequisites, cgroup drift, stale or misplaced
|
||||
outputs, post-command candidate drift, and residual scope cleanup all stop
|
||||
publication.
|
||||
|
||||
Provider documents are strict schema v2. Their Ed25519 signature covers the
|
||||
supervisor-supplied evidence type, validity window, run ID/attempt, independent
|
||||
@@ -115,17 +222,31 @@ never lets a report define its own expected nonce. A report from another
|
||||
attempt, source, archive, nonce, or key fingerprint is fail-closed even when it
|
||||
has been correctly re-signed.
|
||||
|
||||
The immutable archive contains a strict producer-local assessment. Promotion
|
||||
revalidates it from captured archive members without reopening checkout policy
|
||||
or source paths. The finalizer captures the archive, both reports, and both
|
||||
public keys once, generates both verification v3 records in memory, and writes
|
||||
exactly five mode-`0400` files beneath a random mode-`0700` directory in
|
||||
`RUNNER_TEMP`. The exact five are the captured archive, captured vulnerability
|
||||
report, captured provenance attestation, generated provider-verification v3,
|
||||
and generated promotion-verification v3. The promotion record binds the exact
|
||||
provider-record hash, local-assessment hash, both report hashes, run/source/
|
||||
candidate identities, both nonces, both key IDs/fingerprints, and canonical
|
||||
trust-policy hash. It never creates or reuses `.release/promoted-staging`.
|
||||
The immutable archive contains a strict producer-local assessment plus the
|
||||
policy and verifier source bytes needed to validate its archived relationships.
|
||||
Promotion recomputes candidate-internal checks and validates the captured
|
||||
secret-scan policy/rules/SARIF/digest relationships from an isolated extraction
|
||||
root; it does not reopen checkout policy or source paths or claim to rescan
|
||||
unarchived checkout source. The finalizer captures the archive,
|
||||
both reports, and both public keys once, generates both verification v3 records
|
||||
in memory, and writes exactly five mode-`0400` files beneath a random
|
||||
mode-`0700` directory in `RUNNER_TEMP`, independently of a restrictive runner
|
||||
umask. The exact five are the captured archive, captured vulnerability report,
|
||||
captured provenance attestation, generated provider-verification v3, and
|
||||
generated promotion-verification v3. Before returning, the finalizer validates
|
||||
those exact bytes again with live-time provider signature/freshness checks. The
|
||||
promotion record binds the exact provider-record hash, local-assessment hash,
|
||||
both report hashes, run/source/candidate identities, both nonces, both key
|
||||
IDs/fingerprints, canonical trust-policy hash, and the vulnerability provider's
|
||||
signed `secretScanAttestation`. That strict attestation says `PASS` and binds
|
||||
the captured local-assessment, source-set, secret-scan policy, SARIF, and actual
|
||||
scan-input digests. The supervisor derives the expected tuple from the captured
|
||||
archive and exact equality is rechecked at upload and finalization. The trusted
|
||||
vulnerability provider remains responsible for independently scanning that
|
||||
source set and refusing to sign a forged empty SARIF or incomplete scan input;
|
||||
the signature proves the provider made the claim, not that an untrusted
|
||||
provider performed the scan honestly. It never creates or reuses
|
||||
`.release/promoted-staging`.
|
||||
|
||||
The final promotion verification/staging step must be immediately adjacent to
|
||||
the promoted-release upload, and that upload must not use `always()`. This
|
||||
@@ -137,6 +258,14 @@ service and transfer actions also remain outside the candidate's cryptographic
|
||||
identity: every downstream consumer must revalidate the downloaded archive,
|
||||
manifest member digests and signed provider evidence. Producer-side adjacency
|
||||
does not provide consumer-side digest revalidation.
|
||||
The promotion job has no job-level `if`: ordinary `needs` success semantics
|
||||
require immutable build and both provider jobs to succeed, and cancellation is
|
||||
not overridden with `always()` or `cancelled()`. Cleanup alone uses bare `always()`
|
||||
and is guarded by all six finalizer outputs: staging path, token, parent
|
||||
device/inode, and staging-leaf device/inode. Cleanup opens the pinned leaf,
|
||||
requires the exact five names, unlinks only those known files through the
|
||||
descriptor, and uses a non-recursive `rmdir`; an exchanged directory or canary
|
||||
is never recursively removed.
|
||||
The immediately following upload action still reopens pathnames. The
|
||||
descriptor-relative staging and cleanup code does not claim an atomic
|
||||
`renameat2` handoff or close a malicious same-UID Gitea upload adapter; the
|
||||
@@ -144,6 +273,19 @@ staging Gitea smoke/native platform adapter remains the required closure for
|
||||
that boundary. That smoke must exercise exact-five upload and download plus
|
||||
cleanup on success, validation failure, upload failure, and cancellation. No
|
||||
native uploader or `renameat2` guarantee exists in this repository today.
|
||||
Portable Node also cannot make `mkdir` plus the first pathname `lstat` atomic.
|
||||
The implementation compares the immediate post-`mkdir` identity with the
|
||||
subsequent `O_DIRECTORY|O_NOFOLLOW` descriptor before changing permissions, but
|
||||
a malicious same-UID actor that wins before that first `lstat` remains part of
|
||||
the native/privilege boundary. The private `0700` runner-temp ancestor and
|
||||
single-tenant runner requirement are therefore security controls, not merely
|
||||
hardening.
|
||||
Failure cleanup is armed only after that created device/inode matches the opened
|
||||
descriptor. If the opened descriptor is a replacement, it is closed without
|
||||
unlinking or removing anything; the visible replacement is likewise untouched.
|
||||
Portable Node cannot safely rediscover an attacker-moved original directory by
|
||||
scanning the parent, so that residual must be removed by the isolated test
|
||||
fixture or trusted runner/native cleanup after the attacker is excluded.
|
||||
|
||||
Approved vulnerability exceptions require vulnerability/package identity,
|
||||
owner, a different reviewer, reason and expiry. Expired or self-approved
|
||||
|
||||
@@ -74,9 +74,9 @@
|
||||
- Modify: `config/ci/gates.json`
|
||||
- Modify: `tests/unit/ci-artifact-contract.test.ts`
|
||||
|
||||
- [ ] Inventory every artifact still mapped to `generic-json-object` and export/reuse the producer's strict schema, including cross-field status/failure/count invariants. Do not treat a non-empty JSON object as semantic evidence.
|
||||
- [ ] Replace substring-only JUnit/HTML acceptance with bounded well-formed document validation. Reject DTD/entities, malformed nesting, duplicate/invalid roots, and trailing non-whitespace content.
|
||||
- [ ] Add invalid-but-pattern-matching fixtures for all structured kinds and a table proving every configured artifact resolves to a semantic validator.
|
||||
- [x] Inventory every artifact still mapped to `generic-json-object` and export/reuse the producer's strict schema, including cross-field status/failure/count invariants. Do not treat a non-empty JSON object as semantic evidence.
|
||||
- [x] Replace substring-only JUnit/HTML acceptance with bounded well-formed document validation. Reject DTD/entities, malformed nesting, duplicate/invalid roots, and trailing non-whitespace content.
|
||||
- [x] Add invalid-but-pattern-matching fixtures for all structured kinds and a table proving every configured artifact resolves to a semantic validator.
|
||||
- [ ] Run focused artifact tests, `corepack pnpm check:ci`, types, lint, and diff checks; commit separately so this evidence-quality closeout is independently reviewable.
|
||||
|
||||
### Task 4: One authoritative architecture graph
|
||||
|
||||
@@ -0,0 +1,598 @@
|
||||
# Promotion Security Review Fixes Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Make the captured candidate archive and a strict exact-five bundle validator the only promotion authority while closing evidence, lifecycle, staging, output, freshness, and workflow gaps found by security review.
|
||||
|
||||
**Architecture:** Candidate identity and local verification are derived exclusively from the inode-captured tar stream. The finalizer revalidates archived subordinate evidence, provider signatures, freshness, role-separated trust, and then creates and validates an exact-five bundle before returning descriptor- and inode-bound cleanup metadata. Provider execution, CLI output publication, and workflow gating expose small injectable boundaries so failure and cleanup behavior can be tested directly.
|
||||
|
||||
**Tech Stack:** Node.js 24, TypeScript, Zod, Vitest, GNU tar, bubblewrap, Gitea Actions workflow generation.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Work sequentially on the current clean `develop` HEAD and produce one review-fix commit.
|
||||
- Every production change follows focused RED, observed expected failure, minimal GREEN, and regression verification.
|
||||
- Candidate verification performs no checkout reads; test subprocesses from outside the checkout with contradictory canaries.
|
||||
- Preserve real GNU tar and bubblewrap coverage; do not claim native uploader or atomic `renameat2`/`unlinkat` semantics.
|
||||
- Gitea 1.26.4 and act_runner 1.0.0 exact-five upload/download/cancel behavior remains an explicitly documented external smoke boundary.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Canonical Captured Archive and Archived Local Authority
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/lib/release-candidate.ts`
|
||||
- Modify: `scripts/lib/local-release-evidence.ts`
|
||||
- Modify: `scripts/lib/ci-candidate-archive.ts`
|
||||
- Delete: `scripts/lib/promotion-verifier.ts`
|
||||
- Delete: `scripts/verify-provider-evidence.ts`
|
||||
- Delete: `scripts/verify-supply-chain-promotion.ts`
|
||||
- Modify: `tests/unit/security-followup.test.ts`
|
||||
- Modify: `tests/unit/supply-chain.test.ts`
|
||||
- Modify: `tests/integration/security-followup-archive.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: captured tar bytes plus expected SHA-256.
|
||||
- Produces: `withVerifiedCapturedCandidate()` callback data derived only from the extracted, exact-member, digest-verified tar; `verifyArchivedLocalEvidence()` independently recomputes all feasible archived checks.
|
||||
|
||||
- [x] Add failing tests for invalid tar, archive/tree mismatch, contradictory archived subordinate FAIL, exact archived policy bytes, and checkout-independent execution.
|
||||
- [x] Run focused tests and record the expected RED diagnostics in the durable task report.
|
||||
- [x] Archive the exact policy/verifier inputs required for independent release, supply-chain, dependency, license, vulnerability, and secret-scan checks.
|
||||
- [x] Re-run producer checks against the extracted archive and require their result to agree with the assessment and member identities.
|
||||
- [x] Remove the obsolete standalone PASS issuers and route all fixture checking through real captured tar/finalizer validation.
|
||||
- [ ] Run focused archive, supply-chain, and integration tests to GREEN.
|
||||
|
||||
### Task 2: Exact-Five Validator and Role-Separated Trust
|
||||
|
||||
**Files:**
|
||||
- Create: `scripts/lib/exact-promotion-bundle.ts`
|
||||
- Create: `scripts/verify-exact-promotion-bundle.ts`
|
||||
- Modify: `scripts/lib/provider-evidence.ts`
|
||||
- Modify: `scripts/lib/promotion-stager.ts`
|
||||
- Modify: `scripts/contracts/promotion-artifacts.ts`
|
||||
- Modify: `tests/unit/security-followup.test.ts`
|
||||
- Modify: `tests/unit/ci-artifact-contract.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: exactly five captured byte buffers and two trusted Ed25519 identities.
|
||||
- Produces: `verifyExactPromotionBundle()` that requires literal verifier identity/version, provider and subordinate PASS states, exact hashes, and equal run/source/candidate/provider/trust fields.
|
||||
|
||||
- [x] Add failing tests for provider FAIL/absence, arbitrary provider hash, swapped roles, shared-field mismatch, archive/report digest mismatch, and identical role keys.
|
||||
- [x] Run focused tests and record RED.
|
||||
- [x] Implement strict exact-five parsing/cross-record validation and expose a downstream CLI command.
|
||||
- [x] Reject equal DER-SPKI fingerprints and equal role key identity before evaluation/finalization.
|
||||
- [x] Invoke exact-five validation inside the finalizer before publication; the full real-build fixture rerun remains sandbox-blocked below.
|
||||
|
||||
### Task 3: Provider Lifecycle and Freshness
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/lib/provider-supervisor.ts`
|
||||
- Create: `scripts/lib/provider-process-runner.ts`
|
||||
- Modify: `scripts/run-and-validate-provider.ts`
|
||||
- Modify: `tests/unit/security-followup.test.ts`
|
||||
- Modify: `tests/unit/ci-artifact-contract.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: a spawned bubblewrap child, injected timeout/clock, captured report.
|
||||
- Produces: a runner that SIGKILLs on timeout but rejects only after `close`, and supervision that samples freshness after provider/report capture.
|
||||
|
||||
- [x] Add failing stubborn-descendant/short-timeout and sequence-clock expiry tests.
|
||||
- [x] Run focused tests and record RED.
|
||||
- [x] Extract the process runner, wait for close after timeout, and preserve the timeout diagnostic.
|
||||
- [x] Issue provider timestamps immediately before execution, validate with a fresh clock after capture, and reject crossing expiry.
|
||||
- [x] Run focused lifecycle tests, including real stubborn descendants, to GREEN; the shared real-build/bubblewrap fixture remains sandbox-blocked below.
|
||||
|
||||
### Task 4: Inode-Pinned Staging and Output-Failure Cleanup
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/lib/promotion-stager.ts`
|
||||
- Create: `scripts/lib/stage-verified-promotion-cli.ts`
|
||||
- Modify: `scripts/stage-verified-promotion.ts`
|
||||
- Modify: `scripts/cleanup-verified-promotion.ts`
|
||||
- Modify: `tests/unit/ci-artifact-contract.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `FinalizedPromotion.stagingIdentity` and a testable CLI function whose append failure invokes cleanup from the in-memory result.
|
||||
|
||||
- [x] Add failing tests for leaf replacement during writes, final visibility mismatch, partial-failure cleanup, GITHUB_OUTPUT open/write failure, and expiry during staging.
|
||||
- [x] Run focused tests and record RED.
|
||||
- [x] Open the created leaf with `O_DIRECTORY|O_NOFOLLOW`, write through `/proc/self/fd/<leafFd>`, pin dev/ino, require visible identity equality, and propagate identity through cleanup.
|
||||
- [x] Force directory/file modes with `fchmod(0700/0400)` independent of a restrictive owner-preserving umask.
|
||||
- [x] Extract CLI dependencies; on any post-finalization output failure call direct cleanup before rethrowing.
|
||||
- [x] Revalidate evidence freshness before sealing and immediately before publication; isolated lifecycle/mode/output tests are GREEN and the shared real-build fixture remains sandbox-blocked below.
|
||||
|
||||
### Task 5: Workflow and Install Policy
|
||||
|
||||
**Files:**
|
||||
- Modify: `package.json`
|
||||
- Modify: `scripts/check-ci-contract.ts`
|
||||
- Modify: `scripts/contracts/ci-gates.ts`
|
||||
- Modify: `scripts/generate-ci-workflow.ts`
|
||||
- Modify: `config/ci/gates.json`
|
||||
- Modify: `.gitea/workflows/quality-gates.yml`
|
||||
- Modify: `tests/unit/ci-workflow-generation.test.ts`
|
||||
- Modify: `tests/unit/__snapshots__/ci-workflow-generation.test.ts.snap`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: promotion job `if: ${{ always() && needs.immutable_build.result == 'success' && needs.vulnerability_provider.result == 'success' && needs.provenance_provider.result == 'success' }}` and install-bearing script graph enforcement.
|
||||
|
||||
- [x] Add failing contract/generator tests for the job condition, upload without `always()`, missing cleanup outputs, and nested install scripts lacking `--ignore-scripts`.
|
||||
- [x] Run focused tests and record RED.
|
||||
- [x] Add `--ignore-scripts` to `verify:lockfile` and recursively reject each reachable install invocation without it.
|
||||
- [x] Extend the typed job condition and render the explicit cancellation-resistant exact-needs predicate.
|
||||
- [x] Regenerate workflow/snapshot and run workflow contract/byte tests to GREEN.
|
||||
|
||||
### Task 6: Fixtures, Documentation, Full Verification, and Commit
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/check-supply-chain-provider-fixtures.ts`
|
||||
- Modify: `docs/security/supply-chain.md`
|
||||
- Modify: `docs/operations/ci-quality-gates.md`
|
||||
- Modify: `.superpowers/sdd/2026-08-01-release-evidence-remediation/task-4-report.md` (ignored durable report)
|
||||
|
||||
- [x] Replace plaintext candidate fixtures with a real tar and canonical captured-archive/exact-five validation.
|
||||
- [x] Rewrite operator docs around the sole captured-archive/exact-five authority and retain the Gitea/runner external-smoke residual.
|
||||
- [ ] Run focused fixtures, archive integration, workflow snapshot/bytes, full unit, types, lint, `check:ci`, and diff checks; escalate only a sandbox-caused EPERM.
|
||||
- [x] Append all RED/GREEN and verification evidence/constraints to the durable report.
|
||||
- [ ] Invoke verification-before-completion, review the complete diff, commit once, and report commit/range/status.
|
||||
|
||||
---
|
||||
|
||||
## Review-Fix Wave D: Sealed Bytes, Replay Context, Scan Trust, and Cancellation
|
||||
|
||||
**Constraint:** Work only in the existing uncommitted tree. Do not write `.git`, stage, or commit. Each task follows a focused RED→GREEN cycle and records sandbox `EPERM` separately from product failures.
|
||||
|
||||
### Task D1: Seal the actual staged inode bytes
|
||||
|
||||
**Files:** `scripts/lib/promotion-stager.ts`, `tests/unit/security-followup.test.ts`, `tests/unit/ci-artifact-contract.test.ts`
|
||||
|
||||
**Interface:** The staging writer captures each canonical file through the already-open leaf FD using `O_NOFOLLOW`; it requires a regular single-link inode, mode `0400`, stable dev/ino/size, and the declared SHA-256. `verifyExactPromotionBundle` receives only these captured staged buffers immediately before return.
|
||||
|
||||
- [x] Add RED tests for unlink/recreate and chmod/mutation after a file write.
|
||||
- [x] Implement bounded descriptor-relative capture and exact-five seal validation.
|
||||
- [x] Run focused staging tests to GREEN.
|
||||
|
||||
### Task D2: Bind downstream verification to external expected identity
|
||||
|
||||
**Files:** `scripts/lib/exact-promotion-bundle.ts`, `scripts/verify-exact-promotion-bundle.ts`, `tests/unit/ci-artifact-contract.test.ts`
|
||||
|
||||
**Interface:** `verifyExactPromotionBundle` requires `expected.run.id`, `expected.run.attempt`, `expected.sourceRevision`, and `expected.archiveSha256`; optional bundle/dist/lock/source-set digests are compared when supplied. The CLI obtains these values from dedicated environment variables and never derives them from the bundle.
|
||||
|
||||
- [x] Add a RED signed other-run replay test.
|
||||
- [x] Implement external expected-context comparison in library and CLI.
|
||||
- [x] Run exact-bundle tests to GREEN where the sandbox permits.
|
||||
|
||||
### Task D3: Pin mkdir-to-open identity
|
||||
|
||||
**Files:** `scripts/lib/promotion-stager.ts`, `tests/unit/security-followup.test.ts`, `docs/security/supply-chain.md`, `docs/operations/ci-quality-gates.md`
|
||||
|
||||
**Interface:** A post-mkdir/pre-open test hook can replace the leaf. The implementation compares mkdir-returned pathname metadata with the `O_DIRECTORY|O_NOFOLLOW` handle `fstat` before any write; it never uses pathname chmod.
|
||||
|
||||
- [x] Add a RED pre-open replacement test.
|
||||
- [x] Compare created and opened metadata and reject replacement.
|
||||
- [x] Document the residual portable Node same-UID pre-lstat/native-privilege boundary.
|
||||
|
||||
### Task D4: Conservatively parse install invocations
|
||||
|
||||
**Files:** `scripts/lib/package-script-graph.ts`, `tests/unit/ci-workflow-generation.test.ts`
|
||||
|
||||
**Interface:** A bounded shell/token parser recognizes `pnpm install|i`, `npm install|ci|i`, and `yarn install` after supported manager-global options with split or `=` values. Any reachable package-manager invocation that cannot be classified is rejected.
|
||||
|
||||
- [x] Add the eight required global-option/alias RED cases plus malformed fail-closed cases.
|
||||
- [x] Implement tokenization and manager-specific invocation classification.
|
||||
- [x] Run install-policy tests to GREEN.
|
||||
|
||||
### Task D5: Signed secret-scan attestation
|
||||
|
||||
**Files:** `scripts/lib/provider-evidence.ts`, `scripts/lib/provider-supervisor.ts`, `scripts/lib/provider-upload-validator.ts`, `scripts/lib/promotion-stager.ts`, `scripts/lib/exact-promotion-bundle.ts`, relevant unit/integration tests and docs.
|
||||
|
||||
**Interface:** Vulnerability evidence v2 contains a strict `secretScanAttestation` with `status: PASS`, local-assessment, source-set, policy, SARIF, and scan-input digests. The supervisor derives the expected tuple from captured archive members, exports it to the provider, and upload/final verification requires exact equality under the Ed25519 signature.
|
||||
|
||||
- [x] Add RED forged-empty-SARIF and attestation-mismatch tests.
|
||||
- [x] Derive one captured-archive scan context and bind it through supervisor, signed schema, finalizer records, and exact validation.
|
||||
- [x] Run provider/security tests to GREEN where the sandbox permits.
|
||||
|
||||
### Task D6: Cancellation-safe workflow and exact upload paths
|
||||
|
||||
**Files:** `scripts/contracts/ci-gates.ts`, `scripts/generate-ci-workflow.ts`, `config/ci/gates.json`, generated workflow/snapshot, workflow tests, and operations/security docs.
|
||||
|
||||
**Interface:** Promotion uses a typed dependency-success/no-job-if variant, so cancellation cannot be overridden by job-level `always()`. Step cleanup retains bare `always()` for ordinary failures. Upload documentation names the five canonical paths under `staging_root` and states cancellation cleanup remains a runner/native smoke boundary.
|
||||
|
||||
- [x] Add RED generator/contract assertions for no promotion job `if` and retained cleanup `always()`.
|
||||
- [x] Regenerate workflow and snapshot after the typed condition change.
|
||||
- [x] Correct operator/security wording and run workflow/CI checks to GREEN.
|
||||
|
||||
### Task D7: Verification
|
||||
|
||||
- [x] Run focused suites after each GREEN, then affected/full unit tests, all TypeScript targets, ESLint, `check:ci`, generated-byte check, and both diff checks.
|
||||
- [x] Append exact PASS totals and sandbox-blocked commands to the ignored durable report.
|
||||
- [x] Report modified files and remaining native/Gitea/unsandboxed verification boundaries; do not attempt git staging or commit.
|
||||
|
||||
## Wave E: Unified parser and downstream boundary review
|
||||
|
||||
### Task E1: One tokenized manager parser
|
||||
|
||||
**Files:** `scripts/lib/package-script-graph.ts`, `tests/unit/ci-workflow-generation.test.ts`
|
||||
|
||||
**Interface:** One parse result reports manager invocations, package-script dependencies, unsupported controls, and effective lifecycle suppression. Both graph traversal and install policy consume it. Single `&`, unknown manager grammar, and malformed options fail closed. The last valid `--ignore-scripts` assignment controls the effective value; false, contradictory, valueless, and malformed assignments are unsafe. Lifecycle-capable mutation builtins are never implicit repository scripts and require effective suppression.
|
||||
|
||||
- [x] Add RED tables for single-ampersand segmentation, false/override/malformed suppression, global-option run/implicit dependencies, yarn/corepack reachability, and builtin/script-name collisions.
|
||||
- [x] Replace the regex traversal and separate install scan with one parser result.
|
||||
- [x] Run the parser-focused and full workflow-generation suites.
|
||||
|
||||
### Task E2: Evaluator-owned secret-scan equality
|
||||
|
||||
**Files:** `scripts/lib/provider-evidence.ts`, `tests/unit/security-followup.test.ts`, finalizer tests.
|
||||
|
||||
**Interface:** `evaluatePromotionEvidence` itself compares the parsed vulnerability report's signed `secretScanAttestation` with `expected.secretScanAttestation`. A mismatch makes vulnerability and overall promotion status `FAIL_UNVERIFIED`, including the production finalizer path.
|
||||
|
||||
- [x] Add a RED evaluator mismatch test.
|
||||
- [x] Implement exact equality before vulnerability PASS assignment.
|
||||
- [x] Run security/provider-focused tests.
|
||||
|
||||
### Task E3: Downstream CLI exact-five contract
|
||||
|
||||
**Files:** `tests/unit/ci-artifact-contract.test.ts`, `tests/unit/security-followup.test.ts`, `scripts/verify-exact-promotion-bundle.ts` if required.
|
||||
|
||||
**Interface:** A real finalizer-produced canonical exact-five directory passes the downstream CLI when all required external expected values and trust keys are supplied. Every required expected variable missing or mismatched exits non-zero. Optional digests remain exact when present.
|
||||
|
||||
- [x] Add RED happy-path and required-env negative coverage using real finalizer output where sandbox execution permits.
|
||||
- [x] Make only the minimal CLI/library changes needed for GREEN.
|
||||
- [x] Separate child-process sandbox blockers from library assertions.
|
||||
|
||||
### Task E4: Verification
|
||||
|
||||
- [x] Run focused parser/security/CLI suites, TypeScript, ESLint, `check:ci`, and `git diff --check`.
|
||||
- [x] Run full unit if feasible and report nested-process `EPERM` separately.
|
||||
- [x] Do not stage or commit.
|
||||
|
||||
## Wave F: Manager parser boundary hardening
|
||||
|
||||
**Constraint:** Continue in the existing uncommitted tree. Do not write `.git`, stage, or commit. Add behavior tests before production changes and keep unsupported manager grammar fail-closed.
|
||||
|
||||
### Task F1: Workspace dispatch and authoritative script lookup
|
||||
|
||||
**Files:** `scripts/lib/package-script-graph.ts`, `tests/unit/ci-workflow-generation.test.ts`
|
||||
|
||||
**Interface:** The parser receives the authoritative root `scripts` record. Explicit `run` resolves a root script; pnpm/yarn implicit dispatch resolves only a known root script. Yarn `workspace` and `workspaces` dispatch are unsupported because the root graph does not load workspace package scripts. Builtin aliases are canonicalized before root-script lookup.
|
||||
|
||||
- [x] Add RED policy and graph tables for the three Yarn workspace dispatchers, pnpm `ln`, and unknown manager subcommands.
|
||||
- [x] Remove workspace dispatchers from safe builtins, pass known root scripts into the parser, and canonicalize `pnpm ln` to lifecycle `link` before implicit lookup.
|
||||
- [x] Run the focused dependency/lifecycle cases to GREEN.
|
||||
|
||||
### Task F2: Shell comments and lifecycle option state
|
||||
|
||||
**Files:** `scripts/lib/package-script-graph.ts`, `tests/unit/ci-workflow-generation.test.ts`
|
||||
|
||||
**Interface:** Unquoted `#` in a manager-bearing command is unsupported control syntax; quoted `#` remains ordinary token content. Lifecycle options are parsed in order into a canonical suppression state covering `--ignore-scripts`, `--no-ignore-scripts`, and `--config.ignore-scripts`; conflicting, malformed, unknown, or ineffective states fail closed. Other option-like lifecycle arguments require an explicit manager allowlist.
|
||||
|
||||
- [x] Add RED comment, negative suppression, supported positive, and unknown lifecycle-option tables.
|
||||
- [x] Implement comment-aware tokenization and one ordered lifecycle argument parser.
|
||||
- [x] Preserve the checked-in `--frozen-lockfile --ignore-scripts` path and run focused tests to GREEN.
|
||||
|
||||
### Task F3: Verification
|
||||
|
||||
- [x] Validate every checked-in package script through graph/install consumers without false positives.
|
||||
- [x] Run the full workflow-generation file and related security tests.
|
||||
- [x] Run all TypeScript targets, ESLint, `check:ci`, and `git diff --check`; report nested-process `EPERM` separately.
|
||||
- [x] Update the durable report; do not stage or commit.
|
||||
|
||||
## Wave G: Verified cleanup and complete gate/parser preflight
|
||||
|
||||
**Constraint:** Continue in the existing uncommitted tree. Do not write `.git`, stage, or commit. Every production change follows a focused failing behavior test.
|
||||
|
||||
### Task G1: Verified-FD-only failure cleanup
|
||||
|
||||
**Files:** `scripts/lib/promotion-stager.ts`, `tests/unit/security-followup.test.ts`, security/operations documentation.
|
||||
|
||||
**Interface:** `openedIdentityVerified` becomes true only after the opened directory descriptor matches the post-`mkdir` device/inode. Failure cleanup may unlink canonical files or `rmdir` only through that verified descriptor and a still-matching visible identity. A mismatched opened descriptor and any visible replacement are close-only; a moved original directory remains for fixture/operator cleanup because portable Node cannot safely recover it.
|
||||
|
||||
- [x] Change the pre-open replacement regression to require both the replacement canary and displaced original directory to survive the failure.
|
||||
- [x] Run the focused test to RED against parent-directory identity scanning.
|
||||
- [x] Remove unverified inode discovery/recovery and gate descriptor cleanup on explicit identity verification.
|
||||
- [x] Run staging race and cleanup tests to GREEN and document the native residual.
|
||||
|
||||
### Task G2: Contract-wide lifecycle preflight
|
||||
|
||||
**Files:** `scripts/contracts/ci-gates.ts`, `scripts/run-ci-gate.ts`, optional focused runner helper, `tests/unit/ci-workflow-generation.test.ts`.
|
||||
|
||||
**Interface:** `loadCiGateContract` runs `validateInstallScriptPolicy` over every unique contract command script after script existence and graph checks. The runner enters its execution callback only after this loader succeeds, enabling a no-execute regression without relying on a nested child process.
|
||||
|
||||
- [x] Add RED loader tables for contradictory npm suppression, pnpm config false, and `pnpm ln`, plus a production runner-boundary no-execute spy.
|
||||
- [x] Enforce contract-command install policy and route runner execution through the preflight boundary.
|
||||
- [x] Run loader/runner preflight tests to GREEN.
|
||||
|
||||
### Task G3: Foreign manifest scope by command class
|
||||
|
||||
**Files:** `scripts/lib/package-script-graph.ts`, `tests/unit/ci-workflow-generation.test.ts`.
|
||||
|
||||
**Interface:** Manager-global options that change cwd, manifest or workspace scope are recorded during parsing. Explicit and implicit package-script dispatch with any such option is unsupported under the root-only graph. Lifecycle commands remain classifiable and are accepted only when their own ordered suppression/option grammar is safe.
|
||||
|
||||
- [x] Add RED policy+graph tables for pnpm filter/dir/`-C`, npm workspace/prefix, and Yarn cwd dispatch.
|
||||
- [x] Add positive externally scoped lifecycle cases with verified suppression.
|
||||
- [x] Track scope options and reject only package-script dispatch; run focused tests to GREEN.
|
||||
|
||||
### Task G4: Argument-sensitive builtin grammar and verification
|
||||
|
||||
**Files:** `scripts/lib/package-script-graph.ts`, `tests/unit/ci-workflow-generation.test.ts`, durable report.
|
||||
|
||||
**Interface:** Broad command-name-only safe builtins are replaced by exact per-manager read-only invocations. Init/explore/Yarn npm namespaces are unsupported. Audit is accepted only as an exact bare read-only command; `fix` and all unknown arguments are rejected.
|
||||
|
||||
- [x] Add RED policy+graph coverage for npm init/explore/audit-fix and Yarn npm publish, plus a bare-audit positive.
|
||||
- [x] Replace permissive builtin lookup with exact argument grammar.
|
||||
- [x] Audit every current package script for graph/policy false positives.
|
||||
- [x] Run staging/parser/no-execute/workflow/security suites, all TypeScript targets, ESLint, `check:ci`, and `git diff --check`; record sandbox `EPERM` separately and do not stage or commit.
|
||||
|
||||
## Wave H: npm post-script scope-option boundary
|
||||
|
||||
**Constraint:** Continue in the existing uncommitted tree. Do not write `.git`, stage, or commit. Reproduce every reviewer command in a failing test before changing the parser.
|
||||
|
||||
### Task H1: Explicit and implicit npm dispatch arguments
|
||||
|
||||
**Files:** `scripts/lib/package-script-graph.ts`, `tests/unit/ci-workflow-generation.test.ts`.
|
||||
|
||||
**Interface:** After an explicit `npm run`/`run-script` dependency or an implicit npm lifecycle script, manager options before the first literal `--` are parsed using an exact harmless allowlist. Workspace/prefix selectors (`--workspace`, `-w`, `--workspaces`, `--prefix`, including supported attached/equal forms) and unknown manager options fail closed. Tokens after the first literal `--` are script arguments and cannot change the authoritative manifest scope.
|
||||
|
||||
- [x] Add RED policy-and-graph coverage for all seven reviewer inputs, short/equal forms, unknown pre-delimiter options, the literal `--` boundary, and ordinary current-tree dispatch.
|
||||
- [x] Implement one npm post-script argument parser shared by explicit and implicit dispatch.
|
||||
- [x] Run focused parser tests to GREEN.
|
||||
|
||||
### Task H2: Contract loader and runner boundary
|
||||
|
||||
**Files:** `tests/unit/ci-workflow-generation.test.ts`, contract preflight only if the RED test exposes a separate integration defect.
|
||||
|
||||
**Interface:** Every reviewer input is rejected by contract loading while the referenced root scripts exist and are otherwise safe. `withCiGatePreflight` must not enter its callback for any rejected command.
|
||||
|
||||
- [x] Add a table-driven loader/no-callback regression for the same seven reviewer inputs.
|
||||
- [x] Run focused preflight tests to GREEN.
|
||||
|
||||
### Task H3: Verification
|
||||
|
||||
- [x] Re-audit current package scripts through graph and policy consumers.
|
||||
- [x] Run workflow/security suites, all TypeScript targets, ESLint, `check:ci`, and `git diff --check`.
|
||||
- [x] Record results in the durable report and do not stage or commit.
|
||||
|
||||
**Verification evidence:** The focused npm parser/preflight selection passed
|
||||
31/31. The workflow file passed 208/210; its two remaining tests reached the
|
||||
known nested-spawn sandbox boundary and reported `EPERM`. Security, supply-chain,
|
||||
and local-promotion tests passed 64/64. All six TypeScript targets, ESLint,
|
||||
`check:ci`, and `git diff --check` passed. Auditing the checked-in package found
|
||||
zero policy failures across 109 scripts and zero graph failures across 108
|
||||
entries (excluding the intentionally direct runner entry `ci:gate`). A broader
|
||||
artifact-contract run passed 102 assertions and blocked 23 fixture cases at the
|
||||
same nested `git ls-files` `EPERM` boundary. No `.git` write was performed.
|
||||
|
||||
Local pnpm 11.17 execution showed post-script `--filter`/`--dir` tokens arriving
|
||||
in the root script's argv, and official Yarn run documentation defines all
|
||||
parameters after the script name as script arguments. Those pre-existing
|
||||
negative expectations were therefore corrected to positive regressions; only
|
||||
npm receives the new post-script manager-option grammar.
|
||||
|
||||
## Wave I: npm hook closure and environment scope
|
||||
|
||||
**Constraint:** Continue in the existing uncommitted tree. Do not write `.git`,
|
||||
stage, or commit. Add focused behavior tests and observe RED before each
|
||||
production change.
|
||||
|
||||
### Task I1: npm pre/main/post dependency closure
|
||||
|
||||
**Files:** `scripts/lib/package-script-graph.ts`,
|
||||
`tests/unit/ci-workflow-generation.test.ts`.
|
||||
|
||||
**Interface:** Explicit npm `run`/`run-script` and implicit
|
||||
`start`/`stop`/`restart`/`test` return existing root-manifest lifecycle hooks in
|
||||
`pre`, main, `post` order. Hooks are omitted only when ordered manager/tail
|
||||
suppression is unambiguously effective before the first literal `--`; bare,
|
||||
false, negative, contradictory, malformed, or post-delimiter suppression keeps
|
||||
hook traversal active or fails closed.
|
||||
|
||||
- [x] Add RED policy/graph tables for nested, test, and restart pre/post hooks.
|
||||
- [x] Add RED suppression positives and false/negative/contradictory/delimiter negatives.
|
||||
- [x] Make npm tail parsing update the invocation suppression state and expand dependencies.
|
||||
- [x] Run hook/parser tests to GREEN.
|
||||
|
||||
### Task I2: Tokenized npm scope environment
|
||||
|
||||
**Files:** `scripts/lib/package-script-graph.ts`, `scripts/contracts/ci-gates.ts`,
|
||||
`tests/unit/ci-workflow-generation.test.ts`.
|
||||
|
||||
**Interface:** Case-insensitive assignments to `npm_config_workspace`,
|
||||
`npm_config_workspaces`, or `npm_config_prefix` fail closed when their shell
|
||||
segment executes npm. Direct assignment, `env`, `/usr/bin/env`, and an exported
|
||||
assignment inherited by a later npm segment are covered without raw-substring
|
||||
false positives for quoted text. `withCiGatePreflight` also rejects the same
|
||||
sensitive keys inherited through `process.env` before entering its callback.
|
||||
|
||||
- [x] Add RED policy/graph coverage for all reviewer assignment forms and quoted/current-tree positives.
|
||||
- [x] Add RED loader/no-callback coverage for command assignments and inherited process environment.
|
||||
- [x] Implement token/segment assignment state and the preflight environment boundary.
|
||||
- [x] Run environment/parser/preflight tests to GREEN.
|
||||
|
||||
### Task I3: Verification
|
||||
|
||||
- [x] Audit every current script through graph and policy consumers.
|
||||
- [x] Run focused parser/preflight, workflow/security, all TypeScript targets,
|
||||
ESLint, `check:ci`, and `git diff --check`.
|
||||
- [x] Update durable operations/security documentation and record sandbox-only
|
||||
nested spawn failures separately; do not stage or commit.
|
||||
|
||||
**Verification evidence:** Hook closure began RED 9/9 and GREEN 9/9;
|
||||
ordered suppression began with 6 expected failures and finished GREEN 22/22;
|
||||
wrapper/export/inherited environment coverage began with 7 expected failures
|
||||
and finished GREEN 28/28. A final all-command-class environment RED 3/3
|
||||
closed scoped lifecycle and builtin invocations. The combined Wave I focused
|
||||
selection passed 59/59. The complete workflow file passed 269/271; its only
|
||||
two failures were the existing nested child-spawn `EPERM` fixtures. Security,
|
||||
supply-chain, and local-promotion tests passed 64/64. All six TypeScript
|
||||
targets, ESLint, `check:ci`, and `git diff --check` passed. The checked-in tree
|
||||
had zero policy failures across 109 scripts, zero graph failures across 108
|
||||
entries after excluding the intentional direct runner entry `ci:gate`, and no
|
||||
sensitive inherited npm scope environment. No `.git` write was performed.
|
||||
|
||||
## Wave J: coherent npm environment state and hook semantics
|
||||
|
||||
**Constraint:** Continue in the existing uncommitted tree. Do not write `.git`,
|
||||
stage, or commit. Add each reviewer form as a failing regression before changing
|
||||
the parser.
|
||||
|
||||
### Task J1: Stateful shell npm-scope environment analysis
|
||||
|
||||
**Files:** `scripts/lib/package-script-graph.ts`,
|
||||
`tests/unit/ci-workflow-generation.test.ts`.
|
||||
|
||||
**Interface:** Shell segments carry a conservative npm-scope environment state
|
||||
to later reachable npm invocations. The parser recognizes any static path whose
|
||||
basename is `env`, optionally behind `command`, and rejects case-insensitive
|
||||
scope assignments in direct or env-wrapper contexts. Static assignment/export
|
||||
and `set -a` transitions are modeled across segments. Dynamic assignment names
|
||||
and environment mutations that cannot be modeled accurately (`set +a`,
|
||||
`unset`, `export -n`, `eval`, dot/source) make later npm dispatch unsupported.
|
||||
Quoted harmless text and non-scope static assignments remain accepted; analysis
|
||||
uses token and segment structure rather than raw substring matching.
|
||||
|
||||
- [x] Add RED policy/graph tables for every reviewer state transition, env path,
|
||||
command wrapper, dynamic assignment name, and unsupported mutation.
|
||||
- [x] Implement a shared tokenized shell-environment state machine and immediate
|
||||
npm invocation environment inspection.
|
||||
- [x] Add harmless quoted/static positive regressions and run focused tests GREEN.
|
||||
|
||||
### Task J2: Actual npm lifecycle-hook suppression semantics
|
||||
|
||||
**Files:** `scripts/lib/package-script-graph.ts`,
|
||||
`tests/unit/ci-workflow-generation.test.ts`.
|
||||
|
||||
**Interface:** Existing pre/main/post hooks are traversed for `run`,
|
||||
`run-script`, `start`, `stop`, `restart`, and `test`. Bare `--ignore-scripts`
|
||||
means true and omits hooks just like explicit true. False, negative,
|
||||
contradictory, malformed, and post-delimiter forms retain hook traversal or fail
|
||||
closed according to the existing ordered grammar.
|
||||
|
||||
- [x] Add hook safety/order coverage for run-script, start, and stop.
|
||||
- [x] Move bare suppression forms to positive regressions and retain all false,
|
||||
negative, contradictory, and delimiter negatives.
|
||||
- [x] Remove the explicitly-valued distinction and run focused tests GREEN.
|
||||
|
||||
### Task J3: Contract boundary and verification
|
||||
|
||||
- [x] Run every environment reviewer command through policy, graph, contract
|
||||
loading, and `withCiGatePreflight`, asserting the callback is never entered.
|
||||
- [x] Retain the inherited process-environment regression and audit the current
|
||||
package tree for policy/graph false positives.
|
||||
- [x] Run workflow/security suites, all TypeScript targets, ESLint, `check:ci`,
|
||||
and `git diff --check`; record sandbox-only failures and do not stage or commit.
|
||||
|
||||
**Verification evidence:** The initial Wave J selection produced 38 expected
|
||||
failures across loader/policy/graph environment cases and bare hook suppression,
|
||||
then passed 84/84 after implementation. A separate unsupported dynamic env-wrapper
|
||||
expansion regression went RED 2/2 and GREEN 2/2; the final combined selection
|
||||
passed 86/86. The full workflow file passed 327/329, with only the existing two
|
||||
nested child-spawn `EPERM` fixtures failing at the sandbox boundary. Security,
|
||||
supply-chain, local-promotion, and promotion-readiness tests passed 71/71. All
|
||||
six TypeScript targets, ESLint, `check:ci`, and `git diff --check` passed. The
|
||||
checked-in tree had zero policy failures across 109 scripts, zero graph failures
|
||||
across 108 entries after excluding the intentional direct runner entry `ci:gate`,
|
||||
and no sensitive inherited npm scope environment. No `.git` write was performed.
|
||||
|
||||
## Wave K: common shell-prefix grammar
|
||||
|
||||
**Constraint:** Continue in the existing uncommitted tree. Do not write `.git`,
|
||||
stage, or commit. Every wrapper/prefix reviewer command must be RED in all four
|
||||
public enforcement paths before production changes.
|
||||
|
||||
### Task K1: Shared prefix parser and state-builtin targeting
|
||||
|
||||
**Files:** `scripts/lib/package-script-graph.ts`,
|
||||
`tests/unit/ci-workflow-generation.test.ts`.
|
||||
|
||||
**Interface:** A single token-based prefix helper consumes leading static
|
||||
assignments, then exact `command`/`exec` wrapper chains and their supported
|
||||
separator syntax. It reports the effective command token/index, whether parsing
|
||||
is uncertain, and the leading assignments. Both immediate npm env inspection
|
||||
and persistent `export`/`set` state updates use this result. `command --` is
|
||||
accepted; unknown `command` options and unmodeled `exec` options before npm/env
|
||||
fail closed. Static paths retain basename-`env` behavior.
|
||||
|
||||
- [x] Add common policy/graph RED cases for `exec env`, `exec /bin/env`,
|
||||
`command exec env`, `command -- env`, assignment-prefixed `export`, and
|
||||
assignment-prefixed `set -a`.
|
||||
- [x] Reuse the same reviewer table through `loadCiGateContract` and
|
||||
`withCiGatePreflight`, asserting rejection and no callback entry.
|
||||
- [x] Implement the shared prefix parser, route immediate env inspection and
|
||||
state-builtin updates through it, and run the reviewer selection GREEN.
|
||||
- [x] Preserve split assignment/export ordering, dynamic LHS, quoted text,
|
||||
harmless `MESSAGE=...`, and supported command-wrapper positives.
|
||||
|
||||
### Task K2: Hook selection and final verification
|
||||
|
||||
**Files:** `tests/unit/ci-workflow-generation.test.ts`, durable report.
|
||||
|
||||
- [x] Ensure the final focused selection explicitly includes the bare npm hook
|
||||
suppression table as well as prefix/environment policy and runner tests.
|
||||
- [x] Run the full workflow file and security/supply/local-promotion suites;
|
||||
classify only the known nested-spawn sandbox failures separately.
|
||||
- [x] Audit all current scripts through policy and graph, then run all TypeScript
|
||||
targets, ESLint, `check:ci`, and `git diff --check`; do not stage or commit.
|
||||
|
||||
**Verification evidence:** The nine shared shell-prefix reviewer commands began
|
||||
RED in both enforcement tables, producing 18 expected failures across
|
||||
loader/runner and policy/graph, then passed 18/18 after the common parser was
|
||||
connected. The prefix negatives plus harmless positives passed 39/39. The final
|
||||
focused selection explicitly combined prefix cases, dynamic environment cases,
|
||||
effective/bare npm hook suppression, and harmless positives and passed 106/106.
|
||||
The complete workflow file passed 353/355; its only two failures were the known
|
||||
nested child-spawn `EPERM` fixtures. Security, supply-chain, local-promotion, and
|
||||
promotion-readiness tests passed 71/71. The current package tree had zero policy
|
||||
failures across 109 scripts, zero graph failures across 108 entries after
|
||||
excluding `ci:gate`, and no sensitive inherited npm scope environment. All six
|
||||
TypeScript targets, ESLint, `check:ci`, and `git diff --check` passed. The shared
|
||||
workspace was preserved and no `.git` write was performed.
|
||||
|
||||
## Wave L: structural manager-prefix gap rejection
|
||||
|
||||
**Constraint:** Continue in the shared uncommitted tree. Do not write `.git`,
|
||||
stage, or commit. Generalize the existing parser; do not add wrapper names to an
|
||||
allowlist.
|
||||
|
||||
### Task L1: Reject unmodeled tokens before package managers
|
||||
|
||||
**Files:** `scripts/lib/package-script-graph.ts`,
|
||||
`tests/unit/ci-workflow-generation.test.ts`.
|
||||
|
||||
**Interface:** The common shell-prefix result identifies the first effective
|
||||
command after modeled assignments and `command`/`exec` wrappers. When manager
|
||||
scanning later finds a package manager, every token between that effective
|
||||
command position and the manager position must belong to a grammar explicitly
|
||||
consumed by immediate env or corepack parsing. Otherwise the invocation is
|
||||
unsupported. This structural rule covers `nice`, absolute-path `nice`, `nohup`,
|
||||
and future unknown wrappers without naming them.
|
||||
|
||||
- [x] Add policy/graph RED coverage for the four env-wrapper reviewer commands
|
||||
and direct unknown-wrapper manager commands (`nice npm`, `time pnpm`).
|
||||
- [x] Reuse the env-wrapper reviewer commands through contract loading and
|
||||
`withCiGatePreflight`, asserting the callback remains false.
|
||||
- [x] Implement one structural gap check in manager parsing and run RED cases
|
||||
GREEN without adding wrapper names.
|
||||
- [x] Retain modeled assignment, `command`/`exec`/env/corepack, current-tree,
|
||||
quoted echo, and harmless assignment positives.
|
||||
|
||||
### Task L2: Verification
|
||||
|
||||
- [x] Run a focused selection containing structural negatives and all modeled
|
||||
prefix/environment positives.
|
||||
- [x] Run the workflow and security/supply/local-promotion suites, current-tree
|
||||
policy/graph/environment audit, all TypeScript targets, ESLint, `check:ci`, and
|
||||
`git diff --check`; record sandbox-only failures and do not stage or commit.
|
||||
|
||||
**Verification evidence:** The six unmodeled-prefix reviewer commands began RED
|
||||
in both enforcement tables, producing 12 expected loader/runner and policy/graph
|
||||
failures, then passed 12/12 after one structural prefix-gap check was added. The
|
||||
unmodeled negatives plus harmless/modeled positives passed 36/36. The final
|
||||
focused Wave H–L environment/prefix and effective/bare hook selection passed
|
||||
121/121. The full workflow file passed 368/370, with only the two known nested
|
||||
child-spawn `EPERM` fixtures failing at the sandbox boundary. Security,
|
||||
supply-chain, local-promotion, and promotion-readiness tests passed 71/71. The
|
||||
current package tree had zero policy failures across 109 scripts, zero graph
|
||||
failures across 108 entries after excluding `ci:gate`, and no sensitive inherited
|
||||
npm scope environment. All six TypeScript targets, ESLint, `check:ci`, and
|
||||
`git diff --check` passed. No wrapper-name allowlist was added, the workspace was
|
||||
preserved, and no `.git` write was performed.
|
||||
@@ -0,0 +1,499 @@
|
||||
# Provider Evidence Guardian Transaction Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Make guardian startup cleanup derive authority only from identities allocated before spawn while preserving no-replace raw/sealed publication and immediate safe retry.
|
||||
|
||||
**Architecture:** Before spawn, the client pins the canonical raw/evidence directories and exclusively allocates nonce-private raw-staging/sealed-temp inodes whose handles and identities it retains. The guardian inherits directory fd 3/fd 4 and private-file fd 5/fd 6, binds strictly validated aliases to those inherited identities, and transfers raw authority with a no-replace hard link before authenticated READY. The client and guardian clean only pre-recorded identities; neither promotes a pathname-discovered inode to ownership.
|
||||
|
||||
**Tech Stack:** Node.js 24 TypeScript, Vitest, Linux file identities and procfs, systemd user scopes, bubblewrap, cgroup v2.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Tasks 1-7 are the historical round-four/five record. Round-six Task 8 runs only in `/tmp/guardian-race-fix-y05lvLi1/repo` on top of `d781692`; never modify the original workspace, `/tmp/task3-integration-mU4L7J2u`, or the security-finalizer repository.
|
||||
- Use RED-GREEN-REFACTOR for every production behavior change.
|
||||
- Guardian argv contains only `process.execPath` and the trusted guardian script; its environment is empty, fd 3/fd 4 are the identity-pinned raw/evidence directories, and fd 5/fd 6 are the identity-pinned private raw/sealed allocations.
|
||||
- Every request/ack is canonical length-prefixed JSON with exact ordered fields, strict UTF-8, no NUL, total bounds, a 32-byte nonce, and constant-time authentication.
|
||||
- Canonical raw and sealed paths are derived from guardian `cwd` and provider kind; paths and identities are not accepted in the guard request.
|
||||
- Provider wall timeout is at most 30 minutes and post-processing allowance is exactly 10 minutes; the guardian maximum lease is 40 minutes.
|
||||
- Publication is no-replace and directory-durable. Abort/death/deadline cleans every raw/temp/final path that still names a pinned owned inode.
|
||||
- Preserve all cleanup failures with the primary failure using `AggregateError`.
|
||||
- Do not add PID-exhaustion loops or claim `RLIMIT_NPROC` enforcement.
|
||||
- Do not run or report live systemd/bwrap tests as passing while the approval limit prevents execution.
|
||||
- Never forward raw provider stdout/stderr bytes to supervisor or CI logs.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Versioned Transaction Protocol
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/lib/provider-guardian-protocol.ts`
|
||||
- Modify: `tests/unit/task3-selective-integration.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces:
|
||||
|
||||
```ts
|
||||
type ProviderGuardianGuard = Readonly<{
|
||||
kind: "vulnerability" | "provenance";
|
||||
nonce: Buffer;
|
||||
deadlineEpochMs: number;
|
||||
}>;
|
||||
type ProviderGuardianReady = Readonly<{
|
||||
nonce: Buffer;
|
||||
rawDev: number;
|
||||
rawIno: number;
|
||||
sealedTempLeaf: string;
|
||||
sealedDev: number;
|
||||
sealedIno: number;
|
||||
}>;
|
||||
type ProviderGuardianPublish = Readonly<{
|
||||
nonce: Buffer;
|
||||
sealedDev: number;
|
||||
sealedIno: number;
|
||||
size: number;
|
||||
sha256: string;
|
||||
}>;
|
||||
function encodeProviderGuardianGuard(input: ProviderGuardianGuard): Buffer;
|
||||
function decodeProviderGuardianReady(payload: Buffer, nonce: Buffer): ProviderGuardianReady;
|
||||
function encodeProviderGuardianPublish(input: ProviderGuardianPublish): Buffer;
|
||||
function decodeProviderGuardianPublished(payload: Buffer, nonce: Buffer): void;
|
||||
function encodeProviderGuardianCommit(nonce: Buffer): Buffer;
|
||||
```
|
||||
|
||||
- [ ] **Step 1: Write failing exact-protocol tests**
|
||||
|
||||
Assert that guard contains no path or identity, READY returns authenticated identities, publish binds exact identity/size/SHA-256, PUBLISHED authenticates the same nonce, and duplicate/reordered/trailing/oversized/invalid UTF-8/NUL/short-nonce frames fail.
|
||||
|
||||
```ts
|
||||
expect(JSON.parse(encodeProviderGuardianGuard(guard).subarray(4).toString())).toEqual({
|
||||
type: "guard", version: 2, kind: "vulnerability",
|
||||
nonce: nonce.toString("hex"), deadlineEpochMs,
|
||||
});
|
||||
expect(() => decodeProviderGuardianReady(duplicateNoncePayload, nonce)).toThrow(/canonical|fields/u);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run focused RED**
|
||||
|
||||
Run: `node_modules/.bin/vitest run tests/unit/task3-selective-integration.test.ts --reporter=default --maxWorkers=1`
|
||||
|
||||
Expected: FAIL because the v2 guard/READY/publish/PUBLISHED APIs do not exist and the old guard still accepts identities.
|
||||
|
||||
- [ ] **Step 3: Implement the minimal v2 codecs**
|
||||
|
||||
Use one bounded prefix/strict decode utility, exact ordered key arrays, canonical re-encoding, lowercase 64-hex nonces/SHA-256, safe positive integers, and `timingSafeEqual` for every acknowledgement/authentication comparison.
|
||||
|
||||
- [ ] **Step 4: Run focused GREEN**
|
||||
|
||||
Run the Step 2 command and require the protocol tests to pass.
|
||||
|
||||
### Task 2: Guardian-Owned Raw and Sealed Transaction
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/lib/provider-raw-guardian.ts`
|
||||
- Modify: `scripts/lib/provider-raw-cleanup.ts`
|
||||
- Modify: `tests/unit/task3-selective-integration.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1 codecs.
|
||||
- Produces: real process state machine `guard -> READY -> publish -> PUBLISHED -> commitPending -> EOF success`.
|
||||
|
||||
- [ ] **Step 1: Write failing real-process creation tests**
|
||||
|
||||
Cover no-frame and partial-frame EOF with no files, authenticated READY-created raw/temp identities and modes, full-frame parent EOF cleanup before commit, deadline cleanup, and a near-timeout successful transaction.
|
||||
|
||||
```ts
|
||||
child.stdin.end(partialFrame);
|
||||
await completion;
|
||||
await expect(lstat(rawPath)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
expect(await lstat(rawPath)).toMatchObject({ mode: expect.any(Number) });
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify creation RED**
|
||||
|
||||
Run the focused test and require failure because the existing guardian expects supervisor-created identity and emits line-based READY.
|
||||
|
||||
- [ ] **Step 3: Implement exclusive creation and READY**
|
||||
|
||||
Derive canonical leaves, create raw and random sealed sibling temp with `O_EXCL|O_NOFOLLOW`, set raw/temp `0600`, fstat identities, close raw, keep temp handle, and emit bounded READY. On every error, attempt all owned cleanup before nonzero exit.
|
||||
|
||||
- [ ] **Step 4: Write failing publish/state tests**
|
||||
|
||||
Write validated bytes to the pinned temp, request publish, require PUBLISHED and final mode/hash/identity, then verify commit waits for EOF. Send one later trailing byte after commit and require final cleanup/nonzero exit. Kill the parent after PUBLISHED and require raw/temp/final absence.
|
||||
|
||||
- [ ] **Step 5: Implement no-replace durable publish and serialized terminal cleanup**
|
||||
|
||||
Verify held descriptor/path identity, `nlink=1`, `0400`, size, and SHA-256. Use `link(temp, final)`, `unlink(temp)`, final lstat identity, and parent-directory fsync. Serialize frame and EOF handling so a publish/death race cannot bypass cleanup. Commit removes raw and sets `commitPending`; only clean EOF exits zero.
|
||||
|
||||
- [ ] **Step 6: Run real-process GREEN**
|
||||
|
||||
Run focused tests and require zero raw/temp/final/process residuals in every failure case.
|
||||
|
||||
### Task 3: Authenticated Client Lease and Fallback Cleanup
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/lib/provider-guardian-client.ts`
|
||||
- Modify: `scripts/lib/validated-json-artifact.ts`
|
||||
- Modify: `tests/unit/task3-selective-integration.test.ts`
|
||||
- Modify: `tests/unit/validated-json-artifact.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces:
|
||||
|
||||
```ts
|
||||
type ProviderGuardianLease = Readonly<{
|
||||
pid: number;
|
||||
rawPath: string;
|
||||
rawIdentity: Readonly<{ dev: number; ino: number }>;
|
||||
sealedPath: string;
|
||||
sealedTempPath: string;
|
||||
sealedIdentity: Readonly<{ dev: number; ino: number }>;
|
||||
prematureExit: Promise<Error>;
|
||||
publish(bytes: Buffer): Promise<void>;
|
||||
commit(): Promise<void>;
|
||||
abort(): Promise<void>;
|
||||
}>;
|
||||
function serializeValidatedJsonArtifact(input: ValidatedJsonArtifactInput): Buffer;
|
||||
```
|
||||
|
||||
- [ ] **Step 1: Write failing client transaction tests**
|
||||
|
||||
Require exact guardian argv and empty environment, READY identity capture, pinned temp write/fsync/mode, PUBLISHED wait, exactly-one terminal action, post-READY guardian SIGKILL cleanup of raw/temp/final, and cleanup error aggregation.
|
||||
|
||||
- [ ] **Step 2: Verify client RED**
|
||||
|
||||
Run focused and validated-writer tests. Expect missing publish/identity/serializer APIs.
|
||||
|
||||
- [ ] **Step 3: Implement serialization and lease**
|
||||
|
||||
Extract the existing schema-parse/pretty-JSON/newline serialization without changing `writeValidatedJsonArtifact`. Open the returned temp with `O_NOFOLLOW`, fstat identity, truncate/write/chmod `0400`/fsync/fstat/close, send authenticated publish metadata, and wait for PUBLISHED. Fallback cleanup attempts raw, temp, and final using READY identities and aggregates failures.
|
||||
|
||||
- [ ] **Step 4: Run client GREEN**
|
||||
|
||||
Run the Step 2 tests and require exact bytes, identities, cleanup, and no residual child.
|
||||
|
||||
### Task 4: Supervisor Transaction and Scope-Active Latch
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/run-and-validate-provider.ts`
|
||||
- Modify: `tests/unit/task3-selective-integration.test.ts`
|
||||
- Modify: `tests/unit/ci-artifact-contract.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 3 lease and serializer.
|
||||
- Produces: guardian-owned raw/provider execution, awaited publication, output append, commit/EOF, and scope-confined kill ownership.
|
||||
|
||||
- [ ] **Step 1: Write failing supervisor ordering/latch tests**
|
||||
|
||||
Require no `createProviderOutput`, lease start before provider, lease raw identity passed to scope, serialized bytes published before output append, commit after output append, and postprocess allowance included in lease. Add a pure scope-latch unit boundary or static contract proving the guardian callback can call `killProviderUnit` only while `scopeActive` is true.
|
||||
|
||||
- [ ] **Step 2: Verify supervisor RED**
|
||||
|
||||
Run focused tests and expect the old create/write/cleanup ordering assertions to fail.
|
||||
|
||||
- [ ] **Step 3: Integrate the lease transaction**
|
||||
|
||||
Start guardian in `executeProvider`, use READY raw path/identity for provider bind and capture, publish serialized validated evidence through the lease, append output, then commit. Remove supervisor raw creation and normal sealed writer publication. Keep only identity-bound lease fallback cleanup.
|
||||
|
||||
Set `PROVIDER_POSTPROCESS_TIMEOUT_MS = 600_000` and request `providerWallTimeoutMs + PROVIDER_POSTPROCESS_TIMEOUT_MS`.
|
||||
|
||||
- [ ] **Step 4: Implement scope-active guardian exit ownership**
|
||||
|
||||
Race an awaited scope-completion promise against termination. The guardian callback records its error and invokes termination only while `scopeActive`; the same function sets the latch false exactly once when kill/collection or normal collection completes. The callback never throws or creates an unobserved kill promise after the latch closes.
|
||||
|
||||
- [ ] **Step 5: Run supervisor GREEN**
|
||||
|
||||
Run focused tests and type/lint checks. Live systemd tests remain unexecuted and are not reported as passing.
|
||||
|
||||
### Task 5: Regression Fixtures and Documentation
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/unit/task3-selective-integration.test.ts`
|
||||
- Modify: `tests/unit/ci-artifact-contract.test.ts`
|
||||
- Modify: `docs/operations/ci-quality-gates.md`
|
||||
- Modify: `docs/security/supply-chain.md`
|
||||
- Modify: `docs/superpowers/specs/2026-08-02-provider-raw-guardian-design.md`
|
||||
|
||||
- [ ] **Step 1: Complete real-process regressions**
|
||||
|
||||
Cover no/partial frame, parent kill near READY, post-READY guardian kill, PUBLISHED parent death, publish/commit race, later-chunk commit trailing data, deadline/near-timeout, same-workspace retry, and zero guardian/raw/temp/final residuals.
|
||||
|
||||
- [ ] **Step 2: Specify live regressions**
|
||||
|
||||
Add active-scope guardian kill, post-scope/precommit guardian kill, supervisor hard death after PUBLISHED with same-workspace retry, and detached descendant attempts for both an external marker and raw append. Every case requires zero cgroup/process/file residuals. Do not execute these tests under the current approval limit.
|
||||
|
||||
- [ ] **Step 3: Correct operations and security docs**
|
||||
|
||||
Document guardian-owned creation/publication, READY/PUBLISHED identities, ten-minute postprocess lease, commitPending/EOF success, no-replace link publication, scopeActive kill ownership, regular-file `GITHUB_OUTPUT`, and explicit live-test limitation.
|
||||
|
||||
- [ ] **Step 4: Fresh verification**
|
||||
|
||||
Run focused real-process tests, validated artifact tests, direct Node/test/recipe TypeScript configs, full lint, artifact schemas, CI contract, generated workflow byte check, and `git diff --check`. Record broad-suite sandbox `EPERM` separately and never convert unexecuted live tests into PASS.
|
||||
|
||||
- [ ] **Step 5: Review and commit round four**
|
||||
|
||||
Confirm only the isolated worktree changed, no protocol secret/path enters argv, cleanup checks both sealed names by identity, and only the temp `node_modules` symlink remains untracked. Create a separate round-four implementation commit above the design/plan commit.
|
||||
|
||||
### Task 6: Round-Five Pre-READY Recovery Authority
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/lib/provider-guardian-protocol.ts`
|
||||
- Modify: `scripts/lib/provider-guardian-client.ts`
|
||||
- Modify: `scripts/lib/provider-raw-guardian.ts`
|
||||
- Test: `tests/unit/provider-guardian-transaction.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `providerGuardianSealedTempLeaf(kind, nonce): string`, inherited raw/evidence directory fds 3/4, and descriptor-relative startup/lease cleanup.
|
||||
|
||||
- [x] **Step 1: Write failing pre-READY hard-death tests**
|
||||
|
||||
Start the real client without awaiting READY, observe its direct guardian child,
|
||||
kill the guardian when either deterministic transaction leaf first appears, and
|
||||
require startup rejection, zero raw/temp/final residuals, and a successful
|
||||
same-workspace `startProviderGuardian(...).abort()` retry. Also require the temp
|
||||
leaf computed before spawn to equal READY exactly and inherited fd 3/fd 4 to
|
||||
remain directories during the lease.
|
||||
|
||||
- [x] **Step 2: Run focused RED**
|
||||
|
||||
Run: `node_modules/.bin/vitest run tests/unit/provider-guardian-transaction.test.ts --reporter=default --maxWorkers=1`
|
||||
|
||||
Expected: FAIL because the client has neither pre-spawn directory handles nor a
|
||||
deterministic temp leaf and cannot clean a guardian killed before READY.
|
||||
|
||||
- [x] **Step 3: Implement pinned descriptor recovery**
|
||||
|
||||
Open and verify the canonical raw/evidence directories with
|
||||
`O_DIRECTORY|O_NOFOLLOW`; derive the temp leaf from provider kind and the first
|
||||
16 nonce bytes; spawn with those handles at fd 3/fd 4. Use only
|
||||
`/proc/self/fd/<fd>/<leaf>` for guardian creation, publication, sync, and cleanup.
|
||||
On startup failure, open each exact leaf through the still-live client
|
||||
descriptor, fstat a regular single-link inode, close the discovery handle, and
|
||||
run identity-bound quarantine/unlink. Aggregate primary, cleanup, and directory
|
||||
close errors. Retain both handles until commit/abort terminates.
|
||||
|
||||
- [x] **Step 4: Run focused GREEN**
|
||||
|
||||
Run the Step 2 command and require the pre-READY kill/retry and all round-four
|
||||
transaction tests to pass.
|
||||
|
||||
### Task 7: Round-Five Log Privacy and Terminal Fail-Closed Behavior
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/run-and-validate-provider.ts`
|
||||
- Modify: `scripts/lib/provider-raw-guardian.ts`
|
||||
- Test: `tests/unit/provider-guardian-transaction.test.ts`
|
||||
- Test: `tests/unit/ci-artifact-contract.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 6 descriptor-pinned transaction.
|
||||
- Produces: bounded discard of provider output and nonzero guardian termination even when diagnostic fds are closed.
|
||||
|
||||
- [x] **Step 1: Write failing privacy and closed-stderr tests**
|
||||
|
||||
Run a successful provider that both receives and prints a unique
|
||||
`VULNERABILITY_PROVIDER_*` credential, then assert the credential is absent from
|
||||
supervisor stdout/stderr while the sealed signed evidence succeeds. Replace the
|
||||
FD-limit provider's stderr marker expectations with evidence/side-channel state.
|
||||
Spawn a real guardian with stderr's read side destroyed, establish owned files,
|
||||
then abort or send invalid input and require zero files plus a nonzero exit.
|
||||
|
||||
- [x] **Step 2: Run targeted RED**
|
||||
|
||||
Run the focused guardian and selected CI artifact tests. Expect credential
|
||||
disclosure and the existing raw provider stderr marker assertions to fail the
|
||||
new contract; the EPIPE case can exit without the required nonzero terminal.
|
||||
The executable non-live RED observed six expected failures: missing deterministic
|
||||
leaf/fd inheritance/output limiter, retained pre-READY raw, and closed-stderr
|
||||
exit 0. The live credential-printing fixture is authored but remains NOT RUN.
|
||||
|
||||
- [x] **Step 3: Implement minimal privacy and terminal fixes**
|
||||
|
||||
Continue counting provider stdout/stderr bytes against the aggregate output
|
||||
limit but discard captured bytes instead of retaining or forwarding them. Make
|
||||
guardian fd-close and stderr diagnostics best effort, run cleanup first, and
|
||||
place `process.exit(exitCode)` or self-`SIGKILL` in an unconditional final
|
||||
branch that cannot be skipped by `EPIPE`/`EBADF`.
|
||||
|
||||
- [x] **Step 4: Run targeted GREEN and regression verification**
|
||||
|
||||
Run focused guardian tests, selected non-live privacy tests, Node/test
|
||||
TypeScript, affected ESLint, docs readiness, and `git diff --check`. Do not run
|
||||
live systemd/bwrap tests under the approval limit.
|
||||
|
||||
- [x] **Step 5: Commit round five implementation**
|
||||
|
||||
Commit production, tests, and operational/security documentation separately
|
||||
above this round-five design/plan commit. Record live systemd/bwrap as NOT RUN.
|
||||
|
||||
Round-five verification record:
|
||||
|
||||
- Focused real-process/unit GREEN: 4 files, 52 tests passed.
|
||||
- Direct Node and test TypeScript projects: PASS.
|
||||
- Affected ESLint with zero warnings: PASS.
|
||||
- Documentation readiness: `PASS_SCOPED`.
|
||||
- `git diff --check`: PASS.
|
||||
- Live systemd/bwrap credential, FD-limit, cgroup, and hard-death fixtures:
|
||||
**NOT RUN** because the active approval limit forbids those executions.
|
||||
|
||||
### Task 8: Round-Six Pre-READY Inode Ownership
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/lib/provider-guardian-protocol.ts`
|
||||
- Modify: `scripts/lib/provider-guardian-client.ts`
|
||||
- Modify: `scripts/lib/provider-raw-guardian.ts`
|
||||
- Test: `tests/unit/provider-guardian-transaction.test.ts`
|
||||
- Modify: `docs/security/supply-chain.md`
|
||||
- Modify: `docs/operations/ci-quality-gates.md`
|
||||
- Modify: `docs/superpowers/specs/2026-08-02-provider-raw-guardian-design.md`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces:
|
||||
|
||||
```ts
|
||||
function providerGuardianRawStagingLeaf(
|
||||
kind: ProviderGuardianKind,
|
||||
nonce: Buffer,
|
||||
): string;
|
||||
|
||||
type RecoveryAuthority = Readonly<{
|
||||
rawDirectoryHandle: FileHandle;
|
||||
evidenceDirectoryHandle: FileHandle;
|
||||
rawStagingHandle: FileHandle;
|
||||
sealedTempHandle: FileHandle;
|
||||
rawIdentity: Readonly<{ dev: number; ino: number }>;
|
||||
sealedIdentity: Readonly<{ dev: number; ino: number }>;
|
||||
rawStagingPinnedPath: string;
|
||||
rawPinnedPath: string;
|
||||
sealedTempPinnedPath: string;
|
||||
sealedPinnedPath: string;
|
||||
}>;
|
||||
```
|
||||
|
||||
- [ ] **Step 1: Write the deterministic external-canary RED**
|
||||
|
||||
Create a temporary guardian fixture that writes a spawn marker and remains
|
||||
alive without producing READY. Start the real client, wait for that marker (so
|
||||
`assertRecoveryLeavesMissing` has completed), create a fixed-raw canary, kill
|
||||
the direct guardian, and require startup rejection without canary deletion or
|
||||
mutation.
|
||||
|
||||
```ts
|
||||
const canaryBytes = Buffer.from("external-canary\n");
|
||||
const canaryHandle = await open(rawPath, constants.O_CREAT | constants.O_EXCL |
|
||||
constants.O_WRONLY | constants.O_NOFOLLOW, 0o600);
|
||||
await canaryHandle.writeFile(canaryBytes);
|
||||
const canaryIdentity = await canaryHandle.stat();
|
||||
await canaryHandle.close();
|
||||
process.kill(guardianPid, "SIGKILL");
|
||||
await expect(starting).rejects.toThrow(/provider guardian/u);
|
||||
expect(await readFile(rawPath)).toEqual(canaryBytes);
|
||||
expect(await lstat(rawPath)).toMatchObject({
|
||||
dev: canaryIdentity.dev,
|
||||
ino: canaryIdentity.ino,
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the canary RED and confirm the ownership bug**
|
||||
|
||||
Run:
|
||||
`node_modules/.bin/vitest run tests/unit/provider-guardian-transaction.test.ts -t "preserves an external raw canary" --reporter=default --maxWorkers=1`
|
||||
|
||||
Expected: FAIL with `ENOENT` when reading the canary because
|
||||
`discoverAndCleanupOwnedLeaf` opens the current raw pathname and promotes the
|
||||
external inode to cleanup authority.
|
||||
|
||||
- [ ] **Step 3: Add private-leaf derivation and client allocations**
|
||||
|
||||
Derive raw staging and sealed temp from the same first 16 nonce bytes:
|
||||
|
||||
```ts
|
||||
return `.${baseLeaf(kind)}.guardian-${nonce.subarray(0, 16).toString("hex")}.raw.tmp`;
|
||||
```
|
||||
|
||||
Through the pinned directory paths, create raw staging and sealed temp with
|
||||
`O_RDWR|O_CREAT|O_EXCL|O_NOFOLLOW`, mode `0600`; require regular file, link count
|
||||
one, mode `0600`, and size zero; store identities before spawn. Spawn with fd
|
||||
3-fd 6. If allocation, validation, or spawn fails, identity-clean every private
|
||||
alias and close every opened handle while preserving primary and cleanup/close
|
||||
errors in one `AggregateError`.
|
||||
|
||||
- [ ] **Step 4: Add concurrency, bootstrap, and link-before-READY RED tests**
|
||||
|
||||
Add real-process tests that require:
|
||||
|
||||
```ts
|
||||
// no/partial frame: bootstrap-owned private aliases are removed
|
||||
child.stdin!.end(partialFrame);
|
||||
await expect(readdir(rawDirectory)).resolves.toEqual([]);
|
||||
|
||||
// same kind: exactly one READY lease, loser never removes winner raw
|
||||
const results = await Promise.allSettled([startProviderGuardian(input), startProviderGuardian(input)]);
|
||||
expect(results.filter(({ status }) => status === "fulfilled")).toHaveLength(1);
|
||||
expect(results.filter(({ status }) => status === "rejected")).toHaveLength(1);
|
||||
|
||||
// canonical raw link exists but READY has not been accepted
|
||||
process.kill(guardianPid, "SIGKILL");
|
||||
await expect(starting).rejects.toThrow(/provider guardian/u);
|
||||
await expect(lstat(rawPath)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
```
|
||||
|
||||
The link-before-READY test watches only the fixed raw basename, obtains the
|
||||
direct child pid before the event, and kills on that exact link event so private
|
||||
allocation events cannot satisfy the synchronization point. Each test performs
|
||||
a same-workspace retry and requires no owned private/canonical residue.
|
||||
|
||||
- [ ] **Step 5: Implement guardian bootstrap identity binding**
|
||||
|
||||
At process bootstrap, fstat fd 5/fd 6 and read `/proc/self/fd/5|6`. Accept an
|
||||
alias only if `dirname(readlink)` is the canonical expected directory, basename
|
||||
is a direct child matching the exact raw-staging or sealed-temp lowercase-hex
|
||||
grammar, both names encode the same kind/nonce prefix, and descriptor-relative
|
||||
lstat equals the inherited fd identity/type/mode/size/link count. Store the fd
|
||||
identity before reading any pathname; the pathname only becomes an alias for
|
||||
that identity.
|
||||
|
||||
On valid guard, require exact `providerGuardianRawStagingLeaf(kind, nonce)` and
|
||||
`providerGuardianSealedTempLeaf(kind, nonce)` matches. Use
|
||||
`link(rawStaging, rawCanonical)` without replacement, check both aliases equal
|
||||
the inherited raw identity with link count two, unlink raw staging, fsync fd 3,
|
||||
and check raw canonical remains the same identity with link count one before
|
||||
READY. Use the inherited sealed identity for READY and publication.
|
||||
|
||||
- [ ] **Step 6: Replace discovery cleanup and close all private fds**
|
||||
|
||||
Delete `discoverAndCleanupOwnedLeaf`. Client pre-READY and fallback cleanup
|
||||
attempts raw staging/canonical with only `recovery.rawIdentity`, then sealed
|
||||
temp/final with only `recovery.sealedIdentity`. Guardian no/partial-frame and
|
||||
terminal cleanup uses only its bootstrap fd identities and bound aliases.
|
||||
|
||||
On success and every failure branch, attempt all cleanup first, close guardian
|
||||
fd 5/fd 6 duplicates and client fd 3-fd 6 handles exactly once, and append every
|
||||
close failure to the existing aggregate. Never open a current leaf to obtain a
|
||||
new cleanup identity.
|
||||
|
||||
- [ ] **Step 7: Run focused GREEN and regressions**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node_modules/.bin/vitest run tests/unit/provider-guardian-transaction.test.ts --reporter=default --maxWorkers=1
|
||||
node_modules/.bin/vitest run tests/unit/provider-output-limiter.test.ts tests/unit/ci-artifact-contract.test.ts --reporter=default --maxWorkers=1
|
||||
node_modules/.bin/tsc --project tsconfig.node.json
|
||||
node_modules/.bin/tsc --project tsconfig.test.json
|
||||
node_modules/.bin/eslint scripts/lib/provider-guardian-protocol.ts scripts/lib/provider-guardian-client.ts scripts/lib/provider-raw-guardian.ts tests/unit/provider-guardian-transaction.test.ts --max-warnings=0
|
||||
node scripts/verify-documentation-readiness.ts
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Require focused tests, Node/test TypeScript, affected ESLint, documentation
|
||||
readiness, and whitespace verification to pass. Live systemd/bwrap fixtures
|
||||
remain **NOT RUN** under the current approval limit.
|
||||
|
||||
- [ ] **Step 8: Review and commit round six**
|
||||
|
||||
Confirm the original workspace, `/tmp/task3-integration-mU4L7J2u`, and the
|
||||
security-finalizer repository are unchanged; only the temporary `node_modules`
|
||||
symlink is untracked. Commit production/tests/docs together above design commit
|
||||
`0b1a1db` and report the isolated path, commit SHA, RED evidence, and fresh GREEN
|
||||
evidence.
|
||||
@@ -0,0 +1,222 @@
|
||||
# Provider Evidence Guardian Transaction Design
|
||||
|
||||
## Goal
|
||||
|
||||
Make one guardian process own the provider evidence filesystem transaction from
|
||||
raw creation through sealed publication. A supervisor or provider hard death
|
||||
must leave neither canonical raw evidence nor a guardian-owned sealed temp/final
|
||||
inode, and the same workspace must be immediately retryable. Only the complete
|
||||
authenticated `publish -> PUBLISHED -> commit -> EOF` sequence preserves the
|
||||
canonical sealed artifact.
|
||||
|
||||
## Chosen Ownership Boundary
|
||||
|
||||
The guardian owns filesystem identity and publication. The supervisor retains
|
||||
archive, trust, schema, signature, and evidence validation. This avoids two
|
||||
unsafe alternatives:
|
||||
|
||||
- Keeping raw-only guardianship would leave the sealed rename-to-supervisor-death
|
||||
cleanup gap.
|
||||
- Moving evidence validation into the guardian would duplicate security policy
|
||||
and make the helper unnecessarily privileged and complex.
|
||||
|
||||
The client opens the canonical `provider-evidence/untrusted` and
|
||||
`provider-evidence` directories with `O_DIRECTORY|O_NOFOLLOW` before spawning
|
||||
the guardian. Those identity-pinned directory descriptors are inherited as fd 3
|
||||
and fd 4; they are never encoded in argv or the environment. The canonical raw
|
||||
and final leaves are fixed by provider kind. Before spawn, the client exclusively
|
||||
creates a nonce-private raw staging inode and the nonce-private sealed temp
|
||||
inode, records both identities, and inherits their open descriptors as fd 5 and
|
||||
fd 6. The guardian validates each inherited descriptor against its
|
||||
descriptor-relative pathname, then publishes raw staging to the fixed raw leaf
|
||||
with a no-replace hard link. Startup recovery authority is therefore an inode
|
||||
identity acquired before spawn, never an identity discovered later from an
|
||||
expected pathname. Both processes perform transaction I/O through
|
||||
`/proc/self/fd/<fd>/<leaf>` so pathname substitution cannot redirect creation or
|
||||
recovery into another directory. No raw path, sealed path, identity, provider
|
||||
command, or credential is an argv value. The initial request contains only the
|
||||
version, kind, random control nonce, and absolute deadline.
|
||||
|
||||
## Transaction Invariants
|
||||
|
||||
1. Before a complete valid guard frame, the guardian has not published a
|
||||
canonical filesystem object. The client may have allocated only zero-byte,
|
||||
mode `0600`, nonce-private raw staging and sealed temp inodes whose identities
|
||||
it already holds. EOF with no frame or a partial frame removes both allocations.
|
||||
2. Before spawning, the client validates that both pinned descriptors name the
|
||||
expected canonical directories; computes fixed raw/final leaves and
|
||||
nonce-private raw-staging/sealed-temp leaves; and creates the private leaves
|
||||
with `O_CREAT|O_EXCL|O_NOFOLLOW`, mode `0600`, size zero, and link count one.
|
||||
It retains both handles and inherits them as fd 5/fd 6 in addition to directory
|
||||
fd 3/fd 4.
|
||||
3. At bootstrap, the guardian fstats fd 5/fd 6, reads only their
|
||||
`/proc/self/fd/5|6` link targets, and accepts each basename only when it is a
|
||||
direct child of the canonical fd 3/fd 4 directory and matches the exact
|
||||
provider-kind/32-lowercase-hex private-leaf grammar. It then requires
|
||||
descriptor-relative lstat of that basename to match the already-fstat fd
|
||||
identity, type, mode, size, and link count. This binds a deletion alias to an
|
||||
inherited identity; it never promotes a pathname-discovered identity to
|
||||
ownership. The two basenames must encode the same kind and nonce prefix.
|
||||
4. After guard validation, the guardian verifies that the received kind/nonce
|
||||
derives those exact bootstrapped private leaves. It verifies fd 5/fd 6 remain
|
||||
regular zero-byte single-link `0600` files and exactly match the derived
|
||||
private pathnames. It
|
||||
uses `link(raw staging, canonical raw)` without replacement, verifies both
|
||||
names have the inherited raw identity and link count two, unlinks the private
|
||||
raw name, fsyncs the raw directory, and verifies the canonical raw link count
|
||||
is one. READY is emitted only after this authority transfer succeeds.
|
||||
5. READY is authenticated by the request nonce and returns raw dev/inode plus
|
||||
sealed temp leaf/dev/inode. The supervisor starts the provider only after it
|
||||
validates this exact bounded response with constant-time nonce equality.
|
||||
6. The supervisor writes only schema-validated sealed bytes to the temp inode.
|
||||
It opens with `O_NOFOLLOW`, checks dev/inode before and after writing, applies
|
||||
mode `0400`, writes the complete bounded bytes, fsyncs, and closes.
|
||||
7. Publish metadata contains the nonce, sealed dev/inode, byte length, and
|
||||
SHA-256. The guardian checks the held descriptor and temp pathname identity,
|
||||
regular-file type, link count, exact mode/size/hash, and canonical final-path
|
||||
absence.
|
||||
8. Publication uses atomic no-replace `link(temp, final)`, then unlinks temp and
|
||||
fsyncs the parent directory. If death occurs between link and unlink, both
|
||||
names refer to the same pinned inode and both are cleanup candidates.
|
||||
9. PUBLISHED is authenticated and is emitted only after final pathname identity
|
||||
and directory durability are verified.
|
||||
10. Commit is legal only after PUBLISHED. It removes the pinned raw inode and
|
||||
enters `commitPending`; it does not exit. EOF with no pending bytes is the
|
||||
sole success terminal and preserves only the sealed final inode.
|
||||
11. Any data after commit, including a separate later chunk, is a protocol error.
|
||||
EOF/abort/deadline/protocol failure before the success terminal cleans raw,
|
||||
temp, and final only when each path still names the guardian-owned identity.
|
||||
12. If the guardian dies before READY is accepted, the client attempts cleanup
|
||||
of raw staging, canonical raw, sealed temp, and sealed final aliases using
|
||||
only the two identities recorded before spawn. A current pathname is never
|
||||
opened and promoted to an owned identity. A competing canary or same-kind
|
||||
transaction therefore survives every startup failure.
|
||||
13. Cleanup attempts every owned target and reports cleanup failures together
|
||||
with the primary failure using `AggregateError` at the supervisor boundary.
|
||||
Client fd 3-fd 6 handles and guardian fd 5/fd 6 duplicates are closed on
|
||||
every success and failure branch; close errors join the same aggregate rather
|
||||
than skipping remaining cleanup.
|
||||
|
||||
Client-side exclusive private allocation is the startup ownership token. The
|
||||
guardian accepts that token only after inherited-fd, descriptor-relative
|
||||
pathname, type, mode, size, and link-count checks. Every cleanup identity is
|
||||
recorded at allocation or authenticated READY; pathname discovery never creates
|
||||
authority. Creation, validation, link, unlink, chmod, fstat, close, publish,
|
||||
sync, and cleanup failures all fail closed.
|
||||
|
||||
## Bounded Authenticated Protocol
|
||||
|
||||
Every control or acknowledgement message is a four-byte big-endian length plus
|
||||
canonical JSON with an exact ordered field set, strict UTF-8, no NUL, and a
|
||||
total payload bound. Unknown, duplicate, reordered, oversized, truncated, or
|
||||
trailing fields are rejected.
|
||||
|
||||
The state sequence is:
|
||||
|
||||
```text
|
||||
guard -> READY(raw identity, sealed temp identity)
|
||||
-> publish(size, sha256, sealed identity)
|
||||
-> PUBLISHED(sealed identity)
|
||||
-> commit
|
||||
-> EOF success
|
||||
```
|
||||
|
||||
All messages carry the same 32-byte random nonce. READY and PUBLISHED are
|
||||
validated with `timingSafeEqual`; publish and commit are authenticated the same
|
||||
way. Commit merely changes state, so a byte delivered in a later chunk before
|
||||
EOF remains observable and causes fail-closed cleanup.
|
||||
|
||||
The maximum initial lease is the provider wall timeout plus a fixed ten-minute
|
||||
post-processing allowance. The provider timeout remains bounded at 30 minutes,
|
||||
so the guardian maximum is 40 minutes. Near-provider-timeout tests must show
|
||||
that valid publication still has post-processing time, while an expired lease
|
||||
cleans all owned objects.
|
||||
|
||||
## Supervisor and Scope Exit Ownership
|
||||
|
||||
The lease exposes raw/temp/final identities, `publish(bytes)`, `commit()`,
|
||||
`abort()`, and a non-rejecting premature-exit promise. The client knows all
|
||||
possible leaves and both startup identities before spawn and retains its pinned
|
||||
directory and private-file handles until the lease terminates. Before READY it
|
||||
cleans only aliases that still match those recorded identities. After READY it
|
||||
checks the guardian response against the same identities and fallback-cleans
|
||||
raw, temp, and final by identity if the guardian dies.
|
||||
|
||||
Provider waiting owns an explicit `scopeActive` latch. A guardian exit starts
|
||||
whole-scope kill and collection only while that latch is true. Once the scope
|
||||
completion path has collected the unit, the callback records a lifecycle error
|
||||
but cannot start an unawaited kill. Publication and terminal commit observe the
|
||||
guardian exit through their normal awaited failure path and clean sealed state.
|
||||
|
||||
Provider stdout and stderr are untrusted secret-bearing byte streams. The
|
||||
supervisor counts and bounds them for resource enforcement but never forwards
|
||||
their raw bytes into supervisor/CI stdout or stderr, on either success or
|
||||
failure. Functional provider assertions use signed evidence or a non-log side
|
||||
channel. Sealing/output I/O is allowed to settle; the design does not claim
|
||||
OS-level cancellation. `GITHUB_OUTPUT` is a runner-owned regular file. After
|
||||
output append succeeds, commit makes the guardian remove raw and EOF completes
|
||||
the transaction.
|
||||
|
||||
Guardian diagnostics are best-effort only. A closed stderr or control descriptor
|
||||
must not turn a fail-closed branch into a resolved operation or exit zero:
|
||||
diagnostic and fd-close failures are absorbed after cleanup, and a nonzero exit
|
||||
or requested fatal signal is issued unconditionally.
|
||||
|
||||
## Failure and Recovery
|
||||
|
||||
- No/partial guard EOF: no canonical raw or sealed object is published. The
|
||||
guardian removes both nonce-private allocations through aliases that bootstrap
|
||||
already bound to inherited fd identities, without needing kind/nonce from a
|
||||
complete control frame.
|
||||
- A competing canonical raw canary or another same-kind attempt causes
|
||||
no-replace link failure. The loser removes only its private identities and
|
||||
never removes the winner or canary.
|
||||
- Guardian death after linking raw but before READY: the client uses its
|
||||
pre-recorded raw identity to clean both private and canonical aliases and its
|
||||
pre-recorded sealed identity for temp/final aliases, then retries the same
|
||||
workspace immediately.
|
||||
- Parent death after creation but before READY: stdout/control pipe failure or
|
||||
EOF makes the still-running guardian clean both owned objects.
|
||||
- Guardian death after READY: the supervisor knows raw and sealed identities and
|
||||
cleans raw, temp, and final fallbacks.
|
||||
- Supervisor death after PUBLISHED: guardian EOF cleans raw and the published
|
||||
final inode, including the link/unlink intermediate state.
|
||||
- Publish or commit race: serialized guardian state completes the current file
|
||||
operation, then applies EOF/protocol failure cleanup; success requires clean
|
||||
EOF after commitPending.
|
||||
- Cleanup failure: remaining targets are still attempted and every error is
|
||||
preserved; PASS is impossible.
|
||||
|
||||
There is one bounded crash window before spawn: if the client itself is killed
|
||||
after private allocation but before the guardian is created, zero-byte `0600`
|
||||
nonce-private leaves can remain. They contain no provider or credential bytes
|
||||
and cannot occupy the fixed canonical raw/final names, so they do not block an
|
||||
immediate same-kind retry. Automatic pathname sweeping is intentionally omitted
|
||||
because an unproven stale pathname is not deletion authority.
|
||||
|
||||
After each observable managed-process failure, tests require canonical raw,
|
||||
private staging/temp, canonical final, guardian, and provider cgroup residual
|
||||
counts to be zero before retrying the same workspace successfully. The
|
||||
documented pre-spawn client hard-death window is the sole residual exception.
|
||||
|
||||
## Verification
|
||||
|
||||
Real-process tests cover no/partial frames, a competing raw canary, same-kind
|
||||
concurrency, guardian `SIGKILL` after raw link but before READY followed by
|
||||
same-workspace retry, parent death around READY, valid
|
||||
READY identities, EOF/deadline cleanup, publish/PUBLISHED, post-scope guardian
|
||||
death, supervisor death after publication, commit trailing bytes in a later
|
||||
chunk, closed-stderr fail-closed termination, near-timeout publication, and no
|
||||
residual guardian/files. A provider that successfully prints a supplied
|
||||
credential is verified not to expose it through supervisor stdout/stderr. Live fixtures
|
||||
also specify active-scope guardian kill, detached-child external marker/raw
|
||||
append suppression, cgroup collection, and same-workspace retry. Live
|
||||
systemd/bwrap execution remains explicitly unverified when the approval limit
|
||||
prevents running it.
|
||||
|
||||
The external-canary regression waits for a test guardian spawn marker before
|
||||
creating the fixed raw file, proving that the initial absence check has already
|
||||
completed. The fixed raw bytes and dev/inode must remain unchanged after startup
|
||||
rejection. The pre-READY link regression watches only the fixed raw basename,
|
||||
kills the exact direct child on that link event, and requires identity-bound
|
||||
cleanup plus an immediate same-workspace retry.
|
||||
Reference in New Issue
Block a user