Lot B0 du chantier server/client mode : cartographie des surfaces de transport existantes, préalable à l'extraction du cœur backend commun. - docs/ticket13-b0-backend-transport-inventory.md : inventaire backend. - docs/ticket13-f0-frontend-transport-inventory.md : inventaire frontend. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
23 KiB
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 Tauriinvokemaps to one backend result. HTTP candidate.stream: command usestauri::ipc::Channelor 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<AppState>.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<PtyChunk>, 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<PtyChunk>, starts new pump |
Notes:
PtyChunk = Vec<u8>today (crates/app-tauri/src/pty.rs:29).- The transport-specific registry is
PtyBridge, stored inAppState. - 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<PtyChunk> |
change_agent_profile |
request/response | hot-swap use case; may affect live session |
agent_send |
stream | Structured chat turn, registers Channel<ReplyChunk>, 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<ReplyChunk>, returns chat scrollback |
close_agent_session |
request/response | closes live agent/structured state |
list_resumable_agents |
request/response | resumable agent inventory |
Structured chat stream:
ReplyChunkis tagged bykind:textDelta,toolActivity,final,error(crates/app-tauri/src/dto.rs:1760).ChatBridgeretains bounded scrollback and the current optional channel.agent_sendis 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<PtyChunk> |
PTY output pump | xterm view | WS output frames after open_terminal |
reattach_terminal |
Channel<PtyChunk> |
PTY output pump | xterm view | WS attached + output frames after attach_terminal |
launch_agent |
Channel<PtyChunk> |
PTY output pump for raw CLI agents | xterm/chat terminal view | WS output frames; structured sessions do not use this channel |
agent_send |
Channel<ReplyChunk> |
structured reply pump | chat view | WS chat.output or generic live frames |
reattach_agent_chat |
Channel<ReplyChunk> |
structured reply pump | chat view | WS attached/replay for structured session |
Transport-owned registries:
PtyBridge: session id ->(generation, Channel<PtyChunk>).ChatBridge: session id ->{generation, Option<Channel<ReplyChunk>>, 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 bytype - 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:
ptyOutputexists in DTO but is not emitted by the global relay
Dedicated global events
model_server_status_changed: emitted in addition todomain://eventforModelServerStatusChanged.agent_launch_failed: emitted in addition todomain://eventforAgentLaunchFailed.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<AppState> 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
TokioBroadcastEventBusSystemClockUuidGeneratorLocalFileSystemFsProjectStoreFsWindowStateStorePortablePtyAdapterwith sandbox enforcerTerminalSessionsStructuredSessionsStructuredSessionFactoryLocalProcessSpawnerCliAgentRuntimeFsProfileStore- local model server manager/use cases
- issue/ticket stores and providers
PtyBridgeandChatBridgetransport 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:
React -> @tauri invoke/listen/Channel
-> app-tauri commands/events/bridges
-> AppState concrete composition root
-> application use cases + infrastructure adapters
Target shape:
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: resolvingapp_data_dirvia Tauri path API,app.manage, command registration,AppHandle/WebviewWindow, shutdown hooks, Tauri event emission, TauriChannelbridges, 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
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:
{
"code": "NOT_FOUND",
"message": "project ..."
}
WebSocket first draft
One dedicated WS endpoint is enough for B5 V1:
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:
{
"id": "client-msg-uuid",
"kind": "terminal.input",
"payload": {}
}
Server to client:
{
"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
{
"id": "1",
"kind": "terminal.attach",
"payload": {
"sessionId": "uuid",
"lastSeq": 42
}
}
open_terminal
{
"id": "2",
"kind": "terminal.open",
"payload": {
"projectId": "uuid",
"nodeId": "uuid-or-null",
"cwd": "/abs/path",
"rows": 30,
"cols": 120
}
}
launch_agent
{
"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
{
"id": "4",
"kind": "terminal.input",
"payload": {
"sessionId": "uuid",
"bytesBase64": "DQ=="
}
}
resize
{
"id": "5",
"kind": "terminal.resize",
"payload": {
"sessionId": "uuid",
"rows": 40,
"cols": 140
}
}
detach
{
"id": "6",
"kind": "terminal.detach",
"payload": {
"sessionId": "uuid"
}
}
close
{
"id": "7",
"kind": "terminal.close",
"payload": {
"sessionId": "uuid"
}
}
ping
{
"id": "8",
"kind": "ping",
"payload": {
"atMs": 1784123456789
}
}
PTY server -> client frames
attached
{
"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
{
"kind": "terminal.output",
"payload": {
"sessionId": "uuid",
"seq": 43,
"bytesBase64": "..."
}
}
status
{
"kind": "terminal.status",
"payload": {
"sessionId": "uuid",
"status": "running"
}
}
Suggested status values: starting, running, exited, closed,
detached, reattached.
error
{
"kind": "error",
"replyTo": "7",
"payload": {
"code": "NOT_FOUND",
"message": "terminal session ..."
}
}
pong
{
"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:
{ "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:
{
"kind": "chat.attached",
"replyTo": "9",
"payload": {
"sessionId": "uuid",
"scrollback": [
{ "kind": "textDelta", "text": "..." }
]
}
}
{
"kind": "chat.output",
"payload": {
"sessionId": "uuid",
"chunk": { "kind": "final", "content": "..." }
}
}
Low-frequency live events
Domain events can be pushed on the same WS connection:
{
"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_driftingenerate_handler!is harmless for B0 but should be cleaned in a separate fix. open_projectcurrently 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_agenthas both raw PTY and structured-session branches. B5 PTY V1 can implement raw PTY first, but the WS namespace should reserve chat/live frames.PtyBridgeandChatBridgecontain transport scrollback/replay behavior. Extract the behavior as a core subscription contract; do not leak TauriChannelinto the future core.- Desktop window/focus commands are cleanly separable as desktop-only. Workspace topology can remain shared, OS-window creation cannot.