# P1-2 O5 Preparation — Control Record

**Prompt:** `docs/AGENT-PROMPT-v5-P1-2-o5-prep.md`
**Design of record:** `docs/p1-2-o5-restricted-identity-design.md`, amended 2026-08-07 by
the first remediation pass under Miguel's now-accepted design amendments (recorded
normatively in the design document's D-O5-2 and D-O5-6 amendment subsections) and again
by this second remediation pass (D-O5-1 rotation note, second D-O5-2 amendment for the
per-job Claude CLI cache) — no contradiction with `docs/PROJECT-STATE.md` was found; see
Gate below.
**Branch:** `feature/p1-2-o5-prep`. **Not committed, not pushed** as of this record
(see Commit status below).
**Authority:** Miguel, per the AGENT-PROMPT and the 2026-08-07 remediation instructions
(first and second). This pass authorizes O5 preparation only: provisioning scripts, the
acceptance-battery runner, fixtures, and this runbook/status pair. It does not authorize
the P1-2 extraction handler, pipeline/queue code, skill work, P1-1b work, or any action
against the production host or live database.

## Remediation pass (2026-08-07) — this is not a fresh ACCEPT

The candidate recorded below (deliverables 1–6, first authored 2026-08-06) received an
independent REJECT before this pass. **This record does not claim ACCEPT.** It records
what the REJECT found, what changed in response, and leaves final disposition to the
next independent re-review — no self-certification is offered as a substitute.

| # | REJECT finding | Disposition this pass |
|---|---|---|
| C1a | `Set-O5ExplicitAccessRule -ProtectFromInheritance` called `SetAccessRuleProtection($true, $true)` — **preserve**, not strip. A broad inherited ACE (e.g. `BUILTIN\Users`/`Authenticated Users` Read/RX from a parent directory) was converted to a newly "explicit" rule instead of removed, so the per-identity matrix check could report `PASS` for the one identity named in a row while the broad grant stayed in effect for everyone else. | **Fixed.** Both `Set-O5ExplicitAccessRule` and `New-O5JobDirectory` now call `SetAccessRuleProtection($true, $false)` (strip, never convert). Regression: `scripts/tests/O5Common.Tests.ps1` → *"Inheritance is stripped, never converted..."* (4 tests: sanity check on the broad inherited grant, strip proof, false-PASS-impossible proof, same proof on `New-O5JobDirectory`). |
| C1b | The per-job ACL template (`New-O5JobDirectory`) granted `Modify` to exactly one owning identity. The design's own job model has `svc-sibyla-pdfx` stage a job's text/images and `svc-sibyla-claude` then read them for the *same* job — a single-owner template cannot express that without a contradiction, and Miguel's design amendment (recorded 2026-08-07 in the design document) confirms both identities belong on the one job directory, never on a sibling's. | **Fixed by design amendment + implementation.** `New-O5JobDirectory -OwnerIdentityReferences` (now an array) grants Modify to every identity named — both restricted identities for a real job, the operator identity only for a deliberately unrelated sibling used to prove isolation. `provision-jobroot.ps1 -ProbeOwnerIdentityReferences` and every "real job" directory in `run-o5-acceptance.ps1` now stamp both. Regression: dual-identity grant test, no-other-identity-gets-access test, empty-array-throws test. |
| C1c | Verification only checked exact-identity explicit-ACE shape, not effective/observed behavior, and had no regression proving a broad inherited grant gets stripped. | **Addressed within this pass's off-host reach.** `provision-jobroot.ps1 -Verify` now also asserts a control non-owner identity gets `None`. Isolation probes (item 2) now live-test `JobRootParent` listing denial and sibling **write** denial (previously read-only), against real ACLs, on the target host. Full effective-access proof (group nesting, deny/allow merge across every applicable SID) remains `HOST-SESSION` by nature — it requires the real host's security subsystem, which this preparation cannot simulate; see the design document's own caveat on `Get-O5ExplicitAccess`. |
| C2 | `Start-O5RestrictedProcess` and the concurrency probe called `Process.Kill($true)` — the tree-kill overload is .NET Core-only and throws a `MissingMethodException` on Windows PowerShell 5.1's .NET Framework, exactly at the moment a timeout needs to fail closed; a kill failure was logged as `Write-Warning` and treated as a soft, non-blocking event. | **Fixed.** New `Stop-O5ProcessTree` (O5Common.psm1) kills via `taskkill.exe /T /F` (PS5.1-safe, no .NET Core dependency) and re-polls `Get-Process` to verify death before returning success. `Start-O5RestrictedProcess` and the concurrency probe both use it; a verified-failed kill now produces an explicit `FAIL` evidence row (`TimeoutFailClosedCleanup`), never a silent warning. Regression: 3 off-host Pester tests proving process death, idempotent already-gone handling, and descendant-process (tree) kill. |
| Egress lifecycle | `provision-egress.ps1` implemented both mechanisms but had no removal path for the one the host session does not keep, risking both allow paths staying active simultaneously. | **Fixed.** New `-Remove -Mechanism <FqdnRule\|LoopbackProxy>` (idempotent, `-WhatIf`/`-Confirm` supported) and `-Verify -ExpectAbsent` (report-only, proves the losing mechanism's rule/config/keyword object is gone). `-Verify` (without `-ExpectAbsent`) now also proves SID scoping via `Get-NetFirewallSecurityFilter`, not just rule-name presence. Runbook step 4 gives the exact preview/remove/verify-absent/re-verify-winner command sequence. |
| Acceptance honesty | A missing `-DatabaseProbeTarget` silently produced no `DatabaseConnect` evidence row at all (not even `HOST-SESSION`); `DenyPaths`/`ControlHosts`/`DatabaseProbeTarget` had no positive control, so a nonexistent path, unresolved placeholder, DNS failure, or closed port would make a restricted-identity "not accessible" result a false `PASS`; item 4/7's `HOST-SESSION` rows conflated "needs the target host" with "needs the unbuilt P1-2 handler". | **Fixed.** Missing `-DatabaseProbeTarget` now emits an explicit `HOST-SESSION` row. New `Invoke-O5PositiveControlChecks`, run first and unconditionally, proves every `DenyPath` exists and every network target (`DatabaseProbeTarget`, each `ControlHost`, the Anthropic hostname) is actually reachable from the operator's own context before any restricted-identity result against it is trusted. `BLOCKED-P1-2` is now a distinct `New-O5EvidenceRecord` `Result` value, separate from `HOST-SESSION`, used for item 4's hostile-output-routed-to-review check and item 7's cost-cap/alert-firing check — both cannot exist until the P1-2 handler is built, on any host. |
| Item 4 overclaim | `NoResidualFilesUnderJobRoot`/`CanaryConfinedToOwnJobDirectory` read as a global out-of-job containment proof. | **Fixed.** Both rows now carry `Scope=JobRootParentTreeOnly` in their detail and an inline comment stating what they do and do not prove (nothing about writes or network effects outside the snapshotted tree). |
| Item 5 completeness | No cross-job read/write attempt while jobs were alive; cleanup was reported once in aggregate, hiding which specific job (if any) failed to clean up. | **Fixed, honestly.** The concurrency probe now attempts a real cross-job read/write against every other live job directory while all N are still alive, and reports `JobCleanup:<n>` for every job individually. Because both identities are shared across every job they are each currently processing (D-O5-2 amendment), a same-identity cross-job attempt against an already-known path may legitimately succeed at the NTFS layer — the evidence records the true result with an explanatory detail rather than asserting a containment guarantee the two-identity design does not provide; see the design document's D-O5-6 amendment. |
| Secret handling | `Get-O5SecretCredential` returned whatever `PSCredential` the vault handed back with no check that its `UserName` matched the requested account — a stale or mis-mapped secret-store entry could silently launch a child as the wrong identity. | **Fixed.** The account-name part of the returned `PSCredential.UserName` (bare or domain/UPN-qualified) is now compared against the requested account and the function throws on mismatch. Regression: 4 Pester tests (matching bare name, matching domain-qualified name, mismatched account throws, plaintext-string-from-vault still throws). |

Every fix above is implemented in the current uncommitted candidate; **none of it has run
against the target host**, and this record is not a substitute for the independent
re-review the next assignment must perform before O5 preparation is accepted.

## Follow-up remediation (2026-08-07, same day) — a real PS5.1 blocker found and fixed

While closing out the pass above, a second real defect was found — not one of the
REJECT findings, but a genuine bug of the same kind as the `$exitCode` scoping defect
noted under Deliverable 1 below: `Start-O5RestrictedProcess` (`deploy/o5/O5Common.psm1`)
and the concurrency probe (`scripts/run-o5-acceptance.ps1`) built each child's argument
list with `$psi.ArgumentList.Add(...)`. `System.Diagnostics.ProcessStartInfo.ArgumentList`
is a **.NET Core-only** property; it does not exist at all on this host's Windows
PowerShell 5.1 / .NET Framework `ProcessStartInfo` — the call throws "cannot call a
method on a null-valued expression" the instant a restricted process is launched, in
every code path (identity probes, isolation probes, egress probes, the injection
battery's helper/CLI launches, and the concurrency probe), not a compatibility warning
that only shows up in an edge case.

**Fixed.** A new `ConvertTo-O5ProcessArgumentString` (`deploy/o5/O5Common.psm1`, exported)
builds the classic `Arguments` string instead, applying the same
quoting/backslash-escaping rules .NET Core's own `ArgumentList` implementation uses
(CommandLineToArgvW-compatible: empty args become `""`, args with no space/quote pass
through unchanged, args needing quoting double any backslash run that precedes an
embedded quote or the closing quote, and a bare backslash run with no following quote is
left untouched). `Start-O5RestrictedProcess` and the concurrency probe's direct
`ProcessStartInfo` construction both now set `.Arguments` via this helper instead of
touching `.ArgumentList`. Confirmed no `.ArgumentList` use remains anywhere in
`deploy/o5/` or `scripts/` (the one remaining textual match is this helper's own
doc comment describing the broken pattern it replaces). The test file's own scaffolding
(`scripts/tests/O5Common.Tests.ps1`, the `Stop-O5ProcessTree` sentinel-process setup) had
the same defect and is fixed the same way, since those tests could not have run to set up
their own test processes on this host otherwise.

Regression, all newly added and passing (see Deliverable 5's updated count): 8 direct
tests of `ConvertTo-O5ProcessArgumentString` (empty list, plain argument, empty-string
argument, argument with a space, multiple arguments joined correctly, an embedded quote,
a trailing backslash run before a closing quote, and bare backslashes needing no
quoting), plus a 7-case round-trip suite that spawns a **real** child PowerShell process
with the constructed `Arguments` string and confirms the child's own `$args` — parsed by
the real OS/runtime argv splitter, not this module's algorithm — recovers each original
value exactly, covering every character class named above. One round-trip helper defect
was found and fixed while writing this test itself: piping an unrolled single-element
array into `ConvertTo-Json` in Windows PowerShell 5.1 (`, $rest | ConvertTo-Json`)
serializes it as `{"value":[...],"Count":1}` instead of a plain JSON array — using
`ConvertTo-Json -InputObject $rest` instead avoids the pipeline-unwrapping quirk and
produces the expected `[...]` shape; confirmed directly before relying on it in the test.

Re-measured after this fix (all commands run this session): parser — 0 errors across all
8 files; `PSScriptAnalyzer 1.25.0`, default rule set, all severities, per-file — 0
findings across all 8 files (one transient `PSReviewUnusedParameter` finding on the new
round-trip test's `-TestCases` `$Name` parameter, used only in the Pester `<Name>`
description template and not otherwise read in the test body, was fixed by also using it
in three `-Because` clauses rather than suppressed); `Invoke-Pester 6.0.1` —
**60/60 tests passing** (up from the prior pass's 26; the increase is entirely this
follow-up's new coverage, no prior test was removed), ~8s. `run-o5-acceptance.ps1`'s full
off-host run (no accounts, no control hosts, no DB target, no helper/CLI paths) still
completes cleanly post-fix: 39 evidence rows, 21 `PASS` / 17 `HOST-SESSION` / 1
`BLOCKED-P1-2`, zero `FAIL`, exit code 0 — the same honest shape as before, now backed by
argument construction that actually works on this host's PowerShell instead of one that
would have thrown on first use against a real restricted identity. Fixture regeneration
re-checked against the tracked manifest: all four hashes still match byte-for-byte.

## Second REJECT remediation pass (2026-08-07, third pass) — 2 blocking Mediums + 5 Low advisories

The candidate above (first remediation pass plus the `ArgumentList` follow-up) received
a **second independent REJECT**: 2 blocking Medium findings and 5 Low advisories. **This
record still does not claim ACCEPT.** As before, it records what the REJECT found, what
changed in response, and leaves final disposition to the next independent re-review.

| # | REJECT finding | Disposition this pass |
|---|---|---|
| Blocking Medium 1 — password expiry/rotation | `provision-identities.ps1` created accounts but had no existing-account password rotation/reconciliation path and no verification of expiry state, while D-O5-1 requires rotation under host policy. | **Fixed, narrowly.** New `-RotateExistingPasswords` switch (idempotent, `ShouldProcess`/`-WhatIf`-gated) rotates an *existing* account's password by calling `Set-LocalUser` with a `SecureString` sourced only from the registered secret store — never a script parameter, never printed. Ordinary provisioning re-runs (no `-RotateExistingPasswords`) never touch an existing account's password, so routine idempotency cannot be mistaken for rotation. `PasswordNeverExpires` is not set by either path and is not used as a substitute for rotation. `-Verify` adds a `PasswordNotExpired` check per account, read from the local account's own `PasswordExpires` state, and fails closed (non-zero exit) on an expired password. The runbook's new step 1a gives the exact secret-store-first update → rotate → verify command sequence. Regression (mocked, no real account touched): `scripts/tests/O5Common.Tests.ps1` → *"provision-identities.ps1 (dot-sourced...)"* (idempotent-provisioning-does-not-rotate, rotation calls `Set-LocalUser` with the vault-sourced credential, `-WhatIf` makes no call, `PasswordNotExpired` PASS/FAIL/null-expiry cases, `-RotateExistingPasswords`+`-Verify` mutual-exclusion throw) — 10 new tests. `provision-identities.ps1` itself is now guarded (`$MyInvocation.InvocationName -ne '.'`) so it can be dot-sourced for this direct, mocked testing without ever creating, rotating, or querying a real account. |
| Blocking Medium 2 — Claude CLI cache lifecycle | D-O5-2's last row said any required writable Claude CLI cache is no-secret and wiped on job cleanup, but the prior candidate had no implementation or evidence — `provision-acls.ps1` instead offered a static `-ClaudeCliCacheDirectory` outside the job root, which is exactly the persistent/shared cache the design amendment prohibits. | **Fixed, at the prep-level this pass can reach; production routing is `BLOCKED-P1-2`.** `provision-acls.ps1`'s static `-ClaudeCliCacheDirectory` parameter and its ACL stamps are removed entirely — only the read-only `-ClaudeCliInstallDirectory` remains. New `New-O5JobScopedClaudeCliCacheDirectory` (`O5Common.psm1`) creates the writable cache *inside* each job directory, inheriting that directory's own ACL rather than a separately stamped rule, so it is removed by the same fail-closed `Remove-O5JobDirectory` pass as the rest of the job — no separate wipe step exists to forget. New `Test-O5PathWithinJobRoot` verification-only guard fails (never throws) for any candidate outside the job boundary. `run-o5-acceptance.ps1`'s item 4 creates this directory for every Claude CLI launch, routes the CLI child to it via the `CLAUDE_CONFIG_DIR` environment variable (the documented, bounded mechanism the CLI supports for this), and records `ClaudeCliCacheWithinJobBoundary` (in-job proof) and `ClaudeCliCacheRemovedOnCleanup` (post-cleanup absence proof) evidence rows. A new `WorkerProductionClaudeCliCacheRouting` row is recorded `BLOCKED-P1-2`, distinct from the harness's own proof: the Worker's production per-job spawn path performing the same `CLAUDE_CONFIG_DIR` routing is P1-2 Worker source work, out of this preparation's scope, and does not block preparation closure. Design document, runbook, and status aligned (D-O5-2 table + new D-O5-2 amendment subsection; runbook steps 3 and 6). Regression: 7 new Pester tests (`New-O5JobScopedClaudeCliCacheDirectory`/`Test-O5PathWithinJobRoot` — creation-inherits-ACL, throws on missing job dir, `-WhatIf` no-op, removed-on-job-cleanup, within-job-root true/false, non-existent-candidate false-not-throw). |
| Low 1 — RW/Deny coexistence | `Test-O5AclMatrixEntry`'s `RW` case passed on any `Modify`/`FullControl` allow rule without checking whether a same-identity explicit `Deny` also existed, which NTFS honors over any `Allow`. | **Fixed.** `RW` now also requires zero same-identity `Deny` rules. Regression: 1 new test (`Modify` Allow + `FullControl` Deny for the same identity → `RW` reports `FAIL`). |
| Low 2 — concurrent-probe environment scrubbing | The concurrency probe's own direct `ProcessStartInfo` construction (`run-o5-acceptance.ps1`) inherited the operator's ambient environment instead of applying `Start-O5RestrictedProcess`'s minimal/scrubbed policy. | **Fixed.** New shared `Set-O5MinimalProcessEnvironment` (`O5Common.psm1`) factors the exact clear-then-set-SYSTEMROOT-then-layer-overrides policy; both `Start-O5RestrictedProcess` and the concurrency probe's `ProcessStartInfo` now call it. Regression: 3 new tests (an ambient test-only env var is proven cleared, SYSTEMROOT-only baseline, overrides layered on top). |
| Low 3 — UserName/Domain normalization | Every restricted-process launch set `ProcessStartInfo.UserName` to the credential's raw `UserName` (bare or `MACHINE\user`) without ever setting `.Domain`, an unsupported shape for a local-account secondary-logon launch. | **Fixed.** New `Resolve-O5LocalAccountIdentity` (`O5Common.psm1`) normalizes bare and `MACHINE\`/`.\`-qualified forms to `{UserName, Domain=<this computer>}` deterministically, and throws on a UPN (`user@domain`) form or a qualifier naming a different machine. Used by both `Start-O5RestrictedProcess` and the concurrency probe's manual launch. The runbook's prerequisites pin the vault credential's supported `UserName` shapes. Regression: 5 new tests (bare, `MACHINE\user`, `.\user`, cross-machine-throws, UPN-throws). |
| Low 4 — broad-grant strip on an already-protected path | `Set-O5ExplicitAccessRule -ProtectFromInheritance` only stripped a broad grant the first time it protected a path (`-not $acl.AreAccessRulesProtected` guard); a broad *explicit* `Everyone`/`BUILTIN\Users`/`Authenticated Users` allow ACE added before protection, or surviving from a prior run, was never removed on a subsequent call, so a re-provisioned path could report a secure baseline while the broad grant remained. | **Fixed.** Every `-ProtectFromInheritance` call now strips explicit `Everyone`/`BUILTIN\Users`/`NT AUTHORITY\Authenticated Users`/`BUILTIN\Authenticated Users` allow rules regardless of whether this call is the one that first protected the path, while leaving `SYSTEM`/`Administrators`/lifecycle grants untouched. Regression: 1 new disposable test (protect once, inject a broad explicit rule plus an `Administrators` rule exactly as a real host might carry, protect again for a different identity, confirm the broad rule is gone and `Administrators` and the new identity's own rule both survive). |
| Low 5 — write-probe cleanup evidence | The item 2/5 write-attempt probe (embedded in `run-o5-acceptance.ps1`, run inside the restricted/cross-job child) used `Remove-Item -ErrorAction SilentlyContinue` with no evidence row: an unexpectedly successful denied write that then failed to clean up its probe file would leave residue with no `FAIL` recorded anywhere. | **Fixed.** The embedded probe script now records `CleanupSucceeded` per write attempt (`$null` when nothing was written, `$true`/`$false` when a cleanup was attempted); `Invoke-O5IsolationProbe` and the concurrency cross-job probe each add a `WriteProbeCleanupSucceeded`/`CrossJobWriteProbeCleanupSucceeded` row (`FAIL` on cleanup failure) whenever a write unexpectedly succeeded. `run-o5-acceptance.ps1`'s orchestration is now guarded (`$MyInvocation.InvocationName -ne '.'`) so the embedded probe-script string and its logic can be dot-sourced and exercised directly and for real (a real child process, real writable/missing targets), without running a live battery. Regression: 2 new tests (writable target → `Accessible=$true`/`CleanupSucceeded=$true`/no residual file; missing target → `Accessible=$false`/`CleanupSucceeded=$null`). |

Every fix above is implemented in the current uncommitted candidate; **none of it has run
against the target host**, and this record is not a substitute for the independent
re-review the next assignment must perform before O5 preparation is accepted.

**Re-measured after this pass (all commands run this session):** parser —
`[System.Management.Automation.Language.Parser]::ParseFile` — 0 errors across all 8
files. `PSScriptAnalyzer 1.25.0`, default rule set, all severities, per-file — 0 findings
across all 8 files (one transient `PSProvideCommentHelp` false-positive was found and
fixed along the way: a `.DESCRIPTION` paragraph in the new
`Set-O5MinimalProcessEnvironment` doc comment started with the literal text ".NET
pre-populates...", and a period-prefixed word at the start of a help-comment line is
parsed by the analyzer as a spurious help-comment tag, which corrupted its recognition of
the whole comment block; reworded to "The .NET runtime pre-populates..." and confirmed 0
findings on re-run — not a real missing-help defect, but real enough to trip the gate
until diagnosed). `Invoke-Pester 6.0.1` — **86/86 tests passing** (up from the prior
pass's 60; the increase is entirely this pass's new coverage across the 7 findings above,
no prior test was removed or weakened), ~9–10s. `run-o5-acceptance.ps1 -WhatIf` (real
empty `-JobRootParent`, no accounts/hosts): exits `0`, creates no evidence directory,
leaves the job-root parent empty — zero writes confirmed directly. Bare (non-`-WhatIf`,
no accounts, no control hosts, no DB target, no helper/CLI paths) run: unchanged honest
shape — 39 evidence rows, 21 `PASS` / 17 `HOST-SESSION` / 1 `BLOCKED-P1-2`, zero `FAIL`,
exit code 0 (the new Claude-CLI-cache and write-probe-cleanup evidence rows only fire
when a Claude CLI path or an unexpectedly-successful write probe is actually exercised,
neither of which is reachable off-host without accounts). Fixture regeneration
re-checked against the tracked manifest: all four hashes still match byte-for-byte.
Prohibited-pattern scan: no `.ArgumentList`, `Process.Kill($true)`, `??`, `?.`, or `?:`
usage anywhere outside doc-comment prose describing the pattern each replaces (unchanged
from the prior pass). Secret-pattern scan (regex for common key/token/password-literal
shapes) across every file this pass touched: one incidental match, `$env:O5_TEST_AMBIENT_SECRET
= 'shhh-do-not-leak'` in the new environment-scrubbing test — an obvious synthetic
placeholder, not a real secret; no other hits. `git diff --check`: clean (0 whitespace
errors) on the tracked-file diff; a direct trailing-whitespace/tab scan of every
untracked file this pass touched (which `git diff --check` does not cover) found none
either. Scope re-confirmed via `git status --porcelain`: the changed/untracked path set
is unchanged from the prior pass (`deploy/o5/`, `docs/deployment/o5-target-host-runbook.md`,
`docs/p1-2-o5-prep-status.md`, `scripts/generate-o5-fixtures.ps1`,
`scripts/run-o5-acceptance.ps1`, `scripts/tests/O5Common.Tests.ps1`, `tests/fixtures/o5/`,
plus the `docs/project-todo.md`/design-document edits) — `docs/PROJECT-STATE.md` and every
P1-1b/`Specs`/migration path remain untouched.

## Gate check

`docs/p1-2-o5-restricted-identity-design.md` is present. Its decisions (D-O5-1 through
D-O5-6) were cross-read against `docs/PROJECT-STATE.md`'s O5 entry (open item, blocks
Production, closes only on Miguel's target-host session) and found consistent — no
contradiction, so no stop-and-report was triggered. No file this pass touched is one
P1-1b amendments govern (verified: P1-1b's tracked changes are under `docs/` entries
for O9/O10/P1-1b import-contract records and `Specs/`/migration files; this pass's
files are `deploy/o5/`, `scripts/run-o5-acceptance.ps1`,
`scripts/generate-o5-fixtures.ps1`, `scripts/tests/O5Common.Tests.ps1`,
`tests/fixtures/o5/`, and this file pair — a disjoint surface).

## Deliverable 1 — Provisioning script set (`deploy/o5/`)

| Script | Purpose | -WhatIf | -Verify (no writes) | Status |
|---|---|---|---|---|
| `O5Common.psm1` | Shared primitives (SID resolution, secret-store credential resolution, ACL apply/read/compare, evidence record/file, canary scan, job-directory lifecycle, restricted-process launch, directory snapshot) | n/a (library) | n/a (library) | Done |
| `provision-identities.ps1` | D-O5-1: creates `svc-sibyla-pdfx`/`svc-sibyla-claude`, non-admin, deny interactive/remote-interactive logon, password from secret store only | Yes | Yes | Done |
| `provision-acls.ps1` | D-O5-2: full ACL matrix, explicit deny ACEs, per-job-root traverse-only | Yes | Yes, prints observed-vs-expected matrix | Done |
| `provision-egress.ps1` | D-O5-3: SID-keyed deny-all for both identities, both mechanisms fully implemented (`-Mechanism FqdnRule\|LoopbackProxy`) | Yes | Yes | Done |
| `provision-jobroot.ps1` | Per-job-root layout plus the per-job ACL template (via `New-O5JobDirectory`/`Remove-O5JobDirectory`) | Yes | Yes, exercises a throwaway probe job dir end-to-end and self-cleans | Done |

**Verification evidence (measured this session, off-host, on the current development
host — not the AWS WS2025 target):**

- Parse check: `[System.Management.Automation.Language.Parser]::ParseFile` against
  all 8 script/module files — 0 parse errors.
- `PSScriptAnalyzer` (`Invoke-ScriptAnalyzer`, all severities, per-file, module
  `PSScriptAnalyzer 1.25.0`): initial run found 48 findings (7× missing-BOM on files
  carrying a literal em-dash character, including one file with genuinely corrupted
  mojibake text; 3× `PSUseShouldProcessForStateChangingFunctions` on pure/deterministic
  builder functions; 3× `PSUseSingularNouns`; 1× `PSAvoidAssignmentToAutomaticVariable`
  shadowing `$matches`; 13× `PSProvideCommentHelp` (Information) missing a recognized
  `.SYNOPSIS` tag; 12× `PSReviewUnusedParameter` in `run-o5-acceptance.ps1` and 3× in
  `provision-egress.ps1` — script parameters read only inside nested functions via
  scope fallthrough, invisible to the analyzer's per-parameter usage check; 2×
  `PSUseDeclaredVarsMoreThanAssignments` on `$exitCode`). Every finding was fixed, not
  suppressed-and-ignored: non-ASCII em-dashes replaced with ASCII `-` (and the
  corrupted mojibake sequence in `run-o5-acceptance.ps1` line 7 repaired to the correct
  text); plural function names renamed to the PowerShell singular-noun convention
  (`Get-O5UserRightAssignee`, `Grant-O5DenyLogonRight`, `ConvertTo-O5BigEndianByte`);
  `$matches` renamed to `$lineMatches`; `.SYNOPSIS` tags added to all 13 flagged
  comment blocks; the 15 unused-parameter findings fixed by converting closure-style
  script-scope reads inside nested functions to explicit function parameters passed at
  each call site (`Invoke-O5IdentityProbe`, `Invoke-O5IsolationProbe`,
  `Invoke-O5EgressProbe`, `Invoke-O5InjectionBatteryHarness`,
  `Invoke-O5ConcurrencyProbe`, `Invoke-O5KeyHygieneProbe` in the runner;
  `Publish-O5FqdnAllowRule`, `Publish-O5LoopbackProxyAllowRule` in
  `provision-egress.ps1`); `PSUseShouldProcessForStateChangingFunctions` resolved with
  justified `SuppressMessageAttribute`s on the four functions that are pure in-memory
  construction or a deliberately-always-overwrite deterministic fixture builder (not a
  system-state change `-WhatIf` has anything to gate). **Re-run after fixes: 0
  findings, all severities, all 8 files.**
- **Real bug found and fixed during this pass, not present in the original PSScriptAnalyzer
  triage list under its own name:** `PSUseDeclaredVarsMoreThanAssignments` on
  `$exitCode` in both `provision-identities.ps1` (`-Verify` block) and
  `provision-egress.ps1` (`-Verify` block) was flagging a real defect, not a style
  nit — `$exitCode = 1` was being set inside a `ForEach-Object { }` pipeline
  scriptblock, which runs in a child scope in Windows PowerShell 5.1. The assignment
  never reached the outer `$exitCode` that `exit $exitCode` reads, so **both
  `-Verify` modes would always exit `0` (report success) even when a compliance or
  firewall-rule check failed.** Fixed by converting both to `foreach` statements
  (same-scope, no child-scope boundary). Confirmed by direct negative-path testing
  below that `-Verify` now correctly reports a non-zero exit when checks fail.
- Off-host `-WhatIf` dry runs (measured): `provision-identities.ps1 -WhatIf` previews
  both account creations via `ShouldProcess` with no error. `provision-acls.ps1
  -WhatIf` and `provision-egress.ps1 -WhatIf` both throw a clear, expected
  precondition error ("...does not exist locally. Run provision-identities.ps1
  first.") because ACL/firewall rules need a resolvable SID to construct — this is
  pre-existing, deliberate fail-fast behavior (not a defect; there is no meaningful
  way to preview a rule for a non-existent identity), and is why the design's
  ordering runs identities before ACLs/egress. `run-o5-acceptance.ps1 -WhatIf` and
  `-Confirm:$false` full runs (see Deliverable 2) both completed cleanly.
- Off-host `-Verify` negative-path checks (measured, no accounts/rules provisioned
  on this host): `provision-identities.ps1 -Verify` → `AccountExistsLocally: FAIL`
  for both accounts, **exit code 1** (confirms the scoping fix). `provision-jobroot.ps1
  -Verify` against a non-existent parent → `JobRootParentExists: FAIL`, exit 1.
  `provision-egress.ps1 -Verify -Mechanism FqdnRule` against no provisioned rules →
  all checks `FAIL`, exit 1.
- Off-host `-Verify` positive-path check (measured): `provision-jobroot.ps1` run
  without `-Verify` to create a real temp job-root parent, then `-Verify` against it →
  `JobRootParentExists: PASS`, `ProbeJobDirectoryOwnerRW: PASS`,
  `ProbeJobDirectoryCleanedUp: PASS`, exit 0.
- Identity creation, ACL application, and firewall-rule creation themselves are
  **HOST-SESSION**: this preparation pass does not create real local accounts, ACL
  entries, or firewall rules anywhere, including on this development host — that is
  explicitly prohibited by the AGENT-PROMPT ("No live host/database/services/
  accounts/ACL/firewall/secret-store action").

## Deliverable 2 — Acceptance-battery runner (`scripts/run-o5-acceptance.ps1`)

Implements D-O5-6 items 1–3, 5, 7 fully scripted; item 4's fixture-staging/hash/
canary-confinement harness plus a real restricted-identity launch when helper/CLI
paths are supplied; item 6 is not a separate mode — the runbook re-invokes this exact
script after reboot.

**Verification evidence (measured, off-host):**

- Parse and `PSScriptAnalyzer`: see Deliverable 1 (same run, same result — 0
  findings after fixes).
- Full off-host run with no accounts, no `-ControlHosts`, no `-DatabaseProbeTarget`,
  no helper/CLI paths, no `-KeyHygieneScanPaths` (i.e., every input this script
  cannot supply off-host, omitted): completed without error, wrote a valid evidence
  JSON, printed `statusLines` of `HOST-SESSION` for battery items 1, 2, 3, 5, 7 (each
  correctly attributed to a missing precondition — "Account ... does not exist
  locally", "No -ControlHosts supplied", etc. — not a silent skip) and `PASS`/
  `HOST-SESSION` mixed for item 4 (fixture manifest present, fixture staged with
  matching hash, and canary-confined-to-own-job-directory all `PASS` under the
  current session identity since fixture staging does not require the restricted
  accounts; the actual restricted-identity child launch is `HOST-SESSION` because no
  helper/CLI path was supplied). 39 evidence rows written (re-measured after the
  follow-up `ArgumentList` fix above; this run's argument construction now actually
  works on this host's PowerShell instead of throwing on first restricted-process
  launch, which is what would have happened against a real identity before that fix).
  **Exit code 0** (no `FAIL` anywhere — correct, since every unmet precondition is
  honestly `HOST-SESSION`, not fabricated as `PASS` or silently dropped).
- `-WhatIf` full run: every probe function reports `HOST-SESSION` with reason
  "Skipped under -WhatIf.", the evidence directory is **not** created
  (`Test-Path` on it returned `$false` after the run), and the run still exits `0`.
- Fixture-manifest/hash cross-check (see Deliverable 3) ran as part of the above via
  the injection-battery harness and matched.
- Identity/isolation/egress/concurrency probes against the real restricted accounts,
  the real deny-path set, the real Anthropic/control-host reachability, and the real
  restricted-identity child launch of the PDF helper and Claude CLI are
  **HOST-SESSION**: they require the provisioned accounts, real ACLs, real firewall
  state, and (for the launch sub-checks of item 4) deployed executables that only
  exist on the target host.

## Deliverable 3 — Injection fixtures (`tests/fixtures/o5/`)

Four fixtures plus `manifest.json`, generated by
`scripts/generate-o5-fixtures.ps1` (deterministic — no randomness beyond each
fixture's fixed, documented canary literal baked into the script source):

| Fixture | Type | Attack category | Bytes | SHA-256 (first 12 hex) |
|---|---|---|---|---|
| `malicious-injection-exfiltration.pdf` | PDF | exfiltration | 1012 | `2472087aed28` |
| `malicious-injection-outofjob-write.pdf` | PDF | out-of-job-write | 1018 | `1489349c5a9f` |
| `malicious-injection-tool-abuse.png` | PNG (spliced tEXt chunk) | tool-abuse | 399 | `5d494a2e69f3` |
| `benign-control.pdf` | PDF | none (control) | 841 | `94d1de290c26` |

Content is synthetic only: fixed placeholder invoice text, RFC 2606
`*.example.invalid` hostnames, and a distinct `O5-CANARY-*` marker per fixture. No
real company data, no real secrets, and no filename or path resembling a genuine
production secret location beyond what `docs/deployment/iis-and-worker.md` already
records in general terms.

**Verification evidence (measured, off-host):** regenerated all four fixtures into a
scratch directory and compared SHA-256 against the tracked `manifest.json` — all four
hashes matched exactly, confirming the generator remains deterministic after this
pass's function renames (`ConvertTo-O5BigEndianByte`) touched only identifiers, not
byte-producing logic. `PSScriptAnalyzer`/parse: see Deliverable 1.

## Deliverable 4 — Validation-session runbook

`docs/deployment/o5-target-host-runbook.md`, 225 lines. Ordered session: prerequisites
(secret registration, real deploy paths, control-host list); provision identities,
job-root, ACLs; the egress-mechanism decision point (try `FqdnRule`, verify with a
real reachability probe — not just rule presence — fall back to `LoopbackProxy` and
record which one is kept); stage or confirm the injection-battery binaries; run the
full battery; reboot and re-run the identical command unattended; save evidence; the
exact closure statement text for `docs/PROJECT-STATE.md` (which this branch does not
edit — only the closing session does). Every step names its exact script invocation
and expected `PASS`/`FAIL`/`HOST-SESSION` shape, keyed to D-O5-6 items 1–7. Estimated
active session time: 40–50 minutes plus the host's own reboot wall-clock, under an
hour end to end on a clean run (stated in the runbook itself, not re-measured here —
it cannot be, without the target host).

## Deliverable 5 — Dry-run everything runnable off-host

Covered inline under Deliverables 1–3 above, the follow-up remediation section (the
PS5.1 `ArgumentList` blocker), and the second-REJECT remediation pass section above (2
blocking Mediums, 5 Low advisories). Summary: 0 `PSScriptAnalyzer` findings across all 8
script/module files (all severities); 0 parse errors; **86/86 Pester tests passing**
(`scripts/tests/O5Common.Tests.ps1`, ~9–10s, `Pester 6.0.1`) covering everything the
prior passes covered (ACL-matrix comparison including idempotency/`-WhatIf`/RW-vs-Deny
coexistence; strip-not-preserve inheritance including the already-protected-path broad
explicit-grant strip; evidence formatting; canary scan; fixture/job-directory lifecycle;
PS5.1-safe process-tree kill/verify; PS5.1-safe process-argument quoting; positive-control
TCP reachability; fail-closed secret-store account-name matching) plus this pass's new
coverage: shared environment scrubbing (`Set-O5MinimalProcessEnvironment`), local-account
identity normalization (`Resolve-O5LocalAccountIdentity`), the per-job Claude CLI cache
lifecycle (`New-O5JobScopedClaudeCliCacheDirectory`/`Test-O5PathWithinJobRoot`),
`provision-identities.ps1`'s rotation path and password-expiry verification (dot-sourced,
mocked, no real account ever touched), and the embedded write-probe's cleanup-evidence
field (dot-sourced `run-o5-acceptance.ps1`, a real child process against real
writable/missing temp targets). `-WhatIf` and `-Verify` dry runs exercised against every
provisioning script and the acceptance runner, both positive and negative paths where a
positive path is possible without live accounts (job-root only). Fixture regeneration is
byte-for-byte deterministic against the tracked manifest.

Every check that requires a real restricted account, a real ACL/firewall state, a real
target-host reboot, or the deployed P1-2 handler is marked `HOST-SESSION` in the
runbook, not simulated and not claimed — enumerated in full under "HOST-SESSION items"
below.

## Deliverable 6 — Records

This file and the `docs/project-todo.md` P1-2 `[!]` item update (that item only — no
other P1-2 line, no P1-1b text). `docs/PROJECT-STATE.md` is **not** modified by this
pass; O5 remains open there until the target-host session records its own closure
per the runbook.

## HOST-SESSION items (enumerated in full)

Everything below requires the AWS WS2025 target host and cannot be proven, simulated,
or claimed by this preparation pass:

1. **D-O5-6 item 1 (identity probes):** `whoami /all` under each restricted identity;
   confirms account, SID set, and privileges on the real host.
2. **D-O5-6 item 2 (isolation probes):** real attempts to read `appsettings*`, deploy
   root, secret store, another job's directory, and open a database connection under
   each restricted identity, against real ACLs.
3. **D-O5-6 item 3 (egress probes):** real firewall enforcement — helper blocked
   outbound entirely; CLI reaches the real Anthropic endpoint; a real control-host
   list is blocked. Includes the `FqdnRule` vs. `LoopbackProxy` decision itself,
   which can only be made by observing real enforcement behavior on this host's
   Windows Firewall build.
4. **D-O5-6 item 4 (injection battery), launch sub-checks only:** actually launching
   `Sibyla.PdfTextExtractor.exe` and the headless Claude CLI under the restricted
   identities against the fixtures. (Fixture staging, hash verification, and
   canary-confinement checks are *not* HOST-SESSION — they already run and pass
   off-host, as measured above.)
5. **D-O5-6 item 5 (concurrency):** N simultaneous jobs under the real restricted
   identities, real secondary-logon launch, real distinct job directories.
6. **D-O5-6 item 6 (post-reboot):** a real reboot of the target host followed by an
   unattended re-run with no interactive logon.
7. **D-O5-6 item 7 (key hygiene), two sub-checks:** the canary-absence scan against
   the *real* restricted-profile and job-directory paths (the scan logic itself is
   tested off-host against synthetic temp directories — see Deliverable 5); and the
   cost-cap/usage-alerting proof, which requires the deployed P1-2 extraction handler
   (D-O5-4 backlog) and is explicitly out of this preparation's scope.
8. **Provisioning itself:** real creation of the two local accounts, real NTFS ACL
   application, and real Windows Firewall rule creation — this preparation pass
   verified the scripts' logic, `-WhatIf` previews, and `-Verify` report-only paths
   exhaustively, but did not and must not perform any of these actions itself, on any
   host, per the AGENT-PROMPT's explicit prohibition.

## Discipline confirmation

- No production or live-host action of any kind was taken. No local Windows account,
  NTFS ACL, or firewall rule was created anywhere, including on the development host
  used to run the tests above — every provisioning script was exercised only via
  `-WhatIf` (preview, no write) or `-Verify` (report-only) against ordinary temp
  directories or against its own built-in negative-path (account/rule absent) case.
- No real secret, password, key, or hostname beyond what `docs/deployment/iis-and-worker.md`
  and the design document already record was written to the repository. The runbook
  uses explicit angle-bracket placeholders for every host-specific path and hostname
  it needs, resolved only at session time.
- No secret was accessed, requested, or printed at any point in this pass, including
  this second pass's new `-RotateExistingPasswords` path. No
  `Microsoft.PowerShell.SecretManagement` vault was queried (`Get-O5SecretCredential`
  was covered by code inspection, by the scripts' own precondition checks, and by
  mocked Pester tests, not by invoking it against a real vault).
- No Worker/API/Web source code was changed. No P1-1b file was touched (confirmed by
  `git status` — see Commit status below — showing only this pass's own untracked
  paths). No accepted migration was edited. No history was rewritten.
- **No commit, no push, no staging.** `git status` at the time of this record shows
  every changed/added path as untracked (`deploy/o5/`,
  `docs/deployment/o5-target-host-runbook.md`, `docs/p1-2-o5-prep-status.md`,
  `scripts/generate-o5-fixtures.ps1`, `scripts/run-o5-acceptance.ps1`,
  `scripts/tests/O5Common.Tests.ps1`, `tests/fixtures/o5/`, and the
  `docs/project-todo.md` edit once made); nothing was added to the index and no
  commit was created.

## Status of this candidate (superseded by the section immediately below) — history: second REJECT remediated, third pass overall

This subsection is kept for history; see "Third-pass independent review" immediately
below for the current status. At the time this subsection was written, the record did
**not** claim final ACCEPT: the first candidate was rejected (C1a/C1b/C1c/C2/egress
lifecycle/acceptance honesty/item 4 overclaim/item 5 completeness/secret handling, all
disposed of above), its own follow-up fix for the `ArgumentList` blocker was applied,
and *that* candidate then received a **second independent REJECT** (2 blocking
Mediums, 5 Low advisories, disposed of in the section above this one). That third-pass
candidate (both remediation passes plus the `ArgumentList` follow-up) subsequently went
back for independent re-review and returned **ACCEPT with 2 Low advisories** — recorded
in full immediately below, including their disposition. Self-certification is still not
a substitute for independent review: the two Low fixes below were authored and verified
off-host by the same kind of agent whose earlier candidates were rejected, and the
ACCEPT verdict itself was issued before those two fixes existed, so it is not evidence
that the fixes themselves are correct — only a focused independent confirmation of
those two changes can establish that (see "What remains for O5 to close" below).

## Third-pass independent review: ACCEPT with 2 Low advisories (2026-08-07) — resolved; see focused confirmation below

The second remediation pass above (2 blocking Mediums + 5 Lows, all disposed of) went
back for independent re-review and this time returned **ACCEPT**, with 2 Low
advisories (no blocking findings). Both are disposed of below, in this same
uncommitted candidate.

| # | ACCEPT advisory (Low) | Disposition |
|---|---|---|
| Low 1 — fail-closed parameter set | `provision-egress.ps1`: `-ExpectAbsent` by itself bound the `Verify` parameter set (both `-Verify` and `-ExpectAbsent` belong to it) while `$Verify` itself stayed `$false`, so the script's value-based `if (-not $Verify)` branching could fall through into the provisioning path instead of running verification. | **Fixed, both structurally and with an explicit guard.** `-Verify` is now `Mandatory = $true` within the `Verify` parameter set, so `-ExpectAbsent` supplied with `-Verify` omitted entirely fails immediately at parameter binding, before the script body runs at all. That structural fix alone does not close every case PS5.1's binding semantics allow: an explicit `-Verify:$false -ExpectAbsent` still satisfies "mandatory" (a value was bound) while leaving `$Verify` false — confirmed by direct experiment before relying on it. A second, explicit guard (`if ($ExpectAbsent -and -not $Verify) { throw ... }`) runs immediately after the module import, before `-Remove`/provisioning/verification are reached, and catches that case too. `-Verify -ExpectAbsent` together remain valid and unaffected. Regression (`scripts/tests/O5Common.Tests.ps1`, new Describe `provision-egress.ps1 fail-closed -ExpectAbsent/-Verify parameter guard`): 3 tests, executed directly (not dot-sourced — this script has no dot-source guard) with `Get-NetFirewallRule`/`New-NetFirewallRule` mocked to throw if called, proving both invalid combinations throw before any firewall query, and that `-Verify -ExpectAbsent` still completes (`$LASTEXITCODE -eq 0`) without ever calling `New-NetFirewallRule`. |
| Low 2 — creation inside cleanup boundary | `scripts/run-o5-acceptance.ps1`: `Invoke-O5IsolationProbe` created `$jobPath` then `$siblingJobPath` (two separate `New-O5JobDirectory` calls) *before* the `try`/`finally` began; `Invoke-O5ConcurrencyProbe` created all `N` job directories in a `1..$ConcurrencyCount \| ForEach-Object { New-O5JobDirectory ... }` pipeline, also before `try`/`finally`. In both, if a later creation in the sequence threw, every directory already created earlier in that same sequence had no cleanup registered for it yet and would leak. | **Fixed.** Both functions now open their `try`/`finally` boundary first and create directories one at a time inside it, adding each successfully-created path to a list (`$createdJobPaths` / `$jobPaths`) immediately after it is created and before the next creation is attempted. The `finally` block iterates only what was actually registered, so a throw on creation `N` still cleans up everything created in `1..N-1`; `Invoke-O5ConcurrencyProbe`'s per-job `JobCleanup:<n>` evidence rows are unaffected for the normal (all-succeed) path. Regression (same Describe file, new Describe `Creation-inside-cleanup-boundary for job directories`): 2 disposable, fully-mocked tests (`New-O5JobDirectory`/`Remove-O5JobDirectory` mocked, no real directory ever created) — one makes the isolation probe's second (sibling) creation throw and proves the first-created (primary) directory is passed to `Remove-O5JobDirectory`; one makes the concurrency probe's third creation throw (`-ConcurrencyCount 3`) and proves directories 1 and 2 are both cleaned up while directory 3 (never created) is not. |

Both fixes are implemented in this same uncommitted candidate — nothing else in the
second-remediation-pass candidate was touched. **This is not a self-acceptance.** The
review that returned ACCEPT evaluated the candidate *before* these two Low fixes
existed; applying them was this session's own work, not independent verification of the
result. That focused independent confirmation — of these two changes specifically, not
a full re-review — has since been performed and returned **ACCEPT**; see "Focused
independent confirmation" immediately below. No other file, behavior, or finding from
the second pass was touched.

**Re-measured after this fix (all commands run this session):** parser —
`[System.Management.Automation.Language.Parser]::ParseFile` — 0 errors across all 8
files. `PSScriptAnalyzer 1.25.0`, default rule set, all severities, per-file — 0
findings across all 8 files. `Invoke-Pester 6.0.1` — **91/91 tests passing** (up from
the prior pass's 86; the increase is entirely this pass's 5 new tests — 3 for Low 1, 2
for Low 2 — no prior test was removed or weakened), ~11s. `run-o5-acceptance.ps1
-WhatIf` (real empty `-JobRootParent`, no accounts/hosts): exits `0`, creates no
evidence directory, leaves the job-root parent empty — zero writes confirmed directly.
Bare (non-`-WhatIf`, no accounts, no control hosts, no DB target, no helper/CLI paths)
run: unchanged honest shape — 39 evidence rows, 21 `PASS` / 17 `HOST-SESSION` / 1
`BLOCKED-P1-2`, zero `FAIL`, exit code 0 (identical to the prior pass's measurement,
confirming the Low 2 refactor did not alter behavior on this off-host path, where both
identities are unavailable and the isolation/concurrency probes return `HOST-SESSION`
before ever reaching job-directory creation). `git diff --check`: clean (0 whitespace
errors) on the tracked-file diff; a direct trailing-whitespace/tab scan of the three
untracked files this pass touched (`deploy/o5/provision-egress.ps1`,
`scripts/run-o5-acceptance.ps1`, `scripts/tests/O5Common.Tests.ps1`) found none.
Prohibited-pattern scan (`.ArgumentList`, `Process.Kill($true)`, `??`, `?.`, `?:`): no
matches in either changed script. Secret-pattern scan across both changed files: no
hits (`provision-egress.ps1` has none; the new test Describe blocks in
`O5Common.Tests.ps1` use only the same synthetic `'not-a-real-secret'` placeholder
literal already used throughout the file). Scope re-confirmed via `git status
--porcelain`: unchanged from the prior pass — `deploy/o5/`,
`docs/deployment/o5-target-host-runbook.md`, `docs/p1-2-o5-prep-status.md`,
`scripts/generate-o5-fixtures.ps1`, `scripts/run-o5-acceptance.ps1`,
`scripts/tests/O5Common.Tests.ps1`, `tests/fixtures/o5/`, plus the
`docs/project-todo.md`/design-document edits.

## Focused independent confirmation: ACCEPT (2026-08-07) — candidate confirmed; O5 itself remains open

The two Low-advisory fixes recorded immediately above went back for the focused
independent confirmation that section calls for — of those two changes specifically,
not a full re-review, since the third-pass ACCEPT had evaluated the candidate before
the fixes existed. That confirmation **returned ACCEPT**, with no findings. It was
performed independently of the session that authored the fixes, so it is not
self-certification.

**Measured by the confirming review:** parser —
`[System.Management.Automation.Language.Parser]::ParseFile` — 0 errors across all 8
files. `PSScriptAnalyzer`, default rule set, all severities, per-file — 0 findings
across all 8 files. `Invoke-Pester` — **91/91 tests passing**. `run-o5-acceptance.ps1
-WhatIf` (real empty `-JobRootParent`, no accounts/hosts): exits `0`, creates no
evidence directory, leaves the job-root parent empty — zero writes. Bare
(non-`-WhatIf`, no accounts, no control hosts, no DB target, no helper/CLI paths) run:
39 evidence rows — 21 `PASS` / 17 `HOST-SESSION` / 1 `BLOCKED-P1-2`, zero `FAIL`, exit
code 0. Fixture regeneration re-checked against the tracked manifest: all four hashes
match byte-for-byte. `git diff --check`: clean. Scope (`git status --porcelain`):
unchanged, disjoint from P1-1b. Secret-pattern scan: clean. Prohibited-pattern scan
(`.ArgumentList`, `Process.Kill($true)`, `??`, `?.`, `?:`): clean.

With this, both Low advisories from the third-pass independent review are **resolved
and independently confirmed** — not merely authored and self-verified. This closes the
O5-preparation candidate's own review cycle: every REJECT finding, blocking Medium, and
Low advisory raised against any pass of this candidate is now disposed of and
independently confirmed.

**This does not close O5 itself. O5 remains OPEN.** No target-host `HOST-SESSION` item
has been run: the identity, isolation, egress, and concurrency probes against the real
restricted accounts, real ACLs, and real firewall state; the real restricted-identity
launch of the PDF helper and Claude CLI; and the post-reboot unattended re-run all still
require the AWS WS2025 target host and have not occurred. Item 4's
`HostileOutputRoutedToReview`/`WorkerProductionClaudeCliCacheRouting` and item 7's
`CostCapsAndAlertingFireOnTestThreshold` remain `BLOCKED-P1-2` under the accepted
design — they require the not-yet-built P1-2 extraction handler and cannot be produced
by any preparation pass, on any host. Neither Production nor O5 is closed by this
record; closure remains Miguel's to record in `docs/PROJECT-STATE.md`, only after the
target-host session, per the runbook.

## What remains for O5 to close

The focused independent confirmation above has resolved the candidate's own review
cycle. What remains is the target-host validation session
(`docs/deployment/o5-target-host-runbook.md`) and its recorded, sanitized evidence —
provisioning the two identities for real (including, if the host's password-rotation
policy requires it at session time, running `-RotateExistingPasswords` per the
runbook's step 1a), applying the real ACL matrix and firewall rules, deciding the
egress mechanism against real enforcement and removing the losing mechanism's rules
(using the now-fail-closed `-Verify -ExpectAbsent` to confirm the losing mechanism is
gone), running the full D-O5-6 battery including the restricted-identity launch of the
PDF helper and Claude CLI against the injection fixtures (item 4's
`HostileOutputRoutedToReview` and `WorkerProductionClaudeCliCacheRouting`, and item 7's
`CostCapsAndAlertingFireOnTestThreshold`, remain `BLOCKED-P1-2` until the P1-2 handler
ships and do not block this closure), a post-reboot unattended re-run, and Miguel
recording the closure statement in `docs/PROJECT-STATE.md`.
