Skip to content

test(ssh): add operator-invoked deployment receipt and recovery harness - #155

Draft
abruption wants to merge 24 commits into
mainfrom
abruption/sts050-23-receipt-harness
Draft

abruption wants to merge 24 commits into
mainfrom
abruption/sts050-23-receipt-harness

Conversation

@abruption

@abruption abruption commented Oct 20, 2026 •

Copy link
Copy Markdown
Owner

Superseding decision — 2026-10-20 KST

The user approved stopping the incremental repair approach and redesigning the minimum contract. This PR is on hold as a Draft and excluded from 0.5.0. It must not be merged in its current form. It is no longer a dependency of release preparation #157.

The repository-only validation task expanded into a coupled journal, authority, lock, receipt, caller and recovery system before the overall contract was sufficiently fixed. Repeated review fixes added state and failure paths. More green checks or another patch round do not establish that this architecture is suitable.

Stop additional implementation, push, review retriggers and native/model/host experiments. Preserve all existing commits and evidence for the design review; do not reset or republish them as an accepted implementation. Local unpushed follow-ups through 8115b18 are preserved separately and are not claimed as this remote head. #23 remains open; its deferred design follow-up must define purpose, non-goals, minimum state transitions and acceptance evidence before replacement implementation.

A new design starts with an operator checklist and evaluates whether a read-only static reporter is sufficient. Automatic provisioning, lifecycle ownership, receipt authority, caller tracking, recovery and destructive cleanup are not implicit requirements. Their inclusion would need an explicit separately reviewed contract. No runtime behavior, support cell, release or publishing is authorized by this decision.

Everything below is historical candidate material, not the current release plan or merge approval.


Current integration candidate

Head d992bf58ce6fdf842d7d4d1840bc65db521a59c5 preserves E9 and all peer commits through 45461c1161354739ebab09fe50a280a5c3f01bbf, followed by the independently reviewed root path/framing correction below. Independent source reviews are complete; current-head CI and the five subsequent 35077da Code Review findings remain merge gates. E9 CI passed after one failed-job rerun, but its six review findings required these subsequent changes; older green results are not current-candidate gates. No merge or publication is performed. Historical sections below remain tied to their exact source checkpoints.

Historical E9 checkpoint

E9 included the fddb7c4/9559613 fixes and reviewed synthetic-fixture backport. All 12 E9 checks passed after one failed-job rerun; the original macOS observation-timeout cause remains unproven. Its subsequent six Code Review findings required the later peer and root corrections below. E9 test/review results are historical and do not replace current-head validation.

Summary

Refs #23. On one Linux arm64 test host, requalifies the current receipt/recovery rules and the operator-check → receipt → unlock → caller-switch order. The full rule set (cells L3–L8) ran only with the first harness revision (cc7fe23, harness 210759a1…41e7). Intermediate review revisions were rerun there only for stated subsets, last at c0203fe. The later journal-only revisions, the 934ceab code-review fixes, the 839f2f6 inline-review fixes, the 275ed8d publication-recovery fixes, the root integration cf4626c, the 61f37f8/fddb7c4 and subsequent peer/root fixes have focused Darwin fixture checks only (partly, see below), not a new Linux-host run. The isolated POSIX/Windows destination criterion as a whole remains open.

  • Add scripts/deployment-harness.mjs, a repository-only, operator-invoked validation harness for the remote deployment ADR. dest commands run on a destination with the recorded Node; op commands keep a hash-chained operator run journal and issue the statements the destination steps require. It is not the proposed F6 deploy helper: it never opens SSH, never contacts a registry, never installs remotely on its own, never runs during list/send/doctor, and leaves the exact-version preflight, host-key/BatchMode handling and SSH options unchanged.
  • Safety: dest init-prefix creates a new canonical prefix (refuses existing paths, symlinked paths and ancestors writable by others without the sticky bit) plus a sentinel; every dest command that inspects or changes a prefix refuses without it (dest identity and dest path-inventory take no prefix and write nothing). Only that prefix or the private journal area is written. Journal commands, including statements and dry runs, explicitly create/remove the ephemeral coordinator directory, which may remain after a crash or failed cleanup; they do not change journal contents or destination state in a dry run. Other mutations are dry runs unless --execute; rollback, retention, lock breaking, receipt cleanup, staging discard and prefix removal also need --confirm-destructive. dest path-inventory hashes session-peer entries on PATH read-only, to show that other managers' executables are unchanged. POSIX only; refuses on Windows.
  • Enforced step 8 order: destination verify, then operator doctor --host and then send --dry-run --host recorded in the journal in that order, then an operator-checks-passed statement bound to the lock, marker identity and recomputed manifest, then dest receipt, then dest unlock (refused while the activated target has no matching receipt), then callers_switched (refused before a receipt on the same prefix and before the publishing acquisition is closed by its run's unlock or an exact recorded break).
  • Covers the open rule cells: fresh-lock recovery of an unreceipted target (receipt copies the marker runId), same-run and journal-confirmed rollback (refused without positive confirmation; re-verify only), corrupt/mismatched/orphan receipts with explicit ownership-confirmed cleanup, .receipts trust (mode, symlink, non-regular receipt), unowned/unreadable/unconfirmable lock override, and the activation-table rows.
  • ADR drift for 0.4.0: version range and banner, the bounded handoff preparation request coming after the same preflight, and smol-toml among the lockfile-pinned runtime dependencies. Also states the operator-check order explicitly (a failing operator check writes no receipt and rolls back) and adds an orphan-receipt row (refused before the rename). Documents the harness, its Darwin-local evidence and exact unrun Linux, Windows non-admin and Node 22 steps.

Review fixes (dc0de27)

  • dest stage validates the identity file against a closed schema and requires the staging name to resolve inside the root before any write, in dry runs too (regressions: traversal, absolute, wrong-type and schema-violating identities in dry run and --execute; nothing created outside).
  • Operator dry-run results pass only for the intended no_reachable_target refusal or a normal validated result; transport, host-key, preflight and version refusals are rejected. Results are bound to the JSON host (op init --dest-host) and the doctor version. The --remote-bin used is recorded as operator attestation (remoteBinAttested) and only checked to name the verified install; it is not automatic verification.
  • Launcher and probe effects run only with --execute (dest probe, dest verify); activate/receipt dry runs classify statically; unexecuted verify results cannot be recorded.
  • already_provisioned keeps this run's staging (stagingRetained) for the explicit discard-staging --confirm-destructive step.
  • Regression tests failed before the fix (4 of 11) and pass after (12 of 12). A minimal Linux arm64 rerun with the fixed harness (59 steps, 29 expectations, 0 mismatches) covered the primary order, retained staging, dry-run no-effect checks and identity path refusals; cells L3–L8 keep the earlier harness revision as evidence.

Review fixes, round 2 (005cd4b)

  • Executed dest verify --run results are bound to the run, prefix sentinel ID and current lock; op record refuses a verify result from another run, an earlier lock or another prefix.
  • dest activate writes and syncs an activation intent before the rename (nothing is renamed if that fails); dest unlock checks intent and actual target together and refuses an unreceipted activation even after an interruption right after the rename.
  • dest discard-staging requires --run matching the staging marker; markerless leftovers need the separate dest discard-leftover --declare-abandoned.
  • Regressions failed before (3 of 15) and pass after (15 of 15). A second minimal Linux arm64 rerun (78 steps, 40 expectations, 0 mismatches) exercised these paths; the dc0de27-era and cc7fe23-era Linux records are kept as their own evidence.

Review fixes, round 3 (c0203fe)

  • Each dest lock acquisition gets a random lockId. All lock-bound results, journal records and statements carry the prefix sentinel ID and that lockId. Results of an earlier lock (even of the same run at the same timestamp), another run or another prefix are refused. callers_switched needs the publisher's unlock under the publishing lock.
  • The activation intent's presence and validity are separate. false/0/empty/null/array/malformed/extra- or missing-key intents, or intents for another run/lock/target/staging, refuse unlock, activation and receipts (activation_intent_unreadable); they are never treated as absent.
  • The rename test is split into a deterministic intent-write failure before the rename and the persisted post-rename state. No crash between files is physically injected.
  • Regressions failed before (2 of 18: an invalid intent let unlock release an unreceipted install; an earlier lock's receipt was recorded under a new lock of the same run) and pass after (18 of 18).

Review fixes, round 4 (f29f241)

  • The journal records each lock acquisition (prefix ID, lockId) exactly once. Replaying an earlier acquisition, or re-recording the current one from any run, is refused (acquisition_replayed), so an old unlock or receipt cannot become current again.
  • An acquisition stays current until its run's unlock of the same pair or a break that closes exactly that pair. A break closes the current lock only when its prefix ID and broken lockId name an open acquisition in this journal. Overrides whose owner pair cannot be identified, and breaks of other-prefix, other-journal or already-closed locks, remain audit records that close nothing. An override of a lock with a readable owner whose exact pair is an open acquisition does close it. Duplicate breaks are refused (break_replayed).
  • Regressions failed before (2 of 20) and pass after (20 of 20). This journal-only change was not rerun on the Linux host.

Final review fix (a4323cc)

  • A recorded break is a tombstone for its prefix ID and lockId. An acquisition result for that pair recorded later (break first, acquisition afterwards) is refused as acquisition_replayed and never becomes current. A same-lockId break on another prefix blocks nothing, and recovery is unchanged.
  • The regression failed before and passes after. Verified with the focused harness tests (21 of 21), build, types, repository and diff checks only.
  • A following comment-only commit aligns the op record lock-break comment with the ADR (overrides whose owner pair cannot be identified close nothing). The focused tests, build, types, repository and diff checks were rerun at that head.

Code-review fixes (934ceab)

  • dest discard-leftover removes only a staging tree with no marker entry at all. Any marker entry is refused as staging_has_marker and left in place (the fixture compares recursive path listings and file hashes): this workflow's, another manager's, unparsable, invalid, non-regular or symlinked. --declare-abandoned and the execute/destructive gates are not a permission to delete what something else may own. Own-run discard-staging and the activation-intent protection are unchanged.
  • op record accepts an operator dry-run only after a doctor recorded since the latest verify of the same target under the same lock. The operator-checks statement requires the recorded order verify → doctor → dry-run, and a re-verify makes an earlier doctor stale. This is the order of journal records supplied by the operator. It is not proof of when the operator commands ran, and the existing operator-attestation scope is unchanged.
  • dest receipt-cleanup and dest retire results are recordable as receipt_cleaned and retired. They get the same executed-result, run, prefix and lock binding as the other results, and receipt_cleaned keeps the mode, receipt state and ownership basis. Dry-run, other-run and earlier-lock results are refused; unrecognized destination-result names such as retire are not recordable. Operator checks, switch attestations and run-ended events are separate supported events. Cleanup and retirement entries are audit records: they do not invalidate historical receipts or change switch/statement eligibility. The ADR lists the recordable results.
  • Regressions failed before (4 of 24, one of them only the renamed refusal code) and pass after (24 of 24). Verified with the focused harness tests, build, types, repository and diff checks only. No Linux-host rerun.

Root review clarification (b7fb3fe)

After fresh independent Claude Code and Codex CLI review of 934ceab, a documentation/comment-only commit narrows the event-name description and explicitly states the audit-only scope of cleanup/retirement records. No executable code changed. Root ran node --check scripts/deployment-harness.mjs and git diff --check; focused 24/24 results remain tied to 934ceab, not a new test execution. The reviewers did not run tests or live-host checks.

Inline review fixes (839f2f6)

  • Caller state is keyed by prefix and install name. callers_switched/callers_unswitched take an explicit --prefix-id that must be a prefix the journal has locked; switches no longer cancel across prefixes, and no-caller/receipt-owned statements and receipt lookups are scoped to the current lock's prefix.
  • dest verify --execute runs launcher and native probes only after every static check passes (real owned directory, consistent marker, manifest, staging gone).
  • The explicit lock override removes only known lock metadata entries: a file, an unfollowed symlink or an empty directory. Unknown entries and non-empty directories are refused before any removal.
  • dest receipt syncs a publication intent with the exact receipt bytes before writing the receipt. Retrying after a lost response under the same lock and proof replays the committed receipt (replayed:true; bytes and verifiedAt unchanged), or completes a lost write with those bytes. Changed or corrupt receipts, stale proofs, other targets and later locks fail closed. op record accepts one receipt_published per target under a lock. No native send or host command is retried automatically.
  • After run_ended, only a run-ended statement is issued, even while the run's lock remains on disk.
  • callers_switched accepts a publishing acquisition closed by its run's unlock or by a recorded break that closed exactly that prefix and lockId; other-prefix breaks, stale breaks and unknown closures do not count.
  • dest path-inventory treats empty PATH components (leading, trailing, doubled) as the current directory.
  • Focused regressions: 31 tests at b7fb3fe code gave 22 pass / 9 fail. Two of the new tests and two existing tests failed only on the new --prefix-id argument. All 31 pass at 839f2f6. Build, types, repository and diff checks pass. No Linux-host rerun.

Publication recovery fixes (275ed8d)

  • dest receipt --adopt-existing is an explicit, non-destructive recovery for a receipt whose response was lost after its publishing run ended and its lock was broken. A new run under a fresh lock and fresh verify/doctor/dry-run publishes the still-matching receipt under that lock. Receipt bytes, original runId and verifiedAt are kept. Non-matching or invalid receipts and earlier-lock proofs are refused. Nothing is reinstalled or retried automatically.
  • Completing a lost receipt write from a publication intent requires every static check (and the probes when executed). The current marker runId, identityHash and recomputed manifest must also equal the committed receipt. A rolled-back or changed target is refused (publication_target_changed).
  • Publication intents must embed a receipt with the closed receipt schema; other valid JSON is refused before any write.
  • Lock metadata of other file types (FIFO, socket, device) is refused before removal.
  • Caller records without a prefix (older journal format, unsupported for caller decisions) make the caller count unusable (caller_history_unusable) rather than zero.
  • Documented: statements are issued on the operator side; issued statements become invalid at the destination when the lock changes, and nothing is revoked remotely. A lock holds one publication target, and the journal accepts one receipt_published per target under a lock.
  • The second-prefix test helper now runs the operator CLI against that prefix's own wrapper. It is a fixture within one test, not an independent full run.
  • Focused regressions: 37 tests at 839f2f6 code gave 31 pass / 6 fail. The completion, schema, file-type and legacy-record tests reproduce the findings; the two adoption tests failed only on the new --adopt-existing flag. All 37 pass at 275ed8d. Build, types, repository and diff checks pass. No Linux-host rerun.

Linux arm64 test host (user-authorized, isolated)

Ubuntu, Linux 7.0 aarch64, glibc 2.39, non-root account, Node 24.18.0 (ABI 137), bundled npm 11.16.0; new /tmp test directory (700, /tmp root-owned sticky). The operator side ran the op journal and this branch's session-peer 0.4.0 CLI from macOS arm64 over the existing pinned SSH trust. The harness driver's own ssh calls used BatchMode=yes, StrictHostKeyChecking=yes and UpdateHostKeys=no. For the operator session-peer CLI, UpdateHostKeys=no came only from a test-only local ssh wrapper that prepended it: the ordinary CLI's main SSH argv does not set UpdateHostKeys (only --ssh-jump does). This is kept as a separate candidate product observation and is not changed here. The host's SSH server is Tailscale SSH, not sshd. The published session-peer-0.4.0.tgz (SHA-256 84adc8b5…57c9) and its lockfile-pinned dependencies were installed offline with --ignore-scripts.

  • Historical full run (cc7fe23, harness 210759a1…41e7): 185 harness/operator steps, 89 checked expectations, 0 mismatches: exact order, already_provisioned, journal-guarded retention, fresh-lock recovery without a receipt (recovery receipt keeps the original runId), mismatched/corrupt/orphan receipts, .receipts mode/symlink/non-regular refusals, unowned/unreadable/unconfirmable lock overrides, journal-confirmed rollback, and the empty/file/symlink/inconsistent-marker/failed-manifest rows.
  • Operator doctor --host returned ok:true, diagnosticCompleted:true. The nonexistent-target dry-run returned no_reachable_target, submitted:false. No agent session or message was used.
  • path-inventory was unchanged. The existing user-owned Python session-peer launcher and the Codex binary had byte-identical hashes before and after; neither was executed. Both prefixes and the test directory were removed, and a later read-only check found no leftover path or process.
  • A first driver attempt passed no harness role (usage errors only; nothing written) and was rerun; this is recorded.
  • Intermediate review-revision Linux reruns (subset only; last c0203fe, not current 45461c1): three minimal reruns used harness 29a46eb… (dc0de27), 9d39ce8… (005cd4b) and finally afe2c7f5…d809 (c0203fe). The last had 87 steps, 48 expectations and 0 mismatches. They covered the primary order under host/version/lock binding, the explicit already_provisioned discard, dry-run no-effect, identity path refusals, stale and foreign results (including same-run earlier-lock receipt/unlock), the activation intent (failed write before the rename, invalid intents) and run-owned staging discards. Cells L3–L8 were not re-executed with any review-fixed revision.
  • Not qualified: Linux x64 under the current rules, Node 22 (no binary on the host; none installed), OpenSSH sshd as the server, races, the empty-directory window, and F6.

Validation (Darwin-local)

macOS 27.0.1 arm64, Node 24.16.0, npm 11.15.0.

  • test/deployment-receipt-harness.test.ts (focused): 37 passed, 0 failed at historical checkpoint 275ed8d. Earlier counts: 8 at cc7fe23, 12 at dc0de27, 15 at 005cd4b, 18 at c0203fe, 20 at f29f241, 21 at a4323cc and ea4d23e, 24 at 934ceab, 31 at 839f2f6. Private temp prefixes, a tree assembled from this checkout, and a local ssh stand-in running the real CLI doctor --host and nonexistent-target send --dry-run --host.
  • Ordered runs by hand (at cc7fe23 only; not repeated at later heads). One used the assembled tree. The other used --npm-cli offline installation of published session-peer-0.4.0.tgz, whose SHA-256 84adc8b5…57c9 matches the 0.4.0 record and whose dependency integrities match the lockfile. Both ended owned_receipted with a matching receipt, with the out-of-order refusals observed. The first npm-offline attempt failed because one file was passed as both user and global npm config; separate empty files fixed it, and that attempt is recorded.
  • npm run build, npm run test:types: PASS.
  • npm test with SESSION_PEER_PYTHON_ROOT set to the pinned Python reference checkout (8ca77cf): 409 total, 404 passed, 0 failed, 5 skipped. This full suite ran at f29f241 and was not rerun at a4323cc, ea4d23e, 934ceab, b7fb3fe, 839f2f6 or 275ed8d; that checkpoint has focused harness checks only; later checkpoints are listed below. Without SESSION_PEER_PYTHON_ROOT, the six Python-reference conformance/cross-runtime tests fail on the missing checkout (environment precondition).
  • npm run build, npm run test:types, node scripts/check-repository.mjs and git diff --check pass at 275ed8d. npm run test:package and npm audit --omit=dev (0 vulnerabilities) were last run at c0203fe, and earlier at cc7fe23; no package or dependency file changed after c0203fe.

What remains unqualified

  • Linux x64 under the current rules: unrun. The Linux arm64 result covers one arm64/glibc/Node 24 cell only.
  • Windows (any account; the only authorized host is administrator-only), other architectures and the Node 22 full matrix: unrun. The harness refuses on Windows; those cells need a reviewed Windows port or a manual run of the same order. No Node 22 exists on the Linux or Windows test hosts, and none was installed.
  • The 2026-10-02 Linux and 2026-10-07 Windows (published 0.3.1, administrator, PowerShell 5.1, Node 24.16.0; Node 22.23.3 banner/native smoke only) records keep their dated scope. Darwin-local fixtures do not replace destination evidence.
  • F1–F6 (--capabilities, new error codes, negotiation, launch.mjs identity enforcement, deploy helper) remain proposals. The POSIX empty-directory rename(2) window remains unguaranteed and unexercised.

Out of scope

No runtime source, CLI, SSH, preflight, package-file or version change (package stays 0.4.0). No release, tag, npm publication, skill or Python reference repin. No deploy helper, implicit remote installation, update --host or Python fallback. Relay/device, SideSession/Steer, automatic receipt, MCP and wake are not touched. Human merge gate; live destination tests require separate authorization.

Final integration and completion validation (cf4626c)

Integrated reviewed main ba44171ed5d2c7882006d6bc40d6a300448d0b0b in ebbe627; all dated validation records were preserved. The worker's harness/test/ADR bytes stayed unchanged during that integration. A root follow-up cf4626c then:

  • shares the closed, scalar-typed receipt schema with every completion consumer, including reuse and unlock;
  • shares all static checks, including staging absence, across initial dry-run, replay and adoption;
  • refuses further activation under an acquisition containing publication evidence, including same-identity recreation after rollback and already-provisioned reuse;
  • adds four regression cases covering malformed completion records at reuse/adoption/unlock, replay/dry-run staging checks, rollback/reactivation with an old proof, and initial dry-run refusal.

Fresh paired independent read-only Claude Code2.1.304 (actual claude-opus-5-5) and Codex CLI0.163.0 (isolated default; actual model not exposed) completed the final supplemental review with no mandatory defect. Root syntax, type and diff checks passed. An isolated unit execution of the actual classification functions with dependency stubs demonstrated that missing/invalid timestamps change from matching to corrupt while a valid receipt remains matching; this is not full CLI/trust-guard execution.

The four new CLI cases bring the focused file to41 tests. They have not been executed locally: this machine's authorized external-volume temp ancestry is correctly refused by the harness, and no permissions or guards were changed to bypass it. The latest actual worker execution remains37/37 at 275ed8d; whole-suite and Linux records above remain historical. Current-head CI must execute the integrated CLI cases before merge. Current-head Code/Security review is also required; older review results are not substituted. No new native/model/SSH/ACK or user-host operation was performed.

Review fixes on the integrated head (61f37f8)

On top of cf4626c (root integration and follow-up preserved; no reset or rebase):

  • Retained staging. already_provisioned records the staging tree it keeps (retained.json, bound to run, lock and target). Until that tree is explicitly discarded (discard-staging --run … --confirm-destructive), the following are refused: the final verify (stagingGone:false, no probe), receipts and adoption that depend on it, unlock and further activations. A present but invalid record is refused, never treated as absent. The internal existing-target check inside activate is unchanged.
  • __proto__ manifest names. The manifest's file and link maps have no prototype. A root entry named __proto__ (file or symlink) is recorded, and later byte or link-target changes are detected before verify or receipt.
  • Durability. Before a receipt is reported as written, completed, replayed or adopted, the following are synced in order: the publication intent and lock directory, then the receipt file, .receipts and the root, then a publication.synced.json marker. Existing receipt bytes are never rewritten. unlock keeps the publication evidence and refuses (publication_unsynced) while a receipt or its install exists without the marker. Journal records write every byte (short writes continue), are synced, and are truncated back on failure. Test-only environment hooks trace the fsync order, inject one fsync failure or force short writes. They only add failures and do not qualify real power-loss behavior. The stated unknown-crash, atomic-file-system and same-user limits remain.
  • Fixture. The operator-doctor positive fixture now uses the package version instead of a hard-coded 0.4.0; the 0.3.9 wrong-version negative stays. The package version on this branch is unchanged (0.4.0).
  • Focused regressions. Running the 46-test file against the cf4626c harness gave 41 pass / 5 fail.
    • Behavioral failures: retained staging allowed verify and unlock; a __proto__ change went undetected.
    • Test-hook failures: the two sync tests and the journal-fault test need the new test hooks, which cf4626c does not have.
    • All 46 pass at 61f37f8, including root's four cf4626c cases.
    • These were executed locally with the test file's default temporary directory (the macOS per-user temp directory from os.tmpdir()) and removed afterwards. For this external-volume worktree that location is a deviation from the user's temp-root policy, as root has noted. The results stand as actual executions, but the location was not authorized, and no further CLI runs used it.
    • Build, types, repository and diff checks pass.
  • Not run at 61f37f8. Full suite (historical f29f241 only), Linux, Windows, package, audit, model, native and user-host checks. Current-head CI and current-head Code/Security review are still required.

Bounded fixes (fddb7c4)

  • Publication sync contract. Every executed receipt write, completion, replay and adoption follows the same order before reporting success:

    1. the existing publication intent, then the lock directory;
    2. the receipt, then .receipts, then the root;
    3. the synced marker (written, or the existing valid one), then the lock directory.

    A failed intent sync stops before the receipt sync. A failed marker sync happens after the receipt and its directories were synced: it fails the step but does not by itself mean the receipt is lost.

  • Journal rollback honesty. A failed append attempts a rollback: it truncates to the previous length and syncs. journal_append_failed (rollback restored) is reported only when that is confirmed. Otherwise the result is journal_append_unknown (rollback unconfirmed), which keeps the original error and the rollback error, and the operator must inspect the journal before any next step. A failure never promises the record is absent, and the append is never retried. Success still requires the full line written (short writes continue) and synced. The test hooks add failure-count and truncate-failure injection. None of this qualifies real power-loss behavior.

  • Retained-staging boundary (documentation only). The block applies only while the kept tree is pending. After the explicit discard, the lock's statements remain usable, and no new proof fencing was added.

Coverage at fddb7c4, stated exactly.

Executed:

  • The two journal tests ran in a private 0700 directory under the external volume's approved tmp root. They failed on the 61f37f8 code with harness_error and pass now (2/2).
  • node --check, build, types, repository and diff checks pass.

Unrun locally:

  • The replay sync-order and intent/marker-failure test, and every other dedicated-prefix CLI fixture. The approved root's ancestors are correctly refused by the harness, and no guard, permission or temp location was changed to get around this. These are left to current-head CI.
  • Full suite (historical f29f241 only), Linux, Windows, package, audit, model, native and user-host checks.

The last executed whole-file result remains 46/46 at 61f37f8, run at the deviating temp location noted above.

Root completion-order correction (9559613)

Fresh paired Claude Code 2.1.304 (actual claude-opus-5-5) and Codex CLI 0.163.0 (isolated default; actual model not exposed) independently found one remaining path in fddb7c4: when the receipt was absent, completion wrote/synced it before resyncing a possibly failed earlier intent. Root now resyncs the intent and lock directory before creating that receipt. A dedicated CLI regression checks repeated intent-sync failure leaves the receipt absent and untraced, followed by explicit fault-free completion. That full CLI regression is unrun locally because the approved ancestry is refused; it must pass in current-head CI.

A separate isolated execution of the actual receipt handler and sync helper with dependency stubs demonstrated receipt creation before the injected intent failure in fddb7c4, and no creation after the root fix. It does not exercise the CLI trust guard, filesystem durability, probes or power loss. A fresh supplemental paired review of the exact root delta succeeded with no mandatory findings. Syntax, types and diff checks pass.

Synthetic handoff fixture backport (e9f99e0)

This PR additionally changes test/handoff-delivery.test.ts, backported byte-for-byte from the reviewed release-preparation fixture. Branch scope is now five files: VALIDATION.md, the deployment ADR, repository-only deployment harness, its CLI tests, and this synthetic handoff fixture. Runtime source, product guards, deadlines, support cells and package version are unchanged.

  • Queue and initialize readiness check persisted synthetic evidence on a 25ms poll with the existing 10s limits. Early CLI close reports synthetic output/native-stage diagnostics. No CLI submission is retried.
  • The warm history fixture retains its synthetic WAL connection and closes it before cleanup. The regression closes its mode reader before injection so only the fixture keeper can preserve DB/WAL/SHM identities. A keeper-removed mutation fails and the restored keeper passes. This models a supported synthetic topology; it does not establish that all native versions default to WAL.
  • Exact readiness/WAL inputs were freshly and independently reviewed by Claude and Codex. Both runs succeeded without mandatory production findings; root applied their narrow regression-strengthening suggestions. Related readiness tests passed 6/6 before the final mode clarification, and the clarified early-initialize refusal test passed 1/1 afterward. Earlier WAL-related 8/8 and keeper mutation records remain scoped to their exact checkpoints.
  • The original macOS CI failures remain unattributed: one waited for queue evidence, one failed resumed observation, and a later release-preview test waited for initialize evidence before reaching its SIGINT assertions. These fixture corrections do not prove their cause. Current-head CI must pass rather than substituting older green runs.

No additional user-host, native/model, SSH delivery, ACK or production operation was performed. All root follow-up temp evidence used private directories under the approved external-volume root.

Additional current-head review findings (E9; local fixes in 8c6f224, 418ad93, 6be918d and 45461c1, independently reviewed; current-head CI remains required)

The current-head Code Review identified six further gaps: remote-bin path traversal in attestation, manifest symlink escapes, shared-journal append/rollback concurrency, caller changes under open-prefix acquisitions, historical publication reuse after lifecycle changes, and npm tarball option parsing. Root matched these conditions to the source and delegated bounded corrections from E9. They are not implemented in this head, and neither CI nor earlier independent reviews are evidence of their resolution. The six current review threads remain open.

The previous-head ten fixed threads were resolved only after their source fixes and the current E9 Ubuntu22/24 deployment fixtures passed. Resolution of the six E9 threads still requires current-candidate validation; no earlier green result authorizes merge.

Local fixes for the E9 findings (8c6f224, 418ad93, 6be918d, on top of e9f99e0)

e9f99e0 and every earlier commit are preserved (no reset or rebase). Only the ADR, the harness and its test file changed; test/handoff-delivery.test.ts is untouched. 418ad93 applies root's design refinements on top of 8c6f224, and 6be918d adds a test-only pause point plus a regression for the journal rollback window.

  • remote-bin attestation. The attested --remote-bin must already be a canonical absolute POSIX path (no . or .. segment, no repeated or trailing /) and must equal exactly the wrapper path reported by the verified destination result (a harness-only verify field). A path that merely normalizes to it is refused, because a shell resolves link/.. through the link. Other wrappers and prefixes are refused too. The path is never resolved locally or remotely. It remains operator attestation, not proof of execution.
  • Manifest links. Absolute link texts are always refused, because they go stale or point outside after the activation rename. Relative links must resolve inside the install, including through chains; escaping, dangling and looping links are refused (unsafe_link). This check runs during staging, before the version read, and before every probe. Relative internal links survive activation, and their referents are covered by the manifest.
  • Journal coordination. Every op begin, op record and op statement (dry runs included) holds the ephemeral <journal>.lock (mkdir, then removed).
    • Executed commands hold it across read, validation, append and rollback. Statements and dry runs hold it across their read, so they never see a line a concurrent failed append later rolls back.
    • The lock directory is their only effect, and a crash can leave it behind.
    • Contention is refused (journal_busy) with no waiting, stealing or killing. A leftover lock is removed by the operator after checking that no op command is running.
    • No claim is made about hostile same-user writers or power loss.
  • Caller freshness. callers_switched and callers_unswitched are refused while any recorded acquisition on the prefix is open (acquisition_open). The existing retire test now releases the lock, records the unswitch and takes the lock again, which is equivalent to unswitching before the lock (noted in the test). The guard was not weakened.
  • Lifecycle fence. A publication is superseded by a later rollback, retirement, receipt cleanup or new activation of the same prefix and name (publication_superseded), and a newer completed publication is required. activated records now keep their outcome: already_provisioned reuse does not fence, and records without an outcome (earlier revisions) fence as new activations. The ADR's earlier "audit-only" description is replaced.
  • npm arguments. Tarballs are passed as absolute paths of regular files, so none can be read as an npm option; the hashed path is the one npm receives.

Coverage, stated exactly.

  • Executed locally, in private 0700 directories under the external volume's approved tmp root: seven journal-only tests.
    • They cover journal lock contention, the two-writer refusal, rollback isolation (including a statement refused during a held append), the two earlier journal tests, and caller fences and canonical remote-bin binding on shaped destination results.
    • On the e9f99e0 harness, five of them failed; the two earlier journal tests passed there.
    • On the 8c6f224 harness, the canonical remote-bin case failed.
    • All seven pass at 418ad93.
    • node --check, build, types, repository and diff checks pass.
    • 6be918d holds a complete appended lock_acquired line on disk while its rollback waits. A statement and a second record are refused (journal_busy), and after the confirmed rollback no statement is issued from that acquisition (lock_not_recorded). On 418ad93 this test fails only because the pause point did not exist there.
    • No deterministic regression isolates the narrow window that 8c6f224 left between its busy check and its read. Taking the lock for statements closes that window by design.
    • All eight local journal-only tests pass at 6be918d.
  • Unrun locally: dedicated-prefix CLI fixtures (manifest links, including the internal-link rename and referent checks; npm argv capture; the CLI caller-freshness and supersede flows; the exact-wrapper CLI case) and the existing CLI tests adjusted to the new rules.
    • The approved root's ancestor is refused by the harness, and no guard, permission or temp location was changed.
    • These need current-head CI.
  • Not run: full suite (historical f29f241 only), Linux, Windows, package, audit, model, native and user-host checks.
  • Superseded evidence: the 46/46 at 61f37f8 (non-policy temp location) is not new evidence.

Fixes from the immutable 418ad93 review (45461c1, on root-approved base 6be918d)

  • Link chains. Manifest links are followed as the OS follows them, one component and one link at a time. Every intermediate location must stay inside the install: a hop out through another location, or a .. above the install root, is refused even when the chain comes back. Absolute, dangling and looping links stay refused, and nothing is probed before this check. Relative internal links survive the activation rename, and their referents stay covered by the manifest.
  • Tarball selection. The candidate attempts to preserve the OS-selected file with lstat and realpath of the given path. Root subsequently confirmed that Node's JS realpath still normalizes .. before following links; the native-realpath correction below is required to preserve the intended selection. The final component must be a regular file, and npm gets its absolute real path. The leading-dash guard is kept.
  • Journal aliasing. A journal must have exactly one link (journal_aliased). Before append or rollback, the open file must retain the same single-link identity; before append, its size must also match the read snapshot. Installation and package files are not restricted.
  • Late publication. A cleanup, retirement or rollback recorded since an acquisition closes that acquisition's publication. A later receipt result under the same lock is refused (publication_closed). Recovery needs a new lock and new checks, or the explicit adoption.
  • Init mutex. op init holds the journal lock from the exclusive create until the header line and its newline are written and synced; its dry run holds the lock only to check for an existing journal.
  • Testability. The harness now exports its filesystem-inspection link and manifest helpers for tests; the command runs only when the file is executed. The test file accepts a test-only DEPLOYMENT_HARNESS_SCRIPT override, so before/after comparisons ran against a separate copy of the earlier revision and never replaced the worktree file.

Coverage, stated exactly. Executed in private 0700 directories under the external volume's approved tmp root:

  • Four new tests ran against a copy of the 6be918d harness. All four failed:
    • the hard-linked journal and the late-publication cases reproduce the findings directly;
    • the link-chain unit failed only because 6be918d exports no helper;
    • the init test failed only because its pause point did not exist.
  • At 45461c1, all twelve journal-only and filesystem-unit tests pass. node --check, build, types, repository and diff checks pass.

Unrun locally:

  • The tarball fixture with a symlink ancestor and ...
  • All other dedicated-prefix CLI fixtures; the approved root's ancestor is correctly refused. These need current-head CI.
  • Full suite, Linux, Windows, package, audit, model, native and user-host checks.

Root path and journal-framing follow-up

Fresh paired Claude Code 2.1.304 (actual claude-opus-5-5, user-approved cals_dev) and Codex CLI 0.163.0 (isolated default, actual model unreported) reviewed identical frozen 45461c1 input. Both completed successfully and identified that JS realpath still reinterprets a symlink ancestor followed by ... Root also confirmed the trailing-component link issue and the unterminated-journal append path against the source. Fresh supplemental paired review of the exact three-file root delta completed successfully, with no mandatory findings from either reviewer. Identical frozen input and source/patch hashes were unchanged before and after both runs. Root then corrected two stale init coordination comments only; executable code and tests were unchanged by that comment cleanup.

  • Use realpathSync.native(raw) for the tarball selected by the OS, retaining the final regular-file check and the identical safe absolute path for hash and npm argv. Actual Node24.16 filesystem evidence and an execution of the exact selector with filesystem dependencies show the old code selecting decoy bytes and the correction selecting requested bytes. This is not a full stage/npm CLI run.
  • Require a directory whenever any link-target component remains, including a trailing slash or dot. New file/ and file/. cases failed on 45461c1; the corrected helper also preserves positive directory/ and directory/. cases and the existing link-boundary/rename/referent checks.
  • Require a final LF before reading journal records. A complete JSON record without its LF previously let op begin succeed and concatenate another record. The new regression refuses begin, dry-run begin, statement and record with journal_corrupt and requires unchanged bytes. No repair, migration or automatic retry is added.
  • Clarify append-size versus rollback-identity checks and remove the stale documentation exception for init from journal_busy behavior.

Root executed the two strengthened link/journal tests under the approved private external-volume root: both pass after discriminating before-failures. Syntax, type, repository/link/anchor and diff checks pass. The peer's twelve journal/filesystem tests remain evidence at its exact 45461c1 checkpoint. Dedicated-prefix CLI fixtures, including the OS-selected tarball case, remain unrun locally because the approved ancestry is correctly refused. All current-candidate contract CI and Code/Security reviews remain required. No new native/model/SSH/delivery/ACK or user-host operation occurred.

At root commit 35077da, the two focused regression cases, syntax, types, repository metadata/link/anchor and diff checks passed. Twelve peer journal/filesystem cases remain tied to 45461c1 rather than being relabeled as new executions. The initial failed after-run in the peer validation used a truncated temp path due to a shell quoting error; it created nothing and the successful rerun log replaced that initial log. That limit is recorded, not treated as retained before/after evidence. No worktree files were swapped for the 45461c1/root before-runs: comparison scripts were separate immutable copies under the approved private temp root.

Historical 35077da CI and review follow-ups

Ubuntu22 passes, while Ubuntu24 (Node24.22.0) has 44 prepared-tree stage failures, all ERR_FS_CP_EEXIST. The official pinned Node24.22 onDir implementation now rejects an existing directory when errorOnExist is true and force is false; unlike v24.16/v22.23, it rejects the root this harness intentionally created exclusively. The correction keeps the exclusive 0700 root and all no-overwrite options, and copies top-level entries into initially absent children. Local Node24.16 did not reproduce the old behavior difference; it validates the new helper and collision guards, while current Node24.22 CI remains necessary.

Current-head Code Review also identified five new follow-ups: stale receipt-owned evidence after lifecycle changes, nonregular/unbounded PATH hashing, prefixes outside the remote-bin grammar, lack of single-journal prefix affinity, and npm CLI argument parsing. Root source triage confirmed them. They remain unresolved in 35077da and block merge; subsequent corrections will preserve all prior commits and will need fresh independent review/current-head CI.

Node24.22 prepared-tree compatibility (d992bf5)

The exclusive 0700 staging directory is retained while each source entry is copied to an initially absent child with recursive/verbatimSymlinks/errorOnExist:true/force:false unchanged. Source and destination are interpreted natively; overlap, invalid directory inputs and nonempty destinations are refused. Staging root identity and mode, relative npm links and manifest-before-probe ordering are preserved. The npm installation branch is unchanged.

Cause evidence: official pinned Node v24.22.0 onDir rejects the precreated root; v24.16.0 and v22.23.3 lack that branch. The 44 failed Ubuntu24 stage cases match that exact code path. No blind CI rerun was requested for this deterministic compatibility error.

Fresh paired read-only Claude2.1.304 (actual claude-opus-5-5/cals_dev) and Codex0.163.0 (isolated default, actual model unreported) reviewed identical frozen bounded input/source/patch hashes; both succeeded with no mandatory findings. Root strengthened one test afterward to use a 0755 source versus the private 0700 destination; executable source was unchanged by that test clarification. Four actual prefix-free helper units, types, repository and diff checks pass locally on Node24.16. The older root-copy also passes on 24.16, so this is not claimed as a local failing-before runtime reproduction of 24.22. Current Node24.22/full-stage CI is pending.

The five latest 35077da review findings remain unresolved in d992bf5 and are delegated from this exact preserved base. No new manual review request was added for this interim head while those fixes are pending. No merge, release or publication is performed.

Add scripts/deployment-harness.mjs, a repository-only validation harness
for the remote deployment ADR. It runs the destination checks (dedicated
prefix, root and .receipts trust, staging, probe, mkdir lock, no-replace
activation, verification, receipts, rollback, retention, lock breaking)
and an operator-side hash-chained run journal as separately invoked
commands. It never opens SSH, never installs remotely by itself, never
runs during list/send/doctor and leaves the exact-version preflight
unchanged. Mutations are dry runs unless --execute; destructive steps
also need --confirm-destructive. POSIX only.

The harness enforces the step 8 order: destination verification, then
the operator doctor and dry-run, then the receipt, then unlock, then the
caller switch. It also covers fresh-lock recovery without a receipt,
journal-confirmed rollback, corrupt/mismatched/orphan receipts,
.receipts trust and unowned or unconfirmable lock overrides.

Fix ADR drift for 0.4.0 (version range, handoff preparation after the
same preflight, the smol-toml runtime dependency), state the operator
check order explicitly, refuse an orphan receipt before activation, and
document the harness, its Darwin-local evidence and the unrun Linux,
Windows non-admin and Node 22 cells. Record the Darwin-local results in
VALIDATION.md; they qualify no destination cell.

Refs #23
Record one user-authorized, isolated harness run on a Linux arm64 test
host (glibc 2.39, Node 24.18.0, published 0.4.0 installed offline into a
new /tmp prefix). It covers the exact operator-check, receipt, unlock and
caller-switch order, fresh-lock recovery without a receipt,
corrupt/mismatched/orphan receipts, .receipts trust, unknown and
unverifiable lock-owner overrides, journal-confirmed rollback and
retention, and the activation-table rows. All 89 checked expectations
matched; existing user executables stayed byte-identical and the test
directory was removed.

Scope the ADR and VALIDATION wording to exactly that cell. Linux x64
under the current rules, Node 22 (no binary on either host), Windows
(the harness refuses; the only host is administrator-only), races and
F6 remain unqualified.

Refs #23
Address review findings in the #23 validation harness:

- dest stage validates the identity file against a closed schema
  (exact keys and types, version syntax, SHA-256/SHA-512 forms,
  absolute Node path) and requires the staging name to resolve to a
  direct child of the root before any write, in dry runs too. Markers
  use the same schema.
- op record accepts an operator dry-run only for the intended
  no_reachable_target refusal or a normal validated result; transport,
  host-key, preflight and version refusals no longer pass.
- Operator results are bound to what the CLI JSON shows: host equal to
  op init --dest-host and doctor version equal to the verified install.
  The --remote-bin used is recorded as remoteBinAttested operator
  attestation and only checked to name the verified install.
- Launcher and native-lock probes run only with --execute: dest probe and
  dest verify are execute-gated, activate/receipt dry runs classify
  statically, and unexecuted verify results cannot be recorded.
- already_provisioned keeps this run's staging (stagingRetained) for the
  explicit discard-staging --confirm-destructive step.

Add regression tests (traversal/absolute/type/schema identities in dry
run and execute, refused transport and preflight dry-runs, host/version
/install binding, no launch or probe file in dry runs, retained staging)
and record a minimal Linux arm64 rerun with the fixed harness. Cells not
rerun keep the earlier harness revision as their evidence.

Refs #23
Address the second review round of the #23 validation harness:

- dest verify --run <run> --execute binds its result to the run, the
  prefix sentinel ID and the creation time of the run's current lock;
  op record accepts a verified result only from the recording run's
  current lock on the same prefix, so an earlier lock's or another
  run's verification cannot satisfy the operator-check order.
- dest activate writes and syncs an activation intent (target and
  staging) inside the lock before the rename and renames nothing if
  that write fails; dest unlock checks the intent against the actual
  target and refuses while it exists without a matching receipt or
  the intent is unreadable.
- dest discard-staging requires --run and a valid staging marker naming
  that run; markerless leftovers of a failed stage are removed only by
  the separate dest discard-leftover --declare-abandoned with the same
  execute and destructive gates.
- The ADR scopes the sentinel rule to commands that inspect or change a
  prefix; dest identity and dest path-inventory take none.

Add regression tests that failed before these changes (stale and
foreign verify results recorded, unlock released after a rename whose
record failed, another run's staging deleted) and record a second
minimal Linux arm64 rerun with the fixed harness.

Refs #23
Address the third review round of the #23 validation harness:

- Every dest lock acquisition gets a random lockId. Every lock-bound
  result (activate, verify --run, receipt, unlock, rollback, receipt
  cleanup, retention), journal record and statement carries the prefix
  sentinel ID and that lockId, and op record refuses results of another
  run, another prefix or an earlier lock, including an earlier lock of
  the same run acquired at the same timestamp. callers_switched requires
  the publisher's unlock under the lock that published the receipt.
- The activation intent's presence and validity are separate: a present
  intent that is not exactly this lock's closed record (false, 0, empty,
  null, array, malformed, extra or missing keys, another run, lock,
  target or staging, or not a regular file) refuses unlock, activation
  and receipts as activation_intent_unreadable and is never treated as
  absent.
- Split the interrupted-activation test into a deterministic intent
  write failure before the rename and the persisted post-rename state;
  no crash between files is physically injected, and the docs say so.
- Scope the ADR and VALIDATION wording: the full Linux arm64 cells
  L3-L8 are historical evidence of harness revision 210759a1 (cc7fe23);
  the final revision was rerun on that host only for the stated subset.

Add regressions that failed before (an invalid intent let unlock
release an unreceipted install; an earlier lock's receipt was recorded
under a new lock of the same run) plus same-timestamp and
another-prefix negatives.

Refs #23
…he journal

Close two operator-journal freshness gaps in the #23 validation harness:

- op record accepts a lock acquisition (prefix ID and lockId) only once
  per journal: re-recording it, whether replayed after it closed or
  repeated while current and from any run, is refused as
  acquisition_replayed and never deduplicated, so an earlier
  acquisition cannot become current again and revive its old unlock
  or receipt results.
- An acquisition stays current until its own run's unlock of the same
  pair or a recorded break that closed exactly that pair. A break
  result closes it only when its prefix ID and broken lockId name an
  open acquisition recorded in this journal; unowned overrides, other
  prefixes' or journals' locks and already closed locks are kept as
  audit records that close nothing, and recording the same break twice
  is refused as break_replayed.

Add regressions that failed before (a replayed earlier acquisition was
accepted; a break on another prefix ended the current holder's lock)
plus stale-break, duplicate-break and legitimate-recovery cases. The
Linux host was not rerun for this journal-only change.

Refs #23
Close the out-of-order journal gap in the #23 validation harness: a
break recorded before its acquisition (break first, acquisition result
recorded later) no longer lets that acquisition become current. op
record refuses a lock_acquired whose prefix ID and lockId already
appear anywhere in the journal as an acquisition or as a recorded
break's broken lockId (acquisition_replayed). A break naming the same
lockId on another prefix is an unrelated generation and blocks nothing;
existing recovery is unchanged.

The ADR now limits "closes nothing" to overrides whose owner pair
cannot be identified: a break of a lock with a readable owner whose
exact pair is an open acquisition here does close it.

Add a regression that failed before (a late-recorded acquisition was
accepted after its break) plus the other-prefix case. Verified with
the focused harness tests only.

Refs #23
The op record comment still listed an unowned override as always
closing nothing. Limit it to overrides whose owner pair cannot be
identified, matching the ADR: a break with a readable owner whose exact
pair is an open acquisition recorded here does close it. Comment-only
change; no code line changed.

Refs #23
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-20T10:33:22.874557Z 35077da Manual request
🔒 Security Review ✅ Completed 2026-10-20T08:16:48.570918Z cf4626c Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ea4d23e93a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/deployment-harness.mjs Outdated
Comment thread scripts/deployment-harness.mjs Outdated
Comment thread scripts/deployment-harness.mjs Outdated
…recordable results

Address three code-review findings on the #23 validation harness:

- dest discard-leftover removes only a staging tree with no marker
  entry at all. Any marker entry, whether this workflow's, another
  manager's, unparsable, invalid, non-regular or symlinked, is refused
  as staging_has_marker and left byte-for-byte unchanged;
  --declare-abandoned and the execute/destructive gates are not a
  permission to delete what something else may own.
- op record accepts an operator dry-run only after a doctor recorded
  since the latest verify of the same target under the same lock, and
  the operator-checks statement requires the recorded order verify,
  doctor, dry-run. A re-verify makes an earlier doctor stale. This is
  the order of journal records, not proof of when the operator
  commands ran.
- dest receipt-cleanup and dest retire results are recordable as
  receipt_cleaned and retired, with the same executed-result, run,
  prefix and lock binding as other results; receipt_cleaned keeps mode,
  receipt state and ownership basis. Other event names stay usage
  errors. The ADR lists the recordable results.

Add regressions that failed before (foreign and invalid markers
deleted by discard-leftover; a dry-run recorded before any doctor;
cleanup and retention results not recordable) plus reverse-order,
stale-doctor, stale-lock, dry-run and other-run negatives. Verified
with the focused harness tests only.

Refs #23
Distinguish destination results from operator events and state that cleanup and retirement records are audit entries, without changing switch or statement eligibility. No executable code changes.

Refs #23
@abruption

Copy link
Copy Markdown
Owner Author

@codex review

@abruption

Copy link
Copy Markdown
Owner Author

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: b7fb3fe969

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b7fb3fe969

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/deployment-harness.mjs Outdated
Comment thread scripts/deployment-harness.mjs Outdated
Comment thread scripts/deployment-harness.mjs
Comment thread scripts/deployment-harness.mjs
Comment thread scripts/deployment-harness.mjs Outdated
Comment thread scripts/deployment-harness.mjs Outdated
Comment thread scripts/deployment-harness.mjs Outdated
…cation replayable

Address seven inline review findings on the #23 validation harness:

- Caller state is keyed by prefix and install name. callers_switched
  and callers_unswitched take an explicit --prefix-id that must be a
  prefix the journal has locked; switches no longer cancel across
  prefixes, and no-caller/receipt-owned statements and the receipt
  lookups are scoped to the current lock's prefix.
- verify runs launcher and native probes only after every static check
  (real owned directory, consistent marker, manifest, staging gone)
  passes.
- The explicit lock override removes only the known lock metadata
  entries, each a file, an unfollowed symlink or an empty directory;
  unknown entries and non-empty directories are refused before any
  removal.
- dest receipt syncs a publication intent with the exact receipt bytes
  before writing the receipt. A retry after a lost response under the
  same lock and proof replays the committed receipt (bytes and
  verifiedAt unchanged) or completes a lost write with those bytes;
  changed, corrupt or orphan receipts, stale proofs, other targets and
  later locks fail closed, and op record accepts one receipt_published
  per target under a lock.
- After run_ended only a run-ended statement is issued, even while the
  run's lock remains on disk.
- callers_switched accepts a publishing acquisition closed by its run's
  unlock or by a recorded break that closed exactly that prefix and
  lockId; other-prefix breaks, stale breaks and unknown closures do
  not count.
- path-inventory treats empty PATH components as the current directory
  and inventories ./session-peer there.

Add regressions for each finding; they failed before (two of them, and
two existing tests, only on the new --prefix-id argument) and pass
after. Verified with the focused harness tests only.

Refs #23
…intents

Address the root review of the #23 harness publication path:

- dest receipt --adopt-existing is an explicit, non-destructive
  recovery: after a run wrote a receipt, lost the response, ended and
  had its lock broken, a new run under a fresh lock with fresh verify,
  doctor and dry-run publishes the still-matching receipt under that
  lock. Receipt bytes, the original runId and verifiedAt are kept;
  receipts not matching the marker and manifest, invalid receipts and
  earlier-lock proofs are refused.
- Completing a lost receipt write from a publication intent now
  requires every static check (and the probes when executed) and the
  current marker runId, identityHash and recomputed manifest to equal
  the committed receipt; a rolled-back or changed target is refused as
  publication_target_changed, and the result is rechecked after the
  write. Replays also recheck the install.
- Publication intents must embed a receipt of the closed schema (exact
  keys, receiptVersion 1, 32-hex runId, 64-hex hashes, ISO verifiedAt);
  other valid JSON is refused before any write.
- Lock metadata entries of other file types (FIFO, socket, device) are
  refused before removal.
- Caller records without a prefix make the caller count unusable
  (caller_history_unusable) instead of counting as zero; older journal
  formats are not supported for caller decisions.
- The test helper for a second prefix runs the operator CLI against that
  prefix's own wrapper. The ADR states statement issuance scope and the
  one-publication-per-lock versus one-record-per-target distinction.

Regressions: the completion, schema, file-type and legacy-record tests
failed on 839f2f6 and pass now; the adoption tests failed there on the
new --adopt-existing flag. Verified with the focused harness tests only.

Refs #23
…groundwork

Preserve each dated validation section while integrating main ba44171. Harness, tests and ADR retain the reviewed 275ed8d bytes. Refs #23
Use one closed receipt schema and one static-check predicate for initial publication, replay and adoption. Reject activation after publication scope is committed so rollback cannot revive an old proof under the same acquisition. Add reuse, adoption, unlock, staging and reactivation regressions; full CLI execution remains required in CI.
@abruption

Copy link
Copy Markdown
Owner Author

@codex review

@abruption

Copy link
Copy Markdown
Owner Author

@codex security review

abruption added a commit that referenced this pull request Oct 20, 2026
Keep release PR draft until dependency #155 merges and final current-main checks complete. No staging or publication.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cf4626c203

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/deployment-harness.mjs Outdated
Comment thread scripts/deployment-harness.mjs Outdated
Comment thread scripts/deployment-harness.mjs Outdated
@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: cf4626c203

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

… manifests

Address three review findings and one fixture issue on the #23
validation harness:

- already_provisioned records the staging tree it keeps in the lock
  (retained.json, bound to run, lock and target). Until that tree is
  explicitly discarded, the final verify (stagingGone false, no probe),
  receipts and adoption depending on it, unlock and further
  activations are refused; a present but invalid record is refused,
  never treated as absent.
- The manifest's file and link maps have no prototype, so a root entry
  named __proto__ (file or symlink) is recorded and later changes are
  detected before verify or receipt.
- Receipt publication is synced before success: intent and lock
  directory, then the receipt file, .receipts and the root, then a
  publication.synced.json marker, for first writes, completed writes,
  replays and adoption (existing bytes are never rewritten). Unlock
  keeps the evidence and refuses while a receipt or its install exists
  without the marker. Journal records write every byte (short writes
  continue), are synced, and are truncated back on failure. Test-only
  environment hooks trace fsync order, inject one fsync failure or force
  short writes; they add failures only and do not qualify power-loss
  behavior.
- The operator-doctor positive fixture uses the package version instead
  of a hard-coded 0.4.0; the wrong-version negative stays 0.3.9.

Regressions for each failed before (retained staging released, a
__proto__ change undetected, no sync guard or journal fault handling)
and pass now. Verified with the focused harness tests only.

Refs #23
…lback honestly

Close two bounded gaps in the #23 validation harness:

- Every executed receipt write, completion, replay and adoption now
  syncs the existing publication intent and the lock directory before
  the receipt, .receipts and the root, and then writes or resyncs the
  synced marker and the lock directory before reporting success. A
  failed intent sync stops before the receipt sync; a failed marker sync
  happens after the receipt and its directories were synced and fails
  the step without implying the receipt is lost.
- A failed journal append attempts a rollback (truncate to the previous
  length and sync). journal_append_failed with rollback restored is
  reported only when that is confirmed; otherwise journal_append_unknown
  with rollback unconfirmed keeps the original and rollback errors, and
  the operator must inspect the journal first. A failure never promises
  the record is absent and the append is never retried. Test-only hooks
  can now fail only the first N matching fsyncs and fail the truncate.

The ADR also states that retained staging only blocks while it is
pending; after the explicit discard the lock's statements remain
usable.

Journal regressions failed on 61f37f8 (harness_error) and pass now,
run in a private directory under the external volume's tmp root. The
replay sync-order test needs a dedicated prefix, which that root's
ancestors correctly refuse; it is unrun locally and left to CI.

Refs #23
Preserve the publication ordering contract when an earlier intent write failed its sync. Require the intent and lock directory sync before creating the missing receipt. Add a regression for repeated intent-sync failure; full CLI execution remains a current-head CI gate. Read-only paired Claude/Codex review found no mandatory defect. Refs #23
Backport the independently reviewed release-preparation test corrections: persisted queue and initialize readiness with unchanged deadlines, early-close diagnostics, and a retained synthetic warm WAL connection. Include the discriminating notification and keeper regressions. Runtime guards, delivery assertions and platform support are unchanged; the original CI causes remain unproven. Refs #23
@abruption

Copy link
Copy Markdown
Owner Author

@codex review

Please review the current head e9f99e0c08e4b7ea017b977bf7d2e424a12be743, including the bounded publication/journal fixes and synthetic fixture corrections. Previous-head results are historical; no merge or publication requested.

@abruption

Copy link
Copy Markdown
Owner Author

@codex security review

Please review the current head e9f99e0c08e4b7ea017b977bf7d2e424a12be743, including the bounded publication/journal fixes and synthetic fixture corrections. Previous-head results are historical; no merge or publication requested.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e9f99e0c08

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/deployment-harness.mjs Outdated
if (event === 'operator_doctor' && value.version !== verified.version) throw new Refusal('operator_check_version_mismatch');
const remoteBin = a.values.get('--remote-bin');
if (!remoteBin) throw new Usage('--remote-bin required: the exact value passed to the operator CLI');
if (!remoteBin.split(/[\\/]/).includes(target)) throw new Refusal('operator_check_target_mismatch', { event });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require the attested path to resolve inside the target

A POSIX --remote-bin such as /prefix/<target>/../other/session-peer passes this segment check even though the shell resolves it to a different install; src/cli.ts accepts absolute paths containing .. and executes them verbatim. If that other executable reports the same version and produces acceptable doctor/dry-run output, the harness can issue operator-checks-passed and publish a receipt without testing the verified target through SSH. Normalize the attested path and require it to name the target's actual wrapper.

Useful? React with 👍 / 👎.

Comment on lines +188 to +190
const s = lstatSync(join(dir, path));
if (s.isSymbolicLink()) links[path] = readlinkSync(join(dir, path));
else if (s.isDirectory()) walk(path);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject manifest symlinks that escape the install

When a prepared tree contains an escaping symlink, this records only the link text and never hashes the referent. In particular, node_modules/session-peer can point outside the staging tree: the version check and probes then follow that link and execute the external package, while later changes to its files leave the recomputed manifest unchanged. The harness can consequently publish and retain a matching receipt for executable code that is not covered by the per-file integrity check; refuse links resolving outside the install or otherwise include their referents in the integrity boundary.

Useful? React with 👍 / 👎.

Comment on lines +831 to +835
const line = JSON.stringify({ seq: journal.entries.length, prev: journal.head, at: now(), ...event });
const fd = openSync(path, constants.O_WRONLY | constants.O_APPEND | constants.O_NOFOLLOW);
try {
const size = fstatSync(fd).size;
try { writeAll(fd, `${line}\n`); syncFd(fd, path); }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Serialize appends to a shared operator journal

If two op commands use the same journal concurrently—for example, runs managing two prefixes—both can read the same head, construct the same seq and prev, and then successfully append under O_APPEND. Both commands report success, but the next journalRead rejects the duplicate sequence as corrupt; worse, if one append fails its sync, its rollback can truncate a concurrently successful record because both captured the earlier size. Hold an exclusive journal lock across the read, validation, append, and rollback.

Useful? React with 👍 / 👎.

Comment thread scripts/deployment-harness.mjs Outdated
Comment on lines +984 to +987
const prefixId = a.values.get('--prefix-id');
if (!target || !HEX32.test(prefixId ?? '')) throw new Usage('--target and --prefix-id required');
if (!journal.entries.some(entry => entry.event === 'lock_acquired' && entry.prefixId === prefixId)) throw new Refusal('prefix_unknown_to_journal');
if (event === 'callers_switched') {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prevent caller changes from invalidating no-caller proofs

A run can issue a no-caller statement under its current lock and then another op record callers_switched can append a switch for the same prefix and target because this branch does not refuse caller changes while an acquisition is open. The destination accepts the older statement solely by its still-current lockId, so retire or cross-run rollback can delete the install after the journal says a caller was configured for it. Refuse caller-state records while an acquisition for that prefix is open, or otherwise make the destination proof freshness-checkable.

Useful? React with 👍 / 👎.

Comment thread scripts/deployment-harness.mjs Outdated
Comment on lines +988 to +993
const published = journal.entries.findLast(entry => entry.event === 'receipt_published' && entry.prefixId === prefixId && entry.target === target);
if (!published) throw new Refusal('switch_before_receipt');
// The publishing acquisition must be closed: by its run's unlock or by a
// recorded break that closed exactly that (prefix, lockId).
const acquired = journal.entries.find(entry => entry.event === 'lock_acquired' && entry.prefixId === prefixId && entry.lockId === published.lockId);
if (!acquired || !acquisitionClosed(journal, acquired)) throw new Refusal('switch_before_unlock');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Invalidate publications after destructive lifecycle events

This lookup accepts the latest historical receipt_published even when a later retired or receipt_cleaned event invalidated that publication. If the same identity-derived name is later activated again, the old closed acquisition can therefore authorize callers_switched before the new install has a receipt or releases its lock, allowing callers to use an unverified activation. Require a still-current publication occurring after the latest cleanup, retirement, or other removal event for this prefix and target.

Useful? React with 👍 / 👎.

Comment on lines +465 to +467
const result = spawnSync(identity.node, [npmCli, 'install', '--prefix', dir, '--offline', '--ignore-scripts', '--no-audit', '--no-fund',
'--cache', join(scratch, 'cache'), '--userconfig', join(scratch, 'userconfig'), '--globalconfig', join(scratch, 'globalconfig'), ...tarballs],
{ encoding: 'utf8', timeout: 300000, env: { PATH: [dirname(identity.node), '/usr/bin', '/bin'].join(delimiter), HOME: join(scratch, 'home') } });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Terminate npm option parsing before tarball paths

The tarball values are appended directly to npm's argument list, so a relative file path beginning with - is interpreted as an npm option rather than a package spec. For example, a readable local path named --prefix=/tmp/out can override the earlier controlled prefix and make npm write outside the dedicated tree before the later version check refuses. In the inspected npm 11.4.2 CLI, npm install --help explicitly lists option parsing such as [-g|--global], and a repeated --prefix resolves to the last value; require absolute safe paths or insert npm's -- option terminator before the tarballs.

Useful? React with 👍 / 👎.

… the harness

Address six review findings on the #23 validation harness:

- The attested --remote-bin must be an absolute POSIX path that, after
  lexical normalization (never resolved remotely), equals exactly the
  wrapper path reported by the verified destination result (a new
  harness-only verify field); detours, other wrappers and other
  prefixes are refused. It remains operator attestation.
- Manifest links must resolve inside the install, also through chains;
  absolute, escaping, dangling or looping links are refused as
  unsafe_link during staging before anything reads through the tree,
  and before every probe. Bounded internal links stay allowed.
- Executed op begin and op record hold an exclusive <journal>.lock
  (mkdir) across read, validation, append and rollback; contention is
  refused as journal_busy without waiting, stealing or killing, and dry
  runs and op statement refuse while it is held without writing. A lock
  left by a crash is removed by the operator after checking. No claim
  covers hostile same-user writers or power loss.
- callers_switched and callers_unswitched are refused while any recorded
  acquisition on the prefix is open (acquisition_open), so a statement
  issued under an open lock stays true.
- A publication is superseded by a later rollback, retirement, receipt
  cleanup or new activation of the same prefix and name
  (publication_superseded); a newer completed publication is required.
  The ADR replaces the earlier audit-only description.
- npm receives tarballs as absolute paths of regular files, so none can
  be read as an npm option; the hashed path is the one npm receives.

Journal-only regressions (journal lock contention and rollback isolation,
caller fences and remote-bin binding on shaped destination results) failed
on e9f99e0 and pass now in a private directory under the external
volume's tmp root. Dedicated-prefix fixtures (manifest links, npm argv,
CLI caller and supersede flows) are unrun locally and left to CI.

Refs #23
…n the harness

Apply root's design refinements to the six E9 fixes:

- The attested --remote-bin must already be a canonical absolute POSIX
  path (no ., .., repeated or trailing /) equal to the verified wrapper;
  a path that only normalizes to it is refused, because a shell
  resolves link/.. through the link. Nothing is resolved locally or
  remotely.
- op statement and journal dry runs now hold the ephemeral journal lock
  across their read, so a statement never snapshots a line that a
  concurrent failed append later rolls back. The ADR states the lock
  directory as their only effect, that a crash can leave it, and that it
  is removed by the operator; no stale takeover.
- The ADR records that absolute links are always refused (they go stale
  or outside after the activation rename) while relative internal links
  survive it with referents covered, and that activated records without
  an outcome from earlier revisions fence as new activations.

Tests: the shaped remote-bin case now requires the exact wrapper (it
fails on 8c6f224's normalizing check), the rollback-isolation test also
checks that a statement is refused during a held append, and the
internal-link fixture checks that the link survives activation and that
referent changes are detected.

Refs #23
Add a test-only pause point after a failed journal append and before
its rollback. A deterministic regression holds a complete appended
lock_acquired line on disk while its rollback waits: a statement and a
second record are refused as journal_busy, and after the confirmed
rollback no statement can be issued from that acquisition
(lock_not_recorded). The retire test notes that releasing, unswitching
and locking again is the same as unswitching before the lock, with the
guard unchanged. No production behavior changes.

Refs #23
…he OS does

Address five review findings on the #23 validation harness:

- Manifest links are followed one component and one link at a time as
  the OS would; every intermediate location must stay inside the
  install, so a hop out through another location or a .. above the
  install root is refused even when the chain comes back. Absolute,
  dangling and looping links stay refused, and nothing is probed before
  the check. Relative internal links survive the activation rename and
  their referents stay covered.
- Tarball paths are interpreted by the OS (lstat and realpath of the
  given path), never lexically, so a symlinked ancestor followed by ..
  selects the file the OS reads; the final component must be a regular
  file and npm gets its absolute real path.
- A journal must have exactly one link (journal_aliased), and the open
  file is checked to be that same single-link file, unchanged since the
  read, before an append and before its rollback.
- A cleanup, retirement or rollback recorded since an acquisition closes
  its publication: a later receipt result under the same lock is
  refused (publication_closed); recovery needs a new lock and checks or
  the explicit adoption.
- op init holds the journal lock from the exclusive create until the
  header line and newline are written and synced; its dry run holds it
  only to check for an existing journal.

The harness can now be imported by its tests (pure helpers exported;
the command runs only when executed), and the test file accepts a
test-only DEPLOYMENT_HARNESS_SCRIPT to compare against another
revision's copy without replacing the worktree file.

Refs #23
Use native realpath for the raw tarball path so symlink ancestors followed by .. select the OS file. Require directories for every remaining link component, including a trailing slash or dot. Refuse a journal lacking its final LF before any append, without repair or migration. Add discriminating filesystem and journal regressions and align init coordination comments. Two focused regressions, syntax, types and repository checks passed locally; dedicated-prefix stage fixtures remain a current-head CI gate. Fresh paired read-only Claude and Codex supplemental review found no mandatory issue. Refs #23
@abruption

Copy link
Copy Markdown
Owner Author

@codex review

Please review current head 35077dab68b834c0e6ce1361ed87960e839a5afb: the six E9 findings and subsequent path/framing corrections are implemented. Current-head CI is pending; previous-head results are historical. No merge or publication requested.

@abruption

Copy link
Copy Markdown
Owner Author

@codex security review

Please review current head 35077dab68b834c0e6ce1361ed87960e839a5afb: the six E9 findings and subsequent path/framing corrections are implemented. Current-head CI is pending; previous-head results are historical. No merge or publication requested.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 35077dab68

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1189 to +1191
if (kind === 'receipt-owned') {
if (!journal.entries.some(entry => entry.event === 'receipt_published' && entry.prefixId === prefixId && entry.target === target))
throw new Refusal('receipt_unknown_to_journal');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Invalidate receipt ownership after lifecycle removal

When a receipt for this prefix and target was previously published but a later receipt_cleaned or retired event removed it, this historical lookup still issues receipt-owned. If a new corrupt or orphan receipt subsequently appears at the same name, dest receipt-cleanup accepts that stale statement and deletes or quarantines a file the journal never published. Require a publication after the latest cleanup/retirement and preferably bind the proof to the current receipt bytes.

Useful? React with 👍 / 👎.

Comment on lines +839 to +842
if (!lstat(path)) continue;
let real, digest;
try { real = realpathSync(path); digest = sha256(readFileSync(real)); } catch { real = null; digest = null; }
entries.push({ path, realpath: real, sha256: digest, symlink: lstatSync(path).isSymbolicLink() });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Skip non-regular PATH entries before hashing

When a PATH directory contains session-peer as a FIFO, or as a symlink to one, readFileSync(real) blocks waiting for a writer; a device such as /dev/zero can also cause unbounded reading. Thus the supposedly read-only inventory can hang or exhaust memory on an entry that a shell could not execute as a regular binary. Inspect the resolved target and report a null digest for anything other than a bounded regular file.

Useful? React with 👍 / 👎.

Comment on lines +446 to +450
'init-prefix'(a) {
const prefix = a.need('--prefix');
const p = layout(prefix);
if (lstat(prefix)) throw new Refusal('prefix_exists');
ancestorsTrusted(dirname(prefix));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject prefixes unusable as remote-bin paths

When the dedicated prefix contains non-ASCII or control characters, initialization and all destination-side stages accept it, but the resulting wrapper can never be supplied to the operator CLI: src/cli.ts rejects POSIX --remote-bin values outside printable ASCII. A valid Unicode home path can therefore reach activation and then become unable to complete the required doctor/dry-run/receipt sequence. Validate the prefix against the CLI's remote-bin path grammar before creating it.

Useful? React with 👍 / 👎.

Comment on lines +1184 to +1186
if (!journal.entries.some(entry => entry.prefixId === prefixId && entry.target === target && ['activated', 'verified', 'receipt_published'].includes(entry.event)))
throw new Refusal('target_unknown_to_journal');
if (switches(journal, prefixId, target) !== 0) throw new Refusal('configured_for_caller');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bind each prefix to a single caller-state journal

When the same prefix is managed from a newly initialized journal, this check treats a verification recorded in that journal as sufficient target knowledge and switches() sees a zero count, even if another journal records an outstanding caller for the same prefix and target. The new journal can therefore issue no-caller and authorize rollback or retirement of an install still in use; the other journal can also record a caller switch after the statement because its acquisition fence cannot see this journal's open lock. Bind the prefix to one journal/operator label or persist and coordinate caller state outside individual journals.

Useful? React with 👍 / 👎.

Comment on lines +520 to +522
const result = spawnSync(identity.node, [npmCli, 'install', '--prefix', dir, '--offline', '--ignore-scripts', '--no-audit', '--no-fund',
'--cache', join(scratch, 'cache'), '--userconfig', join(scratch, 'userconfig'), '--globalconfig', join(scratch, 'globalconfig'), ...tarballs],
{ encoding: 'utf8', timeout: 300000, env: { PATH: [dirname(identity.node), '/usr/bin', '/bin'].join(delimiter), HOME: join(scratch, 'home') } });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Canonicalize the npm CLI before invoking Node

When --npm-cli begins with a Node option, it is passed as the first argument to the recorded Node executable without validation. For example, --npm-cli=--require=/tmp/payload.cjs preloads that module before Node attempts to run the following install argument, allowing code and writes outside the dedicated prefix even though the later install fails. The inspected node --help documents -r, --require=... as a CommonJS module preload; require an absolute regular-file realpath for the bundled npm-cli.js before spawning it.

Useful? React with 👍 / 👎.

Preserve the exclusive 0700 staging root while copying each prepared top-level entry with the existing no-overwrite options. Node 24.22 now rejects the precreated destination root with those options, as shown by its pinned official source and the failed CI. Keep native path semantics, reject overlapping or non-directory roots and nonempty destinations, and preserve link/manifest/probe ordering. Four prefix-free helper tests pass locally on Node 24.16, including a 0755 source versus 0700 destination; actual Node 24.22 stage validation remains a CI gate. Fresh paired Claude/Codex review found no mandatory issue. Refs #23
@abruption

Copy link
Copy Markdown
Owner Author

User-approved scope decision: this PR is now Draft, removed from 0.5.0, and excluded from #157. Stop incremental implementation/review rounds and preserve the existing branch/evidence. Minimum-contract redesign is tracked in #158; #23 stays open. No merge or publication is requested.

abruption added a commit that referenced this pull request Oct 20, 2026
Exclude the #155 harness, its dedicated fixtures and new deployment qualification claims. Restore the existing main-branch ADR and retain independently reviewed handoff fixture readiness fixes. Preserve all preparation history; #23 remains open and minimum-contract redesign is tracked separately.

Refs #158
abruption added a commit that referenced this pull request Oct 20, 2026
Align the release procedure with the user-approved scope: #153, #156 and #154 only; defer #155 to checklist-first contract redesign #158.
abruption added a commit that referenced this pull request Oct 20, 2026
* test(ssh): add operator-invoked deployment receipt and recovery harness

Add scripts/deployment-harness.mjs, a repository-only validation harness
for the remote deployment ADR. It runs the destination checks (dedicated
prefix, root and .receipts trust, staging, probe, mkdir lock, no-replace
activation, verification, receipts, rollback, retention, lock breaking)
and an operator-side hash-chained run journal as separately invoked
commands. It never opens SSH, never installs remotely by itself, never
runs during list/send/doctor and leaves the exact-version preflight
unchanged. Mutations are dry runs unless --execute; destructive steps
also need --confirm-destructive. POSIX only.

The harness enforces the step 8 order: destination verification, then
the operator doctor and dry-run, then the receipt, then unlock, then the
caller switch. It also covers fresh-lock recovery without a receipt,
journal-confirmed rollback, corrupt/mismatched/orphan receipts,
.receipts trust and unowned or unconfirmable lock overrides.

Fix ADR drift for 0.4.0 (version range, handoff preparation after the
same preflight, the smol-toml runtime dependency), state the operator
check order explicitly, refuse an orphan receipt before activation, and
document the harness, its Darwin-local evidence and the unrun Linux,
Windows non-admin and Node 22 cells. Record the Darwin-local results in
VALIDATION.md; they qualify no destination cell.

Refs #23

* docs(ssh): record Linux arm64 deployment harness requalification

Record one user-authorized, isolated harness run on a Linux arm64 test
host (glibc 2.39, Node 24.18.0, published 0.4.0 installed offline into a
new /tmp prefix). It covers the exact operator-check, receipt, unlock and
caller-switch order, fresh-lock recovery without a receipt,
corrupt/mismatched/orphan receipts, .receipts trust, unknown and
unverifiable lock-owner overrides, journal-confirmed rollback and
retention, and the activation-table rows. All 89 checked expectations
matched; existing user executables stayed byte-identical and the test
directory was removed.

Scope the ADR and VALIDATION wording to exactly that cell. Linux x64
under the current rules, Node 22 (no binary on either host), Windows
(the harness refuses; the only host is administrator-only), races and
F6 remain unqualified.

Refs #23

* fix(ssh): harden deployment harness input, operator binding and dry runs

Address review findings in the #23 validation harness:

- dest stage validates the identity file against a closed schema
  (exact keys and types, version syntax, SHA-256/SHA-512 forms,
  absolute Node path) and requires the staging name to resolve to a
  direct child of the root before any write, in dry runs too. Markers
  use the same schema.
- op record accepts an operator dry-run only for the intended
  no_reachable_target refusal or a normal validated result; transport,
  host-key, preflight and version refusals no longer pass.
- Operator results are bound to what the CLI JSON shows: host equal to
  op init --dest-host and doctor version equal to the verified install.
  The --remote-bin used is recorded as remoteBinAttested operator
  attestation and only checked to name the verified install.
- Launcher and native-lock probes run only with --execute: dest probe and
  dest verify are execute-gated, activate/receipt dry runs classify
  statically, and unexecuted verify results cannot be recorded.
- already_provisioned keeps this run's staging (stagingRetained) for the
  explicit discard-staging --confirm-destructive step.

Add regression tests (traversal/absolute/type/schema identities in dry
run and execute, refused transport and preflight dry-runs, host/version
/install binding, no launch or probe file in dry runs, retained staging)
and record a minimal Linux arm64 rerun with the fixed harness. Cells not
rerun keep the earlier harness revision as their evidence.

Refs #23

* fix(ssh): bind harness verification to its lock and guard activation

Address the second review round of the #23 validation harness:

- dest verify --run <run> --execute binds its result to the run, the
  prefix sentinel ID and the creation time of the run's current lock;
  op record accepts a verified result only from the recording run's
  current lock on the same prefix, so an earlier lock's or another
  run's verification cannot satisfy the operator-check order.
- dest activate writes and syncs an activation intent (target and
  staging) inside the lock before the rename and renames nothing if
  that write fails; dest unlock checks the intent against the actual
  target and refuses while it exists without a matching receipt or
  the intent is unreadable.
- dest discard-staging requires --run and a valid staging marker naming
  that run; markerless leftovers of a failed stage are removed only by
  the separate dest discard-leftover --declare-abandoned with the same
  execute and destructive gates.
- The ADR scopes the sentinel rule to commands that inspect or change a
  prefix; dest identity and dest path-inventory take none.

Add regression tests that failed before these changes (stale and
foreign verify results recorded, unlock released after a rename whose
record failed, another run's staging deleted) and record a second
minimal Linux arm64 rerun with the fixed harness.

Refs #23

* fix(ssh): bind harness results to a per-lock id and validate the intent

Address the third review round of the #23 validation harness:

- Every dest lock acquisition gets a random lockId. Every lock-bound
  result (activate, verify --run, receipt, unlock, rollback, receipt
  cleanup, retention), journal record and statement carries the prefix
  sentinel ID and that lockId, and op record refuses results of another
  run, another prefix or an earlier lock, including an earlier lock of
  the same run acquired at the same timestamp. callers_switched requires
  the publisher's unlock under the lock that published the receipt.
- The activation intent's presence and validity are separate: a present
  intent that is not exactly this lock's closed record (false, 0, empty,
  null, array, malformed, extra or missing keys, another run, lock,
  target or staging, or not a regular file) refuses unlock, activation
  and receipts as activation_intent_unreadable and is never treated as
  absent.
- Split the interrupted-activation test into a deterministic intent
  write failure before the rename and the persisted post-rename state;
  no crash between files is physically injected, and the docs say so.
- Scope the ADR and VALIDATION wording: the full Linux arm64 cells
  L3-L8 are historical evidence of harness revision 210759a1 (cc7fe23);
  the final revision was rerun on that host only for the stated subset.

Add regressions that failed before (an invalid intent let unlock
release an unreceipted install; an earlier lock's receipt was recorded
under a new lock of the same run) plus same-timestamp and
another-prefix negatives.

Refs #23

* fix(ssh): refuse replayed lock acquisitions and unrelated breaks in the journal

Close two operator-journal freshness gaps in the #23 validation harness:

- op record accepts a lock acquisition (prefix ID and lockId) only once
  per journal: re-recording it, whether replayed after it closed or
  repeated while current and from any run, is refused as
  acquisition_replayed and never deduplicated, so an earlier
  acquisition cannot become current again and revive its old unlock
  or receipt results.
- An acquisition stays current until its own run's unlock of the same
  pair or a recorded break that closed exactly that pair. A break
  result closes it only when its prefix ID and broken lockId name an
  open acquisition recorded in this journal; unowned overrides, other
  prefixes' or journals' locks and already closed locks are kept as
  audit records that close nothing, and recording the same break twice
  is refused as break_replayed.

Add regressions that failed before (a replayed earlier acquisition was
accepted; a break on another prefix ended the current holder's lock)
plus stale-break, duplicate-break and legitimate-recovery cases. The
Linux host was not rerun for this journal-only change.

Refs #23

* fix(ssh): treat a recorded lock break as a tombstone for its acquisition

Close the out-of-order journal gap in the #23 validation harness: a
break recorded before its acquisition (break first, acquisition result
recorded later) no longer lets that acquisition become current. op
record refuses a lock_acquired whose prefix ID and lockId already
appear anywhere in the journal as an acquisition or as a recorded
break's broken lockId (acquisition_replayed). A break naming the same
lockId on another prefix is an unrelated generation and blocks nothing;
existing recovery is unchanged.

The ADR now limits "closes nothing" to overrides whose owner pair
cannot be identified: a break of a lock with a readable owner whose
exact pair is an open acquisition here does close it.

Add a regression that failed before (a late-recorded acquisition was
accepted after its break) plus the other-prefix case. Verified with
the focused harness tests only.

Refs #23

* docs(ssh): align the harness lock-break comment with the ADR

The op record comment still listed an unowned override as always
closing nothing. Limit it to overrides whose owner pair cannot be
identified, matching the ADR: a break with a readable owner whose exact
pair is an open acquisition recorded here does close it. Comment-only
change; no code line changed.

Refs #23

* fix(ssh): tighten harness leftover removal, operator-check order and recordable results

Address three code-review findings on the #23 validation harness:

- dest discard-leftover removes only a staging tree with no marker
  entry at all. Any marker entry, whether this workflow's, another
  manager's, unparsable, invalid, non-regular or symlinked, is refused
  as staging_has_marker and left byte-for-byte unchanged;
  --declare-abandoned and the execute/destructive gates are not a
  permission to delete what something else may own.
- op record accepts an operator dry-run only after a doctor recorded
  since the latest verify of the same target under the same lock, and
  the operator-checks statement requires the recorded order verify,
  doctor, dry-run. A re-verify makes an earlier doctor stale. This is
  the order of journal records, not proof of when the operator
  commands ran.
- dest receipt-cleanup and dest retire results are recordable as
  receipt_cleaned and retired, with the same executed-result, run,
  prefix and lock binding as other results; receipt_cleaned keeps mode,
  receipt state and ownership basis. Other event names stay usage
  errors. The ADR lists the recordable results.

Add regressions that failed before (foreign and invalid markers
deleted by discard-leftover; a dry-run recorded before any doctor;
cleanup and retention results not recordable) plus reverse-order,
stale-doctor, stale-lock, dry-run and other-run negatives. Verified
with the focused harness tests only.

Refs #23

* docs(ssh): clarify deployment journal event and audit scope

Distinguish destination results from operator events and state that cleanup and retirement records are audit entries, without changing switch or statement eligibility. No executable code changes.

Refs #23

* chore(release): prepare 0.5.0 platform qualification groundwork

Carry forward the four exact skill profiles, preserve historical evidence and update current multilingual version examples. Linux observation, Windows observation/storage and Windows jump remain unsupported. Preparation only; no staging or publication.

* docs(release): align API examples and retain immutable validation history

Preserve historical evidence text, keep the supported handoff scope explicit, and pin previous-version SSH refusal plus plausible unreviewed skill profiles in focused fixtures.

* fix(ssh): scope harness caller state by prefix and make receipt publication replayable

Address seven inline review findings on the #23 validation harness:

- Caller state is keyed by prefix and install name. callers_switched
  and callers_unswitched take an explicit --prefix-id that must be a
  prefix the journal has locked; switches no longer cancel across
  prefixes, and no-caller/receipt-owned statements and the receipt
  lookups are scoped to the current lock's prefix.
- verify runs launcher and native probes only after every static check
  (real owned directory, consistent marker, manifest, staging gone)
  passes.
- The explicit lock override removes only the known lock metadata
  entries, each a file, an unfollowed symlink or an empty directory;
  unknown entries and non-empty directories are refused before any
  removal.
- dest receipt syncs a publication intent with the exact receipt bytes
  before writing the receipt. A retry after a lost response under the
  same lock and proof replays the committed receipt (bytes and
  verifiedAt unchanged) or completes a lost write with those bytes;
  changed, corrupt or orphan receipts, stale proofs, other targets and
  later locks fail closed, and op record accepts one receipt_published
  per target under a lock.
- After run_ended only a run-ended statement is issued, even while the
  run's lock remains on disk.
- callers_switched accepts a publishing acquisition closed by its run's
  unlock or by a recorded break that closed exactly that prefix and
  lockId; other-prefix breaks, stale breaks and unknown closures do
  not count.
- path-inventory treats empty PATH components as the current directory
  and inventories ./session-peer there.

Add regressions for each finding; they failed before (two of them, and
two existing tests, only on the new --prefix-id argument) and pass
after. Verified with the focused harness tests only.

Refs #23

* fix(ssh): complete harness receipt recovery and validate publication intents

Address the root review of the #23 harness publication path:

- dest receipt --adopt-existing is an explicit, non-destructive
  recovery: after a run wrote a receipt, lost the response, ended and
  had its lock broken, a new run under a fresh lock with fresh verify,
  doctor and dry-run publishes the still-matching receipt under that
  lock. Receipt bytes, the original runId and verifiedAt are kept;
  receipts not matching the marker and manifest, invalid receipts and
  earlier-lock proofs are refused.
- Completing a lost receipt write from a publication intent now
  requires every static check (and the probes when executed) and the
  current marker runId, identityHash and recomputed manifest to equal
  the committed receipt; a rolled-back or changed target is refused as
  publication_target_changed, and the result is rechecked after the
  write. Replays also recheck the install.
- Publication intents must embed a receipt of the closed schema (exact
  keys, receiptVersion 1, 32-hex runId, 64-hex hashes, ISO verifiedAt);
  other valid JSON is refused before any write.
- Lock metadata entries of other file types (FIFO, socket, device) are
  refused before removal.
- Caller records without a prefix make the caller count unusable
  (caller_history_unusable) instead of counting as zero; older journal
  formats are not supported for caller decisions.
- The test helper for a second prefix runs the operator CLI against that
  prefix's own wrapper. The ADR states statement issuance scope and the
  one-publication-per-lock versus one-record-per-target distinction.

Regressions: the completion, schema, file-type and legacy-record tests
failed on 839f2f6 and pass now; the adoption tests failed there on the
new --adopt-existing flag. Verified with the focused harness tests only.

Refs #23

* test(handoff): observe queue evidence without relying on file notifications

Keep the 10-second readiness deadline and inspect the persisted synthetic queue record with bounded polling. Report complete child-close output when send exits before queue evidence. Add notification-suppression and early-refusal regressions. This hardens a reproduced fixture weakness; it does not establish the cause of earlier CI failures or retry a CLI submission.

* fix(harness): share receipt validation across completion consumers

Use one closed receipt schema and one static-check predicate for initial publication, replay and adoption. Reject activation after publication scope is committed so rollback cannot revive an old proof under the same acquisition. Add reuse, adoption, unlock, staging and reactivation regressions; full CLI execution remains required in CI.

* test(release): derive successful deployment doctor fixture from package version

Keep the explicit wrong-version refusal while allowing the intended wrong-target regression to reach its own guard after a release version change.

* fix(ssh): track retained staging, sync receipt publication and harden manifests

Address three review findings and one fixture issue on the #23
validation harness:

- already_provisioned records the staging tree it keeps in the lock
  (retained.json, bound to run, lock and target). Until that tree is
  explicitly discarded, the final verify (stagingGone false, no probe),
  receipts and adoption depending on it, unlock and further
  activations are refused; a present but invalid record is refused,
  never treated as absent.
- The manifest's file and link maps have no prototype, so a root entry
  named __proto__ (file or symlink) is recorded and later changes are
  detected before verify or receipt.
- Receipt publication is synced before success: intent and lock
  directory, then the receipt file, .receipts and the root, then a
  publication.synced.json marker, for first writes, completed writes,
  replays and adoption (existing bytes are never rewritten). Unlock
  keeps the evidence and refuses while a receipt or its install exists
  without the marker. Journal records write every byte (short writes
  continue), are synced, and are truncated back on failure. Test-only
  environment hooks trace fsync order, inject one fsync failure or force
  short writes; they add failures only and do not qualify power-loss
  behavior.
- The operator-doctor positive fixture uses the package version instead
  of a hard-coded 0.4.0; the wrong-version negative stays 0.3.9.

Regressions for each failed before (retained staging released, a
__proto__ change undetected, no sync guard or journal fault handling)
and pass now. Verified with the focused harness tests only.

Refs #23

* test(handoff): keep warm fixture WAL identities stable

* docs(release): distinguish current source from historical platform trials

* fix(ssh): resync existing publication metadata and report journal rollback honestly

Close two bounded gaps in the #23 validation harness:

- Every executed receipt write, completion, replay and adoption now
  syncs the existing publication intent and the lock directory before
  the receipt, .receipts and the root, and then writes or resyncs the
  synced marker and the lock directory before reporting success. A
  failed intent sync stops before the receipt sync; a failed marker sync
  happens after the receipt and its directories were synced and fails
  the step without implying the receipt is lost.
- A failed journal append attempts a rollback (truncate to the previous
  length and sync). journal_append_failed with rollback restored is
  reported only when that is confirmed; otherwise journal_append_unknown
  with rollback unconfirmed keeps the original and rollback errors, and
  the operator must inspect the journal first. A failure never promises
  the record is absent and the append is never retried. Test-only hooks
  can now fail only the first N matching fsyncs and fail the truncate.

The ADR also states that retained staging only blocks while it is
pending; after the explicit discard the lock's statements remain
usable.

Journal regressions failed on 61f37f8 (harness_error) and pass now,
run in a private directory under the external volume's tmp root. The
replay sync-order test needs a dedicated prefix, which that root's
ancestors correctly refuse; it is unrun locally and left to CI.

Refs #23

* fix(ssh): sync pending intent before completing a missing receipt

Preserve the publication ordering contract when an earlier intent write failed its sync. Require the intent and lock directory sync before creating the missing receipt. Add a regression for repeated intent-sync failure; full CLI execution remains a current-head CI gate. Read-only paired Claude/Codex review found no mandatory defect. Refs #23

* test(handoff): observe persisted initialize readiness and early exits

* test(handoff): stabilize synthetic observation and readiness fixtures

Backport the independently reviewed release-preparation test corrections: persisted queue and initialize readiness with unchanged deadlines, early-close diagnostics, and a retained synthetic warm WAL connection. Include the discriminating notification and keeper regressions. Runtime guards, delivery assertions and platform support are unchanged; the original CI causes remain unproven. Refs #23

* fix(ssh): bind remote-bin, links, journal writers and caller state in the harness

Address six review findings on the #23 validation harness:

- The attested --remote-bin must be an absolute POSIX path that, after
  lexical normalization (never resolved remotely), equals exactly the
  wrapper path reported by the verified destination result (a new
  harness-only verify field); detours, other wrappers and other
  prefixes are refused. It remains operator attestation.
- Manifest links must resolve inside the install, also through chains;
  absolute, escaping, dangling or looping links are refused as
  unsafe_link during staging before anything reads through the tree,
  and before every probe. Bounded internal links stay allowed.
- Executed op begin and op record hold an exclusive <journal>.lock
  (mkdir) across read, validation, append and rollback; contention is
  refused as journal_busy without waiting, stealing or killing, and dry
  runs and op statement refuse while it is held without writing. A lock
  left by a crash is removed by the operator after checking. No claim
  covers hostile same-user writers or power loss.
- callers_switched and callers_unswitched are refused while any recorded
  acquisition on the prefix is open (acquisition_open), so a statement
  issued under an open lock stays true.
- A publication is superseded by a later rollback, retirement, receipt
  cleanup or new activation of the same prefix and name
  (publication_superseded); a newer completed publication is required.
  The ADR replaces the earlier audit-only description.
- npm receives tarballs as absolute paths of regular files, so none can
  be read as an npm option; the hashed path is the one npm receives.

Journal-only regressions (journal lock contention and rollback isolation,
caller fences and remote-bin binding on shaped destination results) failed
on e9f99e0 and pass now in a private directory under the external
volume's tmp root. Dedicated-prefix fixtures (manifest links, npm argv,
CLI caller and supersede flows) are unrun locally and left to CI.

Refs #23

* fix(ssh): require the canonical wrapper path and lock journal reads in the harness

Apply root's design refinements to the six E9 fixes:

- The attested --remote-bin must already be a canonical absolute POSIX
  path (no ., .., repeated or trailing /) equal to the verified wrapper;
  a path that only normalizes to it is refused, because a shell
  resolves link/.. through the link. Nothing is resolved locally or
  remotely.
- op statement and journal dry runs now hold the ephemeral journal lock
  across their read, so a statement never snapshots a line that a
  concurrent failed append later rolls back. The ADR states the lock
  directory as their only effect, that a crash can leave it, and that it
  is removed by the operator; no stale takeover.
- The ADR records that absolute links are always refused (they go stale
  or outside after the activation rename) while relative internal links
  survive it with referents covered, and that activated records without
  an outcome from earlier revisions fence as new activations.

Tests: the shaped remote-bin case now requires the exact wrapper (it
fails on 8c6f224's normalizing check), the rollback-isolation test also
checks that a statement is refused during a held append, and the
internal-link fixture checks that the link survives activation and that
referent changes are detected.

Refs #23

* test(ssh): pin the journal rollback window and the retire unswitch order

Add a test-only pause point after a failed journal append and before
its rollback. A deterministic regression holds a complete appended
lock_acquired line on disk while its rollback waits: a statement and a
second record are refused as journal_busy, and after the confirmed
rollback no statement can be issued from that acquisition
(lock_not_recorded). The retire test notes that releasing, unswitching
and locking again is the same as unswitching before the lock, with the
guard unchanged. No production behavior changes.

Refs #23

* fix(ssh): follow link chains, tarball paths and journal identity as the OS does

Address five review findings on the #23 validation harness:

- Manifest links are followed one component and one link at a time as
  the OS would; every intermediate location must stay inside the
  install, so a hop out through another location or a .. above the
  install root is refused even when the chain comes back. Absolute,
  dangling and looping links stay refused, and nothing is probed before
  the check. Relative internal links survive the activation rename and
  their referents stay covered.
- Tarball paths are interpreted by the OS (lstat and realpath of the
  given path), never lexically, so a symlinked ancestor followed by ..
  selects the file the OS reads; the final component must be a regular
  file and npm gets its absolute real path.
- A journal must have exactly one link (journal_aliased), and the open
  file is checked to be that same single-link file, unchanged since the
  read, before an append and before its rollback.
- A cleanup, retirement or rollback recorded since an acquisition closes
  its publication: a later receipt result under the same lock is
  refused (publication_closed); recovery needs a new lock and checks or
  the explicit adoption.
- op init holds the journal lock from the exclusive create until the
  header line and newline are written and synced; its dry run holds it
  only to check for an existing journal.

The harness can now be imported by its tests (pure helpers exported;
the command runs only when executed), and the test file accepts a
test-only DEPLOYMENT_HARNESS_SCRIPT to compare against another
revision's copy without replacing the worktree file.

Refs #23

* fix(ssh): preserve native paths and reject incomplete journal records

Use native realpath for the raw tarball path so symlink ancestors followed by .. select the OS file. Require directories for every remaining link component, including a trailing slash or dot. Refuse a journal lacking its final LF before any append, without repair or migration. Add discriminating filesystem and journal regressions and align init coordination comments. Two focused regressions, syntax, types and repository checks passed locally; dedicated-prefix stage fixtures remain a current-head CI gate. Fresh paired read-only Claude and Codex supplemental review found no mandatory issue. Refs #23

* chore(release): defer deployment harness and narrow 0.5.0 scope

Exclude the #155 harness, its dedicated fixtures and new deployment qualification claims. Restore the existing main-branch ADR and retain independently reviewed handoff fixture readiness fixes. Preserve all preparation history; #23 remains open and minimum-contract redesign is tracked separately.

Refs #158

* docs(release): remove the deferred harness from 0.5.0 procedure

Align the release procedure with the user-approved scope: #153, #156 and #154 only; defer #155 to checklist-first contract redesign #158.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant