Skip to content

[#159] Implement the minimal client interface (ls, activate, attach, destroy) - #434

Merged
0chroma merged 1 commit into
mainfrom
0chroma/feat-minimal-client-interface
Jun 19, 2026
Merged

[#159] Implement the minimal client interface (ls, activate, attach, destroy)#434
0chroma merged 1 commit into
mainfrom
0chroma/feat-minimal-client-interface

Conversation

@0chroma

@0chroma 0chroma commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Closes gominimal/inbox#159

Implements the client-side CLI interface in crates/minimal2 to interact with minimald over the SSH-based control protocol.

What's implemented

  • ls — Lists sessions via ListSessions RPC. --raw outputs one session ID per line for scripting (minimal ls --raw | fzf | xargs minimal attach).
  • activate — Creates a session via CreateSession RPC with optional --name and project path. --attach chains into attach after creation.
  • attach — Resolves a session by UUID or name via GetSessionRecord, then shells out to ssh for interactive PTY attachment. --command runs a non-interactive command in the session context. Uses a hidden proxy subcommand as the SSH ProxyCommand (no socat/nc dependency).
  • destroy — Resolves by UUID or name, then calls DestroySession RPC (feat(minimald): implement session destroy + RPC #462).
  • completions — Shell completion script generation via clap_complete.

Auto-spawn

On Linux, the CLI auto-spawns minimald run as a detached background process if the daemon UDS isn't connectable, then polls until ready (4s timeout). On macOS, auto-spawns minvmd via the existing state machine. No lifecycle state machine or state.toml — just socket polling. The lifecycle management PR (#435) can layer richer state tracking on top later.

Design decisions

  • The client binary (minimal2) has zero dependency on minimald internals — it talks exclusively through the minimald-rpc wire contract over SSH.
  • Both interactive and --command attach shell out to ssh rather than reimplementing termios/PTY management. The daemon's shell_request handler mints the PTY-backed session shell; ssh handles terminal reconfiguration and cleanup.
  • exec_in_session was removed entirely — per @twitchyliquid64's feedback, it used the daemon's old exec codepath which doesn't hit the sessions module. exec_request remains on the daemon side for git-receive-pack / vscode remote only.

Removed

  • dash subcommand — temporary fzf-based picker, removed in favor of a future proper TUI.

Summary by CodeRabbit

New Features

  • Added ls command to list active sessions
  • Added activate command to create and activate new sessions
  • Added attach command to connect to existing sessions
  • Added destroy command to remove sessions
  • CLI now supports comprehensive session management capabilities

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: c66fa106-e85f-4940-a9e8-b6e15827fd98

📥 Commits

Reviewing files that changed from the base of the PR and between 5896722 and 7e8a98d.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • crates/minimal2/Cargo.toml
  • crates/minimal2/src/client.rs
  • crates/minimal2/src/main.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • crates/minimal2/src/client.rs
  • crates/minimal2/Cargo.toml
  • crates/minimal2/src/main.rs

📝 Walkthrough

Walkthrough

Adds workspace dependencies to minimal2 (russh, serde, sessions, etc.), introduces a new client.rs module implementing SSH-over-UDS transport with retry-connect, passwordless auth, and JSON oneshot RPC, and rewrites main.rs into a full async CLI with ls, activate, attach, destroy, and hidden proxy subcommands.

Changes

minimal2 CLI daemon connectivity and session commands

Layer / File(s) Summary
Dependencies and project configuration
crates/minimal2/Cargo.toml
Adds workspace dependencies (camino, chrono, dirs, libc, minimald-rpc, paths, russh, serde, serde_json, sessions) and reorders entries so minvmd path dep precedes ot.workspace.
SSH-over-UDS client transport
crates/minimal2/src/client.rs
Defines MinimalClientHandler (always-accept host key), Client::connect with bounded UDS retries and passwordless SSH auth, Client::oneshot_rpc for JSON subsystem RPC, and resolve_socket_path for OS-specific daemon socket location.
CLI command structure and arguments
crates/minimal2/src/main.rs
Adds mod client, tokio/std IO imports, expands Command enum with Ls, Activate, Attach, Destroy, and hidden Proxy variants, and defines clap arg structs for each subcommand.
CLI dispatcher, daemon connection, and proxy bridge
crates/minimal2/src/main.rs
main dispatches to async handlers; connect_daemon resolves socket and constructs Client; cmd_proxy bridges stdin/stdout to a UDS using Tokio try_join for SSH ProxyCommand use.
Session lifecycle management commands
crates/minimal2/src/main.rs
Implements cmd_ls (formatted table or raw IDs), cmd_activate (path validation, CreateSession, optional attach chain), cmd_attach (session lookup, exec ssh with embedded proxy), cmd_destroy (session lookup, DestroySession), and shell_quote helper.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant CLI as minimal2 CLI
  participant Client as client::Client (SSH/UDS)
  participant minimald

  User->>CLI: minimal activate --path /proj --attach
  CLI->>Client: connect(resolve_socket_path())
  Client->>minimald: UnixStream + SSH handshake (retry)
  minimald-->>Client: authenticated
  CLI->>Client: oneshot_rpc(CreateSession)
  minimald-->>Client: session_id
  CLI->>Client: oneshot_rpc(GetSessionRecord)
  minimald-->>Client: session record
  CLI->>CLI: exec ssh -o ProxyCommand="minimal proxy <sock>"
  note over CLI,minimald: ProxyCommand bridges stdin/stdout to UDS
  CLI-->>User: interactive session
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐇 Hop hop through a socket so slim,
A tunnel of SSH on a whim,
oneshot_rpc flies,
Under UNIX-dark skies,
Sessions attached on a branch's slim rim! 🌿


Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

Comment @coderabbitai help to get the list of available commands and usage tips.

@0chroma

0chroma commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

Build Plan Summary

This implements the client-side CLI for the Minimal client interface (gominimal/inbox#159). See plans/2026-06-16-define-minimal-client-interface-v1.md for the full spec.

What this PR delivers (Phases 1-4)

Phase 1: Subcommand Structure -- Added activate, attach, destroy, and dash subcommands to the Command enum in crates/minimal2/src/main.rs, with typed argument structs and --minimal-dir global option propagation.

Phase 2: SSH Client Transport -- New crates/minimal2/src/client.rs module with:

  • Client::connect() -- UDS connection to minimald with retry logic, TOFU host key acceptance, and passwordless SSH auth
  • Client::oneshot_rpc::<R>() -- Generic RPC helper that opens a channel, requests the RPC-specific SSH subsystem (e.g. minimald-v1-list-sessions), writes the serialized JSON request, and decodes the response

Phase 3: Core Commands -- Wired in main.rs:

  • minimal ls -- Calls ListSessions RPC, outputs a formatted table (Session ID, Name, Title, Last Activity)
  • minimal activate -- Calls CreateSession RPC with project path and optional name; prints session ID; chains into attach with --attach
  • minimal destroy -- Stubbed; blocked on a DestroySession RPC in the wire contract

Phase 4: Exec-Based Attachment -- minimal attach <session> resolves sessions by UUID or name via GetSessionRecord, and supports non-interactive command execution via --command. PTY interactive shell is blocked on daemon-side PTY support (exec.rs:650-654).

Phase 5: Future -- minimal dash (k9s-like TUI) and fzf-based shell completion session picker are scoped for later iterations.

Key design decisions

  • The client binary (minimal2) has zero dependency on minimald internals -- it talks exclusively through the minimald-rpc wire contract over SSH
  • UDS connection uses retry logic to absorb the ~2s post-boot race on macOS/libkrun
  • Session lookup supports both UUID (SessionId::parse_str) and name fallback for ergonomics

@0chroma

0chroma commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

Build Plan Summary :

Phase 1: Subcommand Structure and Argument Parsing (DONE)

  • CLI subcommands: ls, activate, attach, destroy, dash, completions
  • activate accepts --name, project path, --attach
  • attach accepts session identifier + optional --command
  • Global --minimal-dir propagates to socket resolution

Phase 2: SSH Client Transport and RPC Wrapper (DONE)

  • UDS connection via minvmd::sock::resolve_uds_path() with retry logic
  • TOFU host key acceptance for local UDS
  • Generic oneshot_rpc::<R>() helper for typed RPC invocation

Phase 3: Core Oneshot Commands (DONE)

  • ls: ListSessions RPC with aligned table output (ID, Name, Title, Last Activity)
  • activate: CreateSession RPC, prints session ID, chains into attach with --attach
  • destroy: stubbed (blocked on DestroySession RPC in wire contract)

Phase 4: Interactive Session Attachment (DONE — exec path)

  • Name-or-UUID resolution via GetSessionRecord RPC
  • exec_in_session: SSH exec channel with MINIMAL_SESSION_ID env var
  • PTY interactive shell blocked on daemon-side PTY support

Phase 5: fzf Session Picker (DONE)

  • ls --raw: one session ID per line for minimal ls --raw | fzf scripting patterns
  • dash: interactive fzf picker that chains into attach; falls back to table if fzf not installed
  • Shell completions via clap_complete; future k9s-like TUI scoped for later iteration

Outstanding / Blocked

Item Blocker
destroy command No DestroySession RPC in minimald-rpc
PTY interactive shell Daemon rejects PTY requests (exec.rs:650-654)
Full k9s-like TUI Future iteration

@twitchyliquid64

Copy link
Copy Markdown
Member

Phase 4: Non-interactive exec-in-session (PTY attachment blocked on daemon PTY support at exec.rs:650-654)

FYI the run in session stuff is already implemented as a shell request, not an exec request. The exec request stuff is really just there for supporting git receive-pack and from when i was figuring out how it all worked- ill probably rip out that path soon.

In the short term, you could get around needing to implement management of the local terminal/termios and such just by shelling out to the ssh client (though i imagine we probably want to ProxyCommand to ourselves rather than assume the user has socat):

$> MINIMAL_SESSION_ID=<session uuid> ssh -o SendEnv=MINIMAL_SESSION_ID -o ProxyCommand='socat - UNIX-CONNECT:<socket>' -o 'UserKnownHostsFile=<socket>/../known_hosts' local-0

@0chroma
0chroma force-pushed the 0chroma/feat-minimal-client-interface branch 2 times, most recently from 4f4ab38 to 0c53669 Compare June 18, 2026 20:42
@0chroma

0chroma commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

Status update

Rebased on main (now includes the DestroySession RPC from #462) and applied all of @twitchyliquid64's feedback.

What's done

  • ls / activate — wired to ListSessions / CreateSession RPCs
  • destroy — wired to the new DestroySession RPC (feat(minimald): implement session destroy + RPC #462), resolves session by UUID or name
  • attach — both interactive and --command now shell out to ssh via a self-hosted proxy subcommand (ProxyCommand), so we don't depend on socat/nc. The daemon's shell_request handler mints the PTY-backed session shell; ssh handles termios/PTY reconfiguration for us.
  • dash — fzf-based session picker, chains into attach; falls back to plain ls table if fzf isn't installed
  • ls --raw — one session ID per line for piping

Removed

  • exec_in_session — dropped entirely per @twitchyliquid64's note that it used the daemon's old exec codepath which doesn't hit the sessions module. exec_request remains on the daemon side for git-receive-pack / vscode remote only.

Still outstanding

  • dash is pretty bare right now (no preview pane, no keybinds for destroy, etc.) — manual UX eval pending
  • fmt/clippy/tests all green locally; CI should be clean now

@0chroma
0chroma requested a review from twitchyliquid64 June 18, 2026 22:00
@0chroma

0chroma commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

Ready for review. All reviewer feedback applied (ssh ProxyCommand attach, DestroySession RPC, exec_in_session removed), E2E tested, fmt/clippy/tests green. Would appreciate a look when you're back from travels — no rush.

@0chroma
0chroma requested a review from norrietaylor June 18, 2026 22:01
Comment thread crates/minimal2/src/autospawn.rs Outdated
use super::super::client::resolve_socket_path;

/// Timeout for waiting on the UDS after spawning minimald.
const UDS_POLL_TIMEOUT: Duration = Duration::from_secs(4);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than wait 4 seconds on first start, it might be better to look for the PID file, see if that PID is alive, and only if so then timeout waiting for a connect() to succeed.

@norrietaylor do you have the deets on if/where the PID file?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be in xdg dir according to mike

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently pid files for minvmd are going here: ~/.local/state/minimal/minvmd/.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Glad to move them though 👍

@twitchyliquid64 twitchyliquid64 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hell yeah

0chroma added a commit that referenced this pull request Jun 19, 2026
minimald now writes a PID file at startup. The minimal2 auto-spawn logic
checks this file before spawning: if the PID is alive the daemon is
already starting (e.g. spawned concurrently), so it waits for the UDS
rather than spawning a duplicate. The spawn path uses Child::try_wait
to detect a crash during startup and fail fast instead of exhausting
the 4s timeout.

Addresses review feedback on #434.
@0chroma
0chroma enabled auto-merge (squash) June 19, 2026 21:47
@0chroma
0chroma force-pushed the 0chroma/feat-minimal-client-interface branch from 5896722 to 7e8a98d Compare June 19, 2026 21:53
@gominimal-aw-bot

Copy link
Copy Markdown
Contributor

This pull request has no accompanying spec. Comment /derive-spec to have one derived retrospectively from the code — it opens a separate spec/<slug> documentation PR with demoable units, acceptance criteria, and a gap analysis (implementation gaps, missing failure paths, weak acceptance criteria). Ignore this to defer; the weekly unspecced-PR scan will re-surface it. See ADR 0027.

@0chroma
0chroma merged commit eaca741 into main Jun 19, 2026
28 checks passed
@0chroma
0chroma deleted the 0chroma/feat-minimal-client-interface branch June 19, 2026 22:09
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.

3 participants