# Ticket #13 B0 - Backend transport inventory Date: 2026-07-15 Branch: `feature/ticket13-server-client-mode` Scope: read-only inventory of the current Tauri backend transport surface in `crates/app-tauri`. No production code change is part of B0. Architect ticket #13 target: keep desktop Tauri and add server/client mode on a shared backend core. Future web transport is HTTP request/response plus a dedicated WebSocket for PTY/live streams. ## Source anchors - Tauri command registration: `crates/app-tauri/src/lib.rs:153` - Composition root and `AppState`: `crates/app-tauri/src/state.rs:738`, `crates/app-tauri/src/state.rs:1058` - PTY Tauri Channel bridge: `crates/app-tauri/src/pty.rs:1` - Structured chat Tauri Channel bridge: `crates/app-tauri/src/chat.rs:1` - Domain event relay: `crates/app-tauri/src/events.rs:23`, `crates/app-tauri/src/events.rs:1107` - Desktop window commands/events: `crates/app-tauri/src/commands.rs:2325` - Shutdown/window lifecycle wiring: `crates/app-tauri/src/lib.rs:80` ## Classification rules - `request/response`: one Tauri `invoke` maps to one backend result. HTTP candidate. - `stream`: command uses `tauri::ipc::Channel` or controls a live stream/session. WebSocket candidate, sometimes with a small HTTP control equivalent. - `event`: backend push currently emitted through Tauri global events. WebSocket live/event frame candidate unless desktop-specific. - `desktop-only`: OS windows, Tauri path/lifecycle/dialog composition. Not part of the web client transport. ## Exposed Tauri commands ### Core/projects/permissions | Command | Class | Current boundary | | --- | --- | --- | | `health` | request/response | `State.health` | | `create_project` | request/response | `create_project` use case | | `open_project` | request/response | `open_project`, plus open-time reconciliation/wiring | | `close_project` | request/response | `close_project` | | `list_projects` | request/response | `list_projects` | | `read_project_context` | request/response | project FS read use case | | `update_project_context` | request/response | project FS write use case | | `get_project_permissions` | request/response | permission read use case | | `update_project_permissions` | request/response | permission write use case | | `update_agent_permissions` | request/response | permission override write use case | | `resolve_agent_permissions` | request/response | effective permission read use case | ### PTY terminals | Command | Class | Current boundary | | --- | --- | --- | | `open_terminal` | stream | Spawns PTY, registers `Channel`, starts output pump | | `write_terminal` | stream/control | Writes bytes to live PTY | | `resize_terminal` | stream/control | Resizes live PTY | | `close_terminal` | stream/control | Kills PTY and unregisters channel | | `reattach_terminal` | stream | Reads scrollback, replaces `Channel`, starts new pump | Notes: - `PtyChunk = Vec` today (`crates/app-tauri/src/pty.rs:29`). - The transport-specific registry is `PtyBridge`, stored in `AppState`. - High-frequency PTY bytes are explicitly excluded from global Tauri events. ### Layouts | Command | Class | Current boundary | | --- | --- | --- | | `load_layout` | request/response | layout read use case | | `mutate_layout` | request/response | layout mutation use case, emits `LayoutChanged` | | `list_layouts` | request/response | named layout list | | `create_layout` | request/response | named layout create | | `rename_layout` | request/response | named layout rename | | `delete_layout` | request/response | named layout delete | | `set_active_layout` | request/response | active layout write | ### First-run, profiles, local models, embedders | Command | Class | Current boundary | | --- | --- | --- | | `first_run_state` | request/response | first-run read use case | | `reference_profiles` | request/response | reference profile catalog | | `detect_profiles` | request/response | runtime detection use case | | `list_profiles` | request/response | profile store read | | `save_profile` | request/response | profile upsert | | `clone_opencode_profile_from_seed` | request/response | profile clone | | `delete_profile` | request/response | profile delete | | `configure_profiles` | request/response | first-run save | | `list_model_servers` | request/response | local model server config read | | `save_model_server` | request/response | local model server config write | | `preview_model_server_command` | request/response | pure command preview | | `delete_model_server` | request/response | local model server config delete | | `list_embedder_profiles` | request/response | embedder profile read | | `save_embedder_profile` | request/response | embedder profile write | | `delete_embedder_profile` | request/response | embedder profile delete | | `describe_embedder_engines` | request/response | embedder engine capabilities | | `dismiss_embedder_suggestion` | request/response | suggestion state write | ### Agents, live state, chat and resume | Command | Class | Current boundary | | --- | --- | --- | | `create_agent` | request/response | agent manifest/context creation | | `list_agents` | request/response | manifest read | | `get_project_work_state` | request/response | read-model aggregation | | `read_conversation_page` | request/response | paginated transcript read | | `list_live_agents` | request/response | in-memory live registry read | | `attach_live_agent` | request/response | live session rebind use case | | `stop_live_agent` | request/response | live session stop use case | | `read_agent_context` | request/response | agent Markdown read | | `update_agent_context` | request/response | agent Markdown write | | `delete_agent` | request/response | manifest/context delete | | `inspect_conversation` | request/response | best-effort conversation inspection | | `launch_agent` | stream | Spawns/reuses PTY or structured session, may register `Channel` | | `change_agent_profile` | request/response | hot-swap use case; may affect live session | | `agent_send` | stream | Structured chat turn, registers `Channel`, pumps reply stream | | `cancel_resume` | request/response | cancels scheduled auto-resume | | `set_resume_at` | request/response | human-set resume time | | `interrupt_agent` | request/response | orchestrator interrupt | | `delegation_delivered` | request/response | frontend write-portal ack | | `set_front_attached` | request/response | frontend mounted/unmounted signal | | `reattach_agent_chat` | stream | Replaces `Channel`, returns chat scrollback | | `close_agent_session` | request/response | closes live agent/structured state | | `list_resumable_agents` | request/response | resumable agent inventory | Structured chat stream: - `ReplyChunk` is tagged by `kind`: `textDelta`, `toolActivity`, `final`, `error` (`crates/app-tauri/src/dto.rs:1760`). - `ChatBridge` retains bounded scrollback and the current optional channel. - `agent_send` is a turn command plus streaming response; in HTTP/WS terms it should become a WS client frame that starts a turn on an attached session. ### Templates | Command | Class | Current boundary | | --- | --- | --- | | `create_template` | request/response | template create | | `update_template` | request/response | template update, emits drift/update events | | `list_templates` | request/response | template list | | `delete_template` | request/response | template delete | | `create_agent_from_template` | request/response | agent creation from template | | `detect_agent_drift` | request/response | drift inventory | | `sync_agent_with_template` | request/response | sync write | ### Git | Command | Class | Current boundary | | --- | --- | --- | | `git_status` | request/response | git status use case | | `git_stage` | request/response | git stage use case | | `git_unstage` | request/response | git unstage use case | | `git_commit` | request/response | git commit use case | | `git_branches` | request/response | branch list | | `git_checkout` | request/response | branch checkout | | `git_log` | request/response | log read | | `git_init` | request/response | repo init | | `git_graph` | request/response | graph read | ### Tickets and sprints | Command | Class | Current boundary | | --- | --- | --- | | `ticket_create` | request/response | ticket create | | `ticket_read` | request/response | ticket read | | `ticket_delete` | request/response | ticket delete | | `open_ticket_chat` | request/response | ephemeral ticket assistant open | | `close_ticket_chat` | request/response | ephemeral ticket assistant close | | `ticket_list` | request/response | ticket list | | `ticket_update` | request/response | ticket update | | `ticket_read_carnet` | request/response | carnet read | | `ticket_update_carnet` | request/response | carnet write | | `ticket_link` | request/response | link tickets | | `ticket_unlink` | request/response | unlink tickets | | `ticket_assign` | request/response | assign/unassign ticket agent | | `sprint_create` | request/response | sprint create | | `sprint_list` | request/response | sprint list | | `sprint_rename` | request/response | sprint rename | | `sprint_reorder` | request/response | sprint reorder | | `sprint_delete` | request/response | sprint delete | | `ticket_assign_sprint` | request/response | ticket sprint assignment | | `ticket_unassign_sprint` | request/response | ticket sprint removal | ### Skills and memory | Command | Class | Current boundary | | --- | --- | --- | | `create_skill` | request/response | skill create | | `update_skill` | request/response | skill update | | `list_skills` | request/response | skill list | | `delete_skill` | request/response | skill delete | | `assign_skill_to_agent` | request/response | skill assignment | | `unassign_skill_from_agent` | request/response | skill unassignment | | `create_memory` | request/response | memory create | | `update_memory` | request/response | memory update | | `list_memories` | request/response | memory list | | `get_memory` | request/response | memory read | | `delete_memory` | request/response | memory delete | | `read_memory_index` | request/response | memory index read | | `recall_memory` | request/response | memory recall | | `resolve_memory_links` | request/response | memory link resolution | ### Background tasks | Command | Class | Current boundary | | --- | --- | --- | | `spawn_background_command` | request/response | creates command-backed background task | | `cancel_background_task` | request/response | cancels task | | `retry_background_task` | request/response | retries task | | `list_background_tasks` | request/response | task read-model list | Lifecycle is pushed through `BackgroundTaskChanged` domain events; initial list is request/response. ### Desktop windows and focus | Command | Class | Current boundary | | --- | --- | --- | | `set_focused_project` | desktop-only + event | Tauri window/panel focus state; emits `focused-project://changed` | | `get_focused_project` | desktop-only | Tauri panel state read | | `list_open_view_windows` | desktop-only | `AppHandle.webview_windows()` | | `open_view_window` | desktop-only | `WebviewWindowBuilder` | | `close_view_window` | desktop-only | closes Tauri OS window | | `move_tab_to_new_window` | desktop-only | use case plus `WebviewWindowBuilder` | These do not migrate as-is to web. The shared backend may still keep workspace topology use cases, but the OS-window adapter remains desktop-only. ## Tauri Channel usage | Location | Channel type | Producer | Consumer | Web mapping | | --- | --- | --- | --- | --- | | `open_terminal` | `Channel` | PTY output pump | xterm view | WS `output` frames after `open_terminal` | | `reattach_terminal` | `Channel` | PTY output pump | xterm view | WS `attached` + `output` frames after `attach_terminal` | | `launch_agent` | `Channel` | PTY output pump for raw CLI agents | xterm/chat terminal view | WS `output` frames; structured sessions do not use this channel | | `agent_send` | `Channel` | structured reply pump | chat view | WS `chat.output` or generic live frames | | `reattach_agent_chat` | `Channel` | structured reply pump | chat view | WS attached/replay for structured session | Transport-owned registries: - `PtyBridge`: session id -> `(generation, Channel)`. - `ChatBridge`: session id -> `{generation, Option>, scrollback}`. These should not descend into domain/application. Their backend-core equivalent should be a transport-neutral outbound sink/subscription registry. ## Events emitted today ### Global domain event relay `events::spawn_relay` subscribes to `TokioBroadcastEventBus` and emits: - Tauri event name `domain://event` - Payload `DomainEventDto`, tagged by `type` - Skips `DomainEvent::PtyOutput`; PTY bytes use per-session channels Current `DomainEventDto` variants: - Project/lifecycle: `projectCreated`, `layoutChanged`, `remoteConnected` - Agents: `agentLaunched`, `agentLaunchFailed`, `agentExited`, `agentBusyChanged`, `agentProfileChanged`, `agentLivenessChanged`, `agentRateLimited`, `agentResumeScheduled`, `agentResumeCancelled`, `agentResumed`, `agentRateLimitSuspected` - Delegation/orchestration: `delegationReady`, `agentReplied`, `agentAnnouncement`, `orchestratorRequestProcessed`, `orchestratorChanged` - Background work: `backgroundTaskChanged`, `agentInboxChanged`, `agentWakeChanged` - Templates/skills: `templateUpdated`, `agentDriftDetected`, `agentSynced`, `skillAssigned` - Git: `gitStateChanged` - Tickets/sprints: `issueCreated`, `issueUpdated`, `issueDeleted`, `issueStatusChanged`, `issuePriorityChanged`, `issueCarnetUpdated`, `issueLinked`, `issueUnlinked`, `issueAgentAssigned`, `issueAgentUnassigned`, `sprintCreated`, `sprintRenamed`, `sprintReordered`, `sprintDeleted`, `issueSprintChanged` - Memory/embedder: `memorySaved`, `memoryDeleted`, `memoryIndexRebuilt`, `embedderSuggested` - Model server: `modelServerStatusChanged` - PTY placeholder: `ptyOutput` exists in DTO but is not emitted by the global relay ### Dedicated global events - `model_server_status_changed`: emitted in addition to `domain://event` for `ModelServerStatusChanged`. - `agent_launch_failed`: emitted in addition to `domain://event` for `AgentLaunchFailed`. - `view-window://lifecycle`: desktop-only, payload `{kind, panel, label}`. - `focused-project://changed`: desktop-only, payload `{project}`. For web, the domain-event set maps naturally to a low-frequency live WS stream. The duplicate dedicated events should be normalized in the shared core contract or kept as compatibility aliases only in the Tauri adapter. ## `tauri::State` composition root inventory `AppState::build(app_data_dir)` is the current composition root. It constructs concrete adapters and use cases, then Tauri stores it through `app.manage`. ### Concrete adapters and infra state currently built there - `TokioBroadcastEventBus` - `SystemClock` - `UuidGenerator` - `LocalFileSystem` - `FsProjectStore` - `FsWindowStateStore` - `PortablePtyAdapter` with sandbox enforcer - `TerminalSessions` - `StructuredSessions` - `StructuredSessionFactory` - `LocalProcessSpawner` - `CliAgentRuntime` - `FsProfileStore` - local model server manager/use cases - issue/ticket stores and providers - `PtyBridge` and `ChatBridge` transport bridges - profile/template/git/skill/memory/embedder stores and use cases - orchestrator service, watchers and per-project MCP servers - session-limit service, resume contexts and turn watcher - background task runner/store - focused project mutex - home/app-data derived paths ### Boundary that must be extracted Current shape: ```text React -> @tauri invoke/listen/Channel -> app-tauri commands/events/bridges -> AppState concrete composition root -> application use cases + infrastructure adapters ``` Target shape: ```text React -> TS gateways -> Tauri adapter OR HTTP+WS adapter -> driving adapter calls shared backend core facade -> application use cases + infrastructure adapters ``` The extraction line is inside `app-tauri::state`: - Move reusable construction into a shared backend core crate/facade, e.g. `BackendCore::build(config)`. - Keep Tauri-only concerns in `app-tauri`: resolving `app_data_dir` via Tauri path API, `app.manage`, command registration, `AppHandle`/`WebviewWindow`, shutdown hooks, Tauri event emission, Tauri `Channel` bridges, dialog plugin. - The future web server adapter should own HTTP route registration, TLS/pairing integration, WS connection lifecycle, CORS/origin checks and WS sink mapping. - The core should expose transport-neutral methods for the request/response use cases and transport-neutral subscription/sink APIs for PTY/chat/live output. Do not move `PtyBridge` or `ChatBridge` as-is. They are Tauri transport adapters. Move the underlying concept: attach/detach a sink to a live session, replay bounded scrollback, and deliver ordered output. ## HTTP DTO first draft Naming below keeps current DTO semantics and camelCase fields. Exact paths are a proposal for B1/B3; they can be namespaced by project where useful. ### Request/response examples ```http GET /api/health POST /api/projects GET /api/projects POST /api/projects/{projectId}/open POST /api/projects/{projectId}/close GET /api/projects/{projectId}/context PUT /api/projects/{projectId}/context GET /api/projects/{projectId}/layout POST /api/projects/{projectId}/layout/mutate GET /api/projects/{projectId}/agents POST /api/projects/{projectId}/agents GET /api/projects/{projectId}/agents/{agentId}/context PUT /api/projects/{projectId}/agents/{agentId}/context DELETE /api/projects/{projectId}/agents/{agentId} GET /api/projects/{projectId}/work-state GET /api/projects/{projectId}/conversation-page GET /api/projects/{projectId}/git/status POST /api/projects/{projectId}/git/stage POST /api/projects/{projectId}/git/commit GET /api/tickets POST /api/tickets GET /api/tickets/{ref} PATCH /api/tickets/{ref} GET /api/templates POST /api/templates GET /api/background-tasks POST /api/background-tasks ``` Errors should preserve `ErrorDto`: ```json { "code": "NOT_FOUND", "message": "project ..." } ``` ## WebSocket first draft One dedicated WS endpoint is enough for B5 V1: ```text GET /ws/live?token=... (token normally via Authorization/cookie, not URL in prod) ``` Authentication note from ticket #13: pairing/token must travel only over HTTPS and not as a URL secret. The query above is only a sketch; production should use `Authorization: Bearer` during WS upgrade or an HTTPS-only secure cookie. ### Common envelope Client to server: ```json { "id": "client-msg-uuid", "kind": "terminal.input", "payload": {} } ``` Server to client: ```json { "id": "server-msg-uuid", "replyTo": "client-msg-uuid", "kind": "terminal.status", "payload": {} } ``` `replyTo` is present for command acknowledgements, absent for unsolicited live events/output. ### PTY client -> server frames `attach_terminal` ```json { "id": "1", "kind": "terminal.attach", "payload": { "sessionId": "uuid", "lastSeq": 42 } } ``` `open_terminal` ```json { "id": "2", "kind": "terminal.open", "payload": { "projectId": "uuid", "nodeId": "uuid-or-null", "cwd": "/abs/path", "rows": 30, "cols": 120 } } ``` `launch_agent` ```json { "id": "3", "kind": "agent.launch", "payload": { "projectId": "uuid", "agentId": "uuid", "nodeId": "uuid-or-null", "rows": 30, "cols": 120, "conversationId": "engine-conversation-id-or-null" } } ``` `input` ```json { "id": "4", "kind": "terminal.input", "payload": { "sessionId": "uuid", "bytesBase64": "DQ==" } } ``` `resize` ```json { "id": "5", "kind": "terminal.resize", "payload": { "sessionId": "uuid", "rows": 40, "cols": 140 } } ``` `detach` ```json { "id": "6", "kind": "terminal.detach", "payload": { "sessionId": "uuid" } } ``` `close` ```json { "id": "7", "kind": "terminal.close", "payload": { "sessionId": "uuid" } } ``` `ping` ```json { "id": "8", "kind": "ping", "payload": { "atMs": 1784123456789 } } ``` ### PTY server -> client frames `attached` ```json { "kind": "terminal.attached", "replyTo": "1", "payload": { "session": { "sessionId": "uuid", "projectId": "uuid", "nodeId": "uuid", "rows": 30, "cols": 120 }, "nextSeq": 43, "scrollback": [ { "seq": 1, "bytesBase64": "..." } ], "gap": false } } ``` `output` ```json { "kind": "terminal.output", "payload": { "sessionId": "uuid", "seq": 43, "bytesBase64": "..." } } ``` `status` ```json { "kind": "terminal.status", "payload": { "sessionId": "uuid", "status": "running" } } ``` Suggested status values: `starting`, `running`, `exited`, `closed`, `detached`, `reattached`. `error` ```json { "kind": "error", "replyTo": "7", "payload": { "code": "NOT_FOUND", "message": "terminal session ..." } } ``` `pong` ```json { "kind": "pong", "replyTo": "8", "payload": { "atMs": 1784123456789 } } ``` ### Structured chat frames The ticket #13 PTY contract does not require structured chat in B5, but the current backend already has a second stream. To avoid a later incompatibility, reserve a parallel namespace: Client: ```json { "id": "9", "kind": "chat.attach", "payload": { "sessionId": "uuid" } } { "id": "10", "kind": "chat.send", "payload": { "sessionId": "uuid", "prompt": "..." } } { "id": "11", "kind": "chat.detach", "payload": { "sessionId": "uuid" } } ``` Server: ```json { "kind": "chat.attached", "replyTo": "9", "payload": { "sessionId": "uuid", "scrollback": [ { "kind": "textDelta", "text": "..." } ] } } ``` ```json { "kind": "chat.output", "payload": { "sessionId": "uuid", "chunk": { "kind": "final", "content": "..." } } } ``` ### Low-frequency live events Domain events can be pushed on the same WS connection: ```json { "kind": "event.domain", "payload": { "type": "agentBusyChanged", "agentId": "uuid", "busy": true } } ``` This replaces Tauri `listen("domain://event")` for web. Desktop can keep the existing event relay. ## Plan impact / risks found No finding invalidates the ticket #13 lot plan. Points to decide early in B1/B2: - The duplicate Tauri registration of `detect_agent_drift` in `generate_handler!` is harmless for B0 but should be cleaned in a separate fix. - `open_project` currently also triggers open-time reconciliation and per-project runtime side effects. The shared backend core facade should make those explicit methods, not hide them in transport commands. - `launch_agent` has both raw PTY and structured-session branches. B5 PTY V1 can implement raw PTY first, but the WS namespace should reserve chat/live frames. - `PtyBridge` and `ChatBridge` contain transport scrollback/replay behavior. Extract the behavior as a core subscription contract; do not leak Tauri `Channel` into the future core. - Desktop window/focus commands are cleanly separable as desktop-only. Workspace topology can remain shared, OS-window creation cannot.