返回 CodeWhale
ssh-args.mjs
根目录 / crates / tui / plugins / computer-use / src / ssh-args.mjs
1 // One set of ssh rules for every ssh this plugin starts (the remote agent,
2 // its persistent channel, and the agent install copy). Codewhale's Fleet SSH
3 // host applies the same rules; tests/fixtures/ssh-destinations.json holds the
4 // vectors both implementations are tested against.
5 // - host keys must already be known: StrictHostKeyChecking=yes, no updates;
6 // a computer may name its own known-hosts file, which is then the only one
7 // - a host or user is a destination, never an option: neither may start
8 // with "-", and "--" ends the options before the destination
9
10 export const SSH_HOST_RE = /^[A-Za-z0-9_][A-Za-z0-9._-]*$/;
11 export const SSH_USER_RE = /^[A-Za-z0-9_][A-Za-z0-9._-]*$/;
12
13 export class SshTargetError extends Error {
14 constructor(code, message) { super(message); this.name = "SshTargetError"; this.code = code; }
15 }
16
17 /** Throw when an ssh computer entry cannot be used as a destination. */
18 export function validateSshTarget({ host, user, port, knownHosts } = {}) {
19 if (typeof host !== "string" || !SSH_HOST_RE.test(host)) {
20 throw new SshTargetError("invalid_host", "ssh computers need a valid host (letters, digits, dot, dash, underscore; not starting with '-')");
21 }
22 if (user != null && (typeof user !== "string" || !SSH_USER_RE.test(user))) {
23 throw new SshTargetError("invalid_user", "user must be a plain name (not starting with '-')");
24 }
25 if (port != null && (!Number.isInteger(port) || port < 1 || port > 65535)) {
26 throw new SshTargetError("invalid_port", "port must be an integer in 1..65535");
27 }
28 if (knownHosts != null && (typeof knownHosts !== "string" || !knownHosts.startsWith("/") || /[\s"\\]/.test(knownHosts))) {
29 throw new SshTargetError("invalid_known_hosts", "knownHosts must be an absolute path without spaces or quotes");
30 }
31 }
32
33 /** ssh options (no destination) for a computer entry. */
34 export function sshOptions(computer, { portFlag = "-p" } = {}) {
35 const options = [
36 "-o", "BatchMode=yes",
37 "-o", "ConnectTimeout=8",
38 "-o", "StrictHostKeyChecking=yes",
39 "-o", "UpdateHostKeys=no",
40 ];
41 if (computer.knownHosts) {
42 options.push("-o", `UserKnownHostsFile=${computer.knownHosts}`, "-o", "GlobalKnownHostsFile=/dev/null");
43 }
44 if (computer.port) options.push(portFlag, String(computer.port));
45 return options;
46 }
47
48 export function sshDestination(computer) {
49 return computer.user ? `${computer.user}@${computer.host}` : computer.host;
50 }
51
52 /** Full ssh argv prefix: options, "--", destination. The remote command follows. */
53 export function sshArgv(computer) {
54 validateSshTarget(computer);
55 return [...sshOptions(computer), "--", sshDestination(computer)];
56 }
57
57 lines Plain Text