500 lines
24 KiB
Markdown
500 lines
24 KiB
Markdown
# 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.
|