Skip to content

Fleet Studio B2b: wire the Enable-control act end-to-end + re-home it over the composer #1551

Description

@jeonghun-jj-lee

Important

Problem — On a base peer-studio session (a remote Studio session opened on the MacBook), the Enable control affordance is a no-op and is mispositioned. Clicking it does nothing: the app posts the amicode.fleet.enableControl command over the app→extension bridge, but that command is not on the bridge allowlist and no extension command is registered for it, so the envelope is dropped before anything runs. The self-owned enable path (evaluateControlBootstrap / enableSelfOwnedControl) has zero live callers — it is unit-tested but never invoked. Separately, the affordance renders as a fixed button in the top-right over the titlebar, not over the composer. Net effect: B2b's headline act (take control of a peer session) cannot actually be performed from the UI despite the state channel and write plane being wired.
Approach — Wire the enable act end-to-end and re-home the affordance. (1) Replace the bare command post with a payload-carrying bridge envelope (carrying the target owner machineId + sessionID), mirroring the existing project-selected / bug-report envelope kinds — the bare command lane cannot carry a target. The extension handler shows the VS Code native-modal confirm (ADR 0034 D4) and, on confirm, calls enableSelfOwnedControl to mint + persist the self-owned control grant; the next fleet-projection poll flips amicode_control to interactive and lights the driving banner. (2) Re-home the affordance from the fixed top-right Portal into a composer-anchored overlay: blur the composer and center a "This session lives on <peer> — Enable control to drive it" card with the button; the composer is gated (not typeable) until control is held.
Scope — in: the app→extension bridge envelope + allowlist for the enable act; the registered extension handler (native modal → enableSelfOwnedControl → grant persistence); the composer-scrim placement + gating; the projection→interactive flip driving the banner. · out: the shared-peer Request control handshake (that stays inert, backend is #1545); any change to the observation read/write planes (#1542/#1543) or the state-channel projection shape (#1544); real-board device writes.
Assumptions — Self-owned peer (JJ owns both machines) → isSelfOwned is true, so eligibility is enable-control (live), not request-control. The control grant is persisted through the existing lifecycle-grant writer; the projection resolver (buildControlResolver) already reads control grants keyed by targetMachineId, so no resolver change is needed once a grant exists.

Acceptance Criteria

  • Clicking Enable control on a self-owned remote peer session shows a VS Code native-modal confirm; confirming mints and persists an active self-owned control grant for the target peer.
  • After the grant is minted, the session's projected amicode_control resolves to interactive, the affordance disappears, and the persistent "Driving <peer>" banner appears.
  • Cancelling the native modal mints no grant and leaves the affordance and gated composer unchanged.
  • The enable envelope carries the target owner machineId (and sessionID) and is honored only for that peer; an envelope for a non-self-owned peer does not mint a grant (no-privilege-bleed, matching evaluateControlBootstrap).
  • The affordance renders as an overlay over the composer, not the titlebar: the composer is blurred and its input is not editable while control is not held; the CTA card is centered on it with the peer name.
  • The composer becomes editable (control held / interactive) or when the session is local — the scrim never appears on a local or already-controlled session.
  • The bridge allowlist / handler additions are exercised by a test; the sidebar-fleet regression guard (Fleet Studio B2b · slice 4 — Control UI: enable, driving banner, fail-closed affordances #1544 AC5 — no control markers in the read-only sidebar) still holds.

Testing Decisions

  • Extend the existing fleet_control_bootstrap test suite for the handler's decision path (authorize on self-owned+verified, refuse otherwise) — reuse, don't recreate.
  • Extend the chat-bridge test(s) that pin BRIDGE_ALLOWED_COMMANDS / envelope handling to cover the new enable envelope (allow + payload shape + reject unknown peer).
  • Extend the session-header app test suite (which already asserts the affordance markers and drivingBannerFromProjection wiring) for the composer-scrim placement + gated-composer state, replacing the top-right-Portal assertions.
  • Keep the sidebar_fleet_section regression guard green (control affordances never leak into the read-only sidebar).

Key Decisions

  • Payload envelope, not the command lane. postAmicode is command-string-only; the enable act needs a target. Add a dedicated bridge kind (e.g. fleet-enable-control) carrying { ownerMachineId, sessionID }, handled beside the existing kind handlers — do not overload the generic command relay.
  • Modal + mint live in the extension host. The native modal and enableSelfOwnedControl run extension-side (the app cannot show a VS Code modal nor persist a grant); the app only posts intent and re-reads the projection.
  • Grant persistence flips the projection; the UI does not self-declare interactive. The banner/affordance derive from the projected amicode_control, which re-resolves from the persisted grant on the next /amicode/fleet/sessions poll — the single source of truth stays the SoT, not optimistic local state.
  • Composer gating is the observe-vs-drive signal. Blurred + non-editable composer with the CTA is the deliberate "you are observing, not driving" state; enabling control is the one act that lifts it.

Constraints & Invariants

Prior Art

  • fleet_control_bootstrap (evaluateControlBootstrap, enableSelfOwnedControl) — the dead-but-tested enable modules this issue gives a live caller.
  • remote_session_state (projectSessionControlState, buildControlResolver) — the SoT→projection resolver that flips to interactive once a grant exists.
  • chat_bridge (BRIDGE_ALLOWED_COMMANDS, handleAmicodeBridgeMessage) + the chat panel's onDidReceiveMessage kind handlers (app-ready / session-context / watch-files / project-selected) — the envelope precedent to mirror.
  • session-fleet-peers (controlAffordance, drivingBannerFromProjection) + the session header's affordance/banner render — the app surface to re-home.
  • ADR 0034 (D4 native-modal confirm, D5 fail-closed reasons, D7 surface placement) — the design of record.

Source

Notes

This is the last mile of B2b: #1544 shipped the state channel (projection → chip/affordance) and #1542 the write plane (which only bites once a grant exists), but the enable act that produces the grant was never connected to a command. It matches the honest gap recorded during the campaign ("CONFIRM safety NOT wired live … NO live caller").

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    afkImplementable without human interaction

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions