opencode-pair-autonomy 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.
Files changed (128) hide show
  1. package/README.md +90 -0
  2. package/bin/opencode-pair-autonomy.js +20 -0
  3. package/dist/__tests__/comment-guard.test.d.ts +1 -0
  4. package/dist/__tests__/config.test.d.ts +1 -0
  5. package/dist/__tests__/learning.test.d.ts +1 -0
  6. package/dist/__tests__/plan-mode.test.d.ts +1 -0
  7. package/dist/agents.d.ts +2 -0
  8. package/dist/cli.d.ts +1 -0
  9. package/dist/cli.js +15351 -0
  10. package/dist/commands.d.ts +2 -0
  11. package/dist/config.d.ts +3 -0
  12. package/dist/hooks/comment-guard.d.ts +15 -0
  13. package/dist/hooks/file-edited.d.ts +7 -0
  14. package/dist/hooks/index.d.ts +46 -0
  15. package/dist/hooks/post-tool-use.d.ts +5 -0
  16. package/dist/hooks/pre-compact.d.ts +4 -0
  17. package/dist/hooks/pre-tool-use.d.ts +5 -0
  18. package/dist/hooks/prompt-refiner.d.ts +38 -0
  19. package/dist/hooks/runtime.d.ts +91 -0
  20. package/dist/hooks/sdk.d.ts +6 -0
  21. package/dist/hooks/session-end.d.ts +4 -0
  22. package/dist/hooks/session-start.d.ts +19 -0
  23. package/dist/hooks/stop.d.ts +5 -0
  24. package/dist/i18n/index.d.ts +15 -0
  25. package/dist/index.d.ts +3 -0
  26. package/dist/index.js +17823 -0
  27. package/dist/installer.d.ts +12 -0
  28. package/dist/learning/analyzer.d.ts +15 -0
  29. package/dist/learning/store.d.ts +4 -0
  30. package/dist/learning/types.d.ts +32 -0
  31. package/dist/mcp.d.ts +4 -0
  32. package/dist/project-facts.d.ts +8 -0
  33. package/dist/prompts/coordinator.d.ts +2 -0
  34. package/dist/prompts/shared.d.ts +5 -0
  35. package/dist/prompts/workers.d.ts +8 -0
  36. package/dist/types.d.ts +81 -0
  37. package/dist/utils.d.ts +6 -0
  38. package/examples/opencode-pair-autonomy.jsonc +35 -0
  39. package/examples/opencode.jsonc +17 -0
  40. package/package.json +103 -0
  41. package/vendor/mcp/pg-mcp/README.md +91 -0
  42. package/vendor/mcp/pg-mcp/config.example.json +26 -0
  43. package/vendor/mcp/pg-mcp/config.json +15 -0
  44. package/vendor/mcp/pg-mcp/package-lock.json +1288 -0
  45. package/vendor/mcp/pg-mcp/package.json +18 -0
  46. package/vendor/mcp/pg-mcp/src/config.js +71 -0
  47. package/vendor/mcp/pg-mcp/src/db.js +85 -0
  48. package/vendor/mcp/pg-mcp/src/index.js +203 -0
  49. package/vendor/mcp/pg-mcp/src/sqlGuard.js +75 -0
  50. package/vendor/mcp/pg-mcp/src/tools.js +89 -0
  51. package/vendor/mcp/ssh-mcp/README.md +46 -0
  52. package/vendor/mcp/ssh-mcp/config.example.json +23 -0
  53. package/vendor/mcp/ssh-mcp/config.json +6 -0
  54. package/vendor/mcp/ssh-mcp/package-lock.json +1142 -0
  55. package/vendor/mcp/ssh-mcp/package.json +18 -0
  56. package/vendor/mcp/ssh-mcp/src/config.js +140 -0
  57. package/vendor/mcp/ssh-mcp/src/index.js +130 -0
  58. package/vendor/mcp/ssh-mcp/src/ssh.js +163 -0
  59. package/vendor/mcp/sudo-mcp/README.md +51 -0
  60. package/vendor/mcp/sudo-mcp/config.example.json +28 -0
  61. package/vendor/mcp/sudo-mcp/config.json +28 -0
  62. package/vendor/mcp/sudo-mcp/package-lock.json +1145 -0
  63. package/vendor/mcp/sudo-mcp/package.json +18 -0
  64. package/vendor/mcp/sudo-mcp/src/config.js +57 -0
  65. package/vendor/mcp/sudo-mcp/src/index.js +267 -0
  66. package/vendor/mcp/sudo-mcp/src/runner.js +168 -0
  67. package/vendor/mcp/web-agent-mcp/package-lock.json +2886 -0
  68. package/vendor/mcp/web-agent-mcp/package.json +28 -0
  69. package/vendor/mcp/web-agent-mcp/src/adapters/cloakbrowser/adapter.ts +335 -0
  70. package/vendor/mcp/web-agent-mcp/src/adapters/cloakbrowser/auth-heuristics.ts +324 -0
  71. package/vendor/mcp/web-agent-mcp/src/adapters/cloakbrowser/launcher.ts +1340 -0
  72. package/vendor/mcp/web-agent-mcp/src/config/env.ts +107 -0
  73. package/vendor/mcp/web-agent-mcp/src/core/action-flow.ts +82 -0
  74. package/vendor/mcp/web-agent-mcp/src/core/artifact-store.ts +109 -0
  75. package/vendor/mcp/web-agent-mcp/src/core/errors.ts +108 -0
  76. package/vendor/mcp/web-agent-mcp/src/core/observation-flow.ts +38 -0
  77. package/vendor/mcp/web-agent-mcp/src/core/policy-engine.ts +113 -0
  78. package/vendor/mcp/web-agent-mcp/src/core/retry-policy.ts +42 -0
  79. package/vendor/mcp/web-agent-mcp/src/core/session-manager.ts +670 -0
  80. package/vendor/mcp/web-agent-mcp/src/core/session-restart-policy.ts +34 -0
  81. package/vendor/mcp/web-agent-mcp/src/core/task-history.ts +97 -0
  82. package/vendor/mcp/web-agent-mcp/src/index.ts +3 -0
  83. package/vendor/mcp/web-agent-mcp/src/schemas/act.ts +167 -0
  84. package/vendor/mcp/web-agent-mcp/src/schemas/common.ts +56 -0
  85. package/vendor/mcp/web-agent-mcp/src/schemas/observe.ts +214 -0
  86. package/vendor/mcp/web-agent-mcp/src/schemas/page.ts +21 -0
  87. package/vendor/mcp/web-agent-mcp/src/schemas/policy.ts +42 -0
  88. package/vendor/mcp/web-agent-mcp/src/schemas/runtime.ts +21 -0
  89. package/vendor/mcp/web-agent-mcp/src/schemas/session.ts +63 -0
  90. package/vendor/mcp/web-agent-mcp/src/server.ts +75 -0
  91. package/vendor/mcp/web-agent-mcp/src/tools/act/click.ts +68 -0
  92. package/vendor/mcp/web-agent-mcp/src/tools/act/drag.ts +57 -0
  93. package/vendor/mcp/web-agent-mcp/src/tools/act/enter-code.ts +78 -0
  94. package/vendor/mcp/web-agent-mcp/src/tools/act/fill.ts +65 -0
  95. package/vendor/mcp/web-agent-mcp/src/tools/act/pinch.ts +58 -0
  96. package/vendor/mcp/web-agent-mcp/src/tools/act/press.ts +67 -0
  97. package/vendor/mcp/web-agent-mcp/src/tools/act/shared.ts +73 -0
  98. package/vendor/mcp/web-agent-mcp/src/tools/act/swipe.ts +59 -0
  99. package/vendor/mcp/web-agent-mcp/src/tools/act/wait-for.ts +56 -0
  100. package/vendor/mcp/web-agent-mcp/src/tools/act/wheel.ts +59 -0
  101. package/vendor/mcp/web-agent-mcp/src/tools/observe/a11y.ts +60 -0
  102. package/vendor/mcp/web-agent-mcp/src/tools/observe/auth-state.ts +92 -0
  103. package/vendor/mcp/web-agent-mcp/src/tools/observe/boxes.ts +66 -0
  104. package/vendor/mcp/web-agent-mcp/src/tools/observe/console.ts +67 -0
  105. package/vendor/mcp/web-agent-mcp/src/tools/observe/dom.ts +60 -0
  106. package/vendor/mcp/web-agent-mcp/src/tools/observe/network.ts +67 -0
  107. package/vendor/mcp/web-agent-mcp/src/tools/observe/page-state.ts +93 -0
  108. package/vendor/mcp/web-agent-mcp/src/tools/observe/screenshot.ts +73 -0
  109. package/vendor/mcp/web-agent-mcp/src/tools/observe/text.ts +70 -0
  110. package/vendor/mcp/web-agent-mcp/src/tools/observe/wait-for-network.ts +70 -0
  111. package/vendor/mcp/web-agent-mcp/src/tools/page/navigate.ts +59 -0
  112. package/vendor/mcp/web-agent-mcp/src/tools/policy/recommend-observation.ts +40 -0
  113. package/vendor/mcp/web-agent-mcp/src/tools/register-tools.ts +55 -0
  114. package/vendor/mcp/web-agent-mcp/src/tools/runtime/evaluate-js.ts +83 -0
  115. package/vendor/mcp/web-agent-mcp/src/tools/session/close.ts +41 -0
  116. package/vendor/mcp/web-agent-mcp/src/tools/session/create.ts +86 -0
  117. package/vendor/mcp/web-agent-mcp/src/tools/session/restart.ts +72 -0
  118. package/vendor/mcp/web-agent-mcp/src/utils/fs.ts +28 -0
  119. package/vendor/mcp/web-agent-mcp/src/utils/ids.ts +9 -0
  120. package/vendor/mcp/web-agent-mcp/src/utils/time.ts +7 -0
  121. package/vendor/mcp/web-agent-mcp/tsconfig.json +22 -0
  122. package/vendor/skills/editorial-technical-ui/SKILL.md +84 -0
  123. package/vendor/skills/figma-console/SKILL.md +839 -0
  124. package/vendor/skills/go-fiber-postgres/SKILL.md +31 -0
  125. package/vendor/skills/opencode-plugin-dev/SKILL.md +31 -0
  126. package/vendor/skills/rust-media-desktop/SKILL.md +30 -0
  127. package/vendor/skills/vue-vite-ui/SKILL.md +31 -0
  128. package/vendor/skills/web-agent-browser/SKILL.md +140 -0
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "ssh-mcp-server",
3
+ "version": "2.0.0",
4
+ "description": "Validated SSH MCP server with secure defaults",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "bin": {
8
+ "ssh-mcp": "./src/index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node src/index.js",
12
+ "check": "node --check src/index.js && node --check src/config.js && node --check src/ssh.js"
13
+ },
14
+ "dependencies": {
15
+ "@modelcontextprotocol/sdk": "^1.0.0",
16
+ "zod": "^3.24.2"
17
+ }
18
+ }
@@ -0,0 +1,140 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { z } from "zod";
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+
8
+ const hostSchema = z.object({
9
+ host: z.string().min(1),
10
+ port: z.coerce.number().int().min(1).max(65535).default(22),
11
+ user: z.string().min(1).default("root"),
12
+ description: z.string().default(""),
13
+ keyPath: z.string().optional(),
14
+ password: z.string().optional(),
15
+ passwordEnv: z.string().optional(),
16
+ connect_timeout_seconds: z.coerce.number().int().min(1).max(120).optional(),
17
+ default_timeout_seconds: z.coerce.number().int().min(1).max(600).optional(),
18
+ max_timeout_seconds: z.coerce.number().int().min(1).max(3600).optional(),
19
+ max_output_bytes: z.coerce
20
+ .number()
21
+ .int()
22
+ .min(1024)
23
+ .max(10_000_000)
24
+ .optional(),
25
+ ready_command: z.string().min(1).default("echo SSH_OK"),
26
+ strict_host_key_checking: z
27
+ .enum(["yes", "accept-new", "no"])
28
+ .default("accept-new"),
29
+ command_allowlist: z.array(z.string().min(1)).optional(),
30
+ });
31
+
32
+ const configSchema = z.object({
33
+ default_timeout_seconds: z.coerce.number().int().min(1).max(600).default(60),
34
+ max_timeout_seconds: z.coerce.number().int().min(1).max(3600).default(600),
35
+ max_output_bytes: z.coerce
36
+ .number()
37
+ .int()
38
+ .min(1024)
39
+ .max(10_000_000)
40
+ .default(131072),
41
+ hosts: z.record(z.string(), hostSchema).default({}),
42
+ });
43
+
44
+ function expandHome(pathValue) {
45
+ if (!pathValue || !pathValue.startsWith("~/")) return pathValue;
46
+ return join(process.env.HOME || "", pathValue.slice(2));
47
+ }
48
+
49
+ function resolvePassword(host, hostName) {
50
+ if (host.password) return host.password;
51
+ if (!host.passwordEnv) return undefined;
52
+
53
+ const value = process.env[host.passwordEnv];
54
+ if (!value) {
55
+ throw new Error(
56
+ `Host '${hostName}' expects env var '${host.passwordEnv}' but it is not set.`,
57
+ );
58
+ }
59
+
60
+ return value;
61
+ }
62
+
63
+ function loadRawConfig() {
64
+ const candidates = [
65
+ process.env.SSH_MCP_CONFIG_PATH,
66
+ join(__dirname, "../config.json"),
67
+ join(process.cwd(), "config.json"),
68
+ ].filter(Boolean);
69
+
70
+ for (const configPath of candidates) {
71
+ if (!existsSync(configPath)) continue;
72
+ try {
73
+ return {
74
+ configPath,
75
+ raw: JSON.parse(readFileSync(configPath, "utf8")),
76
+ };
77
+ } catch (error) {
78
+ const message = error instanceof Error ? error.message : String(error);
79
+ throw new Error(
80
+ `Could not read SSH MCP config '${configPath}': ${message}`,
81
+ );
82
+ }
83
+ }
84
+
85
+ throw new Error(
86
+ "SSH MCP config not found. Set SSH_MCP_CONFIG_PATH or create config.json next to this server.",
87
+ );
88
+ }
89
+
90
+ export function loadConfig() {
91
+ const { configPath, raw } = loadRawConfig();
92
+ const parsed = configSchema.parse(raw);
93
+
94
+ const hosts = Object.fromEntries(
95
+ Object.entries(parsed.hosts).map(([name, host]) => {
96
+ const normalized = {
97
+ ...host,
98
+ keyPath: host.keyPath ? expandHome(host.keyPath) : undefined,
99
+ password: resolvePassword(host, name),
100
+ default_timeout_seconds:
101
+ host.default_timeout_seconds ?? parsed.default_timeout_seconds,
102
+ max_timeout_seconds:
103
+ host.max_timeout_seconds ?? parsed.max_timeout_seconds,
104
+ max_output_bytes: host.max_output_bytes ?? parsed.max_output_bytes,
105
+ };
106
+
107
+ if (normalized.default_timeout_seconds > normalized.max_timeout_seconds) {
108
+ throw new Error(
109
+ `Host '${name}' has default_timeout_seconds greater than max_timeout_seconds.`,
110
+ );
111
+ }
112
+
113
+ return [name, normalized];
114
+ }),
115
+ );
116
+
117
+ return {
118
+ configPath,
119
+ hosts,
120
+ default_timeout_seconds: parsed.default_timeout_seconds,
121
+ max_timeout_seconds: parsed.max_timeout_seconds,
122
+ max_output_bytes: parsed.max_output_bytes,
123
+ };
124
+ }
125
+
126
+ export function getHost(config, connection) {
127
+ const host = config.hosts[connection];
128
+ if (host) return host;
129
+
130
+ const available = Object.keys(config.hosts);
131
+ if (available.length === 0) {
132
+ throw new Error(
133
+ "No SSH hosts configured. Update ssh-mcp/config.json first.",
134
+ );
135
+ }
136
+
137
+ throw new Error(
138
+ `Unknown connection '${connection}'. Available: ${available.join(", ")}`,
139
+ );
140
+ }
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
6
+ import { z } from "zod";
7
+ import { getHost, loadConfig } from "./config.js";
8
+ import { runSshCommand, testSshConnection } from "./ssh.js";
9
+
10
+ const config = loadConfig();
11
+ const hostNames = Object.keys(config.hosts);
12
+
13
+ const runCommandSchema = z.object({
14
+ connection: z.string().min(1),
15
+ command: z.string().min(1),
16
+ timeout_seconds: z.coerce.number().int().min(1).max(3600).optional(),
17
+ });
18
+
19
+ const connectionSchema = z.object({
20
+ connection: z.string().min(1),
21
+ });
22
+
23
+ function ok(payload) {
24
+ const text = typeof payload === "string" ? payload : JSON.stringify(payload, null, 2);
25
+ return { content: [{ type: "text", text }] };
26
+ }
27
+
28
+ function fail(message) {
29
+ return { content: [{ type: "text", text: message }], isError: true };
30
+ }
31
+
32
+ const server = new Server(
33
+ { name: "ssh-mcp-server", version: "2.0.0" },
34
+ { capabilities: { tools: {} } }
35
+ );
36
+
37
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
38
+ tools: [
39
+ {
40
+ name: "list_hosts",
41
+ description: "List configured SSH hosts",
42
+ inputSchema: { type: "object", properties: {} },
43
+ },
44
+ {
45
+ name: "test_connection",
46
+ description: "Test SSH connectivity with each host's ready command",
47
+ inputSchema: {
48
+ type: "object",
49
+ properties: {
50
+ connection: { type: "string", enum: hostNames.length > 0 ? hostNames : undefined },
51
+ },
52
+ required: ["connection"],
53
+ },
54
+ },
55
+ {
56
+ name: "run_command",
57
+ description: "Run a command on a configured SSH host",
58
+ inputSchema: {
59
+ type: "object",
60
+ properties: {
61
+ connection: { type: "string", enum: hostNames.length > 0 ? hostNames : undefined },
62
+ command: { type: "string" },
63
+ timeout_seconds: { type: "number", minimum: 1, maximum: 3600 },
64
+ },
65
+ required: ["connection", "command"],
66
+ },
67
+ },
68
+ ],
69
+ }));
70
+
71
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
72
+ const { name, arguments: args } = request.params;
73
+
74
+ try {
75
+ switch (name) {
76
+ case "list_hosts": {
77
+ const hosts = Object.entries(config.hosts).map(([connectionName, host]) => ({
78
+ connection: connectionName,
79
+ host: host.host,
80
+ port: host.port,
81
+ user: host.user,
82
+ description: host.description,
83
+ auth: host.password ? "password" : host.keyPath ? "key" : "agent/default",
84
+ has_allowlist: Boolean(host.command_allowlist && host.command_allowlist.length > 0),
85
+ default_timeout_seconds: host.default_timeout_seconds,
86
+ max_timeout_seconds: host.max_timeout_seconds,
87
+ max_output_bytes: host.max_output_bytes,
88
+ }));
89
+
90
+ return ok({
91
+ config_path: config.configPath,
92
+ host_count: hosts.length,
93
+ hosts,
94
+ });
95
+ }
96
+
97
+ case "test_connection": {
98
+ const parsed = connectionSchema.parse(args || {});
99
+ const host = getHost(config, parsed.connection);
100
+ const result = await testSshConnection(host);
101
+ return ok(result);
102
+ }
103
+
104
+ case "run_command": {
105
+ const parsed = runCommandSchema.parse(args || {});
106
+ const host = getHost(config, parsed.connection);
107
+ const result = await runSshCommand(host, parsed.command, {
108
+ timeout_seconds: parsed.timeout_seconds,
109
+ });
110
+ return ok(result);
111
+ }
112
+
113
+ default:
114
+ return fail(`Unknown tool: ${name}`);
115
+ }
116
+ } catch (error) {
117
+ const message = error instanceof Error ? error.message : String(error);
118
+ return fail(message);
119
+ }
120
+ });
121
+
122
+ async function main() {
123
+ await server.connect(new StdioServerTransport());
124
+ }
125
+
126
+ main().catch((error) => {
127
+ const message = error instanceof Error ? error.message : String(error);
128
+ console.error(`Failed to start ssh-mcp-server: ${message}`);
129
+ process.exit(1);
130
+ });
@@ -0,0 +1,163 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ function quoteSingle(value) {
4
+ return `'${String(value).replace(/'/g, "'\\''")}'`;
5
+ }
6
+
7
+ function clampTimeout(requested, host) {
8
+ const parsed = Number(requested);
9
+ const fallback = host.default_timeout_seconds;
10
+ const timeout = Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
11
+ return Math.min(timeout, host.max_timeout_seconds);
12
+ }
13
+
14
+ function toWildcardRegex(pattern) {
15
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
16
+ return new RegExp(`^${escaped}$`);
17
+ }
18
+
19
+ function commandAllowed(command, allowlist) {
20
+ if (!allowlist || allowlist.length === 0) return true;
21
+ const normalized = command.trim();
22
+ return allowlist.some((pattern) => toWildcardRegex(pattern).test(normalized));
23
+ }
24
+
25
+ function buildSshInvocation(host, command) {
26
+ const sshArgs = [
27
+ "-p",
28
+ String(host.port),
29
+ "-o",
30
+ `StrictHostKeyChecking=${host.strict_host_key_checking}`,
31
+ "-o",
32
+ "ServerAliveInterval=15",
33
+ "-o",
34
+ "ServerAliveCountMax=2",
35
+ "-o",
36
+ "LogLevel=ERROR",
37
+ ];
38
+
39
+ if (typeof host.connect_timeout_seconds === "number") {
40
+ sshArgs.push("-o", `ConnectTimeout=${host.connect_timeout_seconds}`);
41
+ }
42
+
43
+ if (host.keyPath) {
44
+ sshArgs.push("-i", host.keyPath);
45
+ }
46
+
47
+ const target = `${host.user}@${host.host}`;
48
+ const remoteCommand = `bash -lc ${quoteSingle(command)}`;
49
+ sshArgs.push(target, remoteCommand);
50
+
51
+ if (!host.password) {
52
+ return {
53
+ cmd: "ssh",
54
+ args: sshArgs,
55
+ env: process.env,
56
+ auth: host.keyPath ? "key" : "agent/default",
57
+ };
58
+ }
59
+
60
+ return {
61
+ cmd: "sshpass",
62
+ args: ["-e", "ssh", ...sshArgs],
63
+ env: { ...process.env, SSHPASS: host.password },
64
+ auth: host.keyPath ? "key+password" : "password",
65
+ };
66
+ }
67
+
68
+ function appendChunk(buffer, chunk, maxBytes) {
69
+ if (buffer.length >= maxBytes) return { text: buffer, full: false };
70
+ const remaining = maxBytes - buffer.length;
71
+ const slice = chunk.length > remaining ? chunk.slice(0, remaining) : chunk;
72
+ return {
73
+ text: buffer + slice,
74
+ full: chunk.length <= remaining,
75
+ };
76
+ }
77
+
78
+ export async function runSshCommand(host, command, options = {}) {
79
+ if (!commandAllowed(command, host.command_allowlist)) {
80
+ throw new Error(
81
+ "Command rejected by allowlist. Update command_allowlist in ssh-mcp config for this host."
82
+ );
83
+ }
84
+
85
+ const timeoutSeconds = clampTimeout(options.timeout_seconds, host);
86
+ const maxOutputBytes = host.max_output_bytes;
87
+
88
+ return await new Promise((resolve) => {
89
+ const start = Date.now();
90
+ const invocation = buildSshInvocation(host, command);
91
+ const proc = spawn(invocation.cmd, invocation.args, { env: invocation.env });
92
+
93
+ let stdout = "";
94
+ let stderr = "";
95
+ let timedOut = false;
96
+ let outputTruncated = false;
97
+ let killedByOutputLimit = false;
98
+
99
+ const finish = (code, errorMessage) => {
100
+ const durationMs = Date.now() - start;
101
+ const ok = code === 0 && !timedOut && !killedByOutputLimit && !errorMessage;
102
+
103
+ resolve({
104
+ ok,
105
+ connection: `${host.user}@${host.host}:${host.port}`,
106
+ auth: invocation.auth,
107
+ command,
108
+ timeout_seconds: timeoutSeconds,
109
+ max_output_bytes: maxOutputBytes,
110
+ timed_out: timedOut,
111
+ output_truncated: outputTruncated,
112
+ exit_code: code,
113
+ duration_ms: durationMs,
114
+ stdout,
115
+ stderr: errorMessage ? `${stderr}\n${errorMessage}`.trim() : stderr,
116
+ });
117
+ };
118
+
119
+ const timer = setTimeout(() => {
120
+ timedOut = true;
121
+ proc.kill("SIGKILL");
122
+ }, timeoutSeconds * 1000);
123
+
124
+ proc.stdout.on("data", (chunk) => {
125
+ const result = appendChunk(stdout, chunk.toString(), maxOutputBytes);
126
+ stdout = result.text;
127
+ if (!result.full) {
128
+ outputTruncated = true;
129
+ killedByOutputLimit = true;
130
+ proc.kill("SIGKILL");
131
+ }
132
+ });
133
+
134
+ proc.stderr.on("data", (chunk) => {
135
+ const result = appendChunk(stderr, chunk.toString(), maxOutputBytes);
136
+ stderr = result.text;
137
+ if (!result.full) {
138
+ outputTruncated = true;
139
+ killedByOutputLimit = true;
140
+ proc.kill("SIGKILL");
141
+ }
142
+ });
143
+
144
+ proc.on("error", (error) => {
145
+ clearTimeout(timer);
146
+ finish(null, error.message);
147
+ });
148
+
149
+ proc.on("close", (code) => {
150
+ clearTimeout(timer);
151
+ if (killedByOutputLimit && !stderr.includes("output limit")) {
152
+ stderr = `${stderr}\nProcess terminated because output exceeded max_output_bytes.`.trim();
153
+ }
154
+ finish(code, null);
155
+ });
156
+ });
157
+ }
158
+
159
+ export async function testSshConnection(host) {
160
+ return await runSshCommand(host, host.ready_command, {
161
+ timeout_seconds: Math.min(host.default_timeout_seconds, 20),
162
+ });
163
+ }
@@ -0,0 +1,51 @@
1
+ # sudo-mcp
2
+
3
+ Two-step approval MCP server for running local `sudo` commands safely.
4
+
5
+ ## Safety Model
6
+
7
+ - Every command must pass policy checks (`deny_patterns`, optional `allow_patterns`)
8
+ - Commands are requested first, then executed with a separate approval step
9
+ - Each request has an approval code and TTL
10
+ - Default policy enforces non-interactive sudo (`sudo -n`)
11
+ - Default mode is allow-all with a deny list for destructive operations
12
+
13
+ ## Tools
14
+
15
+ - `get_sudo_policy`
16
+ - `request_sudo_execution`
17
+ - `run_approved_sudo`
18
+ - `list_pending_sudo_requests`
19
+ - `cancel_pending_sudo_request`
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ npm install
25
+ ```
26
+
27
+ ## Configure
28
+
29
+ ```bash
30
+ cp config.example.json config.json
31
+ ```
32
+
33
+ Tune:
34
+
35
+ - `allow_patterns` (optional when `require_allowlist` is `false`)
36
+ - `require_allowlist`
37
+ - `deny_patterns`
38
+ - `approval_ttl_seconds`
39
+ - timeout/output limits
40
+
41
+ ## Approval Flow
42
+
43
+ 1. Call `request_sudo_execution`
44
+ 2. Show `expected_user_phrase` to the human
45
+ 3. Run `run_approved_sudo` only after the user sends that phrase
46
+
47
+ Example expected phrase:
48
+
49
+ ```text
50
+ APPROVE_SUDO <request_id> <approval_code>
51
+ ```
@@ -0,0 +1,28 @@
1
+ {
2
+ "approval_ttl_seconds": 300,
3
+ "default_timeout_seconds": 60,
4
+ "max_timeout_seconds": 300,
5
+ "max_output_bytes": 131072,
6
+ "require_non_interactive_sudo": true,
7
+ "require_allowlist": false,
8
+ "allow_patterns": [],
9
+ "deny_patterns": [
10
+ "rm -rf *",
11
+ "rm -fr *",
12
+ "rm -r -f *",
13
+ "rm -rf --no-preserve-root /",
14
+ "shred -n * -z *",
15
+ "wipefs*",
16
+ "mkfs*",
17
+ "fdisk*",
18
+ "parted*",
19
+ "poweroff*",
20
+ "shutdown*",
21
+ "reboot*",
22
+ "init 0",
23
+ "init 6",
24
+ "halt*",
25
+ "dd of=/dev/*",
26
+ "dd if=* of=/dev/*"
27
+ ]
28
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "approval_ttl_seconds": 300,
3
+ "default_timeout_seconds": 60,
4
+ "max_timeout_seconds": 300,
5
+ "max_output_bytes": 131072,
6
+ "require_non_interactive_sudo": true,
7
+ "require_allowlist": false,
8
+ "allow_patterns": [],
9
+ "deny_patterns": [
10
+ "rm -rf *",
11
+ "rm -fr *",
12
+ "rm -r -f *",
13
+ "rm -rf --no-preserve-root /",
14
+ "shred -n * -z *",
15
+ "wipefs*",
16
+ "mkfs*",
17
+ "fdisk*",
18
+ "parted*",
19
+ "poweroff*",
20
+ "shutdown*",
21
+ "reboot*",
22
+ "init 0",
23
+ "init 6",
24
+ "halt*",
25
+ "dd of=/dev/*",
26
+ "dd if=* of=/dev/*"
27
+ ]
28
+ }