Imported from Capsem triage report ui-02-gateway-client-duplication.md.
- Severity:
medium
- Category:
duplication
- Area:
capsem-tray, capsem-tui
- Location:
crates/capsem-tray/src/gateway.rs:1-201 and crates/capsem-tui/src/gateway_provider.rs:135-525
- Confidence:
verified
Summary
The tray and the TUI each independently reimplement the same loopback gateway
client: token discovery, gateway.port/gateway.token file reading, Bearer
auth, status fetch, and the identical VM-action endpoint set
(/vms/{id}/stop, /vms/{id}/pause, /vms/{id}/resume, /vms/{id}/delete,
/vms/create, /purge, /profiles/list). There is no shared gateway-client
crate (confirmed: capsem-core and capsem-proto expose none). This violates the
project's "one way to do things" / "capsem-core is the shared library"
invariant and means endpoint or auth changes must be made in (at least) two
Rust places plus the TS client.
Evidence
Tray (crates/capsem-tray/src/gateway.rs):
discover() reads run_dir/gateway.port + run_dir/gateway.token (lines 66-88).
stop_vm/delete_vm/suspend_vm/resume_vm hit /vms/{id}/stop|delete|pause|resume (159-177).
provision_temp() POSTs /vms/create (180-200).
TUI (crates/capsem-tui/src/gateway_provider.rs):
gateway_port() reads run_dir/gateway.port (187-191); fetch_token GETs /token (135-148).
invoke_action() hits the same /vms/{id}/stop|pause|resume|delete, /vms/create, /purge (324-441).
fetch_status GETs /status and deserializes a StatusResponse/VmSummary pair (150-166, 532-572).
Both define their own StatusResponse/VmSummary serde structs with
overlapping fields. The endpoint paths and the port/token discovery rules are
duplicated verbatim.
Notably the two clients have already diverged in subtle ways:
- The tray's
parse_port_file validates the port (rejects non-numeric/overflow,
gateway.rs:35-40), while the TUI silently swallows a bad port with
.ok()? and falls back to 19222 (gateway_provider.rs:187-191).
- The tray authenticates
/status with a Bearer token; the TUI does too, but
fetches the token from a different endpoint shape. Divergence like this is
exactly what a shared client prevents.
Impact
Endpoint, auth, or discovery changes must be applied in multiple crates and can
silently drift (as the port-validation difference shows). Higher maintenance
cost and a real risk of behavioral inconsistency between the tray and the TUI
talking to the same gateway.
Suggested fix
Extract a single capsem-gateway-client (or a module in capsem-core, honoring
the thin-shell rule) that owns: port/token discovery, the authenticated
reqwest::Client, the status/profiles structs, and the VM-action methods. Have
both the tray's GatewayClient and the TUI's GatewayProvider delegate to it.
Triage
Confirmed from the local reviewed report in /Users/elie/git/capsem/tmp/bugs/ui-02-gateway-client-duplication.md. Track implementation in the triage sprint; add regression coverage before fixing.
Imported from Capsem triage report
ui-02-gateway-client-duplication.md.mediumduplicationcapsem-tray, capsem-tuicrates/capsem-tray/src/gateway.rs:1-201andcrates/capsem-tui/src/gateway_provider.rs:135-525verifiedSummary
The tray and the TUI each independently reimplement the same loopback gateway
client: token discovery,
gateway.port/gateway.tokenfile reading, Bearerauth, status fetch, and the identical VM-action endpoint set
(
/vms/{id}/stop,/vms/{id}/pause,/vms/{id}/resume,/vms/{id}/delete,/vms/create,/purge,/profiles/list). There is no shared gateway-clientcrate (confirmed: capsem-core and capsem-proto expose none). This violates the
project's "one way to do things" / "capsem-core is the shared library"
invariant and means endpoint or auth changes must be made in (at least) two
Rust places plus the TS client.
Evidence
Tray (
crates/capsem-tray/src/gateway.rs):discover()readsrun_dir/gateway.port+run_dir/gateway.token(lines 66-88).stop_vm/delete_vm/suspend_vm/resume_vmhit/vms/{id}/stop|delete|pause|resume(159-177).provision_temp()POSTs/vms/create(180-200).TUI (
crates/capsem-tui/src/gateway_provider.rs):gateway_port()readsrun_dir/gateway.port(187-191);fetch_tokenGETs/token(135-148).invoke_action()hits the same/vms/{id}/stop|pause|resume|delete,/vms/create,/purge(324-441).fetch_statusGETs/statusand deserializes aStatusResponse/VmSummarypair (150-166, 532-572).Both define their own
StatusResponse/VmSummaryserde structs withoverlapping fields. The endpoint paths and the port/token discovery rules are
duplicated verbatim.
Notably the two clients have already diverged in subtle ways:
parse_port_filevalidates the port (rejects non-numeric/overflow,gateway.rs:35-40), while the TUI silently swallows a bad port with.ok()?and falls back to 19222 (gateway_provider.rs:187-191)./statuswith a Bearer token; the TUI does too, butfetches the token from a different endpoint shape. Divergence like this is
exactly what a shared client prevents.
Impact
Endpoint, auth, or discovery changes must be applied in multiple crates and can
silently drift (as the port-validation difference shows). Higher maintenance
cost and a real risk of behavioral inconsistency between the tray and the TUI
talking to the same gateway.
Suggested fix
Extract a single
capsem-gateway-client(or a module in capsem-core, honoringthe thin-shell rule) that owns: port/token discovery, the authenticated
reqwest::Client, the status/profiles structs, and the VM-action methods. Haveboth the tray's
GatewayClientand the TUI'sGatewayProviderdelegate to it.Triage
Confirmed from the local reviewed report in
/Users/elie/git/capsem/tmp/bugs/ui-02-gateway-client-duplication.md. Track implementation in the triage sprint; add regression coverage before fixing.