refactor: adapter 구현중..
This commit is contained in:
@@ -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