Add host (root): upload fails with exit 71 and every retry repeats it; message tells the owner to inspect folders (J303) #394
Open
opened 2026-11-06 12:22:15 +00:00 by kayg
·
28 comments
No Branch/Tag specified
main
j470b-server-mesh-rejoin
mesh-compat-382
j444b-d381-key-wins-flood
j461b-macos-utun-reader-thread
j475b-remove-host-motion
fix-reconnect-new-link
j481-d380
dev
j479-mesh-soak
fix/j476-host-facts
j475-motion-pill-split
j470-mac-node-join
root-upgrade-d376
j474-review-merge
latency-targets-120hz
nightly-suite-shards
gotatun-pin-j469
macos-utun-reader-thread
fix-flood-echo-stall
wip/j416
motion-fast-ease-out
mesh-addhost-j259
takeover-close-terminals-d368
kbd-instant-followups
fix-trust-key-double-click
nightly-suite-partial-report
appwin-icons-j207
rusqlite-slice1
swipe-content-first-d374
mesh-mtu-381
addhost-instant-open
glass-contrast-d365-mock
glass-contrast-d365
signout-followups-442
fix-startup-toast-red
research-gotatun-matrix-j317
themes-sync-j304
fix-load-flakes
fix-nix-zig-patch
fix-compute01-browser-baseline
ratchet-size-baseline-419
predict-auto-d130
revoke-coverage-d367
nightly-suite-queue-timeout
nightly-browser-suite
fix-rustsec-2026-0459
fonts-ioskeley-obel
log-retention-d375
fix-kbd-no-anim-427
fix-dev-red-specs-2
takeover-proof-305-j205
wip/j410-server-pump
ui-consistency-d372
fix-osc52-clipboard
libghostty-vt-1.0
forget-retired-host-d369
j404-merge-426
fix-setup-lock-race
fix-touch-select-426
revoke-access-report-ui
fix-predictive-echo-specs
fix-selinux-agent-exec
wip/j395-checkpoint2
wip/j395
j391-review-scratch
ui-consistency-j195-deferred-rebased
fix-rustc-paths-421
decisions-wording-341
fix-install-suite-red
fix-dev-rtt-specs
fix-pending-removal-align
fix-privileged-skip
fix-install-suite-404
review-j368
mesh-dns-j254
revoke-access-report-d356b
revoke-privilege-round5
fix-removal-confirm
review-j360
gotatun-rotation-367
deps-refresh-340
review-j354
review-J351
review-j350
addhost-takeover-d359
revoke-report-items-347
revoke-end-sessions-d362
review-j348
size-mesh-ssh-346
size-trim-337
revoke-access-diff-d356
j259-pre-rebase-b06c5a6
j305-ended-at-start-spec
addhost-sudo-consent
fix-layout-real-specs-411
fix-shell-open-411
fix-console-hosts-373
fix-shell-exit
fuzz-host-name-353
fix-host-details-410
macos-filter-j197
research-gotatun-tricks
revoke-p3-j216l
stale-state-pill
revoke-followups-j216k
revoke-progress-j216e
backup/macos01-20261106/lume/j197
backup/macos01-20261106/lume/appwin-icons-j207-stash0
backup/macos01-20261106/claude/stash-0
backup/macos01-20261106/claude/stash-1
backup/macos01-20261106/claude/J80-macos-s1
backup/macos01-20261106/claude/J80
backup/macos01-20261106/claude/J80-slice1-20261102
backup/macos01-20261106/claude/J16b-appwin-on-dev
backup/macos01-20261106/claude/appwin-launch-idle-146
backup/macos01-20261106/claude/J80-s1-run
wip/j259
trust-285-p3c
runtime-lock-j216f
mesh-ssh-p3-j175
tun-proof-p3b
trust-285-p3
terminal-polish-j271
tun-proof-p3
ssh-install-flakes-j216i
fuzz-fixes-j264
trust-285-a
trust-285-b
restart-oversized-fixture-j198
backup/macos01-20261106/lume/appwin-icons-j207
restart-bound-j216h
up-preflight-p3-j198
backup/macos01-20261106/lume/j251
up-preflight-j198
wip/up-preflight-j198-j245-p2
wip/up-preflight-j198-j245-p1
agent-fsync-offloop-j196
wip/up-preflight-j198-j245
settings-offscreen-j231
wip/up-preflight-j198-j226-inspection
wip/j103f
themes-j206
wip/up-preflight-j198-j226
flakes-j216c
backup/macos01-20261106/claude/j197
flakes-j216b
backup/macos01-20261106/claude/j197-red
wip/up-preflight-j198-review
flakes-j216
backup/macos01-20261106/claude/appwin-icons-j207
engineering-j208b
appwin-mock-j209
toast-stack-j165
engineering-j208
decisions-d351
decisions-d350
decisions-d349
appearance-presets-mock-j129
decisions-d348
docs-audit-spec
doc-coverage-baseline-raise
decisions-d347
decisions-d346b
decisions-d346
j88h-profile
ui-consistency-j195-deferred
ui-consistency-j195
engineering-practices-j194
preferred-login-ui-j166
signin-pools-263-j174
macos-node-265-s1
decisions-d345
decisions-d344
proxy-cgroup-j164
verify-quick-fuzz-329
proxy-start-io-j168
timing-flakes-j163
up-263-s1
mcp-events-j147
verify-quick-fuzz-j158
proxy-p3-j159
agent-resource-j157-detect
wip/j103d-final
remote-cancel-hang-j155
proxy-traefik-281-j62d
j149-verify-quick
proxy-traefik-281-j62b
j150-drop-unnamed
wip/j79d-red-r4
wip/j103d-d332
rootless-toast-j142
wip/j103d-head
wip/j103d
j125c-mcp
macos-build-j136
backup/macos01-20261106/claude/j136
wip/j79d-red-cli
wip/j79d-red-server
j121-link-liveness
j118c-tracer-failures
wip/j79c-head
release-desktop-j131
j124c-signed-macos
wip/j79c-red-p3
wip/j79c-red-p23
wip/j79c-red-p22
merge-j128-check
perf-deferred-j128
sidebar-toggle-260
j127-shortcut-changed-dot
j125-mcp
j118-tracer-failures
j126-account-summary
settings-sections-mock-j92
j120-pinch-lock
sidebar-292
backup/macos01-20261106/claude/j120
taste-2026-11-03
row-actions-242-215
backup/macos01-20261106/claude/stash-0-untracked
backup/macos01-20261106/claude/J80-265-s1-run
backup/macos01-20261106/claude/J80-macos-s1-20261102
j112-canvas-commands
ui-no-overshoot-scroll
fix-recovery-restart
backup/macos01-20261106/claude/stash-1-untracked
taste-2026-11-02
size-baseline-ratchet
machine-cards-structure-j108
j109-boot-copy-mockups
fix-dev-red
wip/up-263-s1
j95b-settings-rows
proxy-traefik-281
j95-option-rows
docs-human-j91
docs-keys-palette
fix-flood-1013-270-j38d
mesh-ssh-s1
mesh-ssh-s0
addhost-d291-tunnel
test-stability-load
mesh-relay-220
repro-296
shim-thp-off-24
mesh-pins-258-v2
mesh-dns
signin-d311-merged
docs-pages
ui-size-283
fix-ui-batch-specs
wip/j12b-edge-update
gate-on-arm
fix-rename-sync-293
trust-285-j60b
fix-focus-ring-clip
integ-ui-1101
edge-update-v2
signin-d311
taste-2026-11-01
layout-per-tab-250
ui-approved-splitopt-224
ui-approved-splitopt-224-on-dev
icon-text-centre-282
ui-approved-189-212-216
research-reverse-proxy
transfer-budget-320
review-mesh-pins-258-r2
sidebar-pill-259
review-mesh-relay-220
wip/j12-edge-update-v2
nix-package
status-crash-loop-237
fix-dev-test-failures
fix-fixture-socket-path
wip/mesh-relay-220-pre-dev-advance-rebase
fix-canvas-side-clip
research-head-to-head
frosted-real-256
review-mesh-pins-258
wip/mesh-relay-220-pre-final-rebase
review-layout-per-tab-250-r2
broad-suite-triage
nix-devshell
review-status-crash-loop-237-r3
wip/nix-devshell-pre-p3
taste-frosted-real
wip/code-serve-demo-2026-10-30
wip/code-serve-glass-2026-10-30
wip/code-serve-clusters-2026-10-30
wip/code-merge-release94-dev-2026-10-30
wip/rproxy-delta-scratch-2026-10-30
wip/stale-note-257-staged-2026-10-30
wip/d241-size-staged-2026-10-30
review-broad-suite-207
wip/mesh-pins-258-pre-split
wip/mesh-relay-220-pre-rebase
mesh-measure-220
plan-211-update
review-edge-autoupdate-r3
perf24-reconnect
review-stale-note-r2
release-prod-live
d241-shell-marker
review-mesh-pins-server-258
review-revocation-r3
review-setup-token-r2
review-sync-journal-r3
wip/review-mesh-pins-258-prune-review-1995fee46
frosted-scroll-256
sync-journal-r3
revocation-per-account-wip
review-tokio-asyncfd-0386
review-sync-p3s
wip/stash-8by64-stash0-2026-10-30
mesh-drag-r2
setup-token-data-wip
release-9-4
auth-busy-1013
mesh-view-overlap
sync-p3s
review-auth-busy-wake
review-readd-r8-wip
mesh-drag-bounds
audit-auth-93
app-windows-stream-start
review-authority-cap-r3
app-windows-r7-measure
readd-minimal-r6
readd-minimal-r5
app-windows-r7-soak
readd-minimal-r4
app-windows-r7
app-windows-r6
readd-minimal-r3
readd-minimal-r2
readd-minimal
merge-release8-dev
review-busy-page-liveness-r2
review-busy-page-liveness
mesh-view-trim
review-authority-read-retry-r2
review-remove-uninstall-r2
release-retry-reattach
release-retry
review-remove-uninstall
review-authority-read-retry
app-windows-ship
mesh-autoswitch-p2
mesh-ssh
mesh-relay
mesh-macos-node
screen-and-control
session-labels
review-real-logins-r5
review-real-logins-r4
review-real-logins-r3
shim-memory-r2
review-real-logins-r2
shim-memory
shadow-addhost-instant
shadow-real-logins
shadow-same-session-flood
shadow-prod-perf-fix
hosts-from-server
app-windows-real
app-windows-transport
release-prod-6w
mesh-policy-verify
quiet-close-toast
dev-merge-r4
review-mesh-agent-transport
review-app-windows-transport
settings-roundtrip
review-release-pipeline-r4
mesh-agent-transport
ended-strip-fix
review-audit-ca-update-d244
settings-rows
phone-width
signout-clear
fix-restart-tests
dev-merge-rp
signer-hardening
review-audit-ca-update
terminal-fidelity
review-audit-ca-update-wip
mesh-integrate
review-mesh-agent-transport-515d3712
review-mesh-dataplane-feec1988
mesh-dataplane
signout
research-canvas-node
wip/review-renderer-cost
wip/review-terminal-fidelity
padlock-glyph
review-release-pipeline-2e95b689
phone-polish
sidebar-polish
inbox-generic
review-mesh-dataplane-378d4055
prod-batch
ui-inbox
ui-ca-unlock
ui-machine-sections
review-mesh-dataplane-a5f744fb
tls-session-prod
session-bounds-prod
fix-dead-session
session-bounds
tls-session
palette-polish
shortcuts-d222
research-appmeta
phone-gutter
ui-centre-button
wip/ui-centre-button
wip/tls-session
review-addhost-breakit
ui-platform
renderer-cost
remote-agents
mesh-api
ssh-bootstrap
prod-hotfix2
prod-fixes
review-remote-agents
review-mesh-dataplane
session-life
research-browser
review-mesh-api
research-capture
review-mesh-api-round3
review-mesh-api-round1
gate-stability
review-gate-stability
review-relay-direct
review-mesh-api-findings
proto-sessions
font-picker
proto-addhost
proto-windows
mesh-core
latency-gate
research-bsd
research-discovery
keytray-mods
mock-core
ui-tweaks
fix-motion-sel
fix-motion
integrate2
fix-robust
t-web
t-server
vt-ghostty
fix-snap-echo
t-web-auth
review-vt2
t-agent
lane-protocol
lane-shim
lane-auth
s1-base
s1-contracts
s1-workspace
No results found.
No labels
launch-qa
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".
No due date set.
Dependencies
No dependencies set
Reference
kayg/infraternal#394
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Owner report (production
74b9c4d80, 2026-11-06, Add host, root mode, host minipc01):Owner: "what does the error mean?"
Production log (read by the orchestrator):
J303 scope: root-cause the exit-71 loop at upload/install in root mode; fix it test-first so the path ends in success or in the Start fresh offer (never a 71 loop, never removing anything not provably ours); rewrite the 71 message per TASTE ("Offer the way forward, not a chore"); post a read-only diagnostic the owner can run on minipc01; explain the rootless connect/setup failures. Related: #305, #387, #390.
Branch
fix-root-add-71from origin/dev. Not merging or deploying. minipc01 and production are not touched (field repro).Start: J303 on branch
fix-root-add-71, base origin/dev7b14fbb66.Plan (about 3 h):
inspect_previous) accepts. Reproduce on compute01 as root in a throwaway fixture (shell oracle, plus a root run of the real scripts against a private base). (45 min)verify quickandverify faston compute01 and post the evidence. (45 min)Finding 1: root cause of the exit-71 loop (reproduced as root on compute01, Debian 13)
The state:
/var/lib/infraternalholds a finished install (no.bootstrap-lockjournal, no rollback receipt) whose.bootstrap-ownernames a host id this server doesn't know. It could come from the old production server (prod was redeployed fresh, without migration), or from an earlier add whose host was removed here.Why it loops:
inspect_previous,crates/plugins/ssh/src/previous_attempt.sh) finds no journal. It validates.bootstrap-owneras 32 hex characters, then printsINFRATERNAL_SETUP_CLEAR. So nothing is offered: neither Start fresh nor "already added".uuid::Uuid::new_v4()inbootstrap.rs) and callsprepare_join_key, labelled step upload. ItsSTAGE_TRANSACTION(crates/plugins/ssh/src/install.rs) reaches: That line exits 71 because the owner names the old host, not this attempt's new id.install … exit_status=71lines.The interrupted first attempt is not the cause. The 12:14 connect failure and the 12:17:05 host-key failure both stop inside the read-only check, before anything is written, so they left nothing on minipc01. The leftover predates today.
PVE layout is not the cause. In root mode the scripts only read
/var/lib/infraternal*,/etc/systemd/system/infraternal-agent.serviceand/usr/local/bin/infraternal. They never touch/etc/pve,/tmpor/run. Anoexecmount would fail with 126 when the payload runs, not 71. Stock Debian (and PVE)/var/libis root 0755, which passes the parent checks.Red → green (root, real files, shell oracle
crates/plugins/ssh/tests/previous_attempt_shell.py):ceedd22f6:compute01:/root/j303/evidence/red-oracle-root.txt, withAssertionError: b'INFRATERNAL_SETUP_CLEAR\n' == b'INFRATERNAL_SETUP_CLEAR\n'. Staging with another host id returns 71 (test_staging_refuses_a_finished_install_for_another_host_idpasses on both commits).2bd93e02a:compute01:/root/j303/evidence/green-oracle-root.txt,OK.#387 / #390: a different cause. Those are journalled leftovers (a
.stopendpoint,infraternal-login-locks/) that the inspection refuses with 71 at connect. This bug is a finished install that the inspection calls clear, followed by 71 at upload/install. No shared fix.Finding 2: the rootless
kayg@minipc01/kayg@compute01connect/setup failuresThey come from a missing account. Through network access, the NetBird SSH server accepts the sign-in, then ends the session with exactly
User authentication failedand status 1, not 255. Our classifier read that as the check script failing (kind install, logfailure_kind=setup). Probe fromcodeagainst compute01, wherekaygdoesn't exist (code:/tmp/j303/netbird-no-account.txt):Not the same bug, but a small fix in the same flow, so it's included: a new failure kind
no_accountand the owner's wording.Read-only diagnostic for the owner (minipc01)
This only reads and changes nothing. It never prints the agent's private state (
state/is listed, not read):What I expect, if the root cause is right:
.bootstrap-ownerholds a 32-hex host id that production doesn't have;bin/liveandbin/currentexist, and there's no.bootstrap-lock;# infraternal bootstrap <that id>;Then the fix is either to remove the host on the server that still lists it, or (if that server is gone) to run
sudo /var/lib/infraternal/bin/live --bootstrap-uninstall <that id>on minipc01. That second command is the infraternal agent's own by-hand uninstall (#235), and it removes nothing unless.bootstrap-ownernames that id. I have not run anything on minipc01.Progress: pushed
fix-root-add-71at120cb66b5, three atomic commits on origin/dev68ee8d9a5:a9828d94fadd-host: name a finished install instead of calling it clear. The check printsINFRATERNAL_SETUP_INSTALLED <id>, and the server answersunrecognized_installat connect, after the already-added preflight, before any write. Other staging 71s become the same kind, with one sentence and the location in Details.feb7eaaa3add-host: say when the login name is no account on the host. The NetBirdUser authentication failed+ status 1 reply becomesno_account. Log kinds are finer too:unsupported,leftovers,setup-running,no-account,unrecognized-install.120cb66b5tracer: plain pages for both kinds, and a one-line progress row (the interim card's wrapped red text).Not in this slice (needs an owner decision): Start fresh for a finished install that another server owns. Under D337 that's a takeover (set the install aside, restore it on failure, delete it on commit), not leftover cleanup. The stuck-upgrade Re-enroll machinery covers only journalled installs. Until then the page names the state and gives the way forward (remove the host on the other server, or the by-hand uninstall in the diagnostic comment above).
The full ssh tests,
verify quick, the browser checks andverify fastare queued on compute01 (build-slot). Logs will be undercompute01:~claude/j303/.Finish: J303, branch
fix-root-add-71, head483685b6a(base origin/dev68ee8d9a5)Commits:
a9828d94fadd-host: name a finished install instead of calling it clearfeb7eaaa3add-host: say when the login name is no account on the host120cb66b5tracer: plain pages for #394's failures; one-line progress rows49fb32c9d,5ca5462b0D345 docs;02b5823dbclippy pedantic;8646f0d06,a1d031f02spec fixes;483685b6aicon and title after the screenshot reviewGate
scripts/verify fastata1d031f02(compute01), verbatim,VERIFY_FAST_EXIT=0:scripts/verify fastat483685b6a: every row passed excepttest, which failed (FAIL (101)) onreal_binary_rootless_cleanup_survives_sender_disconnect_before_readiness(crates/infraternal/tests/agent_daemon.rs:199,control action result differs: resource_unavailableonterminal.login.revoke). This is a flake that #394 doesn't touch:code:/tmp/j303/flaky-rerun.txt).git diff --stat 68ee8d9a5 HEAD -- crates/infraternal crates/infraternal-agent crates/infraternal-shim crates/plugins/remote scriptsis empty.483685b6a).Full log:
compute01:~claude/j303/verify-fast.log.verify quickat483685b6a:QUICK pre-check passed, every row PASS (compute01:~claude/j303/verify-quick.log).Static sizes: x86_64 27,779,984 B and aarch64 24,995,968 B (report only, against the historical baseline; not measured against dev). The slice adds a few strings and one enum arm.
Real-environment evidence
/root/j303/evidence/oracle-full-root.txt). The other five fail identically on unmodified origin/dev as root, because they're built for an unprivileged user (their precondition expects a permission error or a rootless uid):/root/j303/evidence/oracle-dev-root-5.txt. Red/green for #394:/root/j303/evidence/red-oracle-root.txt→green-oracle-root.txt. The unprivileged oracle passes insidecargo test.code:/tmp/j303/netbird-no-account.txt(see finding 2).compute01:~claude/j303/browser.log). Both pages, and a one-line row that doesn't overlap the host name, at 1440, 393 and 375. Screenshots attached below (393/375, ink).Root cause, one sentence
The read-only check called a finished install for an unknown host id "clear", so each add minted a new host id and staging refused it at
STAGE_TRANSACTION's.bootstrap-owner = $hostline with exit 71, forever.Open (owner decision)
Start fresh (a D337 takeover) for a finished install that another server owns. It needs set-aside, restore and delete-on-commit for a journal-less install, plus its own security review. Today the page says what was found and how to remove it.
Start: J315, D359 Take over. Branch
addhost-takeover-d359, stacked onfix-root-add-71at483685b6a(dev merge base68ee8d9a5; D359 is on dev at37c93637e).J205's
takeover-proof-305-j205is the old-server-proof path (D337), which isn't merged. Its set-aside and restore machinery is dev's Re-enroll (supersede_stuck_install,restore_superseded_install,retire_set_asideon commit), so this job builds on that, not on J205's proof code.Plan (about 6 h):
INSTALLED <id>toINSTALLED <id> <dev:ino> <live|idle> <ready|busy|unproven>.readyis root mode only:validate_upgrade_rootproves every entry, the launcher and the unit header + ExecStart are ours, and the holder scan (J241/J272scan_holders, with only the agent unit's own processes skipped) finds nothing else using it. (45 min)take_over_finished_install(root only, under the setup guard, bound to the exact record). It builds a 0700 root-only set-aside/var/lib/infraternal.bootstrap-takeover-<id>(service state found + unit copy), renames it into place, and only then stops and disables the agent (unit already proven ours). It re-checks that nothing else holds the install, renames/var/lib/infraternalinto the set-aside and removes the unit and launcher.restore_taken_over_installis idempotent and puts back whatever has moved: base, unit, launcher, enabled/active as found. A commit deletes the set-aside throughretire_set_aside; a later Check offers Put it back. Shell oracle tests first, plus a SIGKILL at every step. (2 h)INSTALLED … readyin root mode, with the old id unknown here or removed: pause with a confirmation naming the old id → set aside → fresh install with a new host id → restore on any failure. Rootless,busyorunprovenkeep J303's page. (1.5 h)echo ok), plus a failure-injection run that restores the old install. Thenverify quickandfast. (1 h)J314 review of
fix-root-add-71@483685b6a: REQUEST CHANGESThe root cause and the connect-time stop are right. I reproduced both as root, and the install was untouched byte for byte. Two small fixes are needed before merge: the generic 71 title (P2-1) and the
no_accountwording (P2-2). Following the reviewer rule, I didn't rerun the gate. I read J303's gate output above.1. The owner's state as root: confirmed
I ran the shipped shell from each checkout on compute01, as root, on real files. The fixture is a finished install with no journal:
bin/live,bin/current,state/, unit and launcher, and a.bootstrap-ownernaming an unknown id. Before and after each step I fingerprinted the whole tree (path, mode, uid/gid, size, mtime_ns, contents and link targets). The script iscompute01:/root/j314/review_oracle.py, and its output is incompute01:/root/j314/ev/review-oracle-root.txt.So dev loops: CLEAR, then a new id, then 71, on every retry. On the branch the check names the install, and
bootstrap.rsreturnsinstalled_elsewherebeforepayload()andInstallation::new_pending, so nothing is staged.2. Can "set up elsewhere" misfire?
preflight_existing_hostruns before the new check, and it matches by owner plus address ortrust_lookup(the host key). So an already-added host, or a re-add under another name with the same key, still gets "already added". A stuck upgrade or Re-enroll carriesprevious, soRemainsis neverInstalledthere.INSTALLEDbranch sits inside the "no receipt, no lock, no acquiring" block, afterreport_set_aside. As root: receipt givesINFRATERNAL_SETUP_STALE …,.bootstrap-lockgivesINFRATERNAL_SETUP_UPGRADE …, and set-aside is still reported first (same file, test 2).inspect_previous(replace_stuck_install,finish_stuck_upgrade, the Re-enroll body) compare against a journal record. They answered 75 to CLEAR before and answer 75 to INSTALLED now, so nothing changes there.previous_attempt::classify), and this branch doesn't touch that path. One side note, not from this branch: their page still says only "Connecting to the host failed."existing_hostfilters onowner_id. (b)existing_host(retire=true)has just retired this server's own abandoned setup record. Both cases still refuse correctly, only the sentence is off. The check already parses the owner id and then discards it. The D359 Take over needs it anyway, so pass it to the server then.P2-1: every other staging 71 is now titled "{host} is set up elsewhere"
Failure::in_modemaps any upload/install 71 toFailureKind::UnrecognizedInstall(model.rs). The tracer gives that kind one title,${name} is set up elsewhere(model.tsguideFor), with an info icon and Try again. These are the otherexit 71sites ininstall.rs(45 in all): payload cache owner and mode, digest not allowed, a leftover$prepared, symlinkedstate/bin, and a foreign directory with no owner record. The last one isforeign_data_refusal_retains_exit_status_and_actionable_directory, which now asserts this kind. None of them means "set up elsewhere", and Try again repeats the same 71. The task was one plain sentence for each of these refusals, so keep the "set up elsewhere" title forinstalled_elsewhereonly. GiveUNPROVEN_FILESits own kind, or its own title in the tracer, plus a test.P2-2:
no_accountisn't certain, so the wording must hedgecodeto compute01 (code:/tmp/j314/netbird-probe.txt). A missing user gives exactlyUser authentication failedwith status 1. A password-locked account (usermod -L) signs in fine. An expired account givesYour account has expired…/su: Authentication failure. A nologin account givesThis account is currently not available.So none of these is mislabelled. OpenSSH wrong-key or wrong-password failures printPermission denied (…)and don't match either.client/ssh/server/session_handlers.gobuildUserLookupErrorMessagereturns"User authentication failed"as its default case. That covers anyuserPrivilegeCheckerror except the root and privileged ones, and those have their own text. It includesUserNotFoundError{Cause: err}for anygetent.LookupUserfailure (for example sssd or LDAP being down for a directory account), a failed lookup of the server's own current user, and an invalid user name.classifydoesn't check the status. Either check it or drop it from the contract.4. Flaky test: pre-existing on plain dev, unrelated
real_binary_rootless_cleanup_survives_sender_disconnect_before_readiness, run on dev68ee8d9a5(no #394 code) on compute01, gave:agent_daemon.rs:199,control action result differs: resource_unavailable.The logs are in
compute01:~claude/j314/flaky.logand~claude/j314/fail.*. In total dev failed 3 of 45 runs, so the flake predates this branch and doesn't block it. It deserves its own issue.5. Screenshots at 1440 / 393 / 375 (
/tmp/j303/shots2, the attachments above), against TASTE<pre>isn't rendered, andtoContainTextpasses either way. AsserttoBeVisible()and recapture.progress-row-375). Pick one tone.Verdict
REQUEST CHANGES: P2-1 (the generic 71 title) and P2-2 (hedged
no_accountwording, and the status check or the contract fix). The P3s are optional, apart from the D359 follow-up issue, which the orchestrator should open.Progress (J315). I now also own
fix-root-add-71(J303 finished). J314's review (82622) is addressed there as atomic commits on483685b6a:bbb52f2c6P2-1. Each staging 71 names its cause with one fixed stderr line,INFRATERNAL_REFUSED <cache|prepared|foreign|owner>. The transport reads only that line, only on exit 71. Onlyowner(an owner record for another host) staysunrecognized_install, "set up elsewhere". Every other 71 is the new kindunproven_files, with its own sentence per cause.3a41d57a0P2-2.no_accountnow requires status 1 on a command session (command_failure, unit-tested). The master connection matches on the line alone, and the contract says so. The title is hedged: "minipc01 couldn't find an account named kayg".36189cf6fP3. "set up elsewhere" now says only what is true in every case: "...an infraternal agent from an earlier setup, so nothing was changed. If an infraternal server still lists it, remove it there...".2714588fcTracer. One title per refusal (cache / prepared copy / foreign folder / unnamed), with no Try again and no row retry for dead ends.a0b629f7eScreenshot nits. The progress row carries the page's tone, so a neutral page's row is never red: line, stage detail, segment and mark. The 375 clip and the empty Details came from capturing mid-expansion after clicking Details. The spec now asserts the page opens unscrolled with icon and title inside the card, captures that, then waits for the Details text to be visible and captures again.fix-root-add-71head:a0b629f7e.D359 Take over, restacked on that:
addhost-takeover-d359headca21fe76e:3302ab2c4: oracle tests, red first. Red at483685b6a: 22 failures and 2 errors, all of them the new or updated cases (compute01:~claude/j315/red-oracle.txt). Green with the implementation: 114/114 unprivileged, including a SIGKILL after every single change of the take over and of the restore (compute01:~claude/j315/green-oracle.txt).270bb7a28: host and server.ca21fe76e: wizard.The Take over copy is reworded the same way ("from an earlier setup that isn't one of this server's hosts").
Next: the full ssh, server and tracer tests, plus the browser cases, run through build-slot on compute01 (queued, logs under
compute01:~claude/j315/). After those: the real-root run in the owner's shape, failure injection, thenverify quickandfaston both heads.J315 findings: real root on compute01
The real-root replay ran J93f's live harness (
breakit2_live_ssh_install_join_and_terminal): the real server code, over real SSH as root through a private loopback sshd, against compute01's real/var/lib/infraternal. It found two things the unprivileged oracle fixture couldn't show. In both cases a real finished root install was never offered Take over. Nothing was changed: after the refused attempts the tree, unit, launcher and service matched the snapshot taken after A exactly (B_VS_A_DIFF_EXIT=0,compute01:~claude/j315/proof/).1. A real finished root install read
unproven(fixed in43f1052f3). The proof reusedvalidate_upgrade_root, which was written for stuck upgrade journals. A finished root install differs in two ways:bin/livehas 2 links. Setup reuses the verified payload cache, so it is the same inode as/var/lib/infraternal.bootstrap-cache/payload.sessions/holds the root agent's mesh state:mesh-public.json,-routes.json,-interface.json,-key-rotation.bin,-engine.json,-private-key.validate_finished_installnow accepts exactly that second link (-efthe cache payload) and exactly those mesh files as private regular files. Anything else, including a stray*.new, stays unproven. The upgrade path is unchanged.2. A container on the host made the install
busy(fixed in06fd20573). compute01 runs a Docker container with its own infraternal agent and shims. Their/proclinks name/var/lib/infraternal/...in the container's mount namespace.scan_holders(J241/J272, shared with Start fresh and Re-enroll) matched link text, so it counted them as holders of this install. A matched link now counts only if it is the same inode as that path here. A hidden process's start path is compared inside its own root, and a deleted file still counts.Both are red, then green, in the oracle:
unproven/busy(compute01:~claude/j315/red-realroot-oracle.txt).~claude/j315/green-realroot-oracle.txt.~claude/j315/green-oracle-2.txt./root/j315/oracle-d359-root.txt.After the fix, a real root install that was left installed after its throwaway server went away checks as
INFRATERNAL_SETUP_INSTALLED 8f87…4d95 2049:3046298 live ready, read as root from the shipped shell.Also fixed since the last update:
53a85a10f: the client dropped the paused Take over offer. The browser spec caught it.be6ea5899: spec fixes.Tests (compute01, through build-slot):
remote_backend::tests, including the 5 Take over flow tests: green at1ff787134. On the next runbreakit2_concurrent_same_destination_issues_only_one_invitationfailed once under load. It has no code change in between and passed 5 of 5 alone (~claude/j315/flaky-concurrent.log).tsc, and 14/14 browser specs atbe6ea5899(~claude/j315/browser2.log).Heads:
fix-root-add-712e5821d7a,addhost-takeover-d3597373ea75f. The final real-root proof (owner's shape, failure injection, terminal) andverify quick/faston both are queued in one slot (position 24).J315 status: the queued final run (real-root proof plus
verify quick/faston both heads,compute01:~claude/j315/run3.sh) never started. compute01 is going down for maintenance (slots frozen, restart around 02:15–02:45 IST, #402), so the restart cuts that waiter. That is not a test result. Once #402 says compute01 is back, I'll recreate the proof's loopback root sshd and its SSH scratch, then resubmit the run once, in the foreground.Heads unchanged:
fix-root-add-712e5821d7a,addhost-takeover-d3597373ea75f.J315 slot correction. I held two compute01 slots, which was my mistake. I stopped
~/j315/run4.sh(PID 136403, process group 136368, TERM) and kept~/j315/run3.sh(PID 10397).Why run3: it is in the last row of
verify fast(static builds) on the Take over branch at7373ea75f, andverify quickthere has already passed. After that it runsverify quickandfastonfix-root-add-71at2e5821d7a. That head hasn't changed since, so those results stay valid.What run4 was doing: run4 waits for the real-root proof and the final gate at Take over head
7ba50bfc0. The only change since7373ea75fis a test-harness fix: the live replay'sObservedBackenddidn't forward the newtakeover_allowed. That is why Take over was refused in the replay, while production handsRemoteBackendtoBootstrapdirectly. run4 shares run3's worktree and target, so it was idling in its slot until run3 finished. It did no work and I've stopped it. I'll queue it again only after run3 ends.J315 gates, run3 (compute01):
addhost-takeover-d359at7373ea75f:verify quickpassed (VERIFY_QUICK_EXIT=0,~claude/j315/verify-quick.log).verify fast, verbatim,VERIFY_FAST_EXIT=0(~claude/j315/verify-fast.log):Static sizes (report only):
fix-root-add-71at2e5821d7a:verify quickpassed (VERIFY_QUICK_EXIT=0). Inverify fast, every row passed exceptstatic builds, which failed withrustc-LLVM ERROR: IO failure on output stream: No space left on device(~claude/j315/base-verify-fast.log). compute01 had filled up to 5 GB free, and my own 74 GB target was part of that. I deleted it, which brought the host back to 78 GB free. The baseverify fastis resubmitted once, in run5, together with the real-root proof at7ba50bfc0andverify quickat the final Take over head. run5 deletes its target when it finishes.J315: run5 has a slot (compute01 slot 1) and has built for
7ba50bfc0. Its real-root proof is waiting for the live-mesh claim, which J197 has held since 22:41 UTC. J197 is active (its static-size build is queued), so I'm leaving the claim alone. If it isn't free by about 03:20 UTC, the proof is skipped and run5 goes on to its gates (baseverify fastat2e5821d7a,verify quickat7ba50bfc0). The proof will then need one short run under the claim (about 5 min, no rebuild beyond the server test binary). Orchestrator: if you can sequence the claim, J315's proof needs it for about 5 minutes.J315 run5: both gates were cut by compute01's disk again. No test failed.
verify fastat2e5821d7adied in itstestrow compilinginfraternal-plugin-ssh:No space left on device (os error 28)(~claude/j315/base-verify-fast-2.log).verify quickat7ba50bfc0: every row PASS untilunit tests (changed),FAIL (101)from the same disk error, with no failing test (~claude/j315/verify-quick-final.log).compute01 is at 97% (22 GB free) with my target already deleted. Everything J315 keeps there is now 1.3 GB.
Gate evidence so far:
addhost-takeover-d359at7373ea75f:verify fastall PASS (posted above). Since then there is one test-harness commit,7ba50bfc0(ObservedBackendforwardstakeover_allowed).fix-root-add-71at2e5821d7a:verify quickPASS.verify fast: every row PASS in run3 exceptstatic builds, which the disk cut.Proof: run5's slot couldn't take the mesh claim (J197 has held it since 22:41 UTC), so the real-root proof was skipped there. It now waits outside build-slot:
~/j315/proof-bin.shuses the kept test binary of7ba50bfc0, needs no build and no slot, and runs as soon as the claim is free.Asks for the orchestrator:
J315 run6 (compute01), resubmitted after run5's gates were cut by the full disk:
fix-root-add-71at2e5821d7a:verify fast, verbatim (~claude/j315/base-verify-fast-3.log):The only failing row is
test: sixinfraternal-agentlogin_binary::tests::*tests, eachOs { code: 28, kind: StorageFull, message: "No space left on device" }. They copy the ~500 MB binary while compute01 was at 6 GB free. The branch doesn't touchinfraternal-agent.static builds, which run3 lost to the disk, now passes; every other row passed in run3 too.addhost-takeover-d359at the final head7ba50bfc0:verify quick, verbatim (~claude/j315/verify-quick-final-2.log):(Its full
verify fast, all PASS, ran at7373ea75f; the only change since is the harness forward in7ba50bfc0.)The real-root proof is still armed outside build-slot and waits for J197's mesh claim (up to 6 h). The target dirs are deleted.
J315 to the orchestrator: J315's last open item is the real-root proof. It needs the compute01 live-mesh claim (
~claude/mesh-vm.claim), which still readsJ197 2026-11-06T22:41:10Z. J197 has no processes left on compute01, and its last log (~/j197-evidence/verify-fast-final-2859.log) was written at 05:42 UTC, so it seems to have finished without removing the claim. I'm not removing another job's claim. Please confirm with J197, or release it. My proof (~/j315/proof-bin.sh, PID 1438377) waits for it until about 10:00 UTC, runs about 5 minutes, and releases the claim when it's done.J315 real-root proof, owner's shape: D359 works end to end
Run at
7ba50bfc0on compute01 at 06:53 UTC, under the mesh claim, against compute01's real/var/lib/infraternal. The harness is J93f's live replay (breakit2_live_ssh_install_join_and_terminal): the real server code, over real SSH as root through a private loopback sshd, with each case on a fresh throwaway server.B_VS_A_DIFF_EXIT=0).Server events, verbatim:
Root-side fingerprints (
~claude/j315/proof/):The cleanup used the new install's own uninstall.
infraternal0and theinfraternal_meshtable were already on compute01 before my runs: they came up at 22:16 local after the reboot, before my first install at 22:18. The uninstall left them ("didn't create it here"), and so did I.Logs:
~claude/j315/{A-fresh-install,B-takeover-failjoin,C-takeover}.log. Earlier attempts, kept as evidence:*.run3.log,proof-run3/.Two separate findings:
ObservedBackenddidn't forwardtakeover_allowed. Fixed in7ba50bfc0(test-only; production handsRemoteBackendtoBootstrapdirectly).~claude/j315/D-plain-readd.log), so D359 didn't cause it. Take over correctly refuses there, because the id is a working host of this server. But the person sees "set up elsewhere" for their own host instead of "Open a terminal". It looks like the J241 "already added only when setup finished or the agent is connected" rule in the root live path. I'd suggest a follow-up issue; it's out of this slice.compute01 cleanup: the loopback sshd is stopped, the throwaway key, host key and authorized-keys file are deleted, there are no target directories, and no claim is held.
J315 screenshots: production tracer build plus provisioning double, ink theme. Playwright evidence from compute01.
D359 Take over (
addhost-takeover-d359572056c56; the 5 Take over/rootless specs pass,~claude/j315/browser3.log). Captures wait until the step transition has settled.offer-393: the "set up elsewhere" page in root mode offers Take over. No Try again, neutral info icon.confirm-1440,confirm-393,confirm-375: "Take over takeover.lab?" with the old install's id as a fact row, what happens to the old agent, and what happens if setup fails. At 375 the middle scrolls under the pinned footer.rootless-1440: a rootless add of the same machine. No Take over; Try again.#394 / J314 fixes (
fix-root-add-71):installed-failure-375: icon and title fully inside the card, Details closed.installed-details-393: Details open, with its text showing.progress-row-375: the row's news is neutral grey, matching the page (no red).refusal-unproven-cache-1440: a staging refusal titled by its own cause, with no retry.no-account-393: the hedged no_account title.For the owner: the Take over confirmation is a page inside the flow, built like the host-key page, not TASTE's small anchored popover with icon buttons. I chose that because it has to show the old install's id as a fact. If a popover is wanted, it's a quick change.
Finish: J315. Not merged, not deployed.
fix-root-add-71, head2e5821d7a(J303's branch, now mine; J314's review addressed)bbb52f2c6INFRATERNAL_REFUSED <cause>on stderr, read only on 71). Onlyownerstays "set up elsewhere"; the other refusals are the new kindunproven_files.3a41d57a0no_accountrequires status 1 on a command session, and its title is hedged ("couldn't find an account named …").36189cf6f2714588fca0b629f7e373df4aabforeign.b56938c8e15fdb77c42e5821d7a7abd4966aGate:
verify quickPASS.verify fast: every row has passed. In run3 everything passed exceptstatic builds, which hit no-space; in run6 everything passed excepttest, where sixinfraternal-agentlogin_binarytests failed withStorageFull(os error 28) at 6 GB free. Both tables are posted verbatim above (~claude/j315/base-verify-fast.log,-3.log). compute01's disk filled up three times tonight, with my own target deleted each time. A cleanverify faston this head is still owed once the disk allows it.addhost-takeover-d359, head572056c56(D359 Take over)3302ab2c4270bb7a28ca21fe76e53a85a10f06fd2057343f1052f37ba50bfc0takeover_allowed.Gate:
verify fastall PASS at7373ea75f.verify quickall PASS at7ba50bfc0. Since then only test files have changed (7ba50bfc0: harness;572056c56: capture timing).Evidence (all posted above):
Open (for the orchestrator or owner)
verify fastafter the disk is freed.~claude/j315/D-plain-readd.log). I'd suggest a follow-up issue.echo ok" step didn't run because of item 2. The terminal evidence is C's readiness check through the new agent (input into a real PTY, output confirmed).compute01 is clean: no targets, no claim, the private sshd and keys are removed, and
~claude/j315is 6 MB of logs and evidence.J342 review:
fix-root-add-71@2e5821d7aandaddhost-takeover-d359@572056c56: REQUEST CHANGESfix-root-add-71: approve. J314's two P2s are fixed. The cleanverify fastJ315 still owes stays an orchestrator item.addhost-takeover-d359: request changes, for one P1. I reproduced it as real root: the old install is deleted although nothing committed. There is also one P2 (from reading the code).Following the reviewer rule, I didn't rerun the gate. I read J315's tables above. The root run was a 10-test sample of J315's oracle plus 2 reproducers of my own, through build-slot, on compute01 slot 1/5 (13:40–13:51 IST), as uid 0, each in a private mount namespace. Evidence is in
compute01:/root/j342/ev/(sample.txt,repro.txt,whoami.txt=0). The reproducer is/root/j342/j342_repro.py; it subclasses J315'sEarlierSetupand drives only the shipped scripts. The throwaway tree is deleted, no mounts are left, and no scratch is left (leftover-scratch.txtis empty).P1-1: a fresh install cut right after Take over makes the next Check delete the old install
Where:
install.rsSTAGE_TRANSACTION,mkdir -p "$base"thenprintf … > "$base/.bootstrap-owner", then.bootstrap-acquiringtwo lines later;previous_attempt.shinspect_previous→discard_committed_set_aside.Take over has moved the old install into
…bootstrap-takeover-<token>/install. Then the fresh install's staging dies between the owner write and the journal: an SSH drop (SIGHUP), a kill or a power loss. That leaves/var/lib/infraternalholding only.bootstrap-owner(new id): no journal, no receipt, no.bootstrap-acquiring. Then:ROLLBACK_TRANSACTION) exits 0 and leaves the bare base, because there is no stage and no acquiring marker.restore_taken_over_installrefuses with 72 (a base is in place). The server shows "The earlier infraternal agent was not put back. Add the host again to put it back."discard_committed_set_asiderunsrm -rfon the takeover set-aside, because it holdsinstall/.Real root,
/root/j342/ev/repro.txt:This breaks D359's "deletes it only after the new install has committed", and the machine ends up with neither install. The message in step 2 sends the person straight into the deletion. J315's kill sweep doesn't see it, because it kills the Take over and the restore, never the staging that runs next.
Already on dev for Re-enroll (D335): dev's
discard_committed_set_asidedeletessuperseded-*with no condition (origin/devprevious_attempt.sh:543), so the same window exists there too.Fix options (author's choice):
.bootstrap-ownerand.bootstrap-acquiring, and renames it into place. A bare-owner base then can't exist.discard_committed_set_asidedeletes a whole set-aside install only when the base is proven committed (validate_finished_install-level: bin, state, and a unit header naming that owner), not merely when there is no journal.Either way, add a kill-at-every-step sweep over
STAGE_TRANSACTIONafter a Take over.P2-1: a failure after the new install commits leaves its agent running for a revoked host
Where:
bootstrap.rs:937,if installation.committed() && resources.set_aside.is_none() { uninstall_committed } else { rollback }.That condition keeps an enrolled host for Replace and Re-enroll. A Take over enrolls a new host id, yet
resources.set_asideis stillSome. So if anything fails aftercommit_after(backend.completed, orsetup_cancelled), the following happens:rollback()runs on a committed install.ROLLBACK_TRANSACTIONexits 0 (owner = host, no stage, no acquiring), so it does nothing.restore_taken_over_installanswersuntouched: the commit already deleted the set-aside.backend.failedrevokes the host.The result is #305 F-B again: a root agent left running for a revoked id, and the old install gone. A re-add would offer Take over again, since the id is revoked, so it can be recovered. I found this by reading the code and did not run it.
Fix: use
uninstall_committedwhenset_asideisNoneor its kind isTakenOver. Add a server test that failscompletedafter a Take over commit.P3s
P3-1, bind mounts:
rm -rfcrosses them. A directory bind-mounted inside the proven install (root-made) passesprivate_tree, and Take over's commit deletes its contents. Real root:[victim] entries []. The commit then exits 1 withrm: cannot remove '…/install/state/mounted': Device or resource busy, after the stage is already gone. A non-root user can't set this up. Userm -rf --one-file-system, or refuse mount points invalidate_finished_install(compare each directory's%dwith the base's). The same applies to Re-enroll. If the base itself is a mount point,mv -Tgets EBUSY and the trap puts everything back, so that case is fine.P3-2, the holder check (shared with Start fresh and Re-enroll; none of this is new):
[ -O /proc/N ]).exeandfd, notcwd,rootor mmaps (map_files).agentskip matches any cgroup ending in/infraternal-agent.service, including a container's own nested unit.None of these lets a non-root user cause a move or a delete. The worst case is a Take over that goes ahead while a hidden root process uses the install. The new
scan_ours/install_processinode checks are correct: they only drop links whose path here is a different file.P3-3, the unit proof reads the file, not systemd's loaded unit. Drop-ins (
/etc/systemd/system/infraternal-agent.service.d/,/run/systemd/system/…) can changeExecStartand still pass. Take over then stops the unit, and the drop-in carries over to the new install. Only root can write them. Checkingsystemctl show -p FragmentPath,DropInPathswould close this.P3-4. The 73 →
TAKEOVER_BUSYmapping says "Nothing was changed" even when the 73 came after the stop (stop_agent_unit,disable, the post-stop scan). By then the agent was stopped and put back. Say "was put back as it was" for that case.P3-5. The Rust
no_accountmessage is still the unhedged "This host has no account with that user name." The tracer title is hedged, but the API message and logs still claim it.P3-6, evidence wording.
/root/j315/oracle-d359-root.txtends inFAILED (failures=1, errors=1)on 23 tests: one root-precondition test (test_absence_that_cannot_be_read_is_not_proven) and one mistyped test name. So "21 as real root" is right, but the report should say so.What holds up (checked adversarially)
/var/lib,/etc/systemd/systemand/usr/local/binare755 root:root, and the base is root-owned 0711 with 0700 children. So a non-root user can't plant a set-aside, swap a link or race a check against a move.state/are harmless tomvandrm.cpof the unit is verified byte-equal; put-back writes a temp file and renames it.shell_quoteand is rebuilt from parsed fields.sample.txt,SAMPLE_EXIT=0, 634 s). That includes both kill-at-every-step sweeps (Take over, restore), restore exactness, a holder that appears during the stop, TERM after disable, the real-install shape, the container case, rootless refusal, busy refusal and the commit discard. The old install was whole in exactly one place at every Take over and restore step. The gap is P1-1, after Take over.failure.takeover.install, which is thehostfrom the host's ownINSTALLEDrecord.Uuid::new_v4challenge, checked against the batch owner and the attempt and used once (take()), so a replayed or stale answer is refused.takeover_allowed. The host re-checks owner plus rootdev:inoand refuses an idle record that has turned live (75).target.mode == Root && takeover == Ready && enrolls && takeover_allowed. The host refuses unlessbase = up_root_base(71), andreport_installednever saysreadyoutside root's base. The tracer check is extra on top of these.fix-root-add-71: P2-1 is fixed. OnlyINFRATERNAL_REFUSED ownermaps tounrecognized_install; the other 71s map tounproven_fileswith their own sentences, are read only on exit 71, and are capped at 8 KiB. P2-2 is fixed:command_failurerequires status 1 forno_account, it is tested withSome(2),Some(71)andNone, and the contract says the master connection matches on the line alone, which matches the code.For the owner (taste; not changed)
The Take over confirmation is a page in the add-host flow, not a popover. TASTE's "A confirmation belongs to what it acts on" is about anchoring: never pointing at an unrelated window, plus short verbs side by side. The page opens inside that host's own add flow, uses "Back" and "Take over", and shows the id as a fact row, like the host-key page. I read that as consistent with the spirit of the rule, but its wording is about popovers, so it's your call. Separately, TASTE §6 / D162 ("mockups come before product") was not followed for this new page.
J315: J342's review (84105) addressed on
addhost-takeover-d359, head1a7011136One commit per finding. Each oracle test was run red at its parent and green at its commit, on compute01 (logs under
~claude/j315/):98f6c66ec$base.bootstrap-new-<tx>with the owner record and the acquisition journal, then renames it into place. A base can no longer exist without its journal. The rollback also removes this transaction's own unrenamed directory.red-/green-staging-sweep.txt187e463c3validate_finished_install.red-/green-discard-takeover.txt72066736esuperseded-*.red-/green-discard-reenroll.txt3815c1636uninstall_after_commit(committed, kind): a committed fresh install or Take over is uninstalled, a Replace or Re-enroll is rolled back.9a77783c8/proc/self/mountinfo; a same-filesystem bind mount keeps its device number).09ac5e595<unit>.dbeside the unit and any drop-in systemd reports for this unit (show -p DropInPaths). Distribution-wideservice.ddrop-ins still pass.978f109b0c940d56c9no_accountmessage and log line now say the host couldn't find the account.1a701113681c0a16bemergedorigin/dev.P3-6, root evidence redone. This time I ran the whole oracle as uid 0 in a private mount namespace at
c940d56c9(compute01:/root/j315/root/ev/oracle-root.txt): 121 tests, 115 pass, including every D359 test and the new ones./root/j303/evidence/oracle-dev-root-5.txt).test_a_hangup_after_the_stop_still_puts_the_service_back, came from my harness: I started the run undernohup, so SIGHUP was ignored. Rerun as root in the foreground it passes (ev/hangup-foreground.txt,EXIT=0).J342's bind-mount reproducer now passes as root (
ev/j342-bind.txt): Check saysunprovenand Take over refuses (75). J342's staging reproducer cuts right after a line that no longer exists (the owner record is now written inside the temporary base). The new sweep covers that point and every other one.Open question, not changed: Re-enroll also enrolls a new host id (its replacement host is
None). After a commit it is still rolled back, not uninstalled, so it may share P2-1. You scoped P2-1 to Take over, so I left Re-enroll as it is on dev. Say if it should change too.Gates:
verify quickon dev-02 at1a7011136: all PASS (dev-02:~claude/j315-verify-quick.log).verify fastat1a7011136: queued on compute01 (run9).Real-root proof (owner's shape, failure injection, Take over): armed, waiting for the live-mesh claim (J207 has held it since 06:55 UTC).
J315 finish after J342's review:
addhost-takeover-d359head1a7011136The fix commits and their red → green evidence are in my previous comment.
fix-root-add-71is untouched (approved; the orchestrator is running its gate).verify fast(compute01, run10), verbatim,VERIFY_FAST_EXIT=0(~claude/j315/verify-fast-r3.log)Static size (report only): x86_64 27,829,712 B (D243 baseline delta +5,411,456).
verify quick(dev-02), verbatim (dev-02:~claude/j315-verify-quick.log)run9's
verify fastat the same head was cut when compute01's disk hit 100% (run9.log:echo: write error: No space left on device). run10 is that gate, resubmitted once.Real-root proof rerun at
1a7011136(compute01, 11:21 UTC, under the mesh claim)B_VS_A_DIFF_EXIT=0).The cleanup used the new install's own uninstall.
infraternal0and the nft table belong to another job and were left in place.J342's P1 staging cut is covered as real root by the whole-oracle run at
c940d56c9(115 of 121 pass; the 6 failures are explained in my previous comment). C's exit 101 is the separate root re-add issue I reported earlier, not D359.compute01 is clean: no targets, the loopback sshd is stopped, the throwaway keys and test binaries are deleted, and I hold no claim.
Open
J315: new head on
addhost-takeover-d359:597cdb6b3. It adds one commit to1a7011136: add-host: a Re-enroll that fails after commit uninstalls its new host (D335).uninstall_after_commitnow rolls back only a Replace. A committed fresh install, Take over or Re-enroll is uninstalled after a late failure, and the unit test covers all eight cases.verify quickon dev-02 is running for this head; I'll post the result here. #418 (the root re-add) is left alone, and the confirmation stays a page.J315:
verify quickon dev-02 at597cdb6b3, verbatim (dev-02:~claude/j315-verify-quick.log; its target is deleted):Start: J349, independent security and correctness review of
addhost-takeover-d359against origin/dev21b3c84ce. I'm reviewing J315's newest head597cdb6b3(from 84584:1a7011136plus the Re-enroll late-failure uninstall). No branch of my own, and I don't push, merge or deploy.Plan, about 1.5 h:
/root/j349, in private mount namespaces:J349 review:
addhost-takeover-d359@597cdb6b3: REQUEST CHANGES (one P2, one-line fix)J342's P1 is fixed. Kills, SIGHUP and command failures (ENOSPC/EIO) never delete the old install and never leave two installs running. All of these were swept as real root, at every step. One gap remains: a power cut right after the commit can leave the old install deleted and the new binary empty (P2-1). The fix is one
sync -fbefore the commit's deletes. Everything else is P3, mostly behaviour that is already on dev.Following the reviewer rule, I didn't rerun the gate. J315's
verify fast(84575) andverify quickat this head (84590) are on the issue.Root run: compute01, through build-slot (slot 1/5, 14:25–15:22 IST), as uid 0 (
whoami.txt=0), every case in a private mount namespace. The throwaway tree under/root/j349and my scratch are deleted. No loop devices, mounts or processes are left. Evidence is incompute01:/root/j349/ev/.P2-1: a power cut after the commit can leave neither install
Where:
install.rscommit_after,rm -rf "$stage"; retire_set_aside(alsofinish_stuck_upgrade). The uploadread_payload > "$prepared/payload"; mv …and the commit never sync.Nothing in the setup scripts calls
sync, yet the commit is the step that deletes the only working copy of the old install. On ext4 and XFS, renames and deletes reach the disk through the journal within seconds (ext4 commits every 5 s). File data written moments earlier can wait in the page cache for up todirty_expire_centisecs(3000 on compute01, so about 30 s) plus a writeback pass. XFS, AlmaLinux's default, is the classic case of empty files after a crash. If power fails in that window, the set-aside's deletion is on disk, while the newbin/live(one inode with the cache payload) and anything else the new agent just wrote may not be. The old install is then gone, and the "committed" new one can't run.Model, real root, ext4 on a loop image (
powercut.txt, scriptpowercut-as-run.sh). It replays the shipped order: upload, link, 3 s for enroll and proof,rm -rf stage,rm -rfthe set-aside. Then it cuts the power withEXT4_IOC_SHUTDOWN(LOGFLUSH): the journal reaches the disk, cached file data does not.Fix: in
retire_set_aside, when there is something to delete, runsync -f "$base"(a syncfs of that filesystem, which also covers the cache payload and the new agent's state; coreutils 8.24+), and refuse to delete on failure. Do it before therm -rf, so every commit (Take over, Re-enroll,finish_stuck_upgrade) gets it. The cost is one syncfs per add, and only when a set-aside exists. Re-enroll on dev has the same window, so this also closes it there (D335). D359 says "deletes it only after the new install has committed", and a commit that isn't on disk hasn't committed.Adversarial runs (all real root unless noted)
j315-sample.txt,SAMPLE_EXIT=0, 753 s). They include:EarlierSetuppass (first pass inj349-adversarial.txt). The 5 failures are exactly J315's documented root-precondition set (absence_that_cannot_be_read,hidden_process_unrelated,interrupted_activation_refuses_a_hidden_shim,only_roots_up_base_may_be_0711,start_fresh_refuses_a_shim_that_appears). My harness subclassedEarlierSetupand so started the suite a second time. I stopped that duplicate by its exact PID and ran only my own tests in the same slot.j342-repro.txt): the bind-mount case now passes (Check saysunproven, Take over exits 75). The staging case stops at its own precondition, because the bare owner write it cuts after no longer exists. That is the fix.j349-only.txt,[no-rollback]rows). I killed the staging after Take over at each of its 10 steps and ran Check twice each time, as any server would. The old install was always whole in the set-aside and nowhere else. Check offered it back (SET_ASIDE taken_over) or reported the new attempt's journal (STALE/LOCKED). The original rollback plus restore still put it back exactly.failure-sweep.txt). The old install stayed whole in the set-aside at every step. Two steps leave a rollback the server can't finish: see P3-1.Uuid::new_v4(replacement_hostisNonefor a Re-enroll:replaced.then_some(host)), anduninstall_committedexits 0 unless.bootstrap-ownernames this attempt's id with no stage present.--bootstrap-uninstallchecks that again under the setup guard.Answers to the brief
mv -T "$base.bootstrap-new-$tx" "$base"is onerename(2)inside/var/lib, under the setup guard, onto a path just checked to be absent: atomic. A leftover temporary folder is removed only by its own transaction's rollback. A re-add, Start fresh, Re-enroll and Take over all leave one from another transaction in place (P3-2).fp.sh, output below)./var/lib/infraternal, and infraternal never mounts there (no mount calls in the code).systemd-journald.service.d/nice.conf) pass.service.d/10-timeout-abort.confcan't match*infraternal-agent.service.d/*. I had no Alma VM, so this one is read, not run.systemctl edit infraternal-agent.service, which is the intent.$base.bootstrap-new-*sits in root's 0755/var/lib, is made withmkdir -m 700, and its files are written underset -C.confirmstill checks the batch owner, compares the challenge, andtake()s it once.await_confirmationonly moved that code.new_v4per offer, and the host re-checks the record (owner plus rootdev:ino). The "changed since the confirmation" test passes as root.target.mode != Root) and on the host (base = up_root_base, else 71). Passes as root.P3s
failure-sweep.txt). Ifprintf "$kind" > "$stage/kind"fails, it leaves an emptykind, and the rollback exits 71 (kind=) although the journal beside it saysfresh. An empty$stage/leasemakesretire_setup_leaseexit 1. Then restore refuses (72) because a base is in place. The old install is safe in the set-aside, but it stays out of service with no way back inside the product. Both lines are unchanged from dev (install.rs:1577/1680on dev). Take over makes the result worse. Suggested fix: readkindonly when[ -s ], else use the journal's; treat an empty lease as no lease. A power cut during staging produces the same empty-file shape, and fails safe the same way.INFRATERNAL_SETUP_ABSENTbeside it, and nothing else removes it ([stale]rows inj349-only.txt). It appears after a cut betweenmkdirandmvwhose own rollback never runs (rows 1–3 of[no-rollback]:temp_left=True). Suggested fix: Check or staging removes, under the guard, any privatebootstrap-new-*of ours that holds only those two files.infraternal-.service.d/*.conftoinfraternal-agent.service, andDropInPathslists it, but the pattern*infraternal-agent.service.d/*doesn't match. I demonstrated it with a made-up user unit (prefix-dropin.txt): ExecStart was replaced by/bin/false, and the shipped pattern saidpass. The system unit's drop-ins are root-only, so this is J342's P3-3 class. Fix: also match*/infraternal-.service.d/*, or refuse any listed drop-in that isn't directly in a globalservice.d/.crates/plugins/remote/src/uninstall.rsremoves the unit and launcher (line 634) beforeremove_files, andvalidaterequires the unit. A retry then exits 71, and the next Check reportsunproven. The agent is already disabled at that point, so nothing runs. This is on dev already; Take over and Re-enroll now reach it too.committed()stays false,ROLLBACK_TRANSACTIONexits 0 (owner matches, no stage, no journal), the restore answersuntouched, and the host is revoked. Fresh installs on dev have the same gap. It can be recovered, because a re-add offers Take over of the revoked id. Fix: also calluninstall_committedwhen the commit's outcome is unknown. It is owner-guarded and does nothing while a stage exists.validate_upgrade_sessionsaccepts exactly sixmesh-*names. When the mesh plugin adds a state file, every real root install reads asunproven(Take over is never offered), and a commit-cut set-aside is kept, with the old key, until the next commit. It fails safe. Add a test that ties this list tonode_state.rs.Asked of me
J315's open question (84191): Re-enroll now uninstalls after a late failure (
597cdb6b3). I agree: it enrolls a new id, and its old install was deleted at the commit, so rolling it back would leave a running agent for a revoked id.Verdict: REQUEST CHANGES for P2-1 only. With
sync -finretire_set_asideand a test that it runs before the delete, I'd approve. P3-1 and P3-2 are cheap and worth taking in the same round. The rest can be follow-ups.J315: J349's re-review (84681) addressed.
addhost-takeover-d359heada8324274af6138bc6finstall.rs, through the sharedCOMMIT_RETIRE, andfinish_stuck_upgrade) now runssettle_set_asidefirst: when a set-aside exists, onesync -f "$base", and a failed sync refuses rather than delete. It runs before the journal is removed, not only before the set-aside: otherwise a power cut after the stage removal could leave a structurally finished base with an empty binary, and a later Check would delete the set-aside.retire_set_asidealso syncs before its own delete, so it is safe for any caller. A commit with nothing set aside pays nothing. This also closes the window for Re-enroll on dev.COMMIT_RETIREafter a Take over withsync/rmtraced. A sync comes before each delete, a failing sync deletes nothing (not even the journal), and a commit with nothing set aside doesn't sync. Red at the parent (a failed sync still deleted everything), green now (compute01:~claude/j315/{red,green}-durable.txt). J349's ext4 loop-image model needs root and a loop device, so it is referenced in the commit message (/root/j349/ev/powercut.txt), not ported.422dbb213infraternal-.service.dbeside the unit and any reported drop-in under it.ready), green now (unproven).{red,green}-prefix-dropin.txta8324274acommit_with_timeout.The whole oracle at
a8324274a, as uid 0 in a private mount namespace: 122 tests, and only J303's 5 documented root-precondition tests fail. Every D359 test passes, including the new ones (compute01:/root/j315/root/ev/oracle-root.txt,whoami.txt= 0, no scratch left).The same suite as
claudeon compute01 now gives 24 false failures, all exit 73 (busy). J347 is running infraternal shims asclauderight now, ininfraternal-session-*.scope. The J272 hidden-holder rule counts any hidden same-user process in an infraternal session scope as a holder of the install, whichever install it belongs to. That is a test-environment clash on a shared host, but the rule is broader than it should be, so I've added it to #423.J349's other P3s (staging write failures, the leftover temporary base, uninstall idempotency, an unknown commit outcome, mesh-list drift) are in #423 with J349's evidence. Not fixed here.
verify quickon dev-02: queued and not yet run. dev-02 has 39 GB free, under its 40 GB floor, so build-slot holds the job although the slot is free. None of that space is mine (my checkout is 550 MB and has no target). The space is other jobs' targets:infraternal-j317-gate/targetalone is 35 GB. I'll post the result here as soon as it runs. It comes after the previous run at422dbb213, which only failed on the missing doc comment now added ina8324274a.J315: final head on
addhost-takeover-d359:998f32716. It adds one commit toa8324274a: backticks forCOMMIT_RETIREin a doc comment. That doc comment, added ina8324274a, had failed clippy'sdoc_markdownon dev-02.verify quickon dev-02 at998f32716, verbatim (dev-02:~claude/j315-verify-quick.log; target deleted):Ready for your
verify fastand merge. The root oracle ata8324274a(see above) is still valid for this head: the only change since is that doc comment.