Repository navigation
Conversation
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
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 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".
…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
|
@codex review |
|
@codex security review |
🛡️ Codex Security ReviewSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
There was a problem hiding this comment.
💡 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".
…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
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.
|
@codex review |
|
@codex security review |
Keep release PR draft until dependency #155 merges and final current-main checks complete. No staging or publication.
There was a problem hiding this comment.
💡 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".
🛡️ Codex Security ReviewSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
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
|
@codex review Please review the current head |
|
@codex security review Please review the current head |
There was a problem hiding this comment.
💡 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".
| 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 }); |
There was a problem hiding this comment.
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 👍 / 👎.
| const s = lstatSync(join(dir, path)); | ||
| if (s.isSymbolicLink()) links[path] = readlinkSync(join(dir, path)); | ||
| else if (s.isDirectory()) walk(path); |
There was a problem hiding this comment.
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 👍 / 👎.
| 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); } |
There was a problem hiding this comment.
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 👍 / 👎.
| 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') { |
There was a problem hiding this comment.
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 👍 / 👎.
| 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'); |
There was a problem hiding this comment.
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 👍 / 👎.
| 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') } }); |
There was a problem hiding this comment.
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
|
@codex review Please review current head |
|
@codex security review Please review current head |
There was a problem hiding this comment.
💡 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".
| 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'); |
There was a problem hiding this comment.
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 👍 / 👎.
| 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() }); |
There was a problem hiding this comment.
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 👍 / 👎.
| 'init-prefix'(a) { | ||
| const prefix = a.need('--prefix'); | ||
| const p = layout(prefix); | ||
| if (lstat(prefix)) throw new Refusal('prefix_exists'); | ||
| ancestorsTrusted(dirname(prefix)); |
There was a problem hiding this comment.
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 👍 / 👎.
| 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'); |
There was a problem hiding this comment.
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 👍 / 👎.
| 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') } }); |
There was a problem hiding this comment.
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
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
* 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.
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
d992bf58ce6fdf842d7d4d1840bc65db521a59c5preserves E9 and all peer commits through45461c1161354739ebab09fe50a280a5c3f01bbf, 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 atc0203fe. The later journal-only revisions, the934ceabcode-review fixes, the839f2f6inline-review fixes, the275ed8dpublication-recovery fixes, the root integrationcf4626c, the61f37f8/fddb7c4and 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.scripts/deployment-harness.mjs, a repository-only, operator-invoked validation harness for the remote deployment ADR.destcommands run on a destination with the recorded Node;opcommands 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 duringlist/send/doctor, and leaves the exact-version preflight, host-key/BatchMode handling and SSH options unchanged.dest init-prefixcreates a new canonical prefix (refuses existing paths, symlinked paths and ancestors writable by others without the sticky bit) plus a sentinel; everydestcommand that inspects or changes a prefix refuses without it (dest identityanddest path-inventorytake 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-inventoryhashessession-peerentries onPATHread-only, to show that other managers' executables are unchanged. POSIX only; refuses on Windows.verify, then operatordoctor --hostand thensend --dry-run --hostrecorded in the journal in that order, then anoperator-checks-passedstatement bound to the lock, marker identity and recomputed manifest, thendest receipt, thendest unlock(refused while the activated target has no matching receipt), thencallers_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).runId), same-run and journal-confirmed rollback (refused without positive confirmation; re-verify only), corrupt/mismatched/orphan receipts with explicit ownership-confirmed cleanup,.receiptstrust (mode, symlink, non-regular receipt), unowned/unreadable/unconfirmable lock override, and the activation-table rows.smol-tomlamong 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 stagevalidates 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).no_reachable_targetrefusal or a normalvalidatedresult; transport, host-key, preflight and version refusals are rejected. Results are bound to the JSONhost(op init --dest-host) and the doctorversion. The--remote-binused is recorded as operator attestation (remoteBinAttested) and only checked to name the verified install; it is not automatic verification.--execute(dest probe,dest verify);activate/receiptdry runs classify statically; unexecutedverifyresults cannot be recorded.already_provisionedkeeps this run's staging (stagingRetained) for the explicitdiscard-staging --confirm-destructivestep.Review fixes, round 2 (005cd4b)
dest verify --runresults are bound to the run, prefix sentinel ID and current lock;op recordrefuses a verify result from another run, an earlier lock or another prefix.dest activatewrites and syncs an activation intent before the rename (nothing is renamed if that fails);dest unlockchecks intent and actual target together and refuses an unreceipted activation even after an interruption right after the rename.dest discard-stagingrequires--runmatching the staging marker; markerless leftovers need the separatedest discard-leftover --declare-abandoned.Review fixes, round 3 (c0203fe)
dest lockacquisition gets a randomlockId. All lock-bound results, journal records and statements carry the prefix sentinel ID and thatlockId. Results of an earlier lock (even of the same run at the same timestamp), another run or another prefix are refused.callers_switchedneeds the publisher's unlock under the publishing lock.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.Review fixes, round 4 (f29f241)
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.lockIdname 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).Final review fix (a4323cc)
lockId. An acquisition result for that pair recorded later (break first, acquisition afterwards) is refused asacquisition_replayedand never becomes current. A same-lockIdbreak on another prefix blocks nothing, and recovery is unchanged.op recordlock-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-leftoverremoves only a staging tree with no marker entry at all. Any marker entry is refused asstaging_has_markerand 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-abandonedand the execute/destructive gates are not a permission to delete what something else may own. Own-rundiscard-stagingand the activation-intent protection are unchanged.op recordaccepts an operator dry-run only after a doctor recorded since the latestverifyof 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-cleanupanddest retireresults are recordable asreceipt_cleanedandretired. They get the same executed-result, run, prefix and lock binding as the other results, andreceipt_cleanedkeeps the mode, receipt state and ownership basis. Dry-run, other-run and earlier-lock results are refused; unrecognized destination-result names such asretireare 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.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 rannode --check scripts/deployment-harness.mjsandgit diff --check; focused 24/24 results remain tied to934ceab, not a new test execution. The reviewers did not run tests or live-host checks.Inline review fixes (
839f2f6)callers_switched/callers_unswitchedtake an explicit--prefix-idthat must be a prefix the journal has locked; switches no longer cancel across prefixes, andno-caller/receipt-ownedstatements and receipt lookups are scoped to the current lock's prefix.dest verify --executeruns launcher and native probes only after every static check passes (real owned directory, consistent marker, manifest, staging gone).dest receiptsyncs 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 andverifiedAtunchanged), or completes a lost write with those bytes. Changed or corrupt receipts, stale proofs, other targets and later locks fail closed.op recordaccepts onereceipt_publishedper target under a lock. No native send or host command is retried automatically.run_ended, only arun-endedstatement is issued, even while the run's lock remains on disk.callers_switchedaccepts a publishing acquisition closed by its run's unlock or by a recorded break that closed exactly that prefix andlockId; other-prefix breaks, stale breaks and unknown closures do not count.dest path-inventorytreats emptyPATHcomponents (leading, trailing, doubled) as the current directory.b7fb3fecode gave 22 pass / 9 fail. Two of the new tests and two existing tests failed only on the new--prefix-idargument. All 31 pass at839f2f6. Build, types, repository and diff checks pass. No Linux-host rerun.Publication recovery fixes (
275ed8d)dest receipt --adopt-existingis 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, originalrunIdandverifiedAtare kept. Non-matching or invalid receipts and earlier-lock proofs are refused. Nothing is reinstalled or retried automatically.runId,identityHashand recomputed manifest must also equal the committed receipt. A rolled-back or changed target is refused (publication_target_changed).caller_history_unusable) rather than zero.receipt_publishedper target under a lock.839f2f6code 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-existingflag. All 37 pass at275ed8d. 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
/tmptest directory (700,/tmproot-owned sticky). The operator side ran theopjournal and this branch'ssession-peer0.4.0 CLI from macOS arm64 over the existing pinned SSH trust. The harness driver's own ssh calls usedBatchMode=yes,StrictHostKeyChecking=yesandUpdateHostKeys=no. For the operatorsession-peerCLI,UpdateHostKeys=nocame only from a test-only local ssh wrapper that prepended it: the ordinary CLI's main SSH argv does not setUpdateHostKeys(only--ssh-jumpdoes). This is kept as a separate candidate product observation and is not changed here. The host's SSH server is Tailscale SSH, notsshd. The publishedsession-peer-0.4.0.tgz(SHA-25684adc8b5…57c9) and its lockfile-pinned dependencies were installed offline with--ignore-scripts.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 originalrunId), mismatched/corrupt/orphan receipts,.receiptsmode/symlink/non-regular refusals, unowned/unreadable/unconfirmable lock overrides, journal-confirmed rollback, and the empty/file/symlink/inconsistent-marker/failed-manifest rows.doctor --hostreturnedok:true,diagnosticCompleted:true. The nonexistent-target dry-run returnedno_reachable_target,submitted:false. No agent session or message was used.path-inventorywas unchanged. The existing user-owned Pythonsession-peerlauncher 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.29a46eb…(dc0de27),9d39ce8…(005cd4b) and finallyafe2c7f5…d809(c0203fe). The last had 87 steps, 48 expectations and 0 mismatches. They covered the primary order under host/version/lock binding, the explicitalready_provisioneddiscard, 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.sshdas 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 checkpoint275ed8d. 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 localsshstand-in running the real CLIdoctor --hostand nonexistent-targetsend --dry-run --host.--npm-clioffline installation of publishedsession-peer-0.4.0.tgz, whose SHA-25684adc8b5…57c9matches the 0.4.0 record and whose dependency integrities match the lockfile. Both endedowned_receiptedwith 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 testwithSESSION_PEER_PYTHON_ROOTset to the pinned Python reference checkout (8ca77cf): 409 total, 404 passed, 0 failed, 5 skipped. This full suite ran atf29f241and was not rerun ata4323cc,ea4d23e,934ceab,b7fb3fe,839f2f6or275ed8d; that checkpoint has focused harness checks only; later checkpoints are listed below. WithoutSESSION_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.mjsandgit diff --checkpass at275ed8d.npm run test:packageandnpm 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
--capabilities, new error codes, negotiation,launch.mjsidentity enforcement, deploy helper) remain proposals. The POSIX empty-directoryrename(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 --hostor 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
ba44171ed5d2c7882006d6bc40d6a300448d0b0binebbe627; all dated validation records were preserved. The worker's harness/test/ADR bytes stayed unchanged during that integration. A root follow-upcf4626cthen: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):already_provisionedrecords 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,unlockand further activations. A present but invalid record is refused, never treated as absent. The internal existing-target check insideactivateis 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..receiptsand the root, then apublication.synced.jsonmarker. Existing receipt bytes are never rewritten.unlockkeeps 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.0.4.0; the0.3.9wrong-version negative stays. The package version on this branch is unchanged (0.4.0).cf4626charness gave 41 pass / 5 fail.__proto__change went undetected.cf4626cdoes not have.61f37f8, including root's fourcf4626ccases.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.61f37f8. Full suite (historicalf29f241only), 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:
.receipts, then the root;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(rollbackrestored) is reported only when that is confirmed. Otherwise the result isjournal_append_unknown(rollbackunconfirmed), 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:
61f37f8code withharness_errorand pass now (2/2).node --check, build, types, repository and diff checks pass.Unrun locally:
f29f241only), 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.
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,6be918dand45461c1, 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 ofe9f99e0)e9f99e0and every earlier commit are preserved (no reset or rebase). Only the ADR, the harness and its test file changed;test/handoff-delivery.test.tsis untouched.418ad93applies root's design refinements on top of8c6f224, and6be918dadds a test-only pause point plus a regression for the journal rollback window.--remote-binmust 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 resolveslink/..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.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.op begin,op recordandop statement(dry runs included) holds the ephemeral<journal>.lock(mkdir, then removed).journal_busy) with no waiting, stealing or killing. A leftover lock is removed by the operator after checking that no op command is running.callers_switchedandcallers_unswitchedare 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.publication_superseded), and a newer completed publication is required.activatedrecords now keep their outcome:already_provisionedreuse does not fence, and records without an outcome (earlier revisions) fence as new activations. The ADR's earlier "audit-only" description is replaced.Coverage, stated exactly.
e9f99e0harness, five of them failed; the two earlier journal tests passed there.8c6f224harness, the canonical remote-bin case failed.418ad93.node --check, build, types, repository and diff checks pass.6be918dholds a complete appendedlock_acquiredline 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). On418ad93this test fails only because the pause point did not exist there.8c6f224left between its busy check and its read. Taking the lock for statements closes that window by design.6be918d.f29f241only), Linux, Windows, package, audit, model, native and user-host checks.61f37f8(non-policy temp location) is not new evidence.Fixes from the immutable
418ad93review (45461c1, on root-approved base6be918d)..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.lstatandrealpathof 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_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.publication_closed). Recovery needs a new lock and new checks, or the explicit adoption.op initholds 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.DEPLOYMENT_HARNESS_SCRIPToverride, 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:
6be918dharness. All four failed:6be918dexports no helper;45461c1, all twelve journal-only and filesystem-unit tests pass.node --check, build, types, repository and diff checks pass.Unrun locally:
...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.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.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.