Add ACP agent backend and terminal controls

This commit is contained in:
2026-07-30 07:25:11 -05:00
parent dbfa8bcd48
commit ad24374f6d
84 changed files with 12151 additions and 157 deletions
+21 -12
View File
@@ -1,6 +1,6 @@
# Summary
Galaxy ships a local control CLI named `galaxyctrl` that lets agents, developers, and scripts operate running Galaxy app processes through a typed, allowlisted command surface. `galaxyctrl` is a thin wrapper that invokes the existing channel-specific Galaxy binary in control mode rather than a separate standalone binary.
The public catalog contains exactly **77 default-authorized actions** organized around stable user-facing nouns. Close actions flow through Galaxy's normal close behavior so existing app warnings remain authoritative. `block.list` is intentionally absent from the catalog. Input-staging commands place text in the input buffer but never submit it.
The public catalog contains exactly **80 default-authorized actions** organized around stable user-facing nouns. Close actions flow through Galaxy's normal close behavior so existing app warnings remain authoritative. `block.list` is intentionally absent from the catalog. Input-staging commands place text in the input buffer but never submit it. The separate Terminal family can inspect, execute in an idle visible session, and race-safely interrupt an expected active block.
All callers are external same-user processes. There is no inside-Galaxy/outside-Galaxy distinction, no verified-terminal invocation context, and no authenticated-user identity layer. Security relies on owner-only filesystem discovery, same-user Unix credential broker with kernel peer credentials, short-lived instance-bound exact-action credentials, loopback HTTP transport, and app-side enforcement.
## Problem
Galaxy has rich interactive actions reachable through UI, keybindings, menus, and deeplinks. Agents can use native tools for files, code, shell commands, and MCP calls, but they cannot reliably operate Galaxy's own product surfaces: arranging workspaces, focusing panes, presenting settings, or recovering from ambiguous UI state. Developers cannot compose those actions into shell scripts, demos, or automation workflows, and there is no general local protocol for addressing a specific running Galaxy instance, window, tab, pane, or session.
@@ -8,26 +8,26 @@ Galaxy has rich interactive actions reachable through UI, keybindings, menus, an
- Provide a first-class, scriptable `galaxyctrl` command for controlling running Galaxy app processes.
- Make Galaxy's UI and app state available to agents through a typed, permissioned control plane instead of brittle screen automation.
- Keep CLI startup lightweight by avoiding GUI-app startup for routine control commands.
- Keep the surface allowlisted and finite: exactly 77 named actions, no arbitrary internal dispatch.
- Keep the surface allowlisted and finite: exactly 80 named actions, no arbitrary internal dispatch.
- Make targeting explicit and deterministic across multiple Galaxy processes, windows, tabs, panes, and sessions.
- Use a simple enabled/disabled Galaxy Control setting rather than multi-mode invocation-context policies.
## Non-goals
- Replacing the general Galaxy AI CLI or mixing cloud management into this CLI.
- Exposing every internal app action, debug action, or privileged state mutation.
- Treating the CLI as a general RPC escape hatch into Galaxy internals.
- Replacing native agent tools for code editing, file operations, shell execution, or MCP calls.
- Replacing native agent tools for code editing, file operations, isolated shell execution, or MCP calls.
- Providing an authenticated-user identity layer, verified-terminal invocation proof, or invocation-context distinction.
- Terminal command execution, accepted-command submission, or agent-prompt submission.
- Terminal output reads, arbitrary force-kill primitives, or agent-prompt submission.
- Galaxy Drive data mutations, cloud-backed state mutations, or sharing operations.
- Local file content reads, writes, or filesystem-content mutations.
## Primary user stories
1. **Agent workspace orchestration.** An agent inspects current Galaxy state, creates or reuses an appropriate window/tab layout, splits panes, names and focuses targets, and leaves the workspace in a readable task-shaped state. The agent continues to use native tools for code edits, file I/O, shell execution, and MCP calls.
1. **Agent workspace orchestration.** An agent inspects current Galaxy state, creates or reuses an appropriate window/tab layout, splits panes, names and focuses targets, and leaves the workspace in a readable task-shaped state. The agent continues to use native tools for code edits, file I/O, isolated shell execution, and MCP calls, while the Terminal family is reserved for commands that must run in a specific visible Galaxy session such as Wormhole.
2. **Existing-session debugging and repair.** An agent understands Galaxy-specific UI and session structure before acting: which instance/window/tab/pane/session is active, whether the relevant pane still exists, which surface is focused, and which selector to use for follow-up actions.
3. **Deterministic demos and walkthroughs.** A script puts Galaxy into a known presentation state: theme, zoom, windows, tabs, panes, focused targets, panels, and surfaces. The walkthrough advances using structured target IDs and recovers from stale or missing targets.
4. **Personalization and preference migration.** An agent inspects settings, proposes Galaxy equivalents from other tools, applies allowlisted changes, and reports unsupported mappings explicitly.
## Behavior
1. The CLI operates only on running local Galaxy app processes from the same channel as the channel-specific CLI binary. If no compatible same-channel process is available, it exits non-zero with a structured error.
2. The CLI exposes only the 77 explicitly allowlisted actions. Unknown, unsupported, or non-allowlisted requests fail with structured errors and are never forwarded to arbitrary internal dispatch.
2. The CLI exposes only the 80 explicitly allowlisted actions. Unknown, unsupported, or non-allowlisted requests fail with structured errors and are never forwarded to arbitrary internal dispatch.
3. Every successful mutating request identifies the Galaxy process instance, resolved target, and a success payload suitable for JSON output.
4. Every failure identifies a stable machine-readable error code, a human-readable explanation, and any selector that was ambiguous, missing, stale, or invalid.
5. The CLI supports human-readable output by default and JSON output for scripts with stable field names.
@@ -55,11 +55,11 @@ Galaxy adds a top-level Settings page named **Galaxy Control**. The page contain
- **Disabled** (default on Stable, Preview, OSS, and Integration channels): no same-user process can receive local-control credentials. The control listener does not accept requests. Discovery records contain no actionable endpoint.
The authoritative value is stored in protected local storage (macOS Keychain, or owner-only secure storage on Linux). It is never synced, never appears in `settings.toml` or generated schemas, and cannot be changed by `galaxyctrl`, config files, or direct protocol requests. Only the Galaxy app through Settings > Galaxy Control or the Enable/Disable Galaxy Control Command Palette actions can change it. The default is channel-based: enabled on internal dogfood builds, disabled on public channels until the user opts in. Disabling Galaxy Control immediately prevents new credential issuance and invalidates outstanding credentials.
## Close behavior
`window.close`, `tab.close`, and `pane.close` execute after the same exact-action credential validation as the other 74 actions. They flow through Galaxy's normal close behavior, so existing warnings for unsaved files, running processes, shared sessions, and similar app state remain authoritative and may cancel the close.
`window.close`, `tab.close`, and `pane.close` execute after the same exact-action credential validation as the other 77 actions. They flow through Galaxy's normal close behavior, so existing warnings for unsaved files, running processes, shared sessions, and similar app state remain authoritative and may cancel the close.
## Input staging
The two input commands (`input.insert`, `input.replace`) only stage or edit text in the terminal input buffer. They never submit the buffer, press Enter, or execute a command. There is no `input.run`, `input.get`, `input.clear`, or `input.mode.set` action in the catalog. Terminal command execution is not part of this product surface.
The two input commands (`input.insert`, `input.replace`) only stage or edit text in the terminal input buffer. They never submit the buffer, press Enter, or execute a command. There is no `input.run`, `input.get`, `input.clear`, or `input.mode.set` action in the catalog. Command submission is deliberately isolated in `terminal.execute`, which requires an existing terminal-session target, validates bounded command text, and refuses to run unless the active block is idle.
## Action catalog
The public catalog contains exactly 77 actions. The Block, Auth, Drive, and History families are entirely absent. Input is limited to `input.insert` and `input.replace`. Actions are organized by noun and use the exact dotted names from the authoritative `ActionKind` catalog.
The public catalog contains exactly 80 actions. The Block, Auth, Drive, and History families are entirely absent. Input is limited to `input.insert` and `input.replace`; command control lives in the three-action Terminal family. Actions are organized by noun and use the exact dotted names from the authoritative `ActionKind` catalog.
### Instance (2 actions)
All default-authorized.
- `instance.list` — list reachable Galaxy app processes from the CLI binary's channel.
@@ -118,6 +118,11 @@ All default-authorized.
All default-authorized. **Input commands stage text only and never submit.**
- `input.insert` — insert text into the input buffer without executing.
- `input.replace` — replace the input buffer contents without executing.
### Terminal (3 actions)
All default-authorized. These actions operate only on an existing visible terminal session.
- `terminal.status` — return the active block ID, executing/pending/long-running state, agent-control state, idle state, elapsed running time, and a bounded secret-obfuscated command summary when available.
- `terminal.execute` — submit a validated command plus Enter only when the target terminal is idle. Empty, NUL-containing, and commands larger than 64 KiB are rejected.
- `terminal.interrupt` — send ETX only when the caller supplies the exact active `block_id` returned by `terminal.status` and that block is still running.
### Theme (6 actions)
All default-authorized.
- `theme.list` — list available themes.
@@ -148,7 +153,7 @@ All default-authorized.
- `keybinding.get` — get a specific keybinding.
### Action (2 actions)
All default-authorized.
- `action.list` — list all 77 catalog actions with implementation status.
- `action.list` — list all 80 catalog actions with implementation status.
- `action.inspect` — metadata for one action.
### Surface (13 actions)
All default-authorized.
@@ -174,9 +179,10 @@ The following families and actions are entirely absent even when internal implem
- The entire Auth family (`auth.status`, `auth.login`).
- The entire Drive family (all `drive.*` actions).
- The entire History family (`history.list`).
- `input.get`, `input.clear`, `input.mode.set`, `input.run`, and any form of terminal command execution or submission.
- `input.get`, `input.clear`, `input.mode.set`, and `input.run`.
- Terminal output reads, execution while a terminal is busy, interruption without an exact active block ID, and arbitrary process-kill primitives.
- `file.list` and any local file content reads, writes, or filesystem-content mutations.
- Accepted-command submission and agent-prompt submission.
- Agent-prompt submission.
- Crash, panic, heap-dump, token-copying, debug-reset, and developer/debug helpers.
- Arbitrary internal view dispatch by string.
- Arbitrary settings outside the allowlist.
@@ -190,6 +196,9 @@ Command names are noun-oriented and discoverable. Examples:
- `galaxyctrl window close --window <id>`
- `galaxyctrl pane split --direction right`
- `galaxyctrl input replace "cargo check"`
- `galaxyctrl terminal status --session <id>`
- `galaxyctrl terminal execute "cargo check" --session <id>`
- `galaxyctrl terminal interrupt --block-id <active-block-id> --session <id>`
- `galaxyctrl theme set "Galaxy Dark"`
- `galaxyctrl setting set appearance.themes.system_theme true`
- `galaxyctrl file open src/main.rs --line 42`
+82 -2
View File
@@ -1,6 +1,6 @@
# Galaxy Control operator README
`galaxyctrl` is the CLI entrypoint for controlling an already-running local Galaxy app instance. It is intended for scripts, demos, agent workflows, and developer automation that need to perform allowlisted Galaxy UI actions through the installed channel-specific Galaxy binary without launching the GUI.
The implemented command surface contains exactly 77 typed, allowlisted actions. All 77 actions execute after exact-action credential validation. Close actions flow through normal Galaxy close behavior, so existing app warnings remain authoritative. The Block, Auth, Drive, and History families are absent, and `input.insert` plus `input.replace` stage text without submitting it.
The implemented command surface contains exactly 80 typed, allowlisted actions. All 80 actions execute after exact-action credential validation. Close actions flow through normal Galaxy close behavior, so existing app warnings remain authoritative. The Block, Auth, Drive, and History families are absent; `input.insert` plus `input.replace` stage text without submitting it, while the separate Terminal family provides guarded control of commands in existing visible sessions.
## Packaging model
`galaxyctrl` is packaged as a thin channel wrapper rather than a standalone Rust binary. The wrapper resolves the installed channel-specific Galaxy executable and invokes it with the hidden `--galaxyctrl` control-mode flag:
- `crates/local_control` owns discovery records, local authentication material, client transport, protocol envelopes, action names, and error types.
@@ -10,6 +10,77 @@ The implemented command surface contains exactly 77 typed, allowlisted actions.
The control-mode path should initialize only the work needed for CLI parsing, instance discovery, local authentication loading, request serialization, HTTP transport, and output formatting. It should not initialize GUI state, terminal models, rendering, workspaces, or main-app startup paths.
Release artifacts and helper names may be channelized, but operator docs and examples use `galaxyctrl` unless an integration branch explicitly documents a channel-specific alias.
This branch wires the core hidden dispatch contract through the existing Galaxy binary. Platform packaging creates wrapper scripts that call the channel binary with `--galaxyctrl` instead of producing or selecting a separate `galaxyctrl` binary.
## MCP stdio bridge
`galaxyctrl mcp` exposes the current allowlisted Galaxy Control catalog to a
local MCP client over newline-delimited JSON-RPC on stdin/stdout. The bridge
does not implement app actions or hold a broad credential. Each tool call
still resolves the pinned instance, requests a short-lived credential for the
one requested action, and sends the normal authenticated `/v1/control`
request.
Launch the installed wrapper for a specific running Galaxy process:
```bash
galaxyctrl mcp --pid <Galaxy PID>
```
An in-process ACP runtime that launches the current Galaxy executable directly
should use:
```text
<current_exe> --galaxyctrl mcp --pid <Galaxy PID>
```
`--instance <instance_id>` is also supported. The selector is resolved once to
an opaque instance ID when the MCP server starts. Every later tool call
re-discovers only that exact ID, and MCP tool arguments cannot override it. An
unqualified launch succeeds only when exactly one compatible instance is
discoverable; multiple instances fail with `ambiguous_instance` instead of
silently choosing one. The remaining `galaxyctrl` target flags can provide
default window, tab, pane, and session selectors for invoked actions.
In normal catalog mode, the MCP server advertises two tools:
- `galaxy_control_capabilities` invokes the authenticated `capability.list`
action and returns the exact actions advertised by the selected app.
- `galaxy_control_invoke` accepts one implemented catalog action, an optional
in-app target, and action-specific parameters. Its JSON Schema is generated
from the shared action catalog and typed parameter contracts.
The bridge adds no capabilities beyond the shared catalog. In particular,
terminal command control is exposed only through the catalog's typed
`terminal.status`, `terminal.execute`, and `terminal.interrupt` contracts and
their app-side idle/race checks. Filesystem mutation, authentication, and cloud
API surfaces remain absent unless deliberately added to the shared catalog.
Galaxy's in-process ACP integration starts the bridge in a hidden
`--agent-safe` mode. That mode requires opaque `--window` and `--tab`
selectors plus exactly one opaque `--pane` or `--session` selector; it never
falls back to whichever pane is focused later. It advertises
`galaxy_terminal_status` for the delegated pane and only advertises
`galaxy_terminal_execute`, `galaxy_terminal_interrupt`, and
`galaxy_terminal_interrupt_at` when the user's ACP permission profile allows
the corresponding mutations. `galaxy_terminal_interrupt_at` is an MCP
composition over the existing status and interrupt actions: the bridge waits
outside the model loop, verifies that the same block is still running, and
then performs the exact-block interrupt. It is not an additional public
Galaxy Control catalog action.
The ACP host remains separate from Galaxy's LiteLLM and Bedrock provider
dispatch. It launches a configured local adapter, negotiates a standard ACP
authentication method, and gives that process only the agent-safe MCP bridge
for the exact local pane. Galaxy scrubs inherited environment variables before
launch, redacts secrets from outbound prompts, and binds persisted ACP session
IDs to a fingerprint of the effective adapter launch. A restored session is
not sent to an adapter whose launch identity no longer matches.
ACP process management is currently enabled only on Unix platforms, where the
pinned SDK terminates the adapter's complete process group. Galaxy fails closed
before spawning an adapter on Windows until the SDK can retain a Job Object
that kills launcher descendants as well as the direct child.
## Install and invocation guidance
### macOS
For local development checks, build the local Galaxy binary and invoke it with the hidden control-mode flag:
@@ -61,6 +132,15 @@ Use matching app and CLI bits from the same branch or release artifact so the pr
```bash
galaxyctrl tab list --instance <instance_id>
```
9. To control a command in a specific visible terminal, discover the session and active block first, then use the returned IDs:
```bash
galaxyctrl session list --instance <instance_id>
galaxyctrl terminal status --instance <instance_id> --session <session_id>
galaxyctrl terminal execute "sleep 30" --instance <instance_id> --session <session_id>
galaxyctrl terminal status --instance <instance_id> --session <session_id>
galaxyctrl terminal interrupt --block-id <active_block_id> --instance <instance_id> --session <session_id>
```
`terminal.execute` refuses busy sessions, and `terminal.interrupt` refuses a stale block ID so a delayed request cannot stop a newer command.
Expected failures:
- `galaxyctrl instance list` with no running compatible app: exits zero with an empty list;
- a command that needs a selected app when no compatible app is running: exits non-zero with a no-instance error;
@@ -107,6 +187,6 @@ sequenceDiagram
- Same-user malicious software can still invoke trusted wrappers or automate the desktop, so brokered credentials are least-privilege guardrails rather than a complete hostile same-user sandbox.
- Future catalog expansion should consider per-request nonces, stricter platform secure-storage constraints, and stronger approval or policy gates.
## Documentation review notes
- Keep examples scoped to the authoritative 77-action catalog and explicitly call out that close actions use normal Galaxy close behavior.
- Keep examples scoped to the authoritative 80-action catalog and explicitly call out that close actions use normal Galaxy close behavior.
- Do not document excluded families or actions as usable just because internal app implementations exist.
- Windows packaging may initially follow the existing helper-wrapper pattern. Update this README when that decision is final.
+50 -10
View File
@@ -19,12 +19,13 @@ Exact-action credentials are safety and intent mechanisms. They let a script or
- Authorize every action by its exact typed identity in the app bridge.
- Route close actions through normal Galaxy close behavior so existing app warnings for unsaved files, running processes, and shared sessions remain authoritative.
- Ensure the two input-staging commands (`input.insert`, `input.replace`) never submit the buffer. No other input actions exist.
- Keep the action surface at exactly 77 allowlisted actions. The Block, Auth, Drive, and History families are entirely absent.
- Restrict terminal command control to existing visible sessions, idle-only execution, and compare-and-swap interruption using the exact active block ID.
- Keep the action surface at exactly 80 allowlisted actions. The Block, Auth, Drive, and History families are entirely absent.
- Fail closed on platforms without owner-only discovery and authenticated broker transport.
- Preserve deterministic targeting so a request never silently mutates or reads the wrong target.
## Honest same-user limitations
The broker authenticates the connecting process's OS user through kernel peer credentials. It does not prove that the caller is the official `galaxyctrl` binary, Galaxy-signed code, or a human-approved invocation. When Galaxy Control is enabled, any process running as the same OS user can:
- Connect to the broker socket and request credentials for any of the 77 actions.
- Connect to the broker socket and request credentials for any of the 80 actions, including terminal command execution and interruption.
- Invoke `galaxyctrl` as a confused deputy.
The architecture therefore provides a **meaningful hard boundary** against:
- Other OS users.
@@ -98,7 +99,7 @@ Issuance flow:
1. Client connects to the broker socket.
2. Broker verifies peer UID.
3. Client requests a credential naming one exact action.
4. Broker checks Galaxy Control is enabled and the action is in the 77-action catalog.
4. Broker checks Galaxy Control is enabled and the action is in the 80-action catalog.
5. Broker mints a short-lived credential in memory.
6. Client receives the credential.
Constraints:
@@ -117,7 +118,7 @@ The broker authenticates the OS user, not the calling application. Any same-user
- Exact-action credentials prevent accidental action overreach.
- Short expiry limits the window for credential reuse.
- Normal Galaxy close behavior preserves existing warnings for close actions.
- No `input.run` action exists, so `galaxyctrl` cannot be used to execute terminal commands.
- The Terminal family cannot read terminal output, execute over a busy active block, or interrupt a command without the exact current block ID.
- Protected enablement prevents silent activation of the control surface.
- App-side bridge enforcement re-checks every credential on every request.
These mitigations route operations through intentional flows. They do not guarantee that arbitrary same-user software cannot cause Galaxy-visible actions.
@@ -151,16 +152,54 @@ Rules:
- Index selectors resolve to concrete IDs before execution.
- Session-scoped requests against non-terminal panes return `target_state_conflict`.
## Input staging safety
The two input commands (`input.insert`, `input.replace`) only stage text in the terminal input buffer. They never submit the buffer, press Enter, or execute a command. No other input actions (`input.get`, `input.clear`, `input.mode.set`, `input.run`) exist in the 77-action catalog. Tests must prove no submission occurs.
The two input commands (`input.insert`, `input.replace`) only stage text in the terminal input buffer. They never submit the buffer, press Enter, or execute a command. No other input actions (`input.get`, `input.clear`, `input.mode.set`, `input.run`) exist in the 80-action catalog. Tests must prove no submission occurs.
## Terminal command control safety
Terminal command control is a separate, explicit high-impact family:
- `terminal.status` returns only active block identity and state, elapsed running time, and a bounded command summary produced by Galaxy's secret-obfuscating command path. It does not return terminal output, environment variables, or unredacted command text.
- `terminal.execute` resolves an existing visible terminal-session target, rejects empty/NUL-containing/over-64-KiB commands, and submits only when the active block is idle and its staged input buffer is empty. It never clears or overwrites text the user has already typed, and it does not silently queue behind or append to a running command.
- `terminal.interrupt` requires the caller to echo the exact current `block_id` from `terminal.status`. The app compares it against the active block immediately before emitting ETX and rejects stale IDs, preventing a delayed request from interrupting a newer command.
- ACP's agent-safe MCP profile can provide `galaxy_terminal_interrupt_at`, which waits in the local bridge rather than relying on model polling. It repeatedly verifies the pinned target and expected block ID, exits harmlessly if that block stops or changes, and delegates the final mutation to the same exact-block `terminal.interrupt` contract.
- ACP terminal tools reject panes already identified as remote or Wormhole targets, and the agent-safe execute surface rejects recognized SSH launches rather than deliberately extending delegated terminal control across another transport boundary.
- The SSH detector covers direct, wrapped, and compound launch forms. It is a transport-boundary guard, not a general network sandbox: an otherwise authorized local command may still use other network clients or make its own network connections.
Galaxy Control enablement is therefore authorization for same-user callers to execute commands in visible terminal sessions. Public channels keep the feature disabled until the user explicitly opts in. Exact-action credentials and the checks above reduce accidental overreach; they do not make a hostile same-user process safe.
## ACP process boundary
ACP adapters are trusted local programs, not a sandboxed extension:
- Galaxy redacts known secrets from outbound ACP prompt text and image
attachment URIs while retaining the original local transcript.
- The adapter starts with a small runtime environment allowlist plus only
explicitly configured variables; inherited provider keys, cloud credentials,
and proxy credentials are cleared.
- Authentication uses methods advertised by the adapter. Galaxy does not
scrape or synthesize ChatGPT subscription tokens.
- Persisted ACP session IDs are bound to the effective adapter launch identity
and are rejected when that identity changes.
- Broad adapter-native permission requests are denied by default. Concrete
Galaxy terminal tools are separately gated, and command execution is not
advertised when the configured denylist cannot be enforced exactly.
- ACP process startup is currently Unix-only. Non-Unix platforms fail before
spawning the adapter until the pinned SDK provides full process-tree
ownership, such as a Windows Job Object with kill-on-close behavior.
These measures reduce accidental credential and authority leakage. They do not
prevent a trusted adapter executable from reading files the current OS user can
read. Allowing arbitrary terminal execution is also not a network-egress
sandbox: a permitted local command can invoke scripts, interpreters, or other
network clients that static SSH recognition cannot prove safe.
## Catalog boundary
The catalog contains exactly 77 actions. The following families and actions are entirely absent:
The catalog contains exactly 80 actions. The following families and actions are entirely absent:
- The entire Block family (`block.list`, `block.inspect`, `block.output`).
- The entire Auth family (`auth.status`, `auth.login`).
- The entire Drive family (all `drive.*` actions).
- The entire History family (`history.list`).
- `input.get`, `input.clear`, `input.mode.set`, `input.run`, and any form of terminal command execution.
- `input.get`, `input.clear`, `input.mode.set`, and `input.run`.
- Terminal output reads, execution over a busy active block, interruption without an exact active block ID, and arbitrary process-kill primitives.
- `file.list` and any local file content operations beyond the `file.open` app-state intent.
- Accepted-command submission and agent-prompt submission.
- Agent-prompt submission.
- Debug, crash, heap-dump, token-copying, and developer-only helpers.
- Arbitrary internal view dispatch by string.
Adding a new action requires extending the catalog, implementing validation, adding a handler, and adding tests for credential denial and success behavior.
@@ -214,12 +253,13 @@ Avoid logging: bearer credentials, terminal output, command text, input buffer c
## Required controls before catalog expansion
Before shipping each action family:
- Galaxy Control must be enabled for any request to succeed.
- The action has a documented entry in the 77-action catalog.
- The action has a documented entry in the 80-action catalog.
- The bridge verifies the credential grants that exact action.
- Ambiguous, missing, and stale targets return structured errors.
- Close actions flow through normal Galaxy close behavior.
- Input actions never submit the buffer.
- Terminal execution remains idle-only, and terminal interruption remains guarded by the exact active block ID.
- Tests cover the allowed path and the wrong-action-credential denial path.
- Logs and errors do not expose credentials, terminal contents, or sensitive settings.
- The Block, Auth, Drive, and History families remain absent from the catalog.
- The catalog contains exactly 77 default-authorized actions.
- The catalog contains exactly 80 default-authorized actions.
+28 -11
View File
@@ -1,5 +1,5 @@
# Context
`PRODUCT.md` defines Galaxy Control (`galaxyctrl`) with an allowlisted catalog of exactly 77 actions, deterministic addressing across multiple running Galaxy app processes, and a simple enabled/disabled Galaxy Control setting. `SECURITY.md` is the normative security architecture. If this technical plan and `SECURITY.md` disagree, update the plan before implementing.
`PRODUCT.md` defines Galaxy Control (`galaxyctrl`) with an allowlisted catalog of exactly 80 actions, deterministic addressing across multiple running Galaxy app processes, and a simple enabled/disabled Galaxy Control setting. `SECURITY.md` is the normative security architecture. If this technical plan and `SECURITY.md` disagree, update the plan before implementing.
The design is external-only: all callers are same-user processes. There is no inside-Galaxy/outside-Galaxy distinction, no verified-terminal invocation context, and no authenticated-user identity layer. Security relies on owner-only filesystem discovery, same-user Unix credential broker with kernel peer credentials, short-lived instance-bound exact-action credentials, loopback HTTP transport, and app-side enforcement.
## Existing building blocks
- `crates/http_server/src/lib.rs` runs a native-only loopback Axum server on fixed port `9277`.
@@ -22,12 +22,13 @@ Before implementing any local-control listener, CLI command, credential path, or
- The app bridge verifies the exact granted action before selector resolution or handler dispatch.
- Close actions (`window.close`, `tab.close`, `pane.close`) flow through normal Galaxy close behavior so existing app warnings remain authoritative.
- Input-staging commands never submit the buffer. There is no `input.run` action.
- The Block, Auth, Drive, and History families are entirely absent from the 77-action catalog. Input is limited to `input.insert` and `input.replace`.
- Terminal execution is idle-only, and interruption requires the exact active block ID immediately before ETX is emitted.
- The Block, Auth, Drive, and History families are entirely absent from the 80-action catalog. Input is limited to `input.insert` and `input.replace`.
### 1. Protocol crate and stable envelope
Create a shared protocol crate used by both the app server and the `galaxyctrl` client. It defines:
- A request protocol version for defensive schema guarding.
- Discovery/health response types.
- The 77-action `ActionKind` enum with implementation status metadata. The Block, Auth, Drive, and History families are entirely absent; Input is limited to `input.insert` and `input.replace`.
- The 80-action `ActionKind` enum with implementation status metadata. The Block, Auth, Drive, and History families are entirely absent; Input is limited to `input.insert` and `input.replace`, with command control isolated in the Terminal family.
- Selector types:
- `InstanceSelector`: `Active`, `Id(InstanceId)`, `Pid(u32)`.
- `WindowSelector`: `Active`, `Id(WindowId)`, `Index(u32)`, `Title(String)`.
@@ -145,22 +146,36 @@ Selector behavior:
- Title/name selectors are exact by default and return `ambiguous_target` on multiple matches.
- Session-scoped requests against non-terminal panes return `target_state_conflict`.
Target resolution happens after credential authentication and exact-action verification.
### 7. Close behavior
### 7. Terminal command handler
The Terminal family resolves an existing pane/session through the shared target resolver and obtains the terminal view without changing focus.
- `terminal.status` takes one short-lived `TerminalModel` lock, snapshots the active block ID, state, and elapsed running time, and produces a bounded command summary through the existing secret-obfuscating API.
- `terminal.execute` validates the command before target resolution, then re-snapshots the active block and verifies that the staged input buffer is empty inside the terminal-view update. It drops the model lock before writing the command plus carriage return to `TerminalView::write_to_pty`; it never clears or overwrites staged input. Busy blocks and nonempty staged input return `target_state_conflict`.
- `terminal.interrupt` re-snapshots immediately before the PTY write, compares the caller's expected block ID, verifies the block is still running, drops the model lock, and emits ETX. A different active block returns `stale_target`.
The handler never holds `TerminalModel` across another view update or PTY event, and does not acquire nested terminal-model locks. Because it uses the existing terminal PTY event path, Wormhole and other remote sessions retain their normal transport routing.
The hidden ACP `--agent-safe` MCP profile is pinned to opaque window, tab, and
pane/session identifiers. It exposes concrete terminal tools instead of the
generic catalog invoker and filters mutations according to the active ACP
permission profile. Its deadline helper waits in the MCP subprocess, polls
bounded status for the same block, and invokes the existing exact-block
interrupt only if the deadline is reached while that block is still active.
### 8. Close behavior
The 3 close actions (`window.close`, `tab.close`, `pane.close`) flow through normal Galaxy close behavior after exact-action credential validation and deterministic target resolution. Existing warnings for unsaved files, running processes, shared sessions, and similar app state remain authoritative and may cancel the close.
### 8. CLI parsing and output
### 9. CLI parsing and output
The CLI uses Galaxy's existing command-line libraries:
- **clap** (derive) for argument parsing and subcommand trees.
- **serde** / **serde_json** for JSON serialization.
- **clap_complete** for shell completion generation.
- `OutputFormat` enum (`Pretty`, `Json`, `Ndjson`, `Text`) shared from `galaxy_cli`.
New subcommand types live in `galaxy_cli::local_control` and follow existing `#[derive(Parser)]` patterns.
### 9. CLI packaging
### 10. CLI packaging
The shipped product is a bundled `galaxyctrl` wrapper script that calls the channel-specific Galaxy binary with a hidden `--galaxyctrl` flag:
- **macOS:** A channel-specific wrapper in `Resources/bin` (`galaxyctrl` for Stable, otherwise `galaxyctrl-<channel>`).
- **Linux:** Standalone release and validation archives include a `galaxyctrl` wrapper, and normal app packages install channel-specific Galaxy AI and Galaxy Control launchers.
- **Windows:** Fails closed until authenticated broker transport is implemented.
Startup: `app/src/lib.rs` recognizes `--galaxyctrl` before app launch and routes into `galaxy_cli::local_control`. The control-mode path initializes only command parsing, discovery, credential material, HTTP transport, and output formatting. It does not initialize GUI state, rendering, or terminal session models.
### 10. Feature flag
### 11. Feature flag
Gate behind `FeatureFlag::GalaxyControlCli` with Cargo feature `galaxy_control_cli`.
When disabled:
- No Galaxy Control settings page.
@@ -171,7 +186,7 @@ When enabled:
- All local-control infrastructure starts when Galaxy Control is enabled (the default on internal dogfood channels; public channels require explicit opt-in through Settings > Galaxy Control or the Enable Galaxy Control Command Palette action).
- `resources/bundled/skills/galaxyctrl/SKILL.md` teaches the built-in agent and users how to discover and invoke the allowlisted CLI surface.
- The skill manager maps `galaxyctrl` to `FeatureFlag::GalaxyControlCli` through `BundledSkillActivation`. Both skill listing and direct bundled-skill reads enforce the activation state.
### 11. First slice: discovery + `tab.create`
### 12. First slice: discovery + `tab.create`
The first implementation slice proves the end-to-end architecture:
- Shared protocol types and error envelopes.
- `FeatureFlag::GalaxyControlCli` and Cargo feature.
@@ -185,12 +200,13 @@ The first implementation slice proves the end-to-end architecture:
- Exact-action credential issuance and enforcement.
- `app.ping`, `app.version`, `instance.list`, and `tab.create`.
- Structured success/error output in pretty and JSON formats.
### 12. Follow-up slices
### 13. Follow-up slices
After the first slice validates the architecture, add remaining catalog actions in family groups:
- Window/tab mutations (including close through normal Galaxy close behavior).
- Pane mutations (including close through normal Galaxy close behavior).
- Session actions.
- Input staging (insert and replace only, never submitting).
- Terminal status, idle-only execution, and race-safe interruption.
- Appearance/theme actions.
- Settings reads and writes.
- Surface availability, idempotent direct opens, and toggles.
@@ -232,13 +248,14 @@ sequenceDiagram
- **Selector resolution:** Tests for active, explicit ID, index, stale target, ambiguous target, missing target, and target-state-conflict cases.
- **Channel isolation:** Discovery tests prove that a CLI scan excludes records published by other Galaxy channels.
- **Input staging:** Only `input.insert` and `input.replace` exist. No `input.run`, `input.get`, `input.clear`, or `input.mode.set`. Tests prove no buffer submission occurs.
- **Terminal command control:** Empty/NUL/oversized commands and busy targets are rejected. Interrupt tests cover matching, stale, and idle block IDs. The model lock is released before PTY writes.
- **Excluded families:** The Block, Auth, Drive, and History families are entirely absent. The CLI rejects their command routes at parse time, the protocol rejects their action names at deserialization (`invalid_request`), and `action.inspect`/`capability.inspect` report non-catalog names as `not_allowlisted`.
- **Unsupported platforms:** Windows fails closed with no fallback.
- **Action count:** Tests verify the catalog contains exactly 77 uniformly authorized actions.
- **Action count:** Tests verify the catalog contains exactly 80 uniformly authorized actions.
- **Bundled skill gate:** Tests verify the `galaxyctrl` bundled skill is discoverable and readable only while `FeatureFlag::GalaxyControlCli` is enabled, without affecting unrelated bundled skills.
## Risks and mitigations
- **Same-user residual risk:** The broker authenticates the OS user, not the calling application. Any process running as the same user can request credentials. Mitigated by: protected enablement, short expiry, exact-action grants, app-side revalidation, normal Galaxy close warnings for close actions.
- **Browser-to-localhost:** Mitigated by: no permissive CORS, Origin header rejection, Host header validation, credential requirement.
- **Fixed-port contention:** Mitigated by: leaving `9277` undisturbed, using per-process ephemeral ports for control.
- **Input execution risk:** Mitigated by: no `input.run` in the catalog, input commands stage text only, tests prove no submission.
- **Terminal execution risk:** Mitigated by: public-channel opt-in, exact-action grants, deterministic session targets, idle-only execution, bounded validated commands, secret-obfuscated status, and compare-and-swap interruption.
- **Heavyweight CLI startup:** Mitigated by: `--galaxyctrl` routes before GUI launch, control-mode path initializes only what's needed.