Skip to content
Merged
12 changes: 12 additions & 0 deletions .changeset/ssh-launch-presets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@internal/server": minor
"@internal/node": patch
---

Organize saved launch settings into agent and SSH presets. Save SSH connection choices, including authentication preferences, from the launch form or preset library. Keep recent destination suggestions and replace the personal SSH settings page with the SSH preset tab.

Validate SSH destination syntax before saving, show field-level feedback, and normalize preset names on the server.

An SSH preset update now keeps the stored alias and description when the request omits them, and refuses a failed save in plain words instead of a raw response body. The SSH preset table follows the shared preset-table layout, and a required field left empty now shows its caption as a gold requirement rather than a red error.

The `list_presets` MCP tool now says so in its own words: it lists saved agent presets, and SSH presets stay dashboard-only.
5 changes: 5 additions & 0 deletions .changeset/ssh-preset-documentation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@internal/docs": patch
---

Document agent and SSH presets, the New preset menu, and connection settings saved by SSH presets. Explain when local keys and passwords are supplied and how saved destinations are checked before connecting.
5 changes: 5 additions & 0 deletions .changeset/ssh-website-feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@internal/website": patch
---

Add an SSH feature section describing remote shells, authentication options, and saved connection presets.
5 changes: 3 additions & 2 deletions .claude/rules/security-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ stay at the source. Bounded configuration scanning does not prevent all local
execution during `ssh -G`; trust the connecting account’s SSH configuration. First-use destination confirmation cannot replace
a conflicting pin. Host-pin recovery APIs remain even though their settings UI is
gone. Passwords enter through the terminal, are not saved as credentials, and are
still visible to trusted transport endpoints. Saved destinations are not trust pins.
still visible to trusted transport endpoints. Recent suggestions and SSH presets
are not trust pins.
**Local key signing** (`/api/ssh/local-key*`, cookie-session only) feeds signatures
through a session-scoped, sequence-checked, one-use bridge: one key, ≤8 requests,
90 s. The browser worker's disposal is a lifetime boundary, NOT a defense against
Expand Down Expand Up @@ -185,7 +186,7 @@ Per-app mechanics (command lists and shapes, assistant screens, the reset path,

Read via `GET /api/audit?limit=50` (admin). Rules: auth-family metadata carries method and ids, NEVER tokens/emails/IPs/UAs; the user-management family names its subject by email by convention (§3). Sign-in rows are SUCCESS only (failures write nothing so the trail can't be spam-DoSed); `auth.sign_out` is one row per session ended; trusted-origin probes write no row.

Auth `auth.sign_in` / `auth.sign_out` · users/keys `user.create|role_change|password_reset|disabled_change|approve|reject`, `system-key.create|enable|disable|delete`, `emergency_login.rewrite_credential`, `setup_key.create` (no metadata) / `setup_key.revoke`, `settings.update` · auth providers `auth_provider.create|update|delete` (fields + issuer, never the secret) · subshells `subshell.create|terminate|restart|preset_switch|delete` · transfers `transfer.create|complete` (node ids, paths, byte and entry counts, sync flag, outcome; never contents) · nodes `node.enroll|delete|rename|key_rotate|reregister_key|reregister|allowed_dirs.update|config.update|maintenance.update|ssh_enabled.update|ssh_identity.register (signing-key bootstrap over the link, ids + via only, never the key)|ssh_machine_pin.repair (verified peer replacement; ids only)|ssh_identity.repair (admin-verified server disk identity; metadata null)|ssh_host_pin.create|ssh_host_pin.delete (canonical destination and public host fingerprint only)|ssh_relay.open|ssh_relay.close (plane rows, actorUserId null, named reason word, fingerprint COUNT only)|ssh_upgrade.run (the "Set up Subshell here" act, Task 14: pane/B/A ids and relay ref, canonical destination, exec id, outcome + service-chosen stage word; the minted setup key and ALL installer output appear in NO row and NO refusal copy)|logging.update|update|update.unknown|shares_set|local_share_changed|service`, and the SSH launcher's `ssh.launch` `{nodeId, destination, subshellId, relayRef on a relay launch}` (success only, written after the pane spawned; the canonical `host:port`, never config or key material; saved-host CRUD unaudited, the prompts precedent) · server `server.config.update|restart|logging.update|autostart.update|update|supervision.request` · plugins/networks/installers `plugin.install|enable|disable|uninstall|unpublish`, `network.configure|install|join|publish|unpublish|leave`, `agent.install`, `tmux.install`. Per-event metadata detail: §10.
Auth `auth.sign_in` / `auth.sign_out` · users/keys `user.create|role_change|password_reset|disabled_change|approve|reject`, `system-key.create|enable|disable|delete`, `emergency_login.rewrite_credential`, `setup_key.create` (no metadata) / `setup_key.revoke`, `settings.update` · auth providers `auth_provider.create|update|delete` (fields + issuer, never the secret) · subshells `subshell.create|terminate|restart|preset_switch|delete` · transfers `transfer.create|complete` (node ids, paths, byte and entry counts, sync flag, outcome; never contents) · nodes `node.enroll|delete|rename|key_rotate|reregister_key|reregister|allowed_dirs.update|config.update|maintenance.update|ssh_enabled.update|ssh_identity.register (signing-key bootstrap over the link, ids + via only, never the key)|ssh_machine_pin.repair (verified peer replacement; ids only)|ssh_identity.repair (admin-verified server disk identity; metadata null)|ssh_host_pin.create|ssh_host_pin.delete (canonical destination and public host fingerprint only)|ssh_relay.open|ssh_relay.close (plane rows, actorUserId null, named reason word, fingerprint COUNT only)|ssh_upgrade.run (the "Set up Subshell here" act, Task 14: pane/B/A ids and relay ref, canonical destination, exec id, outcome + service-chosen stage word; the minted setup key and ALL installer output appear in NO row and NO refusal copy)|logging.update|update|update.unknown|shares_set|local_share_changed|service`, and the SSH launcher's `ssh.launch` `{nodeId, destination, subshellId, relayRef on a relay launch}` (success only, written after the pane spawned; the canonical `host:port`, never config or key material; SSH preset CRUD unaudited, the prompts precedent) · server `server.config.update|restart|logging.update|autostart.update|update|supervision.request` · plugins/networks/installers `plugin.install|enable|disable|uninstall|unpublish`, `network.configure|install|join|publish|unpublish|leave`, `agent.install`, `tmux.install`. Per-event metadata detail: §10.

## Deliberate non-validation & accepted risks

Expand Down
2 changes: 1 addition & 1 deletion apps/docs/STYLE.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ Use diagrams for plugin loading, command flow, and dependencies when they clarif

## Presets and reusable prompts

Keep Presets and Reusable Prompts as distinct task-oriented sections. A preset records agent configuration and optional launch defaults; a reusable prompt records task instructions. A library stack references current saved prompts, while selecting a prompt or stack into a launch form or preset copies its text. Explain that distinction when describing edits, reuse, and deletion.
Keep Presets and Reusable Prompts as distinct task-oriented sections. Agent presets record agent configuration and optional launch defaults; SSH presets record connection choices. A reusable prompt records task instructions. A library stack references current saved prompts, while selecting a prompt or stack into a launch form or agent preset copies its text. Explain that distinction when describing edits, reuse, and deletion.

Distinguish typing from submitting: the dashboard's **Insert prompt…** action types text without Enter; the reader reviews it and submits it in the pane. Sharing means every account on the instance, not a selected group. Withdrawing library access cannot recall already-copied text. Prompt instructions are not enforceable security permissions.

Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/docs/developers/publish-plugin.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ Test the packed package against a compiled Subshell server, not only the source
For the Bash example:

1. Confirm Bash is installed on the execution machine.
2. In the dashboard sidebar, open **Presets** → **New preset** and choose **Docs Shell**.
2. In the dashboard sidebar, open **Presets** → **New preset** → **New agent preset** and choose **Docs Shell**.
3. Save a preset with the login-shell setting enabled.
4. Open **Subshells** → **New subshell**, choose that preset, and select an accessible working directory and online node.
5. Confirm that the shell starts. Run `printf 'plugin works\n'` in its pane and inspect the output.
Expand Down
32 changes: 21 additions & 11 deletions apps/docs/content/docs/guides/ssh.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: "Connect over SSH"
description: "Open an SSH terminal through a node, choose keys or a password, and verify unfamiliar destinations."
description: "Open an SSH terminal through a node, choose keys or a password, verify unfamiliar destinations, and reuse connection choices as SSH presets."
---

Open an SSH terminal on another machine through one of your Subshell nodes.
Expand All @@ -17,12 +17,9 @@ For example, you could open the dashboard on your phone, connect from a node in
2. Select **SSH terminal**.
3. Under **Connect from**, choose the node that will make the SSH connection.
4. Under **SSH destination**, enter an address such as `developer@build.example.com:22`, or choose a suggestion. SSH aliases come from the connecting node's SSH configuration; their suggestions show the resolved username, host, and port when available.
5. Select **Remember this destination** if you want to keep it in your saved suggestions when you start the subshell.
5. To reuse these choices, select **Save as preset** and enter a **Preset name**. Subshell saves the preset before trying the connection.

<figure className="docs-screenshot">
<ImageZoom src="/screenshots/ssh-connection.png" alt="SSH form with Build node selected as Connect from and developer@build.example.com:22 as the destination. Remember this destination is checked." width="1100" height="666" loading="lazy" decoding="async" unoptimized />
<figcaption>Choose the node that makes the connection before entering the destination. The example address is sample data. Select the image to enlarge it.</figcaption>
</figure>
To fill the form from an existing preset, choose it under **SSH preset**. Review its connection details before starting; selecting a preset doesn't connect.

If outgoing SSH is disabled on a node you own, the gold notice explains that starting the subshell will enable it. For someone else's node, its owner must enable SSH first. The form names the owner when available and checks every five seconds while you wait. An administrator manages this setting for the built-in node on the server host.

Expand Down Expand Up @@ -98,19 +95,32 @@ Connections using the connecting node's own keys or a password use that node's O

Once connected, the terminal shows the destination's shell. Its top bar has an **SSH connection** icon with a tooltip identifying it as an SSH connection. If you selected password authentication, wait for its password prompt before typing. Only the owner signed in to the dashboard can type in an SSH subshell; MCP and other bearer credentials cannot send input. Session [sharing](/guides/sharing) lets other users view its output.

## Saved and recent destinations
## SSH presets and recent destinations

In the dashboard sidebar, open **Presets** → **New preset** → **New SSH preset**. This option is available from either the **Agent** or **SSH** tab. If your SSH library is empty, you can also select **Create your first SSH preset** on that tab.

Enter a **Name**, choose the connecting node and destination, then select an authentication option. Names can contain up to 120 characters. Select **Save preset** to keep these choices without starting a connection.

A preset's **⋯** menu offers **Edit**, **Clone preset**, **Delete preset**, and **Connect**. **Connect** opens the new-subshell form with those settings filled in; review them and select **Start SSH subshell** when you're ready. Editing or deleting a preset doesn't change an existing connection or reset a verified destination identity.

Presets save the resolved username, host, and port, along with the original SSH alias so its routing and key settings still apply. If that alias later resolves to a different address, Subshell blocks the connection until you edit the preset's destination to approve the new address. Recent destinations remain available as suggestions in the connection form, including separate entries for different usernames on the same host.

The authentication options determine what you save now and what you supply when creating a new SSH subshell:

Open **Settings → SSH** in the dashboard sidebar to manage your destinations. **Saved destinations** contains entries you've remembered; **Recent destinations** lets you save one later. Use a row's **Forget** action to remove it from saved suggestions.
- **Use the connecting node’s keys** uses that node's SSH configuration when connecting.
- **Use another node’s keys** reveals a node picker directly beneath the option. Choose the node and keys to save their source and public-key fingerprints. Subshell checks access and available keys again before connecting.
- **Use a key from my machine** saves the authentication preference. Choose and approve a key when creating a new SSH subshell; your key isn't saved in the preset.
- **Enter a password when connecting** saves the authentication preference. When creating a new SSH subshell, enter your password in the terminal when SSH asks for it.

You don't need a separate label. Subshell uses the address or SSH alias. In the connection form, saved and recent suggestions with an alias show the stored username, host, and port beneath it. Subshell keeps different usernames on the same host as separate destinations. For example, `developer@build.example.com:22` and `operator@build.example.com:22` are distinct entries.
Presets never store your private key, key file, passphrase, native approval, or destination password.

Remembering a destination saves a suggestion, not a password. Forgetting it doesn't close an open subshell or reset its verified identity.
If you select **Save as preset** while launching and the connection fails, the saved preset remains available. Retrying the same choices doesn't create another copy. Previously remembered destinations aren't converted into named presets; create presets for the connections you want to reuse.

## Make the destination a node

As the SSH subshell's owner, open its **⋯** menu and choose **Set up Subshell here…** when available. Review the confirmation before running the installer: it installs Subshell on the destination and enrolls it as a node you own. Your SSH subshell keeps running.

A subshell opened with a one-time key cannot reuse that key for this installer. Run the node setup command in its existing terminal, or reconnect using a node’s keys first.
A subshell opened with a one-time key cannot reuse that key for this installer. Run the node setup command in its existing terminal, or reconnect using a node's keys first.

This requires node enrollment to be allowed and an SSH login that works without interactive prompts. Password-only login can't supply a stored password to the installer. You can close and reopen the dialog to check progress. Results remain available for one hour while the server keeps running. Progress isn't retained across a server restart; check **Nodes** and the destination before retrying an interrupted installation. If it reports enrollment without a confirmed connection, open the new node's page and check its service before trying another installation.

Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/docs/mcp/configure.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ The compiled server and node binaries both include an `mcp` subcommand. The host

## Manual integration

Hermes and pi require one-time host setup. In the server dashboard's sidebar, choose **Presets** → **New preset** and select the agent. Find **Cross-subshell comms** near the bottom of the form for the registration commands. Run those commands on the machine where that agent will execute. See [Hermes Agent](/agents/hermes) and [pi](/agents/pi).
Hermes and pi require one-time host setup. In the server dashboard's sidebar, choose **Presets** → **New preset** → **New agent preset** and select the agent. Find **Cross-subshell comms** near the bottom of the form for the registration commands. Run those commands on the machine where that agent will execute. See [Hermes Agent](/agents/hermes) and [pi](/agents/pi).

For a node with `subshell` on PATH, a standard registration has this shape:

Expand Down
4 changes: 2 additions & 2 deletions apps/docs/content/docs/mcp/tools.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Successful MCP responses contain JSON encoded in a text content block. Failures
| `list_subshells` | List visible sessions and output previews. |
| `get_subshell` | Inspect one session. |
| `list_nodes` | List owned machines and launch availability. |
| `list_presets` | List saved presets and harness catalog entries. |
| `list_presets` | List saved agent presets and harness catalog entries. |
| `create_subshell` | Launch from a saved preset, or open a plain terminal without one. |
| `restart_subshell` | Revive a session in place. |
| `terminate_subshell` | Stop the process and keep the session record. |
Expand Down Expand Up @@ -64,7 +64,7 @@ Each harness row has `harnessId`, `name`, `installed`, and an optional refusal `

## list_presets

Arguments: `{}`. Returns rows with `id`, `name`, and `harnessId`.
Arguments: `{}`. Returns agent presets and harness catalog entries with `id`, `name`, and `harnessId`; SSH presets are dashboard-only and never appear here.

Real presets include `crossCommReady`, true when communication is enabled and the preset supplies a node and directory. Harness catalog rows have `catalogOnly: true`; an agent harness catalog row is informational and needs a human to save a preset before it can launch, but a plain terminal harness can launch presetless straight from `create_subshell`.

Expand Down
4 changes: 3 additions & 1 deletion apps/docs/content/docs/presets/create.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ description: "Configure a coding agent with reusable flags, environment values,

Save a coding-agent configuration from the dashboard.

For SSH connection presets, follow [Connect over SSH](/guides/ssh#ssh-presets-and-recent-destinations).

## Before you start

Sign in to the server dashboard. The agent's harness plugin must be installed and enabled; an administrator manages plugins. Install and authenticate the agent CLI on the execution machine before trying to launch it.
Expand All @@ -13,7 +15,7 @@ This example creates a Codex preset named **Repository review**. Choose flags su

## Save the configuration

1. In the dashboard sidebar, open **Presets** and select **New preset**.
1. In the dashboard sidebar, open **Presets** → **New preset** → **New agent preset**.
2. Choose **Codex** in **Agent**.
3. Enter `Repository review` in **Name**.
4. In **Paste command**, enter the following command:
Expand Down
Loading
Loading