mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 07:23:06 +00:00
175 lines
10 KiB
Markdown
175 lines
10 KiB
Markdown
# pi-ssh
|
|
|
|
`pi-ssh` keeps Pi and its local tools on the local machine while exposing explicit remote tools over a persistent Node `ssh2` connection:
|
|
|
|
- `ssh_connect`
|
|
- `ssh_cd`
|
|
- `ssh_read`
|
|
- `ssh_write`
|
|
- `ssh_edit`
|
|
- `ssh_find`
|
|
- `ssh_grep`
|
|
- `ssh_bash`
|
|
|
|
The extension does not override Pi's local `read`, `write`, `edit`, `find`, `grep`, or `bash` tools.
|
|
|
|
## Architecture
|
|
|
|
Runtime connections are pure `ssh2`; the extension does not spawn OpenSSH and does not require `sshpass`, `ControlMaster`, or passwordless login. Authentication can use a private-key file, a password, or an SSH agent socket such as 1Password's agent. Remote file operations use SFTP and remote shell commands use an SSH exec channel. The SSH transport persists, but commands intentionally use fresh non-interactive Bash processes rather than a hidden stateful PTY.
|
|
|
|
Hosts must be explicitly imported before use. OpenSSH remains only an import source: the configuration helper runs `ssh -G <alias>` once to resolve the selected alias, then stores the resulting endpoint and authentication data in the pi-ssh vault. Later changes to `~/.ssh/config` require re-importing the host.
|
|
|
|
Connections are agent-controlled: when the user explicitly names an imported host as part of a concrete remote task, the model calls `ssh_connect`. The connection request enters the normal permission and auto-review chain before any network connection is opened. The extension does not register `/ssh`, `--ssh`, automatic session resume, or remote user-`!` overrides.
|
|
|
|
ProxyJump and ProxyCommand are intentionally rejected in the first ssh2 release.
|
|
|
|
## Configure hosts
|
|
|
|
From the installed bundle or this repository:
|
|
|
|
```sh
|
|
./ssh_config.sh import
|
|
./ssh_config.sh import packaging-server
|
|
```
|
|
|
|
The helper:
|
|
|
|
1. lets you select concrete aliases from `~/.ssh/config`;
|
|
2. uses `ssh -G` to resolve HostName, User, Port, IdentityFile, and IdentityAgent;
|
|
3. asks whether the selected host uses an SSH agent, private key, or password;
|
|
4. validates the selected agent socket or asks for a private-key passphrase when needed;
|
|
5. obtains and displays the server's SHA256 host-key fingerprint;
|
|
6. connects with `ssh2` to verify authentication;
|
|
7. optionally assigns a display label and group;
|
|
8. writes the encrypted vault.
|
|
|
|
Other commands:
|
|
|
|
```sh
|
|
./ssh_config.sh list
|
|
./ssh_config.sh update packaging-server
|
|
./ssh_config.sh remove packaging-server
|
|
./ssh_config.sh rotate-key
|
|
```
|
|
|
|
Secret prompts require an interactive terminal. Passwords and passphrases are never passed as command-line arguments.
|
|
|
|
## Vault
|
|
|
|
The default paths are:
|
|
|
|
```text
|
|
${XDG_CONFIG_HOME:-$HOME/.config}/my-pi/pi-ssh/
|
|
├── hosts.enc
|
|
└── vault.key
|
|
```
|
|
|
|
On POSIX systems the directory is mode `700` and both files are mode `600`. `hosts.enc` is encrypted and authenticated with AES-256-GCM; `vault.key` contains the adjacent random 256-bit key. Writes use a fresh IV and an atomic temporary-file rename. Plaintext configuration is never written to a temporary file.
|
|
|
|
This is deliberately a **casual-disclosure boundary**, not protection against compromise of the local account: anyone who can read both files can decrypt the vault. Encryption prevents the host configuration and passwords from being exposed by accidentally viewing or copying `hosts.enc` alone.
|
|
|
|
The encrypted payload contains host endpoints, pinned host-key fingerprints, groups, and one of:
|
|
|
|
- an SSH agent socket path;
|
|
- a private-key path plus optional passphrase; or
|
|
- the server password.
|
|
|
|
Private-key contents are not copied into the vault. Agent-backed private keys remain inside the agent (including 1Password); pi-ssh stores only the socket path and asks `ssh2` to authenticate through it.
|
|
|
|
## Usage
|
|
|
|
Ask Pi for a concrete task on an imported host, for example:
|
|
|
|
```text
|
|
Connect to packaging-server, inspect the API logs, and identify the recent 500 errors.
|
|
```
|
|
|
|
The model first calls:
|
|
|
|
```text
|
|
ssh_connect({ hostId: "packaging-server" })
|
|
```
|
|
|
|
It may set `remotePath` to an absolute path, `~`, or a path beginning with `~/`. The model calls sequential `ssh_connect` as a separate step and waits for the reviewed connection to succeed before using other `ssh_*` tools. `ssh_cd` explicitly changes the active remote workspace for subsequent shell, relative file, and relative search operations; it accepts absolute paths, paths relative to the current remote cwd, and `~/` paths relative to remote HOME. The model must also call `ssh_cd` as a separate step and wait for its successful result before issuing dependent remote operations. Only imported host IDs are accepted; arbitrary `user@host` targets are rejected. Unknown IDs report a bounded list of available imported IDs without exposing credentials. A new connection replaces the previous active connection, and session shutdown disconnects it.
|
|
|
|
## Runtime behavior
|
|
|
|
- `ssh_connect` is the only runtime connection surface; it is agent-callable, permission-reviewed, sequential, and cancellable so connection replacement cannot overlap another tool call or remain stuck after user cancellation.
|
|
- One persistent `ssh2` client is used for the active host; no interactive shell or PTY state is retained.
|
|
- `ssh_cd` is a sequential state transition: it validates a remote directory and updates the active workspace without reconnecting. Dependent tool calls must wait for it to succeed.
|
|
- HOME/cwd probes use bounded random-marker framing, require one absolute POSIX path, and preserve the previous state on malformed output, timeout, or cancellation.
|
|
- Independent exec channels use bounded concurrency of four; shared SFTP operations remain serialized.
|
|
- Each `ssh_bash` call opens a fresh exec channel and runs under `bash -lc` in the active remote cwd. A command-local `cd` is temporary, and `cd`, `export`, alias, function, or other shell state inside one call does not persist to the next call.
|
|
- SFTP provides remote reads and writes.
|
|
- Writes use a temporary remote file and prefer OpenSSH's atomic rename SFTP extension when the server supports it.
|
|
- A pinned SHA256 host-key mismatch fails closed.
|
|
- Connection loss fails closed; the extension does not silently replay a command.
|
|
- Remote `AGENTS.md` and `CLAUDE.md` files are never discovered or injected.
|
|
- `ssh_find` and `ssh_grep` perform capability detection inside each approved call and return at most 200 bounded result lines. Search targets are always separate from the execution cwd, so `ssh_grep` can target a single remote file without attempting to enter it as a directory. A 30-second timeout reports the resolved root and instructs the model to narrow it with `ssh_find`.
|
|
- RTK treats only `ssh_bash` as a Bash output-compaction alias; it does not rewrite remote commands or process remote search/read results.
|
|
- Permission previews display resolved absolute remote paths together with the original relative or `~/` request; remote paths never enter local filesystem gates.
|
|
|
|
### Adaptive remote search
|
|
|
|
`ssh_find` performs fixed-substring path matching with this backend order:
|
|
|
|
```text
|
|
fd → fdfind → git ls-files → find
|
|
```
|
|
|
|
`ssh_grep` defaults to literal, case-insensitive content matching with this backend order. Hidden paths are excluded unless `includeHidden` is true, and `include` is a basename-only glob such as `*.ts` (globs containing `/` are rejected):
|
|
|
|
```text
|
|
ripgrep → git grep → find + grep
|
|
```
|
|
|
|
No backend is installed or uploaded. The tools use what the server already provides, report the chosen backend and truncation state, and cap `limit` at 200. `literal: false` accepts the portable POSIX ERE subset shared by the backends; Git and fallback grep are explicitly run in ERE mode. Grep results use bounded NUL-delimited path/line/content records, so newline-containing filenames cannot corrupt match counts or truncation. Backend stderr is kept out of result rows and is reported as a warning on success or failure detail on error.
|
|
|
|
A genuine no-match result—including the `find + grep` fallback—succeeds with zero rows; invalid regexes, missing roots, and backend failures remain errors. Search paths accept absolute paths, relative paths, `~`, and `~/...`; `~user` expansion is rejected. Narrow `path` and `pattern` when truncated rather than increasing the limit. Direct `find`, `fd`, `grep`, and `rg` commands remain forbidden through `ssh_bash`; use the structured search tools instead.
|
|
|
|
The remote host must provide `bash`. SFTP support is required for file tools.
|
|
|
|
## Permission-system integration
|
|
|
|
All remote operations enter the bundle's existing permission chain:
|
|
|
|
- `ssh_connect` starts as `ask`, so AutoReview can verify that the direct user request names the requested imported host;
|
|
- `ssh_cd`, `ssh_read`, `ssh_write`, `ssh_edit`, `ssh_find`, and `ssh_grep` start as `ask`;
|
|
- `ssh_bash` uses the full deterministic Bash policy and `decisionFloor: "ask"`;
|
|
- deterministic hard denies remain denies;
|
|
- asks enter the configured `auto-review` authorizer;
|
|
- reviewer failures defer to the normal terminal prompt.
|
|
|
|
Permission evidence includes the configured host ID, resolved endpoint, port, remote cwd, and a bounded operation summary. It never includes passwords, passphrases, private-key contents, or the vault key. Remote paths are not normalized as local filesystem paths.
|
|
|
|
## Security notes
|
|
|
|
- Import only servers you control or trust.
|
|
- Verify host-key fingerprints through an independent channel before accepting them.
|
|
- Treat both vault files as secrets even though `hosts.enc` is encrypted.
|
|
- The extension's threat model does not protect credentials from malicious code already running as the same local user.
|
|
- Password keyboard-interactive mode reuses the configured password for the server's prompts; use it only with a trusted pinned host.
|
|
- Remote content reaches the model only through an explicit reviewed `ssh_*` tool call.
|
|
|
|
## Development
|
|
|
|
```sh
|
|
npm test
|
|
```
|
|
|
|
Important files:
|
|
|
|
- `index.ts` — Pi extension and tool registration
|
|
- `src/ssh2-transport.ts` — persistent ssh2, exec, and SFTP transport
|
|
- `src/remote-bash.ts` — stateless Bash adapter pinned to the active remote cwd
|
|
- `src/remote-cwd.ts` — explicit remote workspace path resolution
|
|
- `src/config.ts` — validated configuration types
|
|
- `src/vault.ts` — AES-GCM vault
|
|
- `src/import.ts` — `ssh -G` import helpers
|
|
- `scripts/ssh-config.mjs` — interactive configuration CLI
|
|
- `permission-integration.ts` — permission-system bridge
|
|
|
|
## Upstream and license
|
|
|
|
This maintained fork originates from `pansapiens/pi-ssh`; see [UPSTREAM.md](UPSTREAM.md). The pure ssh2 design also references the transport architecture in `@99percentpeople/pi-ssh-remote` without adopting its local-tool override model. Licensed under MIT; see [LICENSE](LICENSE).
|