Skip to content

Add isolated TUI device control with native ask inspection and revoke - #6294

Draft
schickling-assistant wants to merge 12 commits into
schickling-assistant/2026-11-10-client-control-tui-sdkfrom
schickling-assistant/2026-11-10-client-control-tui
Draft

schickling-assistant wants to merge 12 commits into
schickling-assistant/2026-11-10-client-control-tui-sdkfrom
schickling-assistant/2026-11-10-client-control-tui

Conversation

@schickling-assistant

@schickling-assistant schickling-assistant commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Problem

The terminal client has no device-command consumer, run indicator or enrollment revoke control. An isolated client needs to consume the shared bounded-command protocol without gaining message or answer authority.

Goal

Implement client-side grant/run/device/session/actor/verb/screen/window/budget checks; sign renderer-visible state with the paired P-256 key and shared canonical form; show the active run and a global enrollment-revoke binding.

Decisions

  • Reuse the existing session-scoped commands collection through fractal-client. No second protocol and no generated files hand-edited.
  • Explicit separate profile, exactly one fixture pairing, named fixture principal, and endpoint equality against the instance's configured client socket. Never the privileged socket or normal profile.
  • Only phone.inspect and phone.navigate to NowRoot and Attention are supported. Conversation, read-state changes, message.send, answer and close verbs produce no action or signed result. Optional grant item_id binds the exact ask.
  • ctrl+alt+x latches execution closed before requesting enrollment revocation; failure never resumes control. Terminal focus/process-group checks gate execution.
  • The adapter currently reads grant lifecycle resource fields in one small module because the SDK does not yet expose those resources as collection enum variants. Canonical signing uses the generated shared schema, never a handwritten canonicalizer.
  • Keep the handler, rendered-state boundary and its denial tests atomic rather than publish an enabled but incompletely checked control path. The SDK prerequisite is a separate reversible PR.

Seed

The shared seed is an open native ask: attention_kind=person-step, presentation.kind=request, from the fixture agent to the fixture person. request.question carries the seeded body. The journey is NowRoot → the ask's existing Attention screen → inspect. No Conversation navigation is part of the journey, and normal inbox policy is unchanged.

Source checks of the shipped UI:

  • iOS: apps/ios/screens/Now.tsx:28 uses homeRows; :75-86 opens Attention; :150-168 renders the request title/question. :104 explicitly excludes read-on-open. Answering is through explicit RequestAnswers/structured callbacks, not opening.
  • Web: apps/fractal-web/src/web/alertsTab.ts:13-16 admits open asks; AlertsTab.tsx:185 only expands local state, :194 renders title, :215-217 mounts AgentAlerts. conversation/alerts/AgentAlerts.tsx:76-77 selects the same conversation asks, :99-109 renders AnswerAlert; AnswerAlert.tsx:317 renders title and :230-240 renders request.question. Actual answer submission is only its onAct path :297-305.
  • TUI: src/ui/adapt.rs:421-430 and :530 preserve request/question; screens.rs:272 lists title, :1046 renders card title, :1447-1458 renders question. glass.rs:4971-4974 maps Attention to its existing Home card. Navigation synthesizes no answer keys/actions, and fixture sessions suppress ordinary message read receipts entirely.

Title/question are each printable ASCII, at most 60 characters, one paragraph, with no Markdown syntax, JSON, leading/trailing/repeated whitespace. Digest is lowercase hex SHA-256 of exact question bytes. Added a shipped-card terminal-render test including the 60-character boundary.

Forbidden during journeys: Yes/No, named answers, Nothing to do, answer/close/dismiss/Mark read/send controls, TUI answer keys y/n/a/x/c and Enter while answering, and all confirmation actions. Inspection/navigation never invokes these controls.

Verification

  • PASS: python3 -m unittest discover -s crates/fractal-tui/tests -p client_control_capture_test.py (3 tests). Tests cover exact rendered title/body digest, missing title, changed/truncated body, escape payloads and invalid capture bounds.
  • PASS: both release fragments validated with scripts/release_notes.py's validate function.
  • Rust client_control tests: NOT RUN. The required gate-slot --lane build --max-wait 1800 admission attempt exited 94 after 1,799.64 seconds without executing nix or cargo; it was fenced by the shared garbage-collection window with 42 requests ahead. Command: gate-slot --lane build --max-wait 1800 -- nix develop --no-write-lock-file --command cargo test --locked -p fractal-tui client_control. The request recorded prerequisite source b6c0d55 before the final commits; no check ran against any source head. Define canonical client command result contract #6274's generated canonical/result sources are also not available yet. No Rust compilation/test success is claimed.
  • Added D-5 denial tests using the real UI application boundary: no grant, wrong run/device/session/actor, ungranted verb/screen, expiry, budget, wrong ask, Conversation and answer/close/read verbs leave navigation unchanged and construct no result/signature.
  • Added indicator/global revoke tests and a positive single-signature/replay/revocation test.
  • No fixture setup, pairing, enrollment, grant, granted journey, daemon launch/restart, CI dispatch or CI rerun was performed. Push hooks disabled to avoid the prohibited st-test runner.

Complexity

One bounded adapter module, one existing-UI status/render integration, and a separate-process terminal-capture verifier. Uses the existing private SDK profile and generated canonical form.

Concerns

Draft dependency stack: #6293 and #6274's generator output/daemon integration must land before this is build-ready. The local Rust gate is not evidence until admitted and completed. Enrollment/bootstrap, independent fixture manifest/canary checks and live journey acceptance remain separately gated.

Friction & bottlenecks

Generated dependencies are waiting for shared build admission. Managed dependent-base PR creation was correctly refused; this side chain was submitted through gh stack. The release-note validator is a Python function, not a validate CLI subcommand.

Follow-ups

Integrate the generated canonical head and record admitted Rust test evidence. No deployment or enrollment is authorized by this source PR.

References

Depends on #6293; canonical source in #6274. No FPL-held source paths changed.

Published source

TUI head: a4107410bbac9dcfe1a484632844282c421a0e9b. SDK prerequisite head: 62adcce869fbbbf6f21eccbdf19fd5cd997277b5. Both are signed source commits. Source-only; not enrolled or activated.

Read-only identity observer

Signed append-only head 1f8c81a51bd06e8df914b07eb783bd16b792354d adds fractal tui client-control identity --profile PATH --json. It loads exactly one fixture command pairing with Profile::load, refuses the normal profile (including symlink aliases), and emits canonical P-256 public key, SHA-256 fingerprint of its uncompressed 65-byte SEC1 point, device id, session actor, and the full raw profile snapshot. Only pkcs8 and credential are replaced by {redacted: "[REDACTED]", sha256: ...} over the original UTF-8 string. No pairing, network request, or save occurs. Tests cover exact output fields, preservation of other fields, no secret output, changed-secret hashes, normal/symlink and multiple-pairing refusal, and unchanged profile mtime and content hash.

Rust checks for this head have been requested on dev5 in one gate ticket (--lane build --profile disk=cargo-warm --max-wait 10800); no success is claimed while admission is pending.

Generated dependency integration

Signed append-only merge integrates canonical generated head 36592f7090093ae6fe94a87706e319fca78fca17; current head 3d51f91ce34502fb9ef262404c4675104a071222. No generated file was hand-edited. One exact-head dev5 gate ticket requests the relevant Rust checks with --lane build --profile disk=cargo-warm --max-wait 10800; result pending. The prior pre-generation identity ticket was canceled, not executed.

Final generated lifecycle cutover

Current signed head 54adebdd168f53d7d88d3fa43492c4cfd36c04f0 integrates final canonical generated head 7cafdf138efbea937cbefa83323d5d2fc8dda15c append-only. Supersedes earlier generated-integration and pending-head notes. Both intermediate gate tickets were canceled before admission, so no Rust execution is claimed for them.
The temporary lifecycle field adapter is removed: the reducer decodes/stores the generated ClientControlGrantLifecycle type. Lifecycle drives the indicator and terminal latch; each command must match active lifecycle context and checks its embedded control_grant policy. The shipped reducer consumes shared fixtures/clients/client-control-lifecycle-v1.json traces, including stale/duplicate starts, item mismatch, terminal finality and missing lifecycle refusal. Independent identity helper source and sanitized profile snapshot remain included.
One final-head dev5 gate ticket now requests cargo test --locked -p fractal-tui client_control through gate-slot --lane build --profile disk=cargo-warm --max-wait 10800 -- nix develop --no-write-lock-file --command .... Receipt pending; no success claimed.

Final sequence and item-binding revision

Current head e9f65833172818c8c93a34b24d22f726b4223cb9 includes signed merge of regenerated canonical head 6fb398ccf5f0b7c08ea0cd1892aa26fdb7a2c006. Previous bg318 (TUI) and bg319 (SDK) ended queue-only without payload or exit-code receipts: both NOT RUN. These are not passing or failing Rust executions.
An independent source counterexample exposed missing/null command item binding under a bound lifecycle. Fixed with exact normalized optional-item equality before navigation/signing. Attention additionally requires a nonempty item, while fully unbound NowRoot remains supported. Shipped reducer plus native-ask UI tests cover absent/null/explicit-B commands targeting B under lifecycle A, unchanged navigation/no result/signature, positive A, and unbound Attention refusal. Shared traces include the final invalid-sequence expectation. Daemon admission was not changed.
One new exact-head dev5 ticket is pending with the current xvrpad67 gate-slot helper and --lane build --profile disk=cargo-warm --max-wait10800. No Rust success claimed until its receipt.

Current source and exact-head checks

Current signed head b0a77fa0ee62eee39a3b8ecb61ec1938279a659b integrates corrected canonical 972b65b7c4cf8e3612ffdd4d756e11d0b7b011f2. Superseded tickets bg419/bg420 were withdrawn while queued with SIGUSR2 and both exited94: NOT RUN, no payload. New exact-head dev5 ticket requests the same crate test command through xvrpad67 gate-slot. Receipt pending.
FPL accepted the item-binding counterexample fix at f994aa1. The requested P3 hardening is also included: non-null/non-string item_id is refused before navigation/signing; item_id:5 is covered through shipped UI and result path.

Admission receipt

The first current-head dev5 ticket expired before admission after10800seconds with exit94: Rust tests NOT RUN, no nix/cargo payload executed. The single authorized replacement requests the identical command at the unchanged current head; pending. This is an admission constraint, not a passing or failing test result.

Posted on behalf of @schickling
field value
agent_identity unknown
session unknown
agent_persona unknown
agent_supervisor unavailable
agent_tool unknown
agent_tool_version unknown
agent_runtime unknown
agent_model unknown
worktree fractal/2026-11-10-client-control-tui

@schickling-assistant schickling-assistant changed the title Add isolated terminal client command control and revocation Add isolated TUI device control with native ask inspection and revoke Nov 10, 2026
@schickling-assistant

Copy link
Copy Markdown
Contributor Author

Source review at 54adebdd168f53d7d88d3fa43492c4cfd36c04f0, crates/fractal-tui/src/client_control.rs:146-150: lifecycle item binding can be dropped by an omitted/null per-command item.

allows rejects an item mismatch only when control_grant.item_id exists and is non-null. For an active lifecycle row bound to ask A, a command whose embedded grant omits item_id (or sets null) skips that guard. The later Attention navigation guard (167-168) and result opened-item guard (183-184) also treat that missing field as unbound. With all other fields valid, the client can navigate to and sign visible state for ask B despite the active lifecycle grant binding ask A.

Please compare normalized optional item IDs for equality, including missing/null, before any navigation/signature; a bound lifecycle requires the identical bound command item. Keep legitimate fully unbound cases consistent with the product contract. Add real shipped-reducer/UI refusal tests for bound lifecycle A plus absent/null command item and target B, asserting unchanged navigation and no result/signature, as well as matching A positive coverage. Existing tests only mutate wrong-item when BOTH lifecycle and command grant contain an item, so they do not cover this cut.

This is a static source counterexample, not an observed live access violation; daemon admission is a separate authority and must remain checked. Please have the source owner correct/rerun at the final head under the existing admitted/isolated-job rules (do not mutate a checkout while its job is queued/running). No live fixture/device action requested.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant