Files
my-pi/pi-ssh/extension-spec.md
T

8.3 KiB
Raw Blame History

pi-ssh extension specification

Goal

Pi and its default tools remain local. Explicit collision-free tools perform selected operations on one configured remote server:

  • ssh_connect
  • ssh_cd
  • ssh_read
  • ssh_write
  • ssh_edit
  • ssh_find
  • ssh_grep
  • ssh_bash

The extension is for operating servers from local projects, not for replacing Pi's local workspace. It never auto-loads remote project instructions.

Connection model

Runtime communication uses Node ssh2 only. OpenSSH is not spawned during Pi sessions. A user must explicitly import a concrete OpenSSH alias before connecting:

ssh_config.sh import <alias>
  → ssh -G <alias>
  → user chooses private-key or password auth
  → ssh2 obtains host key, user confirms fingerprint
  → ssh2 verifies authentication
  → encrypted vault is updated

The resulting host ID is the only runtime selector. When a direct user request names that imported host as part of a concrete remote task, the agent calls:

ssh_connect({ hostId: "<host-id>", remotePath?: "/absolute/or/~/path" })

ssh_connect is the only runtime connection surface. It is a sequential, cancellable model tool governed by pi-permission-system: the agent calls it as a separate step and waits for success before dependent remote tools, so establishing or replacing a connection cannot overlap another tool call. Unknown IDs return at most ten sorted imported IDs and never endpoint credentials. There is no /ssh command, --ssh flag, session-resume reconnect, or user ! remote-shell override.

Arbitrary user@host, port overrides, ProxyJump, and ProxyCommand are not supported in the first pure-ssh2 version. Unsupported imported configuration is rejected rather than ignored.

Vault

Default paths:

${XDG_CONFIG_HOME:-$HOME/.config}/my-pi/pi-ssh/hosts.enc
${XDG_CONFIG_HOME:-$HOME/.config}/my-pi/pi-ssh/vault.key

The directory is mode 700 and files are mode 600 on POSIX. A random adjacent 256-bit key encrypts and authenticates the complete validated configuration with AES-256-GCM. Each write uses a fresh IV and atomic replacement; plaintext is never written to a temporary file.

The adjacent-key design prevents casual disclosure of hosts.enc alone but does not protect against code able to read both files as the same local user.

The encrypted payload includes endpoint, authentication data, host groups, default cwd, and a pinned SHA256 host-key fingerprint. Private keys remain in their original paths; only their path and optional passphrase are stored.

Authentication

Supported methods:

  • password;
  • single-password keyboard-interactive;
  • OpenSSH private key;
  • encrypted private key with passphrase.

Passwords, passphrases, private-key contents, vault key, and decrypted configuration must never enter command arguments, logs, permission evidence, system prompts, or Pi session entries.

Host-key verification

Import displays the observed host-key algorithm and SHA256 fingerprint for explicit confirmation. Runtime hostVerifier must compare the server key with the pinned fingerprint and fail closed on any mismatch. Key changes require explicit host update.

Transport

One persistent ssh2.Client belongs to the active host. The transport persists, but there is no persistent interactive shell or PTY. Connection loss fails closed and no operation is automatically replayed.

Shell

ssh_bash opens a fresh exec channel and non-interactive Bash process for every call. Commands run through bash -lc after changing to the active remote cwd. The cwd supplied by Pi's local Bash factory is ignored at the transport adapter boundary; only connection state may select the remote cwd. A command-local cd is intentionally temporary; shell-local cd, exports, aliases, functions, and other process state do not persist across calls. Output streams through the normal Pi Bash operations callback. Abort or timeout closes the channel without reconnecting or replaying.

ssh_cd is the explicit persistent workspace operation and declares Pi's sequential execution mode so it cannot overlap sibling tool calls. It resolves absolute paths, paths relative to the active remote cwd, and ~/ paths relative to remote HOME; validates the directory by executing pwd from it; and updates connection state without reconnecting. The model calls it as a separate step and waits for success before issuing subsequent relative file/search operations or ssh_bash calls that depend on the updated cwd.

Files

SFTP implements file operations:

  • read and access checks;
  • recursive directory creation;
  • remote write through a temporary file;
  • atomic OpenSSH rename extension when available;
  • safe direct-write fallback when SFTP v3 cannot replace an existing target.

Remote paths map from Pi's local factory cwd into the selected remote cwd, but permission-system access extractors prevent those paths from entering local path normalization.

Search tools execute one capability-adaptive, bounded shell pipeline inside the already approved tool call. They never install or upload binaries.

  • ssh_find: fdfdfindgit ls-filesfind; fixed filename/path substring semantics.
  • ssh_grep: rggit grepfind + grep; literal case-insensitive semantics by default, optional hidden-path inclusion, basename-only include globs, and a portable POSIX ERE subset when literal mode is disabled.
  • each backend emits at most limit + 1 NUL-delimited path/line/content records so truncation is explicit and newline filenames remain one result; public limit is 1200; each returned line is capped.
  • relative paths resolve against remote cwd and ~/ against remote home; other ~user forms are rejected.
  • the search target root is passed to the backend as an absolute argument, while the search process executes from the active remote cwd; a single-file ssh_grep target is never used as a process cwd.
  • searches time out after 30 seconds with the resolved root and explicit guidance to narrow it before retrying.
  • user strings are single-quoted as shell arguments and NUL/newline input is rejected.
  • backend stdout contains only parseable results; stderr is captured independently for warnings and failure diagnostics.
  • capability detection occurs only within the reviewed search call.
  • search output is normalized by pi-ssh and excluded from RTK compaction.

Direct search commands remain denied through ssh_bash; structured search tools are the authoritative remote search surface.

Permission boundary

There is one permission gate: pi-permission-system.

  • ssh_connect defaults to ask; its preview resolves the imported host ID to the non-secret endpoint, port, and requested/default cwd before connection.
  • ssh_cd, ssh_read, ssh_write, ssh_edit, ssh_find, and ssh_grep default to ask; remote path extractors disable local path normalization for all six tools, while previews resolve relative and ~/ requests against the active remote cwd/HOME and display both requested and absolute paths.
  • ssh_bash is a Bash-semantic shellTools alias with decisionFloor: "ask".
  • Bash hard denies remain denies.
  • All asks enter the configured authorizer chain.
  • Permission service absence or bridge registration failure must not install a permissive fallback.

Evidence includes configured host ID, endpoint, port, remote cwd, and a bounded operation summary, never credentials.

Connection HOME/cwd discovery and ssh_cd validation use a bounded random-marker probe over a cancellable exec channel. The parser accepts exactly one framed absolute POSIX path; startup banners, malformed/multiline values, overflow, timeout, or cancellation cannot update connection state. Independent exec operations use at most four concurrent SSH channels, while SFTP operations remain serialized.

Session lifecycle

Connections are created only by an approved ssh_connect call after session start. They are not persisted or automatically resumed. Connecting another imported host disposes the previous client, and session shutdown disposes the active client.

The system prompt states that default tools are local, ssh_* tools are remote, and each ssh_bash call starts a fresh non-interactive shell after a connection becomes active. It directs persistent workspace changes through ssh_cd and does not include remote file content.