smooth-ssh-mcp 0.1.1
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/LICENSE +21 -0
- package/README.en.md +319 -0
- package/README.md +32 -0
- package/README.zh-CN.md +319 -0
- package/bin/smooth-ssh-mcp-codex +43 -0
- package/dist/audit.d.ts +23 -0
- package/dist/audit.js +140 -0
- package/dist/audit.js.map +1 -0
- package/dist/auth.d.ts +8 -0
- package/dist/auth.js +19 -0
- package/dist/auth.js.map +1 -0
- package/dist/doctor.d.ts +27 -0
- package/dist/doctor.js +169 -0
- package/dist/doctor.js.map +1 -0
- package/dist/forwardManager.d.ts +49 -0
- package/dist/forwardManager.js +141 -0
- package/dist/forwardManager.js.map +1 -0
- package/dist/init.d.ts +21 -0
- package/dist/init.js +80 -0
- package/dist/init.js.map +1 -0
- package/dist/inventory.d.ts +4 -0
- package/dist/inventory.js +262 -0
- package/dist/inventory.js.map +1 -0
- package/dist/mcpServer.d.ts +8 -0
- package/dist/mcpServer.js +403 -0
- package/dist/mcpServer.js.map +1 -0
- package/dist/operations.d.ts +167 -0
- package/dist/operations.js +1240 -0
- package/dist/operations.js.map +1 -0
- package/dist/policy.d.ts +21 -0
- package/dist/policy.js +470 -0
- package/dist/policy.js.map +1 -0
- package/dist/redaction.d.ts +2 -0
- package/dist/redaction.js +64 -0
- package/dist/redaction.js.map +1 -0
- package/dist/runner.d.ts +24 -0
- package/dist/runner.js +90 -0
- package/dist/runner.js.map +1 -0
- package/dist/server.d.ts +9 -0
- package/dist/server.js +130 -0
- package/dist/server.js.map +1 -0
- package/dist/sessionManager.d.ts +77 -0
- package/dist/sessionManager.js +195 -0
- package/dist/sessionManager.js.map +1 -0
- package/dist/sshArgs.d.ts +24 -0
- package/dist/sshArgs.js +135 -0
- package/dist/sshArgs.js.map +1 -0
- package/dist/stateStore.d.ts +27 -0
- package/dist/stateStore.js +99 -0
- package/dist/stateStore.js.map +1 -0
- package/dist/types.d.ts +95 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/docs/mcp-client.example.json +15 -0
- package/examples/hosts.example.yaml +79 -0
- package/package.json +58 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
PROJECT_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)"
|
|
6
|
+
|
|
7
|
+
SECRETS_FILE="${SMOOTH_SSH_MCP_SECRETS:-$HOME/.config/smooth-ssh-mcp/secrets.env}"
|
|
8
|
+
CONFIG_FILE="${SMOOTH_SSH_MCP_CONFIG:-$HOME/.config/smooth-ssh-mcp/hosts.yaml}"
|
|
9
|
+
|
|
10
|
+
if [[ -f "$SECRETS_FILE" ]]; then
|
|
11
|
+
if [[ ! -O "$SECRETS_FILE" ]]; then
|
|
12
|
+
echo "Refusing to load secrets file not owned by current user: $SECRETS_FILE" >&2
|
|
13
|
+
exit 1
|
|
14
|
+
fi
|
|
15
|
+
mode="$(stat -c '%a' "$SECRETS_FILE")"
|
|
16
|
+
case "$mode" in
|
|
17
|
+
400|600) ;;
|
|
18
|
+
*)
|
|
19
|
+
echo "Refusing to load secrets file with mode $mode; run: chmod 600 $SECRETS_FILE" >&2
|
|
20
|
+
exit 1
|
|
21
|
+
;;
|
|
22
|
+
esac
|
|
23
|
+
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
24
|
+
line="${line%$'\r'}"
|
|
25
|
+
if [[ "$line" =~ ^[[:space:]]*$ || "$line" =~ ^[[:space:]]*# ]]; then
|
|
26
|
+
continue
|
|
27
|
+
fi
|
|
28
|
+
if [[ ! "$line" =~ ^[[:space:]]*(export[[:space:]]+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then
|
|
29
|
+
echo "Refusing to load invalid secrets line from $SECRETS_FILE" >&2
|
|
30
|
+
exit 1
|
|
31
|
+
fi
|
|
32
|
+
key="${BASH_REMATCH[2]}"
|
|
33
|
+
value="${BASH_REMATCH[3]}"
|
|
34
|
+
if [[ "$value" == \"*\" && "$value" == *\" && ${#value} -ge 2 ]]; then
|
|
35
|
+
value="${value:1:${#value}-2}"
|
|
36
|
+
elif [[ "$value" == \'*\' && "$value" == *\' && ${#value} -ge 2 ]]; then
|
|
37
|
+
value="${value:1:${#value}-2}"
|
|
38
|
+
fi
|
|
39
|
+
export "$key=$value"
|
|
40
|
+
done < "$SECRETS_FILE"
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
exec node "$PROJECT_DIR/dist/server.js" --config "$CONFIG_FILE"
|
package/dist/audit.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type AuditResultKind = "confirmation_required" | "policy_decision" | "exec_result" | "blocked" | "ok" | "error";
|
|
2
|
+
export type AuditToolCall = {
|
|
3
|
+
tool: string;
|
|
4
|
+
input: unknown;
|
|
5
|
+
result: unknown;
|
|
6
|
+
durationMs: number;
|
|
7
|
+
};
|
|
8
|
+
export type Auditor = {
|
|
9
|
+
recordToolCall(call: AuditToolCall): void;
|
|
10
|
+
};
|
|
11
|
+
export type JsonlAuditorOptions = {
|
|
12
|
+
path?: string;
|
|
13
|
+
enabled?: boolean;
|
|
14
|
+
now?: () => Date;
|
|
15
|
+
};
|
|
16
|
+
export declare class JsonlAuditor implements Auditor {
|
|
17
|
+
private readonly path;
|
|
18
|
+
private readonly enabled;
|
|
19
|
+
private readonly now;
|
|
20
|
+
constructor(options?: JsonlAuditorOptions);
|
|
21
|
+
recordToolCall(call: AuditToolCall): void;
|
|
22
|
+
}
|
|
23
|
+
export declare function summarizeToolCall(call: AuditToolCall, timestamp: Date): Record<string, unknown>;
|
package/dist/audit.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { redactAndTruncate } from "./redaction.js";
|
|
4
|
+
const SENSITIVE_KEYS = new Set(["confirmationToken", "env", "stdin", "input", "password", "passwordEnv", "secret", "token"]);
|
|
5
|
+
export class JsonlAuditor {
|
|
6
|
+
path;
|
|
7
|
+
enabled;
|
|
8
|
+
now;
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this.path = expandHome(options.path ?? process.env.SMOOTH_SSH_MCP_AUDIT_LOG ?? "~/.config/smooth-ssh-mcp/audit.jsonl");
|
|
11
|
+
this.enabled = options.enabled ?? process.env.SMOOTH_SSH_MCP_AUDIT !== "0";
|
|
12
|
+
this.now = options.now ?? (() => new Date());
|
|
13
|
+
}
|
|
14
|
+
recordToolCall(call) {
|
|
15
|
+
if (!this.enabled)
|
|
16
|
+
return;
|
|
17
|
+
const entry = summarizeToolCall(call, this.now());
|
|
18
|
+
mkdirSync(dirname(this.path), { recursive: true, mode: 0o700 });
|
|
19
|
+
appendFileSync(this.path, JSON.stringify(entry) + "\n", { encoding: "utf8", mode: 0o600 });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function summarizeToolCall(call, timestamp) {
|
|
23
|
+
const result = call.result;
|
|
24
|
+
const redactions = collectRedactions(result);
|
|
25
|
+
return {
|
|
26
|
+
timestamp: timestamp.toISOString(),
|
|
27
|
+
tool: call.tool,
|
|
28
|
+
hostId: findString(call.input, "hostId") ?? findString(result, "hostId"),
|
|
29
|
+
operation: inferOperation(call.tool, result),
|
|
30
|
+
resultKind: resultKind(result),
|
|
31
|
+
exitCode: findNumberOrNull(result, "exitCode"),
|
|
32
|
+
durationMs: call.durationMs,
|
|
33
|
+
remoteDurationMs: findNumber(result, "durationMs"),
|
|
34
|
+
truncated: findBoolean(result, "truncated"),
|
|
35
|
+
redactions,
|
|
36
|
+
input: sanitizeValue(call.input)
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function resultKind(value) {
|
|
40
|
+
if (!value || typeof value !== "object")
|
|
41
|
+
return "ok";
|
|
42
|
+
const input = value;
|
|
43
|
+
if (input.confirmationRequired === true)
|
|
44
|
+
return "confirmation_required";
|
|
45
|
+
if (input.blocked)
|
|
46
|
+
return "blocked";
|
|
47
|
+
if (typeof input.exitCode === "number" || input.exitCode === null)
|
|
48
|
+
return "exec_result";
|
|
49
|
+
if (typeof input.allowed === "boolean" && typeof input.confirmationRequired === "boolean")
|
|
50
|
+
return "policy_decision";
|
|
51
|
+
if (input.error)
|
|
52
|
+
return "error";
|
|
53
|
+
return "ok";
|
|
54
|
+
}
|
|
55
|
+
function inferOperation(tool, result) {
|
|
56
|
+
const resultOperation = findString(result, "operation");
|
|
57
|
+
if (resultOperation)
|
|
58
|
+
return resultOperation;
|
|
59
|
+
if (tool.includes("upload"))
|
|
60
|
+
return "upload";
|
|
61
|
+
if (tool.includes("download"))
|
|
62
|
+
return "download";
|
|
63
|
+
if (tool.includes("forward"))
|
|
64
|
+
return "forward";
|
|
65
|
+
if (tool.includes("session"))
|
|
66
|
+
return "pty";
|
|
67
|
+
if (tool.includes("permission"))
|
|
68
|
+
return "permission";
|
|
69
|
+
if (tool.includes("ssh") || tool.includes("task") || tool.includes("cleanup"))
|
|
70
|
+
return "exec";
|
|
71
|
+
return "local";
|
|
72
|
+
}
|
|
73
|
+
function sanitizeValue(value) {
|
|
74
|
+
if (Array.isArray(value))
|
|
75
|
+
return value.map(sanitizeValue);
|
|
76
|
+
if (!value || typeof value !== "object")
|
|
77
|
+
return sanitizeScalar(value);
|
|
78
|
+
const output = {};
|
|
79
|
+
for (const [key, child] of Object.entries(value)) {
|
|
80
|
+
output[key] = isSensitiveKey(key) ? "[REDACTED]" : sanitizeValue(child);
|
|
81
|
+
}
|
|
82
|
+
return output;
|
|
83
|
+
}
|
|
84
|
+
function sanitizeScalar(value) {
|
|
85
|
+
if (typeof value !== "string")
|
|
86
|
+
return value;
|
|
87
|
+
return redactAndTruncate(value, 1024).text;
|
|
88
|
+
}
|
|
89
|
+
function isSensitiveKey(key) {
|
|
90
|
+
const lower = key.toLowerCase();
|
|
91
|
+
return SENSITIVE_KEYS.has(key) || lower.includes("password") || lower.includes("secret") || lower.includes("token");
|
|
92
|
+
}
|
|
93
|
+
function collectRedactions(value) {
|
|
94
|
+
const redactions = findValue(value, "redactions");
|
|
95
|
+
return Array.isArray(redactions) ? redactions : [];
|
|
96
|
+
}
|
|
97
|
+
function findString(value, key) {
|
|
98
|
+
const found = findValue(value, key);
|
|
99
|
+
return typeof found === "string" ? found : undefined;
|
|
100
|
+
}
|
|
101
|
+
function findNumber(value, key) {
|
|
102
|
+
const found = findValue(value, key);
|
|
103
|
+
return typeof found === "number" ? found : undefined;
|
|
104
|
+
}
|
|
105
|
+
function findNumberOrNull(value, key) {
|
|
106
|
+
const found = findValue(value, key);
|
|
107
|
+
return typeof found === "number" || found === null ? found : undefined;
|
|
108
|
+
}
|
|
109
|
+
function findBoolean(value, key) {
|
|
110
|
+
const found = findValue(value, key);
|
|
111
|
+
return typeof found === "boolean" ? found : undefined;
|
|
112
|
+
}
|
|
113
|
+
function findValue(value, key) {
|
|
114
|
+
if (!value || typeof value !== "object")
|
|
115
|
+
return undefined;
|
|
116
|
+
const direct = value[key];
|
|
117
|
+
if (direct !== undefined)
|
|
118
|
+
return direct;
|
|
119
|
+
const blocked = value.blocked;
|
|
120
|
+
if (blocked && typeof blocked === "object") {
|
|
121
|
+
const blockedValue = findValue(blocked, key);
|
|
122
|
+
if (blockedValue !== undefined)
|
|
123
|
+
return blockedValue;
|
|
124
|
+
}
|
|
125
|
+
const result = value.result;
|
|
126
|
+
if (result && typeof result === "object") {
|
|
127
|
+
const resultValue = findValue(result, key);
|
|
128
|
+
if (resultValue !== undefined)
|
|
129
|
+
return resultValue;
|
|
130
|
+
}
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
function expandHome(path) {
|
|
134
|
+
if (path === "~")
|
|
135
|
+
return process.env.HOME ?? path;
|
|
136
|
+
if (path.startsWith("~/"))
|
|
137
|
+
return join(process.env.HOME ?? "", path.slice(2));
|
|
138
|
+
return path;
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=audit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"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"}
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Host } from "./types.js";
|
|
2
|
+
export type ProcessSpec = {
|
|
3
|
+
file: string;
|
|
4
|
+
args: string[];
|
|
5
|
+
env?: NodeJS.ProcessEnv;
|
|
6
|
+
usesPassword: boolean;
|
|
7
|
+
};
|
|
8
|
+
export declare function wrapWithPasswordAuth(host: Host, file: "ssh" | "scp", args: string[], envSource?: NodeJS.ProcessEnv): ProcessSpec;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function wrapWithPasswordAuth(host, file, args, envSource = process.env) {
|
|
2
|
+
if (!host.passwordEnv) {
|
|
3
|
+
return { file, args, usesPassword: false };
|
|
4
|
+
}
|
|
5
|
+
const password = envSource[host.passwordEnv];
|
|
6
|
+
if (!password) {
|
|
7
|
+
throw new Error(`Host ${host.id} requires password environment variable ${host.passwordEnv}, but it is not set`);
|
|
8
|
+
}
|
|
9
|
+
return {
|
|
10
|
+
file: "sshpass",
|
|
11
|
+
args: ["-e", file, ...args],
|
|
12
|
+
env: {
|
|
13
|
+
...envSource,
|
|
14
|
+
SSHPASS: password
|
|
15
|
+
},
|
|
16
|
+
usesPassword: true
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=auth.js.map
|
package/dist/auth.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AASA,MAAM,UAAU,oBAAoB,CAClC,IAAU,EACV,IAAmB,EACnB,IAAc,EACd,YAA+B,OAAO,CAAC,GAAG;IAE1C,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;QACtB,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC;IAC7C,CAAC;IAED,MAAM,QAAQ,GAAG,SAAS,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IAC7C,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,QAAQ,IAAI,CAAC,EAAE,2CAA2C,IAAI,CAAC,WAAW,qBAAqB,CAAC,CAAC;IACnH,CAAC;IAED,OAAO;QACL,IAAI,EAAE,SAAS;QACf,IAAI,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,IAAI,CAAC;QAC3B,GAAG,EAAE;YACH,GAAG,SAAS;YACZ,OAAO,EAAE,QAAQ;SAClB;QACD,YAAY,EAAE,IAAI;KACnB,CAAC;AACJ,CAAC"}
|
package/dist/doctor.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type DoctorStatus = "ok" | "warning" | "error";
|
|
2
|
+
export type DoctorCheck = {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
status: DoctorStatus;
|
|
6
|
+
message: string;
|
|
7
|
+
fix?: string;
|
|
8
|
+
};
|
|
9
|
+
export type DoctorReport = {
|
|
10
|
+
ok: boolean;
|
|
11
|
+
summary: {
|
|
12
|
+
ok: number;
|
|
13
|
+
warnings: number;
|
|
14
|
+
errors: number;
|
|
15
|
+
};
|
|
16
|
+
checks: DoctorCheck[];
|
|
17
|
+
};
|
|
18
|
+
type DoctorOptions = {
|
|
19
|
+
configPath?: string;
|
|
20
|
+
secretsPath?: string;
|
|
21
|
+
nodeVersion?: string;
|
|
22
|
+
commandExists?: (name: string) => boolean;
|
|
23
|
+
env?: NodeJS.ProcessEnv;
|
|
24
|
+
};
|
|
25
|
+
export declare function runDoctor(options?: DoctorOptions): DoctorReport;
|
|
26
|
+
export declare function formatDoctorReport(report: DoctorReport): string;
|
|
27
|
+
export {};
|
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
|
+
import { delimiter } from "node:path";
|
|
3
|
+
import { env as processEnv, version as processVersion } from "node:process";
|
|
4
|
+
import { defaultInventoryPath, loadInventory } from "./inventory.js";
|
|
5
|
+
export function runDoctor(options = {}) {
|
|
6
|
+
const env = options.env ?? processEnv;
|
|
7
|
+
const configPath = expandHome(options.configPath ?? defaultInventoryPath(), env);
|
|
8
|
+
const secretsPath = expandHome(options.secretsPath ?? env.SMOOTH_SSH_MCP_SECRETS ?? "~/.config/smooth-ssh-mcp/secrets.env", env);
|
|
9
|
+
const commandExists = options.commandExists ?? ((name) => commandExistsOnPath(name, env));
|
|
10
|
+
const checks = [
|
|
11
|
+
checkNodeVersion(options.nodeVersion ?? processVersion),
|
|
12
|
+
checkCommand("ssh", commandExists, "Install OpenSSH client so smooth-ssh can connect to remote hosts."),
|
|
13
|
+
checkCommand("scp", commandExists, "Install OpenSSH scp so file transfer tools can work."),
|
|
14
|
+
checkCommand("sshpass", commandExists, "Install sshpass only if you use passwordEnv hosts; key-based hosts do not need it.", true),
|
|
15
|
+
checkInventory(configPath),
|
|
16
|
+
checkFilePermissions("inventory-permissions", "Inventory permissions", configPath, "chmod 600 " + configPath, false),
|
|
17
|
+
checkSecrets(secretsPath),
|
|
18
|
+
checkFilePermissions("secrets-permissions", "Secrets permissions", secretsPath, "chmod 600 " + secretsPath, true)
|
|
19
|
+
];
|
|
20
|
+
const summary = {
|
|
21
|
+
ok: checks.filter((check) => check.status === "ok").length,
|
|
22
|
+
warnings: checks.filter((check) => check.status === "warning").length,
|
|
23
|
+
errors: checks.filter((check) => check.status === "error").length
|
|
24
|
+
};
|
|
25
|
+
return {
|
|
26
|
+
ok: summary.errors === 0,
|
|
27
|
+
summary,
|
|
28
|
+
checks
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function formatDoctorReport(report) {
|
|
32
|
+
const icon = (status) => (status === "ok" ? "OK" : status === "warning" ? "WARN" : "ERROR");
|
|
33
|
+
const lines = [
|
|
34
|
+
`smooth-ssh-mcp doctor: ${report.ok ? "ok" : "issues found"}`,
|
|
35
|
+
`summary: ${report.summary.ok} ok, ${report.summary.warnings} warnings, ${report.summary.errors} errors`,
|
|
36
|
+
...report.checks.map((check) => {
|
|
37
|
+
const fix = check.fix ? ` fix: ${check.fix}` : "";
|
|
38
|
+
return `[${icon(check.status)}] ${check.label}: ${check.message}${fix}`;
|
|
39
|
+
})
|
|
40
|
+
];
|
|
41
|
+
return lines.join("\n");
|
|
42
|
+
}
|
|
43
|
+
function checkNodeVersion(nodeVersion) {
|
|
44
|
+
const major = Number(nodeVersion.replace(/^v/, "").split(".")[0]);
|
|
45
|
+
if (Number.isFinite(major) && major >= 20) {
|
|
46
|
+
return {
|
|
47
|
+
id: "node",
|
|
48
|
+
label: "Node.js",
|
|
49
|
+
status: "ok",
|
|
50
|
+
message: nodeVersion
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
id: "node",
|
|
55
|
+
label: "Node.js",
|
|
56
|
+
status: "error",
|
|
57
|
+
message: `${nodeVersion} is not supported`,
|
|
58
|
+
fix: "Install Node.js >=20"
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function checkCommand(name, commandExists, fix, optional = false) {
|
|
62
|
+
if (commandExists(name)) {
|
|
63
|
+
return {
|
|
64
|
+
id: name,
|
|
65
|
+
label: name,
|
|
66
|
+
status: "ok",
|
|
67
|
+
message: "found on PATH"
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
id: name,
|
|
72
|
+
label: name,
|
|
73
|
+
status: optional ? "warning" : "error",
|
|
74
|
+
message: "not found on PATH",
|
|
75
|
+
fix
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function checkInventory(path) {
|
|
79
|
+
if (!existsSync(path)) {
|
|
80
|
+
return {
|
|
81
|
+
id: "inventory",
|
|
82
|
+
label: "Inventory",
|
|
83
|
+
status: "error",
|
|
84
|
+
message: `not found at ${path}`,
|
|
85
|
+
fix: `Create ${path} or pass --config /path/to/hosts.yaml`
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const inventory = loadInventory(path);
|
|
90
|
+
return {
|
|
91
|
+
id: "inventory",
|
|
92
|
+
label: "Inventory",
|
|
93
|
+
status: "ok",
|
|
94
|
+
message: `${inventory.hosts.length} hosts loaded from ${path}`
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
return {
|
|
99
|
+
id: "inventory",
|
|
100
|
+
label: "Inventory",
|
|
101
|
+
status: "error",
|
|
102
|
+
message: error instanceof Error ? error.message : String(error)
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function checkSecrets(path) {
|
|
107
|
+
if (!existsSync(path)) {
|
|
108
|
+
return {
|
|
109
|
+
id: "secrets",
|
|
110
|
+
label: "Secrets file",
|
|
111
|
+
status: "warning",
|
|
112
|
+
message: `not found at ${path}; this is fine for key-based hosts`
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
id: "secrets",
|
|
117
|
+
label: "Secrets file",
|
|
118
|
+
status: "ok",
|
|
119
|
+
message: `found at ${path}`
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function checkFilePermissions(id, label, path, fix, optional) {
|
|
123
|
+
if (!existsSync(path)) {
|
|
124
|
+
return {
|
|
125
|
+
id,
|
|
126
|
+
label,
|
|
127
|
+
status: optional ? "warning" : "error",
|
|
128
|
+
message: `cannot check permissions; file does not exist`,
|
|
129
|
+
fix: optional ? undefined : fix
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const mode = statSync(path).mode & 0o777;
|
|
133
|
+
if (mode === 0o600 || mode === 0o400) {
|
|
134
|
+
return {
|
|
135
|
+
id,
|
|
136
|
+
label,
|
|
137
|
+
status: "ok",
|
|
138
|
+
message: mode.toString(8)
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
id,
|
|
143
|
+
label,
|
|
144
|
+
status: "error",
|
|
145
|
+
message: `mode ${mode.toString(8)} is too permissive`,
|
|
146
|
+
fix
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function commandExistsOnPath(name, env) {
|
|
150
|
+
const path = env.PATH ?? "";
|
|
151
|
+
const extensions = process.platform === "win32" ? [".exe", ".cmd", ".bat", ""] : [""];
|
|
152
|
+
for (const dir of path.split(delimiter)) {
|
|
153
|
+
if (!dir)
|
|
154
|
+
continue;
|
|
155
|
+
for (const extension of extensions) {
|
|
156
|
+
if (existsSync(`${dir}/${name}${extension}`))
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
function expandHome(path, env) {
|
|
163
|
+
if (path === "~")
|
|
164
|
+
return env.HOME ?? path;
|
|
165
|
+
if (path.startsWith("~/"))
|
|
166
|
+
return `${env.HOME ?? ""}${path.slice(1)}`;
|
|
167
|
+
return path;
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=doctor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor.js","sourceRoot":"","sources":["../src/doctor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Host } from "./types.js";
|
|
2
|
+
export type SpawnedForwardProcess = {
|
|
3
|
+
pid?: number;
|
|
4
|
+
kill: (signal?: NodeJS.Signals) => unknown;
|
|
5
|
+
on: (event: string, listener: (...args: unknown[]) => void) => unknown;
|
|
6
|
+
};
|
|
7
|
+
type ForwardManagerOptions = {
|
|
8
|
+
controlDir: string;
|
|
9
|
+
maxForwards?: number;
|
|
10
|
+
ttlSeconds?: number;
|
|
11
|
+
env?: NodeJS.ProcessEnv;
|
|
12
|
+
spawnProcess?: (file: string, args: string[], env?: NodeJS.ProcessEnv) => SpawnedForwardProcess;
|
|
13
|
+
};
|
|
14
|
+
type ForwardRecord = {
|
|
15
|
+
forwardId: string;
|
|
16
|
+
hostId: string;
|
|
17
|
+
pid?: number;
|
|
18
|
+
localHost: string;
|
|
19
|
+
localPort: number;
|
|
20
|
+
remoteHost: string;
|
|
21
|
+
remotePort: number;
|
|
22
|
+
state: "starting" | "running" | "exited" | "error";
|
|
23
|
+
startedAt: string;
|
|
24
|
+
expiresAt: string;
|
|
25
|
+
argv: string[];
|
|
26
|
+
process: SpawnedForwardProcess;
|
|
27
|
+
timer?: NodeJS.Timeout;
|
|
28
|
+
};
|
|
29
|
+
export type ForwardInfo = Omit<ForwardRecord, "process">;
|
|
30
|
+
export declare class ForwardManager {
|
|
31
|
+
private readonly options;
|
|
32
|
+
private readonly forwards;
|
|
33
|
+
private readonly maxForwards;
|
|
34
|
+
private readonly env;
|
|
35
|
+
private readonly spawnProcess;
|
|
36
|
+
constructor(options: ForwardManagerOptions);
|
|
37
|
+
start(input: {
|
|
38
|
+
host: Host;
|
|
39
|
+
localHost?: string;
|
|
40
|
+
localPort: number;
|
|
41
|
+
remoteHost: string;
|
|
42
|
+
remotePort: number;
|
|
43
|
+
}): ForwardInfo;
|
|
44
|
+
stop(forwardId: string): ForwardInfo;
|
|
45
|
+
list(): ForwardInfo[];
|
|
46
|
+
private toInfo;
|
|
47
|
+
private cleanupExpired;
|
|
48
|
+
}
|
|
49
|
+
export {};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { wrapWithPasswordAuth } from "./auth.js";
|
|
4
|
+
import { buildSshArgs } from "./sshArgs.js";
|
|
5
|
+
export class ForwardManager {
|
|
6
|
+
options;
|
|
7
|
+
forwards = new Map();
|
|
8
|
+
maxForwards;
|
|
9
|
+
env;
|
|
10
|
+
spawnProcess;
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.options = options;
|
|
13
|
+
this.maxForwards = options.maxForwards ?? 8;
|
|
14
|
+
this.env = options.env ?? process.env;
|
|
15
|
+
this.spawnProcess =
|
|
16
|
+
options.spawnProcess ??
|
|
17
|
+
((file, args, env) => spawn(file, args, {
|
|
18
|
+
env,
|
|
19
|
+
shell: false,
|
|
20
|
+
stdio: ["ignore", "ignore", "pipe"]
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
start(input) {
|
|
24
|
+
this.cleanupExpired();
|
|
25
|
+
if (this.forwards.size >= this.maxForwards) {
|
|
26
|
+
throw new Error(`Maximum active forwards reached: ${this.maxForwards}`);
|
|
27
|
+
}
|
|
28
|
+
validatePort(input.localPort, "localPort");
|
|
29
|
+
validatePort(input.remotePort, "remotePort");
|
|
30
|
+
validateForwardHost(input.localHost ?? "127.0.0.1", "localHost");
|
|
31
|
+
validateForwardHost(input.remoteHost, "remoteHost");
|
|
32
|
+
const base = buildSshArgs(input.host, {
|
|
33
|
+
controlDir: this.options.controlDir,
|
|
34
|
+
batchMode: true
|
|
35
|
+
});
|
|
36
|
+
const target = base.pop();
|
|
37
|
+
const separator = base.pop();
|
|
38
|
+
if (!target || separator !== "--")
|
|
39
|
+
throw new Error("Unable to build ssh target for forward");
|
|
40
|
+
const localHost = input.localHost ?? "127.0.0.1";
|
|
41
|
+
const spec = `${localHost}:${input.localPort}:${input.remoteHost}:${input.remotePort}`;
|
|
42
|
+
const argv = [...base, "-o", "ExitOnForwardFailure=yes", "-N", "-L", spec, "--", target];
|
|
43
|
+
const commandSpec = wrapWithPasswordAuth(input.host, "ssh", argv, this.env);
|
|
44
|
+
const child = this.spawnProcess(commandSpec.file, commandSpec.args, commandSpec.env);
|
|
45
|
+
const ttlSeconds = this.options.ttlSeconds ?? 60 * 60;
|
|
46
|
+
const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
47
|
+
const record = {
|
|
48
|
+
forwardId: randomUUID(),
|
|
49
|
+
hostId: input.host.id,
|
|
50
|
+
pid: child.pid,
|
|
51
|
+
localHost,
|
|
52
|
+
localPort: input.localPort,
|
|
53
|
+
remoteHost: input.remoteHost,
|
|
54
|
+
remotePort: input.remotePort,
|
|
55
|
+
state: "starting",
|
|
56
|
+
startedAt: new Date().toISOString(),
|
|
57
|
+
expiresAt,
|
|
58
|
+
argv: sanitizeForwardArgv(commandSpec.args),
|
|
59
|
+
process: child
|
|
60
|
+
};
|
|
61
|
+
record.timer = setTimeout(() => {
|
|
62
|
+
this.stop(record.forwardId);
|
|
63
|
+
}, ttlSeconds * 1000);
|
|
64
|
+
record.timer.unref();
|
|
65
|
+
setTimeout(() => {
|
|
66
|
+
if (record.state === "starting")
|
|
67
|
+
record.state = "running";
|
|
68
|
+
}, 500).unref();
|
|
69
|
+
child.on("close", () => {
|
|
70
|
+
record.state = "exited";
|
|
71
|
+
});
|
|
72
|
+
child.on("error", () => {
|
|
73
|
+
record.state = "error";
|
|
74
|
+
});
|
|
75
|
+
this.forwards.set(record.forwardId, record);
|
|
76
|
+
return this.toInfo(record);
|
|
77
|
+
}
|
|
78
|
+
stop(forwardId) {
|
|
79
|
+
const record = this.forwards.get(forwardId);
|
|
80
|
+
if (!record)
|
|
81
|
+
throw new Error(`Forward not found: ${forwardId}`);
|
|
82
|
+
if (record.timer)
|
|
83
|
+
clearTimeout(record.timer);
|
|
84
|
+
record.process.kill("SIGTERM");
|
|
85
|
+
forceKillLater(record.process);
|
|
86
|
+
record.state = "exited";
|
|
87
|
+
this.forwards.delete(forwardId);
|
|
88
|
+
return this.toInfo(record);
|
|
89
|
+
}
|
|
90
|
+
list() {
|
|
91
|
+
this.cleanupExpired();
|
|
92
|
+
return [...this.forwards.values()].map((record) => this.toInfo(record));
|
|
93
|
+
}
|
|
94
|
+
toInfo(record) {
|
|
95
|
+
const { process: _process, timer: _timer, ...info } = record;
|
|
96
|
+
return info;
|
|
97
|
+
}
|
|
98
|
+
cleanupExpired() {
|
|
99
|
+
const now = Date.now();
|
|
100
|
+
for (const [forwardId, record] of this.forwards) {
|
|
101
|
+
if (Date.parse(record.expiresAt) <= now || record.state === "exited" || record.state === "error") {
|
|
102
|
+
if (record.timer)
|
|
103
|
+
clearTimeout(record.timer);
|
|
104
|
+
record.process.kill("SIGTERM");
|
|
105
|
+
forceKillLater(record.process);
|
|
106
|
+
this.forwards.delete(forwardId);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function forceKillLater(process) {
|
|
112
|
+
setTimeout(() => {
|
|
113
|
+
process.kill("SIGKILL");
|
|
114
|
+
}, 1000).unref();
|
|
115
|
+
}
|
|
116
|
+
function validatePort(value, label) {
|
|
117
|
+
if (!Number.isInteger(value) || value < 1 || value > 65535) {
|
|
118
|
+
throw new Error(`Invalid ${label}: ${value}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function validateForwardHost(value, label) {
|
|
122
|
+
if (value.startsWith("-")) {
|
|
123
|
+
throw new Error(`Invalid ${label}: leading dash is not allowed`);
|
|
124
|
+
}
|
|
125
|
+
if (!/^[A-Za-z0-9._:-]+$/.test(value)) {
|
|
126
|
+
throw new Error(`Invalid ${label}: ${value}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function sanitizeForwardArgv(argv) {
|
|
130
|
+
return argv.map((arg, index) => {
|
|
131
|
+
const previous = argv[index - 1];
|
|
132
|
+
if (previous === "-i")
|
|
133
|
+
return "[REDACTED_IDENTITY_FILE]";
|
|
134
|
+
if (arg.startsWith("ControlPath="))
|
|
135
|
+
return "ControlPath=[REDACTED]";
|
|
136
|
+
if (arg.includes("ControlPath="))
|
|
137
|
+
return arg.replace(/ControlPath=[^\s]+/g, "ControlPath=[REDACTED]");
|
|
138
|
+
return arg;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=forwardManager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"forwardManager.js","sourceRoot":"","sources":["../src/forwardManager.ts"],"names":[],"mappings":"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"}
|