@patimweb/pi-ssh 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.node-version +1 -0
- package/LICENSE +21 -0
- package/README.md +146 -0
- package/index.ts +99 -0
- package/package.json +52 -0
- package/release.config.cjs +16 -0
- package/skills/ssh-key-setup/SKILL.md +71 -0
- package/skills/ssh-remote-work/SKILL.md +81 -0
- package/src/authorize.ts +237 -0
- package/src/clients/ssh-client.ts +490 -0
- package/src/config.ts +189 -0
- package/src/doctor.ts +212 -0
- package/src/formatting/formatters.ts +133 -0
- package/src/keys.ts +191 -0
- package/src/known-hosts.ts +189 -0
- package/src/tools/ssh-authorize.ts +88 -0
- package/src/tools/ssh-doctor.ts +26 -0
- package/src/tools/ssh-download.ts +51 -0
- package/src/tools/ssh-exec.ts +76 -0
- package/src/tools/ssh-keygen.ts +102 -0
- package/src/tools/ssh-list.ts +49 -0
- package/src/tools/ssh-profile.ts +77 -0
- package/src/tools/ssh-setup.ts +96 -0
- package/src/tools/ssh-status.ts +70 -0
- package/src/tools/ssh-upload.ts +51 -0
- package/src/types.ts +144 -0
package/.node-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
26.5.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Patrick Weppelmann
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# pi-ssh
|
|
2
|
+
|
|
3
|
+
SSH client extension for the [pi coding agent](https://github.com/earendil-works/pi).
|
|
4
|
+
|
|
5
|
+
Run commands on remote hosts, move files over SFTP, and turn a password login into a key login — all from a pi session.
|
|
6
|
+
|
|
7
|
+
**Nothing has to be installed.** No `ssh`, no `ssh-keygen`, no `ssh-copy-id`. The SSH protocol comes from [ssh2](https://github.com/mscdex/ssh2), a pure JavaScript implementation, and keys are generated with Node's own crypto. Windows, macOS and Linux behave identically.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Install from npm
|
|
13
|
+
pi install npm:@patimweb/pi-ssh
|
|
14
|
+
|
|
15
|
+
# Install from local path during development
|
|
16
|
+
pi install /path/to/pi-ssh
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
ssh_setup:
|
|
23
|
+
name: staging
|
|
24
|
+
host: staging.example.com
|
|
25
|
+
user: deploy
|
|
26
|
+
password: <password>
|
|
27
|
+
|
|
28
|
+
ssh_authorize # generates a key, installs it, verifies passwordless login
|
|
29
|
+
|
|
30
|
+
ssh_exec:
|
|
31
|
+
command: systemctl status nginx --no-pager
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
After `ssh_authorize` the password is no longer needed. It stays in the profile as a fallback until you pass `removePassword: true`.
|
|
35
|
+
|
|
36
|
+
## Tools
|
|
37
|
+
|
|
38
|
+
| Tool | Description |
|
|
39
|
+
|------|-------------|
|
|
40
|
+
| `ssh_setup` | Store a host: address, user, and a password or key path. |
|
|
41
|
+
| `ssh_status` | List configured hosts and how each authenticates; optionally test a connection. |
|
|
42
|
+
| `ssh_profile` | List, switch, or delete hosts. |
|
|
43
|
+
| `ssh_exec` | Run a command and return its output and exit code. |
|
|
44
|
+
| `ssh_list` | List a remote directory with sizes, permissions and dates over SFTP. |
|
|
45
|
+
| `ssh_upload` | Copy a file to the remote host. |
|
|
46
|
+
| `ssh_download` | Copy a file from the remote host. |
|
|
47
|
+
| `ssh_keygen` | Create an ed25519 key pair in process. |
|
|
48
|
+
| `ssh_authorize` | Install a key on a host and stop needing the password. |
|
|
49
|
+
| `ssh_doctor` | Report what this machine can do and what needs fixing. |
|
|
50
|
+
|
|
51
|
+
### Passwordless login
|
|
52
|
+
|
|
53
|
+
`ssh_authorize` is `ssh-copy-id` without the binary:
|
|
54
|
+
|
|
55
|
+
```yaml
|
|
56
|
+
ssh_authorize:
|
|
57
|
+
# profile: staging # defaults to the active one
|
|
58
|
+
# keyPath: ~/.ssh/id_ed25519_pi_staging
|
|
59
|
+
# removePassword: true # only after verification succeeds
|
|
60
|
+
# authorizedKeysPath: /custom/authorized_keys
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
It generates a key if the profile has none, appends the public key to the remote `~/.ssh/authorized_keys` with the permissions sshd requires, points the profile at the key, and then opens a **second connection using only the key** to prove it works before reporting success. Running it twice is safe: the key is appended only once.
|
|
64
|
+
|
|
65
|
+
An existing key at the target path is reused, never overwritten — overwriting would invalidate every other host that already trusts it.
|
|
66
|
+
|
|
67
|
+
### Running commands
|
|
68
|
+
|
|
69
|
+
```yaml
|
|
70
|
+
ssh_exec:
|
|
71
|
+
command: journalctl -u api --since "1 hour ago" --no-pager
|
|
72
|
+
# cwd: /srv/app
|
|
73
|
+
# timeoutSeconds: 120
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Each call opens a connection, runs one command, and closes it. There is no persistent shell, so `cd` does not carry over — use `cwd` or chain with `&&`. There is also no TTY: anything interactive (a `sudo` password prompt, `top`, an editor) will hang until the timeout.
|
|
77
|
+
|
|
78
|
+
Output is capped at 200,000 characters per stream and marked as truncated rather than silently cut.
|
|
79
|
+
|
|
80
|
+
### Files
|
|
81
|
+
|
|
82
|
+
```yaml
|
|
83
|
+
ssh_list: { path: /var/log }
|
|
84
|
+
ssh_download: { remotePath: /var/log/app.log, localPath: ./logs/app.log }
|
|
85
|
+
ssh_upload: { localPath: ./dist/app.tar.gz, remotePath: /tmp/app.tar.gz }
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Missing local directories are created on download. Prefer these over `cat` through `ssh_exec`: SFTP handles binary content and does not push the file through the model.
|
|
89
|
+
|
|
90
|
+
## Commands
|
|
91
|
+
|
|
92
|
+
| Command | Description |
|
|
93
|
+
|---------|-------------|
|
|
94
|
+
| `/ssh <command>` | Run a command on the active host. |
|
|
95
|
+
| `/ssh-key` | Set up passwordless login for the active host. |
|
|
96
|
+
|
|
97
|
+
## Skills
|
|
98
|
+
|
|
99
|
+
| Skill | Purpose |
|
|
100
|
+
|-------|---------|
|
|
101
|
+
| `ssh-remote-work` | Choosing the right host, the absence of a shell and a TTY, reading before writing, and what each connection error actually means. |
|
|
102
|
+
| `ssh-key-setup` | The password-to-key switch, why an existing key is never overwritten, and why the password stays until the key is proven. |
|
|
103
|
+
|
|
104
|
+
## Host key verification
|
|
105
|
+
|
|
106
|
+
Host keys are checked against `~/.ssh/known_hosts` — the same file OpenSSH uses, so hosts you have already visited with `ssh` are recognised, and hosts recorded here are recognised by `ssh`. Hashed entries (`ssh-keygen -H`) and `[host]:port` forms are both understood.
|
|
107
|
+
|
|
108
|
+
- **Unknown host** → the connection is refused and the fingerprint shown. Confirm it, then re-run with `acceptNewHostKey: true` to record it.
|
|
109
|
+
- **Changed key** → a hard failure with both fingerprints. This is what a machine-in-the-middle looks like; it is also what a reinstalled server looks like. It has to be resolved deliberately with `ssh-keygen -R <host>`.
|
|
110
|
+
- **Revoked key** (`@revoked` in known_hosts) → always refused.
|
|
111
|
+
|
|
112
|
+
`strictHostKey: false` in a profile turns the check off. It removes the only protection SSH has against an attacker on the network path, so it exists for throwaway lab machines and nothing else.
|
|
113
|
+
|
|
114
|
+
## Configuration
|
|
115
|
+
|
|
116
|
+
Profiles live in `~/.pi/ssh-config.json`, written atomically with mode `0600` because it can hold passwords and key passphrases.
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"profiles": {
|
|
121
|
+
"staging": {
|
|
122
|
+
"host": "staging.example.com",
|
|
123
|
+
"port": 22,
|
|
124
|
+
"user": "deploy",
|
|
125
|
+
"privateKeyPath": "/home/pat/.ssh/id_ed25519_pi_staging"
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
"activeProfile": "staging"
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Every tool also takes a one-off `profile` parameter, so several hosts can be used in one session without switching.
|
|
133
|
+
|
|
134
|
+
## Development
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
npm install
|
|
138
|
+
npm test
|
|
139
|
+
npm run test:coverage
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The suite runs end-to-end against a real OpenSSH server: it starts `sshd` on a loopback port with a host key generated by this package, then exercises the handshake, host key verification (including a simulated key change), exit codes, SFTP, and the full key bootstrap. Those tests skip themselves on machines without `sshd` rather than failing.
|
|
143
|
+
|
|
144
|
+
## License
|
|
145
|
+
|
|
146
|
+
MIT
|
package/index.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi SSH Extension
|
|
3
|
+
*
|
|
4
|
+
* Runs commands and moves files on remote hosts over SSH, and can turn a
|
|
5
|
+
* password login into a key login without any external tooling.
|
|
6
|
+
*
|
|
7
|
+
* Tools:
|
|
8
|
+
* - ssh_setup: Store a host, user and credentials
|
|
9
|
+
* - ssh_status: List configured hosts, optionally testing a connection
|
|
10
|
+
* - ssh_profile: List, switch or delete hosts
|
|
11
|
+
* - ssh_exec: Run a command and return its output and exit code
|
|
12
|
+
* - ssh_list: List a remote directory over SFTP
|
|
13
|
+
* - ssh_upload: Copy a file to the remote host
|
|
14
|
+
* - ssh_download: Copy a file from the remote host
|
|
15
|
+
* - ssh_keygen: Create an ed25519 key pair in process
|
|
16
|
+
* - ssh_authorize: Install a key on a host and stop needing the password
|
|
17
|
+
* - ssh_doctor: Report what the environment can do and what needs fixing
|
|
18
|
+
*
|
|
19
|
+
* Nothing here shells out: ssh2 is a pure JavaScript SSH implementation and
|
|
20
|
+
* keys are generated with Node's own crypto, so Windows, macOS and Linux all
|
|
21
|
+
* work without ssh, ssh-keygen or ssh-copy-id being installed.
|
|
22
|
+
*
|
|
23
|
+
* Data-oriented design:
|
|
24
|
+
* - All domain data is represented as plain immutable interfaces (types.ts)
|
|
25
|
+
* - I/O is isolated in the client module (clients/)
|
|
26
|
+
* - Pure helpers have no sockets in them (keys.ts, known-hosts.ts, formatting/)
|
|
27
|
+
* - Each tool is a single-responsibility module (tools/)
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
31
|
+
|
|
32
|
+
import { loadConfig, getConfig } from "./src/config.ts";
|
|
33
|
+
import { SshSetupTool } from "./src/tools/ssh-setup.ts";
|
|
34
|
+
import { SshStatusTool } from "./src/tools/ssh-status.ts";
|
|
35
|
+
import { SshProfileTool } from "./src/tools/ssh-profile.ts";
|
|
36
|
+
import { SshExecTool } from "./src/tools/ssh-exec.ts";
|
|
37
|
+
import { SshListTool } from "./src/tools/ssh-list.ts";
|
|
38
|
+
import { SshUploadTool } from "./src/tools/ssh-upload.ts";
|
|
39
|
+
import { SshDownloadTool } from "./src/tools/ssh-download.ts";
|
|
40
|
+
import { SshKeygenTool } from "./src/tools/ssh-keygen.ts";
|
|
41
|
+
import { SshAuthorizeTool } from "./src/tools/ssh-authorize.ts";
|
|
42
|
+
import { SshDoctorTool } from "./src/tools/ssh-doctor.ts";
|
|
43
|
+
|
|
44
|
+
export default function (pi: ExtensionAPI) {
|
|
45
|
+
// Load saved hosts on startup
|
|
46
|
+
loadConfig();
|
|
47
|
+
|
|
48
|
+
// Register all tools
|
|
49
|
+
pi.registerTool(SshSetupTool);
|
|
50
|
+
pi.registerTool(SshStatusTool);
|
|
51
|
+
pi.registerTool(SshProfileTool);
|
|
52
|
+
pi.registerTool(SshExecTool);
|
|
53
|
+
pi.registerTool(SshListTool);
|
|
54
|
+
pi.registerTool(SshUploadTool);
|
|
55
|
+
pi.registerTool(SshDownloadTool);
|
|
56
|
+
pi.registerTool(SshKeygenTool);
|
|
57
|
+
pi.registerTool(SshAuthorizeTool);
|
|
58
|
+
pi.registerTool(SshDoctorTool);
|
|
59
|
+
|
|
60
|
+
// Run something on the active host without spelling out the tool call
|
|
61
|
+
pi.registerCommand("ssh", {
|
|
62
|
+
description: "Run a command on the active SSH host",
|
|
63
|
+
handler: async (args, ctx) => {
|
|
64
|
+
if (!getConfig()) {
|
|
65
|
+
ctx.ui.notify("No SSH host configured. Use the ssh_setup tool first.", "error");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const command = args.trim();
|
|
70
|
+
if (!command) {
|
|
71
|
+
ctx.ui.notify("Usage: /ssh <command to run on the remote host>", "info");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
pi.sendUserMessage(
|
|
76
|
+
`Run this on the active SSH host with ssh_exec and show me the output: ${command}`,
|
|
77
|
+
{ deliverAs: "steer" },
|
|
78
|
+
);
|
|
79
|
+
ctx.ui.notify("Running on the remote host...", "info");
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// The one-time setup people forget until they are typing a password again
|
|
84
|
+
pi.registerCommand("ssh-key", {
|
|
85
|
+
description: "Set up passwordless login for the active SSH host",
|
|
86
|
+
handler: async (_args, ctx) => {
|
|
87
|
+
if (!getConfig()) {
|
|
88
|
+
ctx.ui.notify("No SSH host configured. Use the ssh_setup tool first.", "error");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
pi.sendUserMessage(
|
|
93
|
+
"Use ssh_authorize on the active SSH profile to generate a key, install it on the host and verify that a key-only login works. Tell me the fingerprint and whether the verification succeeded.",
|
|
94
|
+
{ deliverAs: "steer" },
|
|
95
|
+
);
|
|
96
|
+
ctx.ui.notify("Setting up key-based login...", "info");
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@patimweb/pi-ssh",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "SSH client extension for the pi coding agent. Run commands, transfer files over SFTP, and set up key-based login on remote hosts.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi-extension",
|
|
8
|
+
"ssh",
|
|
9
|
+
"sftp",
|
|
10
|
+
"remote",
|
|
11
|
+
"ssh2"
|
|
12
|
+
],
|
|
13
|
+
"author": "Patrick Weppelmann",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=26.0.0"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/Smotherer007/pi-ssh.git"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/Smotherer007/pi-ssh/issues"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "echo 'No build step defined'",
|
|
28
|
+
"test": "node --experimental-test-module-mocks --experimental-test-coverage --test 'tests/**/*.test.ts'",
|
|
29
|
+
"test:watch": "node --experimental-test-module-mocks --test --watch 'tests/**/*.test.ts'",
|
|
30
|
+
"test:coverage": "node --experimental-test-module-mocks --experimental-test-coverage --test 'tests/**/*.test.ts'"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"ssh2": "^1.17.0",
|
|
34
|
+
"typebox": "^1.2.8"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@semantic-release/git": "^10.0.0",
|
|
41
|
+
"@types/ssh2": "^1.15.0",
|
|
42
|
+
"semantic-release": "^25.0.0"
|
|
43
|
+
},
|
|
44
|
+
"pi": {
|
|
45
|
+
"extensions": [
|
|
46
|
+
"./index.ts"
|
|
47
|
+
],
|
|
48
|
+
"skills": [
|
|
49
|
+
"./skills"
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
branches: ["main", { name: "next", prerelease: true }],
|
|
3
|
+
plugins: [
|
|
4
|
+
"@semantic-release/commit-analyzer",
|
|
5
|
+
"@semantic-release/release-notes-generator",
|
|
6
|
+
"@semantic-release/npm",
|
|
7
|
+
[
|
|
8
|
+
"@semantic-release/git",
|
|
9
|
+
{
|
|
10
|
+
assets: ["package.json", "package-lock.json"],
|
|
11
|
+
message: "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"@semantic-release/github"
|
|
15
|
+
]
|
|
16
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ssh-key-setup
|
|
3
|
+
description: Set up SSH key authentication so a host stops asking for a password, and manage the keys involved. Use when the user configures a new server with a password, says they are tired of typing a password, asks about ssh-copy-id, ssh-keygen, "install my key", "passwordless login", "authorized_keys", or when a login fails because a key is missing or not accepted. Also covers what has to be installed for any of this to work, which on every platform is nothing.
|
|
4
|
+
allowed-tools: ssh_setup, ssh_status, ssh_keygen, ssh_authorize, ssh_exec, ssh_doctor
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Key-based login
|
|
8
|
+
|
|
9
|
+
A password in a config file is a password on disk, and typing one into every
|
|
10
|
+
session is friction. A key fixes both. The whole switch is one tool call.
|
|
11
|
+
|
|
12
|
+
## The normal path
|
|
13
|
+
|
|
14
|
+
1. `ssh_setup` with host, user and the password.
|
|
15
|
+
2. `ssh_authorize` on that profile.
|
|
16
|
+
|
|
17
|
+
`ssh_authorize` does what `ssh-copy-id` does: it generates an ed25519 key if
|
|
18
|
+
the profile has none, appends the public key to the remote
|
|
19
|
+
`~/.ssh/authorized_keys` with the permissions sshd insists on, points the
|
|
20
|
+
profile at the new key, and then **opens a second connection using only the
|
|
21
|
+
key** to prove it works before claiming success.
|
|
22
|
+
|
|
23
|
+
Report the fingerprint and whether verification succeeded. If it did not, say
|
|
24
|
+
so plainly - the key is installed but something is rejecting it, and the
|
|
25
|
+
password is still there as a fallback.
|
|
26
|
+
|
|
27
|
+
## What has to be installed
|
|
28
|
+
|
|
29
|
+
Nothing. Not `ssh`, not `ssh-keygen`, not `ssh-copy-id`. The SSH protocol is
|
|
30
|
+
implemented in JavaScript and keys are generated with Node's own crypto, so
|
|
31
|
+
Windows, macOS and Linux behave identically. If a user asks what to install,
|
|
32
|
+
run `ssh_doctor` and show them the report rather than guessing.
|
|
33
|
+
|
|
34
|
+
The keys produced are ordinary OpenSSH ed25519 keys, so `ssh -i` and any other
|
|
35
|
+
SSH client can use the same file.
|
|
36
|
+
|
|
37
|
+
## Things worth getting right
|
|
38
|
+
|
|
39
|
+
- **Never overwrite an existing key.** Every host that already trusts it would
|
|
40
|
+
stop accepting it. `ssh_authorize` reuses a key at the given path; `ssh_keygen`
|
|
41
|
+
refuses to replace one unless explicitly told to.
|
|
42
|
+
- **Keep the password until the key is proven.** `ssh_authorize` leaves it in
|
|
43
|
+
the profile by default. Only pass `removePassword: true` once verification
|
|
44
|
+
has succeeded, and prefer to ask the user first: if `authorized_keys` is ever
|
|
45
|
+
reset, that password is the way back in.
|
|
46
|
+
- **One key per host is a reasonable default.** The default path is
|
|
47
|
+
`~/.ssh/id_ed25519_pi_<profile>`, which keeps a compromise contained to one
|
|
48
|
+
host. Pass `keyPath` to reuse an existing key across hosts if the user wants
|
|
49
|
+
that.
|
|
50
|
+
- **A passphrase-protected key cannot be bootstrapped.** If the user points at
|
|
51
|
+
one, this flow cannot read it; either use a different key path or configure
|
|
52
|
+
the passphrase in the profile.
|
|
53
|
+
|
|
54
|
+
## If the key is not accepted afterwards
|
|
55
|
+
|
|
56
|
+
The usual causes, in order:
|
|
57
|
+
|
|
58
|
+
1. Permissions. sshd silently ignores `~/.ssh` or `authorized_keys` if they are
|
|
59
|
+
group- or world-writable. `ssh_exec` with `ls -ld ~/.ssh ~/.ssh/authorized_keys`
|
|
60
|
+
shows it; 700 and 600 are what is wanted.
|
|
61
|
+
2. The account's home is not where you think - a different `AuthorizedKeysFile`
|
|
62
|
+
in `sshd_config`, or a chrooted account. `ssh_authorize` takes an
|
|
63
|
+
`authorizedKeysPath` for that case.
|
|
64
|
+
3. The server does not allow public key auth at all (`PubkeyAuthentication no`),
|
|
65
|
+
which needs a change on the server side.
|
|
66
|
+
|
|
67
|
+
## Keys without a host
|
|
68
|
+
|
|
69
|
+
`ssh_keygen` just makes a key pair and prints the public key, for cases where
|
|
70
|
+
the user installs it themselves - a cloud provider's web console, a deploy key
|
|
71
|
+
in a Git host, a colleague who will add it for them.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ssh-remote-work
|
|
3
|
+
description: Work on a remote machine over SSH - run commands, inspect logs and configuration, and move files with SFTP. Use whenever the task is on another host: deploying or restarting a service, reading logs on a server, checking disk or process state, copying a build artefact to or from a machine, or anything the user describes as "on the server", "on the NAS", "on staging", or by a hostname. Also covers what to do when a connection fails and how to read a host key warning.
|
|
4
|
+
allowed-tools: ssh_status, ssh_profile, ssh_exec, ssh_list, ssh_upload, ssh_download, ssh_doctor
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Working on a remote host
|
|
8
|
+
|
|
9
|
+
Commands run through `ssh_exec` land on someone's actual machine. That is the
|
|
10
|
+
whole value and also the whole risk: there is no undo, and the blast radius is
|
|
11
|
+
whatever the account can reach.
|
|
12
|
+
|
|
13
|
+
## Before the first command
|
|
14
|
+
|
|
15
|
+
`ssh_status` shows which hosts are configured and which one is active. **When
|
|
16
|
+
more than one exists, pass `profile` explicitly** rather than relying on
|
|
17
|
+
whichever was active from an earlier session - "restart nginx" on the wrong
|
|
18
|
+
host is not recoverable by apologising.
|
|
19
|
+
|
|
20
|
+
If the user names a host you have no profile for, say so and ask for the
|
|
21
|
+
details rather than guessing at a hostname.
|
|
22
|
+
|
|
23
|
+
## Running commands
|
|
24
|
+
|
|
25
|
+
`ssh_exec` opens a connection, runs one command, and closes it. There is no
|
|
26
|
+
persistent shell, which has two consequences worth remembering:
|
|
27
|
+
|
|
28
|
+
- **State does not carry over.** `cd /var/log` in one call does not affect the
|
|
29
|
+
next. Use the `cwd` parameter, or chain with `&&` in a single command.
|
|
30
|
+
- **There is no TTY.** Anything interactive hangs until the timeout: `sudo`
|
|
31
|
+
that prompts for a password, `top`, `vim`, `apt` without `-y`. Use
|
|
32
|
+
non-interactive forms (`sudo -n`, `apt-get -y`, `systemctl --no-pager`), and
|
|
33
|
+
if a password prompt is genuinely needed, tell the user rather than trying to
|
|
34
|
+
feed it in.
|
|
35
|
+
|
|
36
|
+
Read the exit code, not just the output. A command that printed nothing and
|
|
37
|
+
exited 1 failed; reporting "done" because stdout was empty is wrong.
|
|
38
|
+
|
|
39
|
+
## Reading before writing
|
|
40
|
+
|
|
41
|
+
Prefer commands that observe over commands that change, and look before you
|
|
42
|
+
act: `systemctl status` before `systemctl restart`, `ls` before `rm`, a diff
|
|
43
|
+
before an overwrite.
|
|
44
|
+
|
|
45
|
+
For anything destructive or disruptive - deleting files, restarting services,
|
|
46
|
+
changing configuration, `chmod`/`chown` on system paths, package installs,
|
|
47
|
+
anything with `sudo` - **say what you are about to run and why, and let the
|
|
48
|
+
user confirm**, unless they already asked for exactly that. Piping a remote
|
|
49
|
+
script into a shell (`curl ... | sh`) is not something to do on someone's
|
|
50
|
+
server on your own initiative.
|
|
51
|
+
|
|
52
|
+
## Files
|
|
53
|
+
|
|
54
|
+
`ssh_list` gives a structured listing with sizes, modes and dates - use it
|
|
55
|
+
rather than parsing `ls` output. `ssh_download` and `ssh_upload` move files
|
|
56
|
+
over SFTP; prefer them over `cat`-ing a file through `ssh_exec`, which mangles
|
|
57
|
+
binaries and pushes the whole content through the model for nothing.
|
|
58
|
+
|
|
59
|
+
## When it does not connect
|
|
60
|
+
|
|
61
|
+
The error usually says which layer failed. Work from it rather than retrying:
|
|
62
|
+
|
|
63
|
+
- **"not in known_hosts"** - the host has never been seen. Show the user the
|
|
64
|
+
fingerprint and ask them to confirm it against the server before re-running
|
|
65
|
+
with `acceptNewHostKey: true`. Do not pass that flag reflexively; it is the
|
|
66
|
+
moment the trust decision is made.
|
|
67
|
+
- **"HOST KEY CHANGED"** - stop. This is either a reinstalled server or
|
|
68
|
+
someone between you and it. Never work around it by disabling host key
|
|
69
|
+
checking. Relay the message; the user has to resolve it deliberately.
|
|
70
|
+
- **"rejected the credentials"** - user name, password or key is wrong, or the
|
|
71
|
+
key is not in the remote authorized_keys. `ssh_authorize` fixes the last one.
|
|
72
|
+
- **"refused the connection"** - nothing is listening: wrong port, or sshd is
|
|
73
|
+
down.
|
|
74
|
+
- Something about the local environment - run `ssh_doctor`, which reports what
|
|
75
|
+
is missing and what to do about it.
|
|
76
|
+
|
|
77
|
+
## What to report back
|
|
78
|
+
|
|
79
|
+
Give the user the command's actual output and its exit code. When a command
|
|
80
|
+
failed, quote stderr rather than summarising it - the exact message is what
|
|
81
|
+
they need. Say which host you ran it on.
|