Rootless server: accurate refusal toast, no doomed auto-open; palette unlock row after setup (J142, from #326) #327

Open
opened 2026-11-03 13:33:00 +00:00 by kayg · 4 comments
Owner

Follow-up to #326 (J141 findings, comment 76969). Two product bugs seen on the rootless test server. Both sit within existing decisions.

Bug 1. Every first canvas load on a rootless server shows "This terminal request is not valid. Try opening a new window." The console view sends ssh.session.open{}; the rootless infraternal agent cannot log in as the person's account, maps PermissionDenied to OpenRefusal::Invalid (crates/infraternal-agent/src/daemon.rs:291), and the client words invalid as "not valid".
Plan: give the refusal its true reason through the protocol, show an accurate short message with the next step (add a host), and do not auto-open a session that is known to fail. Root servers unchanged. A failing test first.

Bug 2. Right after first-run setup the palette has no "Unlock host connections" row although /remote/ca/status says locked:true; it appears after a reload. Plan: find why the authority state is not refreshed after setup and fix it. A failing browser spec first.

Branch rootless-toast-j142, base bcca3054f (origin/dev).

Follow-up to #326 (J141 findings, comment 76969). Two product bugs seen on the rootless test server. Both sit within existing decisions. **Bug 1.** Every first canvas load on a rootless server shows "This terminal request is not valid. Try opening a new window." The console view sends `ssh.session.open{}`; the rootless infraternal agent cannot log in as the person's account, maps `PermissionDenied` to `OpenRefusal::Invalid` (`crates/infraternal-agent/src/daemon.rs:291`), and the client words `invalid` as "not valid". Plan: give the refusal its true reason through the protocol, show an accurate short message with the next step (add a host), and do not auto-open a session that is known to fail. Root servers unchanged. A failing test first. **Bug 2.** Right after first-run setup the palette has no "Unlock host connections" row although `/remote/ca/status` says `locked:true`; it appears after a reload. Plan: find why the authority state is not refreshed after setup and fix it. A failing browser spec first. Branch `rootless-toast-j142`, base `bcca3054f` (origin/dev).
Author
Owner

J142 start. Branch rootless-toast-j142, base bcca3054f (origin/dev).

Plan (estimate ~3 h):

  1. Reproduce both bugs on a rootless local server on compute01 (--remote-listen, --remote-ca-dir), never prod.
  2. Bug 1: new typed refusal reasons (permission, no_login) in OpenRefusal (infraternal-plugin-api), mapped in daemon.rs from logins::account errors; client wording in terminal-refusal.ts; skip the doomed auto-open when the server can tell. Rust test and TS test first, red then green.
  3. Bug 2: find why authority.refresh() is not run after first-run setup (control created once, first check 1.2 s after creation, product.ts:188); Playwright spec first.

Findings so far:

  • OpenRefusal::Invalid is the only reason that covers both bad input and permission (daemon.rs:291), so the client cannot tell them apart. The enum is deny-unknown for reasons on the client side (refusalReason, connection.ts:919), so the new reasons are additive on the wire.
J142 start. Branch `rootless-toast-j142`, base `bcca3054f` (origin/dev). Plan (estimate ~3 h): 1. Reproduce both bugs on a rootless local server on compute01 (`--remote-listen`, `--remote-ca-dir`), never prod. 2. Bug 1: new typed refusal reasons (`permission`, `no_login`) in `OpenRefusal` (`infraternal-plugin-api`), mapped in `daemon.rs` from `logins::account` errors; client wording in `terminal-refusal.ts`; skip the doomed auto-open when the server can tell. Rust test and TS test first, red then green. 3. Bug 2: find why `authority.refresh()` is not run after first-run setup (control created once, first check 1.2 s after creation, `product.ts:188`); Playwright spec first. Findings so far: - `OpenRefusal::Invalid` is the only reason that covers both bad input and permission (`daemon.rs:291`), so the client cannot tell them apart. The enum is `deny-unknown` for reasons on the client side (`refusalReason`, `connection.ts:919`), so the new reasons are additive on the wire.
Author
Owner

Progress (branch rootless-toast-j142, WIP head be6e5d289, pushed).

Bug 1, red then green written.

  • Red (compute01, non-root claude), verbatim:
test daemon::refusal_tests::io_kinds_map_to_their_reason ... FAILED   (left: Invalid, right: Permission)
test daemon::refusal_tests::rootless_host_names_why_it_cannot_log_in ... FAILED   (left: Invalid, right: NoLogin)
(fail) a rootless server says why it cannot open a shell and what to do (#327)   (got "The server could not open this terminal. Try again.")
  • Cause: daemon.rs spawn_open folded every PermissionDenied into OpenRefusal::Invalid. Two refusals are rootless facts (logins::account: person has no account here, or the account is not this infraternal agent's own). They now carry a typed RootlessRefusal and map to new wire reasons no_login / permission. Every other PermissionDenied (revocation, worker owned by another account) still maps to invalid, so root servers answer exactly as before.
  • Client: terminal-refusal.ts words both, with the next step (add a host). The reasons are additive on the wire.

Bug 2 hypothesis. Existing real specs (mobile-lease, restart-fixture) dispatch visibilitychange by hand before the Hosts "Unlock" button shows, which is the page's only refresh trigger for the authority state. A real-server red spec is in apps/tracer/real/authority-after-setup.spec.ts. Building the musl binary and running it on compute01 now; logs in ~claude/j142-out/.

Open point for kayg (will repeat in the final report): "do not open a doomed session on first load when the server can tell". The server can only tell by trying: the person's account preference is per person and per host in the database, and the agent owns the account table. A server-side prediction would mean a new protocol call and duplicating that logic. I am keeping the first open as the single probe and making its refusal accurate. Say if you want the prediction built.

Progress (branch `rootless-toast-j142`, WIP head be6e5d289, pushed). **Bug 1, red then green written.** - Red (compute01, non-root `claude`), verbatim: ``` test daemon::refusal_tests::io_kinds_map_to_their_reason ... FAILED (left: Invalid, right: Permission) test daemon::refusal_tests::rootless_host_names_why_it_cannot_log_in ... FAILED (left: Invalid, right: NoLogin) (fail) a rootless server says why it cannot open a shell and what to do (#327) (got "The server could not open this terminal. Try again.") ``` - Cause: `daemon.rs` `spawn_open` folded every `PermissionDenied` into `OpenRefusal::Invalid`. Two refusals are rootless facts (`logins::account`: person has no account here, or the account is not this infraternal agent's own). They now carry a typed `RootlessRefusal` and map to new wire reasons `no_login` / `permission`. Every other `PermissionDenied` (revocation, worker owned by another account) still maps to `invalid`, so root servers answer exactly as before. - Client: `terminal-refusal.ts` words both, with the next step (add a host). The reasons are additive on the wire. **Bug 2 hypothesis.** Existing real specs (`mobile-lease`, `restart-fixture`) dispatch `visibilitychange` by hand before the Hosts "Unlock" button shows, which is the page's only refresh trigger for the authority state. A real-server red spec is in `apps/tracer/real/authority-after-setup.spec.ts`. Building the musl binary and running it on compute01 now; logs in `~claude/j142-out/`. **Open point for kayg (will repeat in the final report):** "do not open a doomed session on first load when the server can tell". The server can only tell by trying: the person's account preference is per person and per host in the database, and the agent owns the account table. A server-side prediction would mean a new protocol call and duplicating that logic. I am keeping the first open as the single probe and making its refusal accurate. Say if you want the prediction built.
Author
Owner

J142 interim (branch rootless-toast-j142, head 933e01552, 4 atomic commits on origin/dev 3cc63cb09; gate and real specs running on compute01).

Bug 1: done, red then green. Typed rootless refusals no_login / permission (agent), accurate wording in the client, toast duration follows message length. Rust and TS tests red first (log ~claude/j142-out/red1-bug1-final.log), green after. Real-server proof, desktop passes; phone failed once because the 4.2 s toast expired, fixed by the length-based duration.

Harness (from J121c, #317): fixed in the harness, the product is right. The fixture runs server and agent as the VM user; specs signed up under made-up names, which are no account on a rootless host, and that host (D18) cannot fall back to root. personName() in real/server.ts now signs up as the account the fixture runs as (root keeps the old name). 27 specs use it. real/mobile-lease.spec.ts passed on compute01 with it (previously blocked).

Bug 2: not reproduced. Real server with --remote-listen + --remote-ca-dir, first-run setup, palette opened immediately after setup, no reload: the "Unlock host connections" row is present. Tried 0, 75 and 150 ms one-way latency, and with the "no login" refusal toast on screen. The code at the test server's build (ff93201e5) is identical to dev for authority-unlock.ts, so a newer build is not what fixed it. Existing real specs dispatch visibilitychange by hand before the Hosts "Unlock" button, which suggests another refresh trigger was once missing, but I have no failing case. I did not make a speculative fix. Specs kept as regression guards: real/authority-after-setup.spec.ts, and the unlock-row check in real/rootless-refusal.spec.ts. If J141 can capture a HAR or the page's console on the test server right after a first-run setup I can look again.

J142 interim (branch `rootless-toast-j142`, head 933e01552, 4 atomic commits on origin/dev 3cc63cb09; gate and real specs running on compute01). **Bug 1: done, red then green.** Typed rootless refusals `no_login` / `permission` (agent), accurate wording in the client, toast duration follows message length. Rust and TS tests red first (log `~claude/j142-out/red1-bug1-final.log`), green after. Real-server proof, desktop passes; phone failed once because the 4.2 s toast expired, fixed by the length-based duration. **Harness (from J121c, #317): fixed in the harness, the product is right.** The fixture runs server and agent as the VM user; specs signed up under made-up names, which are no account on a rootless host, and that host (D18) cannot fall back to root. `personName()` in `real/server.ts` now signs up as the account the fixture runs as (root keeps the old name). 27 specs use it. `real/mobile-lease.spec.ts` passed on compute01 with it (previously blocked). **Bug 2: not reproduced.** Real server with `--remote-listen` + `--remote-ca-dir`, first-run setup, palette opened immediately after setup, no reload: the "Unlock host connections" row is present. Tried 0, 75 and 150 ms one-way latency, and with the "no login" refusal toast on screen. The code at the test server's build (ff93201e5) is identical to dev for `authority-unlock.ts`, so a newer build is not what fixed it. Existing real specs dispatch `visibilitychange` by hand before the Hosts "Unlock" button, which suggests another refresh trigger was once missing, but I have no failing case. I did not make a speculative fix. Specs kept as regression guards: `real/authority-after-setup.spec.ts`, and the unlock-row check in `real/rootless-refusal.spec.ts`. If J141 can capture a HAR or the page's console on the test server right after a first-run setup I can look again.
Author
Owner

J142 final. Branch rootless-toast-j142, head 855e2a262 (4 atomic commits on origin/dev 3cc63cb09). Not merged, not deployed.

scripts/verify fast on compute01, head 855e2a262: VERIFY_EXIT=0. Rust gate table and tail, verbatim:

9058:| Rust gate                 | Result       |
9059-+---------------------------+--------------+
9060-| fmt                       | PASS         |
9061-| VT native fmt             | PASS         |
9062-| CLI plugin compile-out    | PASS         |
9063-| clippy                    | PASS         |
9064-| SSH runtime test helper   | PASS         |
9065-| real-binary fixture paths | PASS         |
9066-| test                      | PASS         |
9067-| SSH inode reuse fixture   | PASS         |
9068-| shim process-per-user     | PASS         |
9069-| cargo-deny                | PASS         |
9070-| gate architecture         | PASS         |
9071-| static builds             | PASS         |
9072-+---------------------------+--------------+
9073-
9074-==> Compiler metadata generator checks

src/mesh-review-resync.test.ts:
(pass) review watch applies same-revision authoritative resync [0.25ms]
(pass) authoritative resync keeps priority over an equal-revision pending list [1.58ms]

src/mesh-vectors.test.ts:
(pass) every shared Mesh action vector passes through the typed client unchanged [2.04ms]
(pass) shared Mesh event vectors drive the typed watch adapter [8.21ms]

 48 pass
 0 fail
 77248 expect() calls
Ran 48 tests across 12 files. [11.52s]
VERIFY_EXIT=0
855e2a262224d7cdb1d730cacfadab1e67b03039
cleaned

Screenshots (real production build, rootless server, person with no OS account): desktop 1440x900 and phone 393x852 at 3x. Review them; the phone toast wraps to five lines.

Static size: x86 delta in the gate's report-only rows; aarch64 +42824 bytes.

J142 final. Branch `rootless-toast-j142`, head `855e2a262` (4 atomic commits on origin/dev 3cc63cb09). Not merged, not deployed. **scripts/verify fast on compute01, head 855e2a262: VERIFY_EXIT=0.** Rust gate table and tail, verbatim: ``` 9058:| Rust gate | Result | 9059-+---------------------------+--------------+ 9060-| fmt | PASS | 9061-| VT native fmt | PASS | 9062-| CLI plugin compile-out | PASS | 9063-| clippy | PASS | 9064-| SSH runtime test helper | PASS | 9065-| real-binary fixture paths | PASS | 9066-| test | PASS | 9067-| SSH inode reuse fixture | PASS | 9068-| shim process-per-user | PASS | 9069-| cargo-deny | PASS | 9070-| gate architecture | PASS | 9071-| static builds | PASS | 9072-+---------------------------+--------------+ 9073- 9074-==> Compiler metadata generator checks src/mesh-review-resync.test.ts: (pass) review watch applies same-revision authoritative resync [0.25ms] (pass) authoritative resync keeps priority over an equal-revision pending list [1.58ms] src/mesh-vectors.test.ts: (pass) every shared Mesh action vector passes through the typed client unchanged [2.04ms] (pass) shared Mesh event vectors drive the typed watch adapter [8.21ms] 48 pass 0 fail 77248 expect() calls Ran 48 tests across 12 files. [11.52s] VERIFY_EXIT=0 855e2a262224d7cdb1d730cacfadab1e67b03039 cleaned ``` Screenshots (real production build, rootless server, person with no OS account): desktop 1440x900 and phone 393x852 at 3x. Review them; the phone toast wraps to five lines. Static size: x86 delta in the gate's report-only rows; aarch64 +42824 bytes.
Sign in to join this conversation.
No labels
launch-qa
No milestone
No project
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#327
No description provided.