Remove leaves the infraternal agent running: uninstall never starts on the host #409
Open
opened 2026-11-06 19:39:31 +00:00 by kayg
·
20 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#409
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?
Found by J320 on the test server and confirmed by J325 on both
74b9c4d80(production) and37c93637e(#305 comment 82827).After Remove of an added rootless host:
The host journal shows the server dropping the infraternal agent connection about 0.3 s after the click, and the uninstall helper never starts on the host.
Logs, host-state snapshots and screenshots: compute01
~claude/j325/.J328 start
Branch
fix-remove-uninstall-409, base origin/devfba745051.Plan (about 3–4 h):
request_uninstall'scontrol_callreturns not-accepted(→unreachable) right away. First suspect:infraternal-agent/src/plugins.rsregistersUninstallPluginonlyif let Ok(..)of a full artifact validation at agent start, and silently skips it on failure. That makes UNINSTALL an unknown action, with nothing logged. (~45 min)J328 finding 1: root cause. The server never sends the uninstall (regression from #285,
6ca0c6a74, 2026-11-02)EmbeddedAgent::control_callhands each control action to the host actor. The actor (crates/infraternal-server/src/embedded.rs,Command::Control) checks a per-action allow-list before anything goes on the wire:Before
6ca0c6a74("schedule Mesh and host key rotation", #285), one check covered every control call, the uninstall included. That commit replaced it with this list and leftremote.host.uninstallout. Since then every Remove goes like this:request_uninstallgetsUnsupportedat once, without touching the wire. That is why the infraternal agent's journal shows nothing.host_cleanup='unreachable', so the toast says "infraternal is still installed… Copy the command".BrokenPipe. The cut is correct (D352). It only looked like the cause.terminal.login.revoke, and the receipt drops the entry. The install stays, and re-add hits.bootstrap-owner(exit 71).Why no gate caught it: the real-binary removal check (
bootstrap_breakit2_tests.rs,BREAKIT2_REMOVE) calls this samerequest_uninstallpath, but only when that environment variable is set, and the gate doesn't set it.Repro on unfixed
37c93637e(J325's release build, throwaway serverhttps://localhost:45328, rootlessj328t@127.0.0.1:2328behind a throwaway sshd, own software passkey under~claude/j328/priv; logs in compute01~claude/j328/logs/repro37c-*):A correction to the issue text: the server entry does retire once the cleanup receipt arrives (
confirmed=1,revoked_atset). The stuck "removal pending" was not a server outbox failure in this run.Fix (test first): the allow-list gets an explicit arm for
remote.host.uninstallthat requires only a remote link. Every infraternal agent build answers a stream-0 plugin call, by running its uninstall handler or by refusing. Red testremote_uninstall_delivery_tests.rs: real TLS, production router, a real enrolled certificate, and the agent side played by hand. It asserts that the server writes the uninstall call on stream 0, with this host's id anddry_run:false, and that anacceptedreply comes back to the removal asrequested. Red/green run queued on compute01 (slot queue position ~19).J328 finding 2: the removal flow has three overlapping paths, and none owns the uninstall end to end
remote_removal::publishremote.host.uninstalldisable --nowon the agent. The receipt arrives only on the agent's next (cleanup-only) reconnect after the cut. If the helper wins, nothing ever confirms, and the entry stays "removal pending" forever.remote_login_cleanup→ cleanup-only reconnect →deliver_login_cleanup)terminal.login.revokeonly.bootstrap-owner.--bootstrap-uninstall <id>Proposal: one path, the outbox
Remove commits intent only. In one transaction: the cleanup row, plus the removal row with
uninstallandhost_cleanup='pending'. No network call under the writer lock. The commit cuts the link, as today (D352).The outbox delivers everything, in order, on the cleanup-only reconnect. For a just-cut online host that is the immediate reconnect; for an offline host it is its return:
terminal.login.revoke, which yields the durable D352 receipt;remote.host.uninstall;host_cleanup(requested/refused) and confirms the receipt, which drops the entry.A reply lost before step 3 confirms nothing, so the next reconnect retries. Because the server confirms right after the agent accepts, the helper stopping the agent can no longer strand the entry.
The cleanup link's write filter also lets the one uninstall call through. It still passes no terminal, plugin or native-stream frames.
The toast says the host "uninstalls itself as soon as it connects", and it always carries the manual command as a fallback, for a host that never comes back.
Legacy non-UUID owners (test and legacy ledgers only; real accounts are UUIDs, as my repro's
remote_login_cleanuprow shows) have no outbox. They keep the one immediate attempt.What we lose: the toast can no longer list, at click time, the kernel objects the host leaves because its install did not create them (#235's "infraternal0 stays…" line). The host still leaves them in place; only that one line goes. If the owner wants it back, it needs a column on
remote_host_removalsand a UI read, as a follow-up.I'm implementing this on
fix-remove-uninstall-409as separate commits after the one-line root-cause fix. So the fix alone can still be taken if the owner prefers the smaller change.J328 progress (21:20 CET)
Pushed
fix-remove-uninstall-409ate7647ead8(rebased on origin/devfba745051):7497a582cfix(remote): send Remove's uninstall request to the host. The allow-list arm, plus the red-first delivery testremote_uninstall_delivery_tests.rs.0d8ce8de0refactor(remote): the removal outbox alone asks a host to uninstall (the single path proposed above). Tests inremote_removal_outbox_tests.rs: receipt → uninstall → one confirming transaction; a lost answer is asked again on the next reconnect; a refusal retires the entry withunreachable.e7647ead8fix(tracer): a pending uninstall is not a failure toast.Waiting for a compute01 build slot (queue position 19 since 20:47). One slot run does: two red proofs (each fix reverse-applied on the final tree), green,
verify quick, then the release build for the end-to-end runs. Script~claude/j328/red.sh, log~claude/j328/logs/red.log, target~claude/.cache/codex-scratch/j328/target.Next: end-to-end runs on the release build. Rootless: throwaway account
j328t+ sshd under/root/j328. Root: a throwaway AlmaLinux qemu VM, because compute01's own/var/lib/infraternalbelongs to another job. Repro evidence:~claude/j328/logs/repro37c-*.J328 paused for compute01 maintenance (02:15–02:45 IST)
build-slot admission is frozen for the VM resize. My queued slot run (
~claude/j328/red.sh: red proofs, green,verify quick, release build) never started: it had waited since 02:17 IST and was at queue position ~3. I stopped it by PID before the restart, so it was cut by the maintenance, not by a failure. I also powered off my throwaway root VM (/root/j328/vm) cleanly. The rootless test account is already removed, and I hold no mesh claim.Code is pushed at
e7647ead8. Once "compute01 back, 5 slots" is posted on #402, I'll resubmit the same slot run once, in the foreground, then run the end-to-end runs (rootless, then root VM).J328 progress (04:45 IST)
compute01 is back. My resubmitted slot run hit two harness mistakes of mine, each found in the slot's first minutes, then stopped by PID and fixed:
--features remote(the workspace gate gets it through feature unification): 0 tests ran;#[path]inside an inlinemod testsneeded../../: it did not compile.Head is now
b627895ae(7497a582cfix,1d8bd2917outbox,b627895aetoast). I also tightenedconfirm_removal_cleanup: itshost_cleanupupdate now requires this person's unconfirmed cleanup, like the receipt statements beside it. Waiting in the slot queue (position ~19). Logs:~claude/j328/logs/red.log, and the earlier attempts inred-*.log.J328 progress (06:30 IST)
~claude/j328/release-b627895ae. The final head14b4d118ddiffers from it only in test-only code and formatting.~claude/j328/tests.sh: red1, red2, green,verify quickat14b4d118d) is queued at position ~25. The earlier slot run's tests did not compile because of the test module's#[path](fixed). That run's log is~claude/j328/logs/red.log.~claude/mesh-vm.claimhas saidJ197 2026-11-06T22:41:10Zfor about 2 h, and no J197 process is running. I'm waiting, not removing it. Orchestrator: please check whether it is stale. The end-to-end runs (rootless, then the root VM) start as soon as I hold it.J328: red/green proven; gate queued but blocked on compute01 disk
Red/green at
c98b7cdaa(log: compute01~claude/j328/logs/tests.log):embedded.rsallow-list arm reverse-applied:uninstall_delivery::removal_uninstall_reaches_the_connected_agent ... FAILED, panicked atremote_uninstall_delivery_tests.rs:109("the server never sent the uninstall request to the connected agent"). That is the production bug.remote_runtime.rsreverse-applied: all 3removal_outbox_testsFAILED atremote_removal_outbox_tests.rs:194("the server sent no control call" after the receipt).uninstall removal cleanup revocation revokefilters,--features remote):127 passed; 1 failed. The failure was my new wording test, and it found a real copy bug, see below.Copy bug found (it exists today too): for a rootless host, the leftover-install messages said "remove it there as the account it was added with with: ". It is fixed in its own commit,
e20c06ec0, with a test covering both rootless messages. The new pending message uses the same "… by running: " form.verify quickatc98b7cdaa: fmt, doc coverage, D351 shape lints, doc comments on diff, fixed test waits, and workspace clippy all PASS. Unit tests (changed) failed only on that wording test. Log:~claude/j328/logs/verify-quick.log.Head
e20c06ec0(on origin/devfba745051, unchanged):aa564b116fix(remote): send Remove's uninstall request to the host0cfeac439refactor(remote): the removal outbox alone asks a host to uninstall7e96ea09bfix(tracer): a pending uninstall is not a failure toaste20c06ec0fix(remote): a rootless leftover's command says "added with" onceBlocked, orchestrator:
verify fastis queued (~claude/j328/gate.sh), but build-slot waits: compute01 has 21 GB free (546/591 G used; below the 25 G floor). My own use is an 11 G target that the gate needs, plus 0.5 G (the throwaway VM disk).J197 2026-11-06T22:41:10Z, so the end-to-end runs can't start.J328: first full gate at
e20c06ec0, and a fix for a test that failed under loadverify fastate20c06ec0(compute01, log~claude/j328/logs/gate-e20c06ec0.log): every row PASS except:954 passed; 1 failed. The failure was mylost_uninstall_answer_is_asked_again_on_the_next_reconnect, panicked atremote_removal_outbox_tests.rs:194:56. Under full-suite load, the server's first cleanup task had not yet seen the dropped link, so the per-host cleanup lane refused the test's reconnect (WouldBlock). The test assumed a fixed 300 ms sleep was enough. Fixed: the test now reconnects the way a real infraternal agent does, retrying until the server asks for the receipt, with no fixed sleep. Product behaviour is unchanged: a real agent retries its reconnect.No space left on devicewhile building musl OpenSSL, because compute01's disk filled up during the run. Environmental. I dropped my own 15 G incremental cache to help: free space went from 7.7 G to 27 G.New head
2db5024f4:aa564b116fix,5ede0f539outbox,83bd06181toast,2db5024f4wording. The final gate is queued. Mesh claim stillJ197 2026-11-06T22:41:10Z, so the end-to-end runs are still waiting on it.J328
verify fastat2db5024f4(compute01; log~claude/j328/logs/gate-2db5024f4.log)Every row passes except test, and the only failing suite is
crates/plugins/ssh/tests/install.rs:Not from this change:
crates/infraternal-serverandapps/tracer, andplugins/sshdoes not depend oninfraternal-server. The failing test binary is byte-identical between my two gate runs:target/debug/deps/install-a8f6df12f81dec5d.e20c06ec0:test result: ok. 105 passed; 0 failed; 2 ignored … finished in 14.19s(gate-e20c06ec0.log). This time it took 304.50 s, more than 20× slower, and the failures are setup-lock refusals (exit 73, "Setup is already running on this host"). That is host load during this run (5 slots, disk churn), not code.uninstall_delivery,removal_outbox_tests,pending_uninstall_says…androotless_manual_wording_reads_onceis among the 150test result: oklines.static buildsnow passes; the earlier ENOSPC was environmental.Still to do: the end-to-end runs. They are blocked on the compute01 mesh claim, which has said
J197 2026-11-06T22:41:10Zfor about 8 h.J328 end-to-end, first pass: rootless passes all four steps; root found two more problems
Release build of
b627895ae(~claude/j328/release-b627895ae), throwaway serverhttps://localhost:45328, own software passkey, mesh claim held.Rootless
j328t@127.0.0.1:2328: PASSLog
~claude/j328/logs/fix-rootless-scenario.log, host snapshots infix-rootless-state-*.txt, screenshots in~claude/j328/shots/fix-rootless-*.Host journal: the outbox in action, online, then the offline host's return.
Root
root@127.0.0.1:2338(throwaway AlmaLinux 10 qemu VM): two problemsavc: denied { execute } … scontext=init_t tcontext=var_lib_t name="payload", so systemd may not exec/var/lib/infraternal/bin/current. This is unrelated to #409. For this proof I set only the throwaway VM to permissive.host_cleanup='requested',confirmed=1, and the helper unit started at the click second. But the helper itself exited 1. Run by hand:infraternal was not removed: Connection refused (os error 111). The login revocation, which the outbox correctly delivers first (D352), kills a root login's unit outright (infraternal-login-…service: code=killed, status=9/KILL, D354). Its shim never removessessions/sessions/<id>.sock, and the helper refused the whole uninstall over that one stale socket. Rootless shims are retired through the shim protocol and clean up after themselves, so rootless never hit it.Fix
fae08b029: a refused or vanished session socket counts as retired, because a shim listens for its whole life, and the helper removes the stale socket. It removes only a real socket owned by the installation's uid, since Linux also answers ECONNREFUSED for a regular file. Tests: stale socket → retired and removed; a regular file at that path → still refused and kept.On the redesign rule: this is a new hole, but in a different mechanism (the helper's tolerance of already-revoked sessions), not another hole in the delivery path. The fix closes the whole class: any already-dead session, whoever killed it.
Next: red/green for
fae08b029, a new release build, a root rerun (fresh VM overlay), then the final gate.J328 progress (15:10 IST)
Helper fix
fae08b029, red/green (compute01~claude/j328/logs/run3.log; the plugin's tests need--features agent,tokio/rt-multi-threadwhen run on their own):retire_shimconnect handling reverted:a_stale_session_socket_counts_as_retired_and_is_removed ... FAILED(panicked atuninstall.rs:1083, "a stale socket refused the uninstall"). The regular-file guard passes.test result: ok. 24 passed; 0 failed(uninstall,retirefilters).Release build of
fae08b029is ready:~claude/j328/release-fae08b029. A fresh root VM is booted and set permissive (#417). The root end-to-end rerun is waiting on the mesh claim (J207 2026-11-07T06:55:47Z). After it come a rootless rerun on the same binary and the finalverify fast.J328 end-to-end proof on the final build: rootless and root both PASS
Release build of
fae08b029(~claude/j328/release-fae08b029), throwaway serverhttps://localhost:45328, own software passkey, mesh claim held 11:23–11:29 UTC. Every phase runs through the UI: A add + shell, then Remove while online; B re-add; C stop the infraternal agent, Remove while offline, start it again; D re-add.Root
root@127.0.0.1:2338: throwaway AlmaLinux 10 VM (SELinux set permissive only because of #417)Log
~claude/j328/logs/fix2-root-scenario.log, snapshotsfix2-root-state-*.txt.VM journal: the stale-socket case that
fae08b029fixes, now passing.Rootless
j328t@127.0.0.1:2328, same buildLog
~claude/j328/logs/fix2-rootless-scenario.log.Toast (production build, 1440×900)
The pending uninstall is no longer an error: it shows the neutral toast with Copy command as the fallback. Screenshot attached (
fix2-rootless-C-remove-offline-toast.png). The root run's toast screenshot caught a later frame without the toast; its text is in the log above. Not mine: the details panel in the background shows the server's own machine, J325's finding A.Cleanup: tunnel, root helper, test account, sshd and VM are stopped or removed; the claim is released. The final
verify fastatfae08b029is running.J328: merged origin/dev; a constraint for D362's implementation; one more load flake
Merged origin/dev (37 commits,
21b3c84ce, clean merge) into the branch: head is nowd2472d8c3. The finalverify fastis re-queued on that head. I stopped the run on the pre-merge head; its log is~claude/j328/logs/gate-fae08b029-premerge.log.Load flake in that pre-merge run:
crates/plugins/ssh/tests/previous_attempt_shell.rs→ownership shell oracle failed … AssertionError: 73 != 0(setup lock busy, exit 73). Its test binaryprevious_attempt_shell-3b7caef3262a2e08is byte-identical to the one that passed in mye20c06ec0gate (test result: ok. 2 passed). The earlier run before it died ofNo space left on devicemid-compile (gate-fae08b029-enospc.log).For whoever implements D362 (sole-holder revoke ends every process of the account; not in code yet): in the removal outbox, the login revocation comes first (D352 receipt), then the uninstall request. On a rootless host the infraternal agent runs as that account. If D362's "end every remaining process" includes the agent, the agent dies at step 1, and a sole-holder rootless removal never receives its uninstall request. The install would stay, and so would the same re-add failure as #409. Two options:
My recommendation is (a): it keeps one ordering for every revoke. The owner decides.
J328 finished: branch
fix-remove-uninstall-409, headbaade609a(0 behind origin/dev6b1bdb81b). Not merged.Root cause (#409)
Since #285 (
6ca0c6a74), the host actor's per-action control allow-list held only thetransport.*certificate actions. Soremote.host.uninstallgotUnsupportedinside the server and was never sent. Remove recordedunreachable, committed the revocation (which cut the link: the ~0.3 sBrokenPipe), and left the infraternal agent running. Re-add then failed on.bootstrap-owner.Commits
aa564b1165ede0f53983bd061812db5024f4fae08b02999ae8b1e6d2472d8c3,baade609aRed/green
uninstall_delivery … FAILEDatremote_uninstall_delivery_tests.rs:109("never sent the uninstall request").removal_outbox_testsFAILED at:194("the server sent no control call").a_stale_session_socket_counts_as_retired_and_is_removed … FAILED.baade609a:removal_outbox3/3 passes in parallel, 3 runs in a row; filtersuninstall removal cleanup revoke revocation:test result: ok. 129 passed; 0 failed; 5 ignored; plugin-remoteuninstall retire:24 passed; 0 failed.Real end-to-end (comments above): both PASS on the final build
Gate:
scripts/verify fastatbaade609a, compute01 (~claude/j328/logs/final.log), verbatimThe only failing row,
test, is not this change. Its one failing suite wascrates/plugins/ssh/tests/install.rs(98 passed; 9 failed … 310.70s): 7× "Setup is already running" / exit 73, plusan_owner_record_for_another_host_is_set_up_elsewhere(UnprovenFiles≠UnrecognizedInstall). Re-run through build-slot (~claude/j328/logs/devcheck.log), that test passes on unmodified origin/dev and onbaade609a:When it failed, compute01 had another job's real root install at
/var/lib/infraternal; it is gone now. The exit-73 failures are setup-lock contention on a box running 5 gates at load ~36: the same suite passed105/105in 14 s in mye20c06ec0gate. My earlier gate runs also hitagent_daemonandinfraternal-agentquota tests under load, which pass 3/3 alone (logsgate-*.log).Static sizes (report only, D243): x86_64
27821808bytes, aarch6425024200bytes. The gate's printed delta is against its fixed old baseline. I did not build pristine dev statically for a per-branch delta.For kayg to decide
5ede0f539): the uninstall now goes only through the outbox. The one thing lost is the click-time line naming kernel objects the host leaves ("infraternal0 stays because…"). The host still leaves them, unreported. If you would rather take only the one-line fix,aa564b116stands alone; but then an offline host is never uninstalled, and the receipt race remains.init_tmay not execvar_lib_t).Cleanup: target dirs deleted; throwaway account, sshd, VM disks and tunnel removed; no claim held. Logs and scripts kept in compute01
~claude/j328/.J351 independent correctness/security review starts. Review branch review-J351 pinned to fix-remove-uninstall-409 head
baade609ae; base origin/dev6b1bdb81b3. Plan (~75 minutes): read decisions and #285/#409; inspect identity binding, outbox ordering, caller authorization and socket cleanup; run focused removal/cleanup/revocation tests on dev-02 through checked build-slot launcher; attempt isolated rootless end-to-end if practical; post APPROVE or REQUEST CHANGES with evidence. Per common brief reviewer rule, read author gate receipt rather than rerunning quick/fast. No merge, deploy or push to dev.J351 finding P3: the pending-uninstall toast is not neutral yet.
host-removal.on-demand.ts:59omits tone when asking, butpackages/ui/src/toast.ts:375-382defaults that tookand the success tick unless aniconis supplied. The production screenshot attached to comment 84420 visibly shows a green check while uninstall is still pending: https://git.kayg.org/attachments/d46fed6d-bb2e-4ce4-a901-1a4b06dc8d33 . TASTE ("The icon matches the news") calls for an info mark for neutral news. Keep the Copy command action and set the existing shared neutral/info icon on the asking branch. The requested copy and the "with with" repair are otherwise present. Nonblocking polish finding; correctness/security review and focused dev-02 tests continue.J351 P2 finding (the trade-off J328 disclosed in 82885/84890): the outbox discards the accepted uninstall response's
leftlist inremote_runtime.rs:132, so a normal UUID-owner removal no longer reports deliberately retained kernel objects. Previouslyrequest_uninstallpassed this list tooutcome, which printed the allow-listed object names. This is an observability regression, not an ownership/deletion escape: the helper still preserves objects it did not create. Keep the outbox ordering, but retain and expose the allow-listedleftreport asynchronously rather than choosing between a safe outbox and the report. Nonblocking under D206; explicitly recorded so the loss is not mistaken for feature parity with the one-line fix. Also updatecontracts/remote.md:483and its finish-result description to match the asynchronous outbox andpendingcleanup state.J351 independent review: APPROVE
Reviewed
fix-remove-uninstall-409atbaade609ae, againstorigin/dev6b1bdb81b3. No P0/P1 findings. P2/P3 below are nonblocking under D206; this is a code-review approval, not a claim that the merge gate is green.Findings
remote_runtime.rsmatchesAccepted(_), dropping the allow-listed names that the former immediate path passed intooutcome. The helper still preserves unrelated objects; the loss is the owner's report. Retain/expose that result asynchronously. Finding comment 84925. The contract atcontracts/remote.md:483also still describes a network handoff inside the writer transaction and omits the newpendingcleanup state; reconcile it with the chosen flow.ok; it does not choose the shared toast's neutral icon. Set its existing info icon for the pending branch. The production screenshot on comment 84420 visibly reproduces it; finding comment 84915. Copy command is present, and both rootless “with with” messages are repaired.Correctness and security
Keep the outbox, rather than taking only
aa564b116. The one-line fix restores online delivery but still leaves offline hosts installed and lets the uninstall stop the daemon before its next-connection revocation receipt. The redesign uses the existing durable tables and cleanup admission, avoids network work under SQLite's writer lock for real UUID accounts, and implements D134/D352 more completely. The non-UUID legacy/test path remains immediate, so “one path” has that explicit exception.The order is revoke → durable daemon reply → uninstall → one SQLite transaction recording acceptance/refusal, confirming the receipt and retiring the entry. Before the receipt, or after it with no uninstall answer, nothing advances and a surviving installation retries on reconnect. Reconstruction of a fresh server runtime reads the same retained SQLite intent. Duplicate confirmation cannot change an already-recorded acceptance. SQLite rolls back an interrupted confirming transaction. After commit the exact old certificate no longer obtains cleanup admission.
Important limit: a successfully written acceptance can arm the helper even if the server never receives it or crashes before committing. If the helper then completes, that old installation cannot reconnect to finish the ledger, and the entry can remain pending. Do not claim unconditional completion after every lost reply. This does not authorize new work or threaten a replacement installation; D355's subsequent no-install/Start-fresh proof provides reconciliation.
requestedis acceptance, not proof that the helper completed. Expired/individually revoked certificates also remain refused by existing cleanup admission; this patch does not add indefinite offline recovery.A stale uninstall cannot target a fresh installation through this path. Admission binds the exact certificate fingerprint and proof to the host UUID;
authenticatechecks the proof's host matches admitted authority. Removal records are keyed by that UUID, not address. Bootstrap explicitly excludes removed identities from replacement/re-enrollment as that same identity (bootstrap_remote::check_stuck_install); fresh installs receive a new UUID. The uninstall handler compares the request against its local context and validates.bootstrap-owner. The independent helper takes the shared setup lock and revalidates that same identity before stopping anything. Old manual commands are bound the same way. My added test sends an old control request to a different installation and verifies refusal, intact artifacts and no armed plan.Caller scope is correct.
control_callis crate-private, and production uninstall calls originate in removal. UNINSTALL is absent from the server/browser descriptor and hasapi_scope: None; widening the actor's wire allow-list does not make raw uninstall key-callable. A properly scoped key can still request the separately authorized, dry-run-bound Remove action, as D333 explicitly requires. Cleanup links are private actors, unpublished to normal routing, and their write filter admits only stream-0 liveness, revoke and uninstall.fae08b029is safe within the installation's private session directory. Cleanup first validates the installation and the private, same-UID sessions directory. On a refused/missing endpoint it usessymlink_metadata, refuses non-sockets and foreign owners, and unlinks only the local socket entry. A regular file, symlink or foreign-UID socket is preserved. This does not delete a socket merely because its filename resembles ours elsewhere. Existing root E2E evidence shows D354's killed login unit leaving precisely this stale socket. D353/D356 revocation still precedes uninstall; J328's D362 constraint remains real: future sole-holder process sweeping must allow this installation to complete its uninstall handoff.Independent validation and artifacts
Launcher checked outside the slot:
nix develop ~/worktrees/J351 -c cargo --version→cargo 1.99.0 (5f94df478 2026-08-27). Ran on dev-02, CARGO_INCREMENTAL=0, one target, -j3, through~/infraternal-ops/build-slot nice -n10 nix develop ~/worktrees/J351 -c bash .../run-focused.sh:Attached
focused.logcontains the full unedited output. Five added independent tests cover pre-receipt disconnect, new-runtime reconstruction plus duplicate confirmation, stale installation identity, foreign UID and symlink guards. The reconstruction test uses a new EnrollmentStore/runtime over the same SQLite pool; it is not a process-kill E2E. Review tests only, pushed onreview-J351at1edb120458; attached patch. The five ignored tests require configured release hosts, exclusive ports or scale/measurement environments; they were not silently counted as passes.A fresh real rootless E2E was not rerun: compute01 had all five slots occupied and four queued jobs (attached capacity receipt), and recreating the root account/sshd/browser fixture was not practical for this review. I read the actual
~claude/j328/logs/fix2-rootless-scenario.logandfix2-root-scenario.log, not only the finish comment: both record successful online removal, re-add, offline return cleanup and second re-add. Those are J328'sfae08b029release receipts, not a freshbaade609aE2E claim. The affected removal/uninstall/toast production files are unchanged between that release and the reviewed head. Screenshot: https://git.kayg.org/attachments/d46fed6d-bb2e-4ce4-a901-1a4b06dc8d33 .Author's gate, verified from the actual log
Per the common brief's explicit reviewer rule, I did not rerun quick/fast.
~claude/j328/logs/final.logatbaade609aends with the following verbatim table:It is a failed gate, not a green one. The failure is the SSH install suite (98 passed, 9 failed), with setup-lock refusals and the owner-record classification case. I also read
devcheck.log: that classification test passes on both pristine origin/dev andbaade609a. The remaining contention attribution is the author's evidence, not an independent full-suite baseline rerun here. The orchestrator still needs the merge gate to pass.Evidence retained locally under
/home/kayg/.cache/codex-scratch/J351/evidence/and on dev-02 at/home/claude/.cache/codex-scratch/J351/evidence/focused.log. Own dev-02 target deleted; no own processes, accounts, units, servers or mesh claim remain. No merge or deployment; no push to dev.Verdict: APPROVE. Reviewed head:
baade609ae.Verdict: APPROVE; head:
baade609ae; comment id: 84927.J328: J351's P2 and P3 done; head
863c810d1(gate green)Branch
fix-remove-uninstall-409, on top of the approvedbaade609a; 42 commits behind origin/dev6ffe37e06. Not merged. One commit each:091751101ICON_INFO, the "Not saved" mark) instead of falling back to the defaultoktick. The spec gains a case at 1440 and 393:data-icon="true", not the error tone, the info glyph's path, the tick marker hidden, and Copy command present.863c810d120261107173825_removal_retained_objectsaddsremote_host_removals.retained(a JSON array of server-known names; oneknown_left()filter covers the live reply and the stored copy). It is written in the same transaction as the answer and the receipt. When the host was connected at the click, Remove waits up to 5 s after its commit (outside the writer lock) for that transaction, then answersremovedwith "…Left infraternal0 because infraternal didn't create it here." andleftfor scripts. The CLI prints it; a new CLI test covers human and machine output. A slower or offline host gets the pending answer at once.contracts/remote.mdnow describes the outbox flow, and thatrequestedmeans accepted, not finished (J351's note).Real environment (release build of
863c810d1, throwaway server, mesh claim held and released)dummyinfraternal0was created after add, so the install did not own it. Log:~claude/j328/logs/p2-root-scenario.log.~claude/j328/logs/p2-rootless-scenario.log.pending-removal-command.spec.ts, compute01,~claude/j328/logs/spec.log):4 passed (1.2m). The 393 and 1440 neutral screenshots and the root result are attached.Tests
On dev-02, outbox tests in parallel ×3:
4 passed; 0 failedeach time (including the newan_unanswered_removal_wait_ends_without_an_outcome). The accept test also checks that onlyinfraternal0is stored from["infraternal0","run me"]and that a concurrent waiter receives it. Filtersuninstall removal cleanup revoke revocation:130 passed; 0 failed; 5 ignored(~claude/j328/t2.logon dev-02).verify quick, dev-02 (aarch64),863c810d1(The two earlier quick runs found a clippy
needless_pass_by_valueinknown_leftand a CLI test that didn't allow for line wrapping. Both were fixed in the P2 commit before this run.)verify fast, compute01,863c810d1(~claude/j328/logs/gate-863c810d1.log), verbatimThe SSH install suite passed this time, so no reruns were needed. Static sizes (report only): x86_64 27832144 B, aarch64 25034312 B, against 27821808 / 25024200 at
baade609a. That is +10336 B / +10112 B for P2+P3.Cleanup: compute01 and dev-02 targets deleted; VM disks, throwaway account, sshd and tunnel removed; no claim held.