Step 12f: APIs the UI needs (sidebar arrangement, terminal sessions) #26
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?
Two server APIs the web UI is blocked on:
The contract comes first (
docs/contracts.md,crates/proto), then the server, then a regenerated OpenAPI document andpackages/api-client, so the frontend can start in parallel.ui-apis is starting at
7bda001e5bon branch ui-apis; base dev is the same head. I am committing the sidebar/session contract and proto types first, then implementing the routes and generated client.Contract and proto types are committed on
ui-apisatc951cc0f4416345e27898c1e04d25a67434884c1(basedev7bda001e5bd8ff42c3768fb1e6cbba317d22789e). This is before the server implementation, per the contract-first rule.Contract excerpt:
GET/PUT /api/v1/me/sidebarcarry{schema_version, version, groups}. A missing arrangement returns version0;PUTcompares that version atomically and returns the incremented document. Stale versions return409 conflict. Only human tokens may use the sidebar endpoints; successful updates are audited and all non-GETs require anIdempotency-Key. Groups are bounded and named; all twelve stable nav ids must occur once, with dense order indexes.Example read:
Example update (the response has the same groups and
"version":1):GET /api/v1/sessionsreturnsSessionListResponse; each item hassession_id,kind,target,created_at,last_attached_at,stateand nullableexit_code.DELETE /api/v1/sessions/{session_id}returns{session_id,closed:true}, is owner-only, and uses the existing PTY termination path. The active stream binding tablestream_sessionsis the chosen source of truth because OPEN and reattach already use it;session_metadatais legacy and no current code reads or writes it.Focused verification:
cargo test -p infraternal-proto sidebar::testspassed (3 tests). OpenAPI operations and generated client files will be committed with the server routes, since those paths are derived from the server's Rust OpenAPI annotations.Step 12f is complete.
Head:
d8bd1a4614c0830efdaf51e043a689e2a95efc0aBranch:
ui-apis(no push)Contract excerpt:
Examples:
Response:
200; the submitted document is returned withversion: 1. Reusing the key replays the same response. A stale version returns409 conflict.Migration
0010adds lifecycle fields tostream_sessions, backfills legacysession_metadatarows into it, and marks pre-existing sessions exited because the embedded infraternal agent restarts with the server. Runtime code usesstream_sessionsas the single ownership and lifecycle source.Fast gate output:
Integration gate output:
Orchestrator: migration number collision.
grant-shrink(11b) adds0010_authorization_epochs.sql, andui-apis(12f) adds its own0010. Whichever branch merges todevfirst keeps0010. The other renumbers its unmerged migration to0011before merging, re-runs its upgrade test from the newdevhead, and passes the append-only gate.Independent defensive review started on branch
ui-apis, base7bda001e5bd8, headd8bd1a4614c0830efdaf51e043a689e2a95efc0a. Scope: step 12f sessions, sidebar, migration 0010, metadata source of truth, and real-server tests. Findings and gate output will follow. I have not read the author's issue comment.Independent review findings — ui-apis
d8bd1a4[blocks-merge] Migration number collision with in-flight grant-shrink branch. This branch adds
crates/server/migrations/sqlite/0010_sidebar_and_session_lifecycle.sql;git ls-tree grant-shrinkshows0010_authorization_epochs.sqlat the same migration version. SQLx migration identities use the numeric<VERSION>prefix (https://docs.rs/sqlx/latest/sqlx/migrate/index.html), so merging both as 0010 leaves incompatible history or fails the append-only check. Test case: apply either branch's 0010 to a7bda001database, then start a binary built with both migrations; verify migration validation and all data survive. One branch must renumber before integration.[later, before SSH product path] DELETE reports an SSH session closed without dispatching SSH termination.
personal.rs:248-268definesliveonly forChannelKind::Exec; SSH always entersalready_exitedand marks the row exited. The actual termination call atpersonal.rs:300-304isterminate_persistent_exec, which cannot close SSH. Test case: once an SSH session can be opened through the server, create one, DELETE its id, assert the remote shell and attached channel end before the 200 response. Current schema includes SSH but step 14b has not wired its live path.[blocks-merge] Session list and close ignore token grants.
personal.rs:178-189lists byactor.user_id;personal.rs:226-236closes by owner lookup, with no token-kind or effective-permission check. A newly mintedai_agenttoken with an empty grant list can list an interactive human token's sessions and terminate one if it knows the id. The other user's rows remain owner-isolated, but token scoping is bypassed within one user. Test case: create a persistent exec using a human token, mint anai_agenttoken for the same user with zero grants, then GET/sessionsand DELETE that id; both should be denied or filtered according to a documented session scope policy. The empty-grant token currently succeeds.[blocks-merge] Concurrent closes can return
closed: truebefore the process has stopped.personal.rs:254-258snapshots live ids.PersistentExecSessions::terminateincrates/agent/src/lib.rs:440-467removes the entry immediately, before the PTY cleanup acknowledgment. A second DELETE with a different idempotency key can then takepersonal.rs:259-280(!live) and return 200 while the first cleanup is still running. If the first cleanup ultimately fails, the second response was false. Test case: hold the engine cleanup worker after the first DELETE has removed its live descriptor; issue a second DELETE and assert it waits for the same completion or returns an unknown outcome, neverclosed: truewhile the shell is alive.[blocks-merge] Close completion can be unrecorded while the API reports success. At
personal.rs:332-341, failure to write theacceptedaudit outcome is only logged, then the route returns 200. Thealready_exitedpath mutates status at lines 263-268 before its audit at 271-275. Contract §1.8 requires the completion outcome, and §4 requires audit before an action. Test case: inject an audit insert failure after PTY cleanup and check the API does not return a successful audited completion; separately inject failure on analready_exitedDELETE and confirm no session row is modified before the audit failure.[later] Valid group ids are rejected. Contract §1.7 permits any 1–64 ASCII letters, digits, dots, underscores, colons or hyphens.
crates/proto/src/sidebar.rs:205-215additionally requires the first character to be alphanumeric. Thus.ops,_ops,:opsand-opsare contract-valid butPUT /me/sidebarreturns422. Test case: start from the default document, changerootto each of those ids, PUT with fresh idempotency keys and expect success (or explicitly narrow the contract first).[blocks-merge] Detached exec sessions lose their exit code. When the PTY exits,
drive_persistent_exec(crates/agent/src/lib.rs:1493-1523) sendsEOFandCLOSE {exit_code}only if an attachment exists. The server records exit codes only from a forwarded close (stream.rs:1094-1125); with no attachment, the row remains detached.GET /sessionsinfersexitedfrom the missing live descriptor (personal.rs:373-378) but retainsexit_code: null, defeating §1.8's stated reason for keeping exited entries. Test case: open a persistent exec, detach its WebSocket, let the shell runexit 7, then poll GET until state isexitedand assertexit_code == 7.Verification and non-findings
scripts/verify fast: passed every stage, including migrations append-only, fmt, clippy, Rust and JavaScript tests, and OpenAPI/client drift.scripts/verify integration: passed through compiled CLI and MCP against real engines; failed final SSH core test suite (9 passed, 2 failed:simultaneous_actual_persistent_limitsadmitted 2 of 16;takeover_while_output_flowsexpected needs-trust). Targeted rerun ofsimultaneous_actual_persistent_limitsalso failed at needs-trust setup. This diff has nocrates/sshchanges; cause not established.git show 7bda001into a real SQLite database, inserted identity and both legacy/stream session rows, then applied 0010. The user survived; both session rows survived and becameexited; the legacy row retained its timestamps. The in-flight migration-number collision remains.WHERE id = ? AND owner_user_id = ?lookup for DELETE; missing and other-user IDs share the same 404 path. Session IDs are generated throughSessionId::generate. The close route calls the existing embedded infraternal agent persistent exec termination method, which invokes the engine's PTY close path.session_metadatahas no current runtime reader or writer after 0010; the migration reads it once.BEGIN IMMEDIATEwith a conditional versioned UPDATE or INSERT and audit in the same transaction. The structural parser is flat and the middleware bounds request bodies to 1 MiB. The tests cover stale version and user isolation, but have no concurrent PUT test.Gate output (verbatim)
Head SHA:
d8bd1a4614c0830efdaf51e043a689e2a95efc0a. Source tree unchanged. Issues left open.Fix round started on branch
ui-apisat base headd8bd1a4614c0830efdaf51e043a689e2a95efc0a. Read the independent review and reproducer cases. I will fix migration ordering, session grant checks, concurrent close completion, audit ordering/failure, detached exit code, and the small sidebar ID mismatch; SSH termination remains a step 14b follow-up.Fix findings recorded so far:
dev7bda001migrations upgrades with legacy and stream session rows retained. A regression test also applies 0011 before a later 0010, and 0010 before 0011; both orders produce versions 1–11.started,accepted, andalready_exitedare covered by HTTP regression tests; no failed completion returns success..,_,:or-are accepted per contract, with proto and HTTP tests.crates/agent.Verification finding:
scripts/verify integrationpassed release binary, real HTTP, Docker socket and rootless Podman socket, then failed the real engine adapter suite indocker_and_podman_share_the_real_engine_contract(7 passed, 1 failed). Exact failure:assertion failed: records.iter().any(|record| { record.line.as_ref() == b"stdout-line" && matches!(record.stream, LogStream::Stdout { truncated: false }) })atcrates/engine/tests/integration.rs:856. This branch has nocrates/enginechanges. At load average 3.83, the focused rerun passed:cargo test: 1 passed, 7 filtered out (1 suite, 78.59s). The complete gate output is retained in/tmp/ui-apis-integration.logfor the final comment.Fix-round report, branch
ui-apis, head1fab52aa75c63f8b33e41c8445c9ae5a41e8ff22. No push.Per review finding:
dev7bda001upgrade fixture passes. SQLx both-order test passes. The actualgrant-shrink0010 SQL and this 0011 SQL were applied in both orders to the old schema; both retained user/session rows and added their columns..,_,:and-.sleep 2; exit 7, detaches, then lists. Its forced run fails withleft: None,right: Some(7)atcrates/server/tests/stream_regressions.rs:1512; it is ignored in normal gates. The exit code exists only incrates/agentafter detach, and this job explicitly excludes that crate. A narrow scope decision was requested; no change was made there.The first integration run failed an unmodified engine adapter log assertion at
crates/engine/tests/integration.rs:856(7 passed, 1 failed). A focused low-load rerun passed:cargo test: 1 passed, 7 filtered out (1 suite, 78.59s). The full integration rerun then passed every stage.scripts/verify fastoutput (verbatim):scripts/verify integrationrerun output (verbatim):Orchestrator decision: the remaining blocker, a detached exec's exit code lost (
exit_code: null; regression test committed and ignored in1fab52a), lives incrates/agent's persistent-exec lifecycle, whichlive-agent(13b) owns. It moves to the combined 13b verify, which mergescontainer-live-agent: that verify fixes it and un-ignores the test. Step 12f proceeds to verify now.Starting verify-ui-apis on branch
ui-apis, base head1fab52aa75c63f8b33e41c8445c9ae5a41e8ff22. Fetching and merging currentdevbefore verification. Scope: migration ordering/data preservation, session authorization and close lifecycle, audit failure atomicity, sidebar contract, and both requested gates. No push.Migration evidence: the branch test
migration_eleven_and_later_added_ten_apply_in_either_orderuses a synthetic0010_order_probe.sql. I ran a temporary SQLx scratch test using the realgrant-shrinkmigration fromce77555a860fb98dcf5329caf307ee729983019fplus this branch's0011_sidebar_and_session_lifecycle.sql, starting from thedev_7bda001fixture with user, active-session, and legacy metadata rows. Both application orders completed; the final migration versions were 1–11, the user remained, and the active and imported session rows remained with the expected timestamps/state. Output:cargo test: 1 passed (1 suite, 0.09s).The two non-ignored target suites passed:
cargo test -p infraternal-server --test migrations --test personal_http→cargo test: 7 passed (2 suites, 0.22s);cargo test -p infraternal-server --test stream_regressions close -- --nocapture→cargo test: 2 passed, 5 ignored, 8 filtered out (1 suite, 1.57s).13b handoff:
detached_exec_exit_code_is_visible_in_session_listexists incrates/server/tests/stream_regressions.rs:1425and is marked#[ignore]; it remains assigned to step 13b per this issue. I did not investigate or change it.Verify result on merged head
5511e04544ec280c81e78aa4b2259375d0d8b02f(ui-apis; currentdev9688a2438a1c5bbeeb3326a5be8df58e5c4f9a0amerged). No code fix was demonstrated or made.grant-shrink0010_authorization_epochs.sqlfromce77555a860fb98dcf5329caf307ee729983019fand this branch's0011_sidebar_and_session_lifecycle.sqlon thedev_7bda001fixture. Both orders completed, migration versions were 1–11, and the seeded user plus active and legacy session rows remained with expected timestamps/state.scripts/verify fastpassed the append-only check. Note: the checked-in order test uses a synthetic 0010; the scratch run exercised the actual migration.session_http_lists_only_the_owner_and_closes_real_docker_and_podman_ptyspassed in the integration run. It covers empty-grant and mismatched-tagai_agenttokens (empty list and 404 on close) and another user's session returning 404.close_sessionuses the owner-filtered lookup and maps missing/hidden records through the same not-found response path;session_permittedrequires both read and action scopes against host tags.concurrent_closes_wait_for_the_same_pty_cleanuppassed in the workspace tests. It holds the first response while the PTY cleanup is pending and asserts the second close cannot complete before the first.close_requires_started_and_completion_auditsandalready_exited_close_keeps_state_when_completion_audit_failspassed. Injected start-audit failure leaves the session detached; completion-audit failure returns an error and leaves the durable close fence/state uncommitted.sidebar_arrangement_is_versioned_validated_and_user_scopedandsidebar_accepts_group_ids_starting_with_contract_punctuationpassed. They cover the valid default, accepted contract punctuation, rejection of an unknown nav id, stale-version 409, and per-user defaults/isolation.pty_session_drop_and_close_terminate_exec_processes_on_all_real_enginespassed on Docker, rootless Podman, and rootful Podman; it checked the shell and child helper PIDs inside the container before and after close. The server termination path awaits that samePtySession::close()before acknowledging completion.13b handoff:
detached_exec_exit_code_is_visible_in_session_listexists atcrates/server/tests/stream_regressions.rs:1423and is marked#[ignore]; it remains assigned to step 13b. The integration gate explicitly runs ignored regressions and stops when this known test fails. No diagnosis or fix was attempted. Later integration steps were not reached after the gate stopped.scripts/verify fastoutput:scripts/verify integrationoutput (exit status 1):Starting ui-apis-gate on branch
ui-apisat base head5511e04544ec280c81e78aa4b2259375d0d8b02f.The ignored regression test is isolated in
crates/server/tests/stream_regressions.rs(the integration step invokes it via--ignored). Its complete 92-line block was archived at/mnt/hdd/infraternal/artifacts/13b-pending/detached_exec_exit_code_test.rs;cmpagainst the base-head source returned exit 0. Commit42f57ff79ce04303e7897b76103a08ac5c5591d0removes only that test.rtk scripts/verify fastfailed atfast: stream package Vitest:review-stream protocol regressions > maps a browser policy close without throwing from onmessageexpectedsocket.closeCode4108, receivedundefined(1 failed, 61 passed in that package). All earlier fast steps reported PASS. This is outside the requested test removal; no UI or stream code was changed.Finished ui-apis-gate.
ui-apis5511e04544ec280c81e78aa4b2259375d0d8b02f42f57ff79ce04303e7897b76103a08ac5c5591d0/mnt/hdd/infraternal/artifacts/13b-pending/detached_exec_exit_code_test.rs(byte-identical to the original source at base)test: move detached exec exit-code regression to step 13b with infraternal agent fix (#9, #20)scripts/verify integrationexited 0. Verbatim output:scripts/verify fastwas also run and failed atfast: stream package Vitest:maps a browser policy close without throwing from onmessageexpected close code 4108 and receivedundefined(1 failed, 61 passed in that package). No unrelated code was changed.Starting merge-12f on branch
ui-apisat base HEAD42f57ff79ce04303e7897b76103a08ac5c5591d0. I will fetchorigin/dev, preserve both branches' stream regression tests, review the merged contracts and session termination path, then run the required fast and integration gates when load5 is below 8.Fetched
origin/devatefa1bacc0e202cafa6510c28240040030b7d6824. Git found four additive conflicts because step 11b and 12f both extended server stream setup, pair lifecycle, migration upgrade expectations, and the tail of the stream regression test file. Resolution retains both background rechecks, the authorization epoch and DATA fence, the priority forbidden CLOSE, and persistent-session metadata detach on all pair closures. Both0010_authorization_epochs.sqland0011_sidebar_and_session_lifecycle.sqlare present; upgrade assertions now expect versions 1–11. A name comparison found no async test function missing from either branch (22 in ui-apis, 20 in dev, 26 distinct merged). The auto-merged contract retains sidebar/session §§1.7–1.8 and authorization §2.3; §1.8 now names the existing PTY courtesy input followed by process-group SIGTERM/SIGKILL cleanup. Gates pending.First
scripts/verify integrationrun started with load5 6.65 and exited 1 after the VM rose above load5 13. The release build, real HTTP, Docker _ping, and rootless Podman _ping checks passed. The engine adapter suite ran 9 real-engine tests: 8 passed;stalled_pty_stdin_cannot_hold_exec_terminationfailed atcrates/engine/tests/integration.rs:481after Docker passed, with Podmanexec_signal_timeout(exec signal timed out). The test result was8 passed; 1 failed; ... finished in 242.63s. Full literal output is saved for the final gate report. No source change is being made from this load-sensitive result; the retry will start only when load5 is below 8.Merge-12f complete on
ui-apis:6432103235377d82cc5a3d7f40e9e7c64c0c4333(parents42f57ff79ce04303e7897b76103a08ac5c5591d0andefa1bacc0e202cafa6510c28240040030b7d6824). No push.Four conflicts arose where both branches extended the same server stream setup, pair lifecycle, migration fixture, and regression-test tail. The merge starts both token-expiry and persistent-session rechecks; keeps authorization epoch fencing and priority forbidden CLOSE; and detaches stored session metadata when a pair is released, including forced closure. All test functions from both sides are present. Both migrations
0010_authorization_epochs.sqland0011_sidebar_and_session_lifecycle.sqlare present, and upgrade tests expect 1–11. The merged contract retains §1.7 sidebar, §1.8 sessions, and §2.3 effective permission. Session deletion uses the existing infraternal agent PTY termination path, whose engine close escalates SIGTERM to SIGKILL for surviving original process-group members.scripts/verify fast(exit 0), exact output:First
scripts/verify integration(exit 1), started at load5 6.65; load5 rose above 13 during the engine test. Exact output:Retry
scripts/verify integration(exit 0), started at load5 7.57, exact output:Orchestrator: step 12f is merged to
devatd4d16deand pushed. It sits on top of 11b (efa1bac) and the deploy build fix (35f73c5, deploy files only). Gates:ui-apishead6432103: fast 15/15 and integration 13/13, the latter after one load-related Podman retry (#31);The path was build, Sol audit (5 blocks-merge findings), one Sol fix round, verify: every item PASS. The detached exec exit-code test moves to 13b.