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.
- package/README.md +90 -0
- package/bin/opencode-pair-autonomy.js +20 -0
- package/dist/__tests__/comment-guard.test.d.ts +1 -0
- package/dist/__tests__/config.test.d.ts +1 -0
- package/dist/__tests__/learning.test.d.ts +1 -0
- package/dist/__tests__/plan-mode.test.d.ts +1 -0
- package/dist/agents.d.ts +2 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +15351 -0
- package/dist/commands.d.ts +2 -0
- package/dist/config.d.ts +3 -0
- package/dist/hooks/comment-guard.d.ts +15 -0
- package/dist/hooks/file-edited.d.ts +7 -0
- package/dist/hooks/index.d.ts +46 -0
- package/dist/hooks/post-tool-use.d.ts +5 -0
- package/dist/hooks/pre-compact.d.ts +4 -0
- package/dist/hooks/pre-tool-use.d.ts +5 -0
- package/dist/hooks/prompt-refiner.d.ts +38 -0
- package/dist/hooks/runtime.d.ts +91 -0
- package/dist/hooks/sdk.d.ts +6 -0
- package/dist/hooks/session-end.d.ts +4 -0
- package/dist/hooks/session-start.d.ts +19 -0
- package/dist/hooks/stop.d.ts +5 -0
- package/dist/i18n/index.d.ts +15 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +17823 -0
- package/dist/installer.d.ts +12 -0
- package/dist/learning/analyzer.d.ts +15 -0
- package/dist/learning/store.d.ts +4 -0
- package/dist/learning/types.d.ts +32 -0
- package/dist/mcp.d.ts +4 -0
- package/dist/project-facts.d.ts +8 -0
- package/dist/prompts/coordinator.d.ts +2 -0
- package/dist/prompts/shared.d.ts +5 -0
- package/dist/prompts/workers.d.ts +8 -0
- package/dist/types.d.ts +81 -0
- package/dist/utils.d.ts +6 -0
- package/examples/opencode-pair-autonomy.jsonc +35 -0
- package/examples/opencode.jsonc +17 -0
- package/package.json +103 -0
- package/vendor/mcp/pg-mcp/README.md +91 -0
- package/vendor/mcp/pg-mcp/config.example.json +26 -0
- package/vendor/mcp/pg-mcp/config.json +15 -0
- package/vendor/mcp/pg-mcp/package-lock.json +1288 -0
- package/vendor/mcp/pg-mcp/package.json +18 -0
- package/vendor/mcp/pg-mcp/src/config.js +71 -0
- package/vendor/mcp/pg-mcp/src/db.js +85 -0
- package/vendor/mcp/pg-mcp/src/index.js +203 -0
- package/vendor/mcp/pg-mcp/src/sqlGuard.js +75 -0
- package/vendor/mcp/pg-mcp/src/tools.js +89 -0
- package/vendor/mcp/ssh-mcp/README.md +46 -0
- package/vendor/mcp/ssh-mcp/config.example.json +23 -0
- package/vendor/mcp/ssh-mcp/config.json +6 -0
- package/vendor/mcp/ssh-mcp/package-lock.json +1142 -0
- package/vendor/mcp/ssh-mcp/package.json +18 -0
- package/vendor/mcp/ssh-mcp/src/config.js +140 -0
- package/vendor/mcp/ssh-mcp/src/index.js +130 -0
- package/vendor/mcp/ssh-mcp/src/ssh.js +163 -0
- package/vendor/mcp/sudo-mcp/README.md +51 -0
- package/vendor/mcp/sudo-mcp/config.example.json +28 -0
- package/vendor/mcp/sudo-mcp/config.json +28 -0
- package/vendor/mcp/sudo-mcp/package-lock.json +1145 -0
- package/vendor/mcp/sudo-mcp/package.json +18 -0
- package/vendor/mcp/sudo-mcp/src/config.js +57 -0
- package/vendor/mcp/sudo-mcp/src/index.js +267 -0
- package/vendor/mcp/sudo-mcp/src/runner.js +168 -0
- package/vendor/mcp/web-agent-mcp/package-lock.json +2886 -0
- package/vendor/mcp/web-agent-mcp/package.json +28 -0
- package/vendor/mcp/web-agent-mcp/src/adapters/cloakbrowser/adapter.ts +335 -0
- package/vendor/mcp/web-agent-mcp/src/adapters/cloakbrowser/auth-heuristics.ts +324 -0
- package/vendor/mcp/web-agent-mcp/src/adapters/cloakbrowser/launcher.ts +1340 -0
- package/vendor/mcp/web-agent-mcp/src/config/env.ts +107 -0
- package/vendor/mcp/web-agent-mcp/src/core/action-flow.ts +82 -0
- package/vendor/mcp/web-agent-mcp/src/core/artifact-store.ts +109 -0
- package/vendor/mcp/web-agent-mcp/src/core/errors.ts +108 -0
- package/vendor/mcp/web-agent-mcp/src/core/observation-flow.ts +38 -0
- package/vendor/mcp/web-agent-mcp/src/core/policy-engine.ts +113 -0
- package/vendor/mcp/web-agent-mcp/src/core/retry-policy.ts +42 -0
- package/vendor/mcp/web-agent-mcp/src/core/session-manager.ts +670 -0
- package/vendor/mcp/web-agent-mcp/src/core/session-restart-policy.ts +34 -0
- package/vendor/mcp/web-agent-mcp/src/core/task-history.ts +97 -0
- package/vendor/mcp/web-agent-mcp/src/index.ts +3 -0
- package/vendor/mcp/web-agent-mcp/src/schemas/act.ts +167 -0
- package/vendor/mcp/web-agent-mcp/src/schemas/common.ts +56 -0
- package/vendor/mcp/web-agent-mcp/src/schemas/observe.ts +214 -0
- package/vendor/mcp/web-agent-mcp/src/schemas/page.ts +21 -0
- package/vendor/mcp/web-agent-mcp/src/schemas/policy.ts +42 -0
- package/vendor/mcp/web-agent-mcp/src/schemas/runtime.ts +21 -0
- package/vendor/mcp/web-agent-mcp/src/schemas/session.ts +63 -0
- package/vendor/mcp/web-agent-mcp/src/server.ts +75 -0
- package/vendor/mcp/web-agent-mcp/src/tools/act/click.ts +68 -0
- package/vendor/mcp/web-agent-mcp/src/tools/act/drag.ts +57 -0
- package/vendor/mcp/web-agent-mcp/src/tools/act/enter-code.ts +78 -0
- package/vendor/mcp/web-agent-mcp/src/tools/act/fill.ts +65 -0
- package/vendor/mcp/web-agent-mcp/src/tools/act/pinch.ts +58 -0
- package/vendor/mcp/web-agent-mcp/src/tools/act/press.ts +67 -0
- package/vendor/mcp/web-agent-mcp/src/tools/act/shared.ts +73 -0
- package/vendor/mcp/web-agent-mcp/src/tools/act/swipe.ts +59 -0
- package/vendor/mcp/web-agent-mcp/src/tools/act/wait-for.ts +56 -0
- package/vendor/mcp/web-agent-mcp/src/tools/act/wheel.ts +59 -0
- package/vendor/mcp/web-agent-mcp/src/tools/observe/a11y.ts +60 -0
- package/vendor/mcp/web-agent-mcp/src/tools/observe/auth-state.ts +92 -0
- package/vendor/mcp/web-agent-mcp/src/tools/observe/boxes.ts +66 -0
- package/vendor/mcp/web-agent-mcp/src/tools/observe/console.ts +67 -0
- package/vendor/mcp/web-agent-mcp/src/tools/observe/dom.ts +60 -0
- package/vendor/mcp/web-agent-mcp/src/tools/observe/network.ts +67 -0
- package/vendor/mcp/web-agent-mcp/src/tools/observe/page-state.ts +93 -0
- package/vendor/mcp/web-agent-mcp/src/tools/observe/screenshot.ts +73 -0
- package/vendor/mcp/web-agent-mcp/src/tools/observe/text.ts +70 -0
- package/vendor/mcp/web-agent-mcp/src/tools/observe/wait-for-network.ts +70 -0
- package/vendor/mcp/web-agent-mcp/src/tools/page/navigate.ts +59 -0
- package/vendor/mcp/web-agent-mcp/src/tools/policy/recommend-observation.ts +40 -0
- package/vendor/mcp/web-agent-mcp/src/tools/register-tools.ts +55 -0
- package/vendor/mcp/web-agent-mcp/src/tools/runtime/evaluate-js.ts +83 -0
- package/vendor/mcp/web-agent-mcp/src/tools/session/close.ts +41 -0
- package/vendor/mcp/web-agent-mcp/src/tools/session/create.ts +86 -0
- package/vendor/mcp/web-agent-mcp/src/tools/session/restart.ts +72 -0
- package/vendor/mcp/web-agent-mcp/src/utils/fs.ts +28 -0
- package/vendor/mcp/web-agent-mcp/src/utils/ids.ts +9 -0
- package/vendor/mcp/web-agent-mcp/src/utils/time.ts +7 -0
- package/vendor/mcp/web-agent-mcp/tsconfig.json +22 -0
- package/vendor/skills/editorial-technical-ui/SKILL.md +84 -0
- package/vendor/skills/figma-console/SKILL.md +839 -0
- package/vendor/skills/go-fiber-postgres/SKILL.md +31 -0
- package/vendor/skills/opencode-plugin-dev/SKILL.md +31 -0
- package/vendor/skills/rust-media-desktop/SKILL.md +30 -0
- package/vendor/skills/vue-vite-ui/SKILL.md +31 -0
- package/vendor/skills/web-agent-browser/SKILL.md +140 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sudo-mcp-server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Two-step approval MCP server for local sudo command execution",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"sudo-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/runner.js"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
16
|
+
"zod": "^3.24.2"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
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 configSchema = z.object({
|
|
9
|
+
approval_ttl_seconds: z.coerce.number().int().min(15).max(3600).default(300),
|
|
10
|
+
default_timeout_seconds: z.coerce.number().int().min(1).max(600).default(60),
|
|
11
|
+
max_timeout_seconds: z.coerce.number().int().min(1).max(3600).default(300),
|
|
12
|
+
max_output_bytes: z.coerce.number().int().min(1024).max(10_000_000).default(131072),
|
|
13
|
+
require_non_interactive_sudo: z.boolean().default(true),
|
|
14
|
+
require_allowlist: z.boolean().default(false),
|
|
15
|
+
allow_patterns: z.array(z.string().min(1)).default([]),
|
|
16
|
+
deny_patterns: z.array(z.string().min(1)).default([]),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function loadRawConfig() {
|
|
20
|
+
const candidates = [
|
|
21
|
+
process.env.SUDO_MCP_CONFIG_PATH,
|
|
22
|
+
join(__dirname, "../config.json"),
|
|
23
|
+
join(process.cwd(), "config.json"),
|
|
24
|
+
].filter(Boolean);
|
|
25
|
+
|
|
26
|
+
for (const configPath of candidates) {
|
|
27
|
+
if (!existsSync(configPath)) continue;
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
return {
|
|
31
|
+
configPath,
|
|
32
|
+
raw: JSON.parse(readFileSync(configPath, "utf8")),
|
|
33
|
+
};
|
|
34
|
+
} catch (error) {
|
|
35
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
36
|
+
throw new Error(`Failed to read sudo-mcp config '${configPath}': ${message}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
throw new Error(
|
|
41
|
+
"sudo-mcp config not found. Set SUDO_MCP_CONFIG_PATH or create config.json next to this server."
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function loadConfig() {
|
|
46
|
+
const { configPath, raw } = loadRawConfig();
|
|
47
|
+
const parsed = configSchema.parse(raw);
|
|
48
|
+
|
|
49
|
+
if (parsed.default_timeout_seconds > parsed.max_timeout_seconds) {
|
|
50
|
+
throw new Error("default_timeout_seconds cannot be greater than max_timeout_seconds.");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
configPath,
|
|
55
|
+
...parsed,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { loadConfig } from "./config.js";
|
|
9
|
+
import { evaluateCommandPolicy, runSudoCommand } from "./runner.js";
|
|
10
|
+
|
|
11
|
+
const config = loadConfig();
|
|
12
|
+
const pendingApprovals = new Map();
|
|
13
|
+
|
|
14
|
+
const requestSchema = z.object({
|
|
15
|
+
command: z.string().min(1),
|
|
16
|
+
timeout_seconds: z.coerce.number().int().min(1).max(3600).optional(),
|
|
17
|
+
reason: z.string().max(300).optional(),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const approveSchema = z.object({
|
|
21
|
+
request_id: z.string().uuid(),
|
|
22
|
+
approval_code: z.string().regex(/^[A-Z0-9]{8}$/),
|
|
23
|
+
approval_text: z.string().min(1),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const idSchema = z.object({
|
|
27
|
+
request_id: z.string().uuid(),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
function ok(payload) {
|
|
31
|
+
const text = typeof payload === "string" ? payload : JSON.stringify(payload, null, 2);
|
|
32
|
+
return { content: [{ type: "text", text }] };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function fail(message) {
|
|
36
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function makeApprovalCode() {
|
|
40
|
+
return randomUUID().replace(/-/g, "").slice(0, 8).toUpperCase();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function nowIso() {
|
|
44
|
+
return new Date().toISOString();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function expiresAtIso(ttlSeconds) {
|
|
48
|
+
return new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function cleanupExpiredRequests() {
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
for (const [requestId, value] of pendingApprovals.entries()) {
|
|
54
|
+
if (value.expiresAtMs <= now) {
|
|
55
|
+
pendingApprovals.delete(requestId);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getPendingRequest(requestId) {
|
|
61
|
+
cleanupExpiredRequests();
|
|
62
|
+
return pendingApprovals.get(requestId);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function buildExpectedApprovalPhrase(requestId, approvalCode) {
|
|
66
|
+
return `APPROVE_SUDO ${requestId} ${approvalCode}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const server = new Server(
|
|
70
|
+
{ name: "sudo-mcp-server", version: "1.0.0" },
|
|
71
|
+
{ capabilities: { tools: {} } }
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
75
|
+
tools: [
|
|
76
|
+
{
|
|
77
|
+
name: "get_sudo_policy",
|
|
78
|
+
description: "Show sudo policy and execution limits",
|
|
79
|
+
inputSchema: {
|
|
80
|
+
type: "object",
|
|
81
|
+
properties: {},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: "request_sudo_execution",
|
|
86
|
+
description: "Create a pending sudo execution request and approval code",
|
|
87
|
+
inputSchema: {
|
|
88
|
+
type: "object",
|
|
89
|
+
properties: {
|
|
90
|
+
command: { type: "string" },
|
|
91
|
+
timeout_seconds: { type: "number", minimum: 1, maximum: 3600 },
|
|
92
|
+
reason: { type: "string" },
|
|
93
|
+
},
|
|
94
|
+
required: ["command"],
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: "run_approved_sudo",
|
|
99
|
+
description: "Execute a previously requested sudo command with user approval",
|
|
100
|
+
inputSchema: {
|
|
101
|
+
type: "object",
|
|
102
|
+
properties: {
|
|
103
|
+
request_id: { type: "string" },
|
|
104
|
+
approval_code: { type: "string" },
|
|
105
|
+
approval_text: { type: "string" },
|
|
106
|
+
},
|
|
107
|
+
required: ["request_id", "approval_code", "approval_text"],
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: "list_pending_sudo_requests",
|
|
112
|
+
description: "List all not-yet-executed sudo requests",
|
|
113
|
+
inputSchema: {
|
|
114
|
+
type: "object",
|
|
115
|
+
properties: {},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: "cancel_pending_sudo_request",
|
|
120
|
+
description: "Cancel one pending sudo execution request",
|
|
121
|
+
inputSchema: {
|
|
122
|
+
type: "object",
|
|
123
|
+
properties: {
|
|
124
|
+
request_id: { type: "string" },
|
|
125
|
+
},
|
|
126
|
+
required: ["request_id"],
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
}));
|
|
131
|
+
|
|
132
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
133
|
+
const { name, arguments: args } = request.params;
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
switch (name) {
|
|
137
|
+
case "get_sudo_policy": {
|
|
138
|
+
return ok({
|
|
139
|
+
config_path: config.configPath,
|
|
140
|
+
approval_ttl_seconds: config.approval_ttl_seconds,
|
|
141
|
+
default_timeout_seconds: config.default_timeout_seconds,
|
|
142
|
+
max_timeout_seconds: config.max_timeout_seconds,
|
|
143
|
+
max_output_bytes: config.max_output_bytes,
|
|
144
|
+
require_non_interactive_sudo: config.require_non_interactive_sudo,
|
|
145
|
+
require_allowlist: config.require_allowlist,
|
|
146
|
+
allow_patterns: config.allow_patterns,
|
|
147
|
+
deny_patterns: config.deny_patterns,
|
|
148
|
+
pending_request_count: pendingApprovals.size,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
case "request_sudo_execution": {
|
|
153
|
+
const parsed = requestSchema.parse(args || {});
|
|
154
|
+
const policy = evaluateCommandPolicy(parsed.command, config);
|
|
155
|
+
if (!policy.allowed) {
|
|
156
|
+
return fail(policy.reason);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const requestId = randomUUID();
|
|
160
|
+
const approvalCode = makeApprovalCode();
|
|
161
|
+
const createdAt = nowIso();
|
|
162
|
+
const expiresAt = expiresAtIso(config.approval_ttl_seconds);
|
|
163
|
+
const expiresAtMs = Date.parse(expiresAt);
|
|
164
|
+
|
|
165
|
+
pendingApprovals.set(requestId, {
|
|
166
|
+
requestId,
|
|
167
|
+
command: parsed.command.trim(),
|
|
168
|
+
timeoutSeconds: parsed.timeout_seconds ?? config.default_timeout_seconds,
|
|
169
|
+
reason: parsed.reason || "",
|
|
170
|
+
createdAt,
|
|
171
|
+
expiresAt,
|
|
172
|
+
expiresAtMs,
|
|
173
|
+
approvalCode,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const expectedApprovalPhrase = buildExpectedApprovalPhrase(requestId, approvalCode);
|
|
177
|
+
|
|
178
|
+
return ok({
|
|
179
|
+
status: "pending_approval",
|
|
180
|
+
request_id: requestId,
|
|
181
|
+
command: parsed.command.trim(),
|
|
182
|
+
reason: parsed.reason || "",
|
|
183
|
+
policy_result: policy.reason,
|
|
184
|
+
created_at: createdAt,
|
|
185
|
+
expires_at: expiresAt,
|
|
186
|
+
approval_code: approvalCode,
|
|
187
|
+
expected_user_phrase: expectedApprovalPhrase,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
case "run_approved_sudo": {
|
|
192
|
+
const parsed = approveSchema.parse(args || {});
|
|
193
|
+
const requestEntry = getPendingRequest(parsed.request_id);
|
|
194
|
+
if (!requestEntry) {
|
|
195
|
+
return fail("Request not found or already expired/cancelled.");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (requestEntry.approvalCode !== parsed.approval_code) {
|
|
199
|
+
return fail("Invalid approval_code for this request.");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const expectedPhrase = buildExpectedApprovalPhrase(
|
|
203
|
+
requestEntry.requestId,
|
|
204
|
+
requestEntry.approvalCode
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
if (!parsed.approval_text.toUpperCase().includes(expectedPhrase.toUpperCase())) {
|
|
208
|
+
return fail(
|
|
209
|
+
`approval_text must include exact phrase: ${expectedPhrase}`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
pendingApprovals.delete(parsed.request_id);
|
|
214
|
+
const result = await runSudoCommand(requestEntry.command, requestEntry.timeoutSeconds, config);
|
|
215
|
+
|
|
216
|
+
return ok({
|
|
217
|
+
approval: {
|
|
218
|
+
request_id: requestEntry.requestId,
|
|
219
|
+
approved_at: nowIso(),
|
|
220
|
+
},
|
|
221
|
+
execution: result,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
case "list_pending_sudo_requests": {
|
|
226
|
+
cleanupExpiredRequests();
|
|
227
|
+
const list = [...pendingApprovals.values()].map((entry) => ({
|
|
228
|
+
request_id: entry.requestId,
|
|
229
|
+
command: entry.command,
|
|
230
|
+
reason: entry.reason,
|
|
231
|
+
created_at: entry.createdAt,
|
|
232
|
+
expires_at: entry.expiresAt,
|
|
233
|
+
}));
|
|
234
|
+
return ok({
|
|
235
|
+
pending_count: list.length,
|
|
236
|
+
requests: list,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
case "cancel_pending_sudo_request": {
|
|
241
|
+
const parsed = idSchema.parse(args || {});
|
|
242
|
+
const deleted = pendingApprovals.delete(parsed.request_id);
|
|
243
|
+
if (!deleted) return fail("Request not found.");
|
|
244
|
+
return ok({
|
|
245
|
+
status: "cancelled",
|
|
246
|
+
request_id: parsed.request_id,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
default:
|
|
251
|
+
return fail(`Unknown tool: ${name}`);
|
|
252
|
+
}
|
|
253
|
+
} catch (error) {
|
|
254
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
255
|
+
return fail(message);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
async function main() {
|
|
260
|
+
await server.connect(new StdioServerTransport());
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
main().catch((error) => {
|
|
264
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
265
|
+
console.error(`Failed to start sudo-mcp-server: ${message}`);
|
|
266
|
+
process.exit(1);
|
|
267
|
+
});
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
function wildcardToRegex(pattern) {
|
|
4
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
5
|
+
return new RegExp(`^${escaped}$`);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function matchesPattern(command, pattern) {
|
|
9
|
+
return wildcardToRegex(pattern).test(command);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function validateCommandSyntax(command) {
|
|
13
|
+
const normalized = command.trim();
|
|
14
|
+
if (!normalized) return "Command must not be empty.";
|
|
15
|
+
if (normalized.includes("\n") || normalized.includes("\r")) {
|
|
16
|
+
return "Multiline commands are not allowed.";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function evaluateCommandPolicy(command, config) {
|
|
23
|
+
const syntaxError = validateCommandSyntax(command);
|
|
24
|
+
if (syntaxError) {
|
|
25
|
+
return {
|
|
26
|
+
allowed: false,
|
|
27
|
+
reason: syntaxError,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const normalized = command.trim();
|
|
32
|
+
const deniedBy = config.deny_patterns.find((pattern) => matchesPattern(normalized, pattern));
|
|
33
|
+
if (deniedBy) {
|
|
34
|
+
return {
|
|
35
|
+
allowed: false,
|
|
36
|
+
reason: `Command blocked by deny pattern '${deniedBy}'.`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (config.require_allowlist && config.allow_patterns.length === 0) {
|
|
41
|
+
return {
|
|
42
|
+
allowed: false,
|
|
43
|
+
reason: "No allow patterns configured. Add allow_patterns in sudo-mcp config.",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (config.require_allowlist) {
|
|
48
|
+
const allowedBy = config.allow_patterns.find((pattern) => matchesPattern(normalized, pattern));
|
|
49
|
+
if (!allowedBy) {
|
|
50
|
+
return {
|
|
51
|
+
allowed: false,
|
|
52
|
+
reason: "Command does not match allow_patterns.",
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
allowed: true,
|
|
58
|
+
reason: `Command matched allow pattern '${allowedBy}'.`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
allowed: true,
|
|
64
|
+
reason: "Allowlist not required by policy.",
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function clampTimeout(requested, config) {
|
|
69
|
+
const parsed = Number(requested);
|
|
70
|
+
const fallback = config.default_timeout_seconds;
|
|
71
|
+
const value = Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
|
|
72
|
+
return Math.min(value, config.max_timeout_seconds);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function appendChunk(buffer, chunk, maxBytes) {
|
|
76
|
+
if (buffer.length >= maxBytes) {
|
|
77
|
+
return { text: buffer, full: false };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const remaining = maxBytes - buffer.length;
|
|
81
|
+
const slice = chunk.length > remaining ? chunk.slice(0, remaining) : chunk;
|
|
82
|
+
return {
|
|
83
|
+
text: buffer + slice,
|
|
84
|
+
full: chunk.length <= remaining,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function runSudoCommand(command, requestedTimeoutSeconds, config) {
|
|
89
|
+
const policy = evaluateCommandPolicy(command, config);
|
|
90
|
+
if (!policy.allowed) {
|
|
91
|
+
throw new Error(policy.reason);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const timeoutSeconds = clampTimeout(requestedTimeoutSeconds, config);
|
|
95
|
+
|
|
96
|
+
return await new Promise((resolve) => {
|
|
97
|
+
const args = [];
|
|
98
|
+
if (config.require_non_interactive_sudo) args.push("-n");
|
|
99
|
+
args.push("bash", "-lc", command);
|
|
100
|
+
|
|
101
|
+
const start = Date.now();
|
|
102
|
+
const proc = spawn("sudo", args, {
|
|
103
|
+
env: process.env,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
let stdout = "";
|
|
107
|
+
let stderr = "";
|
|
108
|
+
let timedOut = false;
|
|
109
|
+
let outputTruncated = false;
|
|
110
|
+
let killedByOutputLimit = false;
|
|
111
|
+
|
|
112
|
+
const finish = (code, errorMessage) => {
|
|
113
|
+
const durationMs = Date.now() - start;
|
|
114
|
+
const ok = code === 0 && !timedOut && !killedByOutputLimit && !errorMessage;
|
|
115
|
+
|
|
116
|
+
resolve({
|
|
117
|
+
ok,
|
|
118
|
+
command,
|
|
119
|
+
timeout_seconds: timeoutSeconds,
|
|
120
|
+
non_interactive_sudo: config.require_non_interactive_sudo,
|
|
121
|
+
exit_code: code,
|
|
122
|
+
duration_ms: durationMs,
|
|
123
|
+
timed_out: timedOut,
|
|
124
|
+
output_truncated: outputTruncated,
|
|
125
|
+
stdout,
|
|
126
|
+
stderr: errorMessage ? `${stderr}\n${errorMessage}`.trim() : stderr,
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const timer = setTimeout(() => {
|
|
131
|
+
timedOut = true;
|
|
132
|
+
proc.kill("SIGKILL");
|
|
133
|
+
}, timeoutSeconds * 1000);
|
|
134
|
+
|
|
135
|
+
proc.stdout.on("data", (chunk) => {
|
|
136
|
+
const result = appendChunk(stdout, chunk.toString(), config.max_output_bytes);
|
|
137
|
+
stdout = result.text;
|
|
138
|
+
if (!result.full) {
|
|
139
|
+
outputTruncated = true;
|
|
140
|
+
killedByOutputLimit = true;
|
|
141
|
+
proc.kill("SIGKILL");
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
proc.stderr.on("data", (chunk) => {
|
|
146
|
+
const result = appendChunk(stderr, chunk.toString(), config.max_output_bytes);
|
|
147
|
+
stderr = result.text;
|
|
148
|
+
if (!result.full) {
|
|
149
|
+
outputTruncated = true;
|
|
150
|
+
killedByOutputLimit = true;
|
|
151
|
+
proc.kill("SIGKILL");
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
proc.on("error", (error) => {
|
|
156
|
+
clearTimeout(timer);
|
|
157
|
+
finish(null, error.message);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
proc.on("close", (code) => {
|
|
161
|
+
clearTimeout(timer);
|
|
162
|
+
if (killedByOutputLimit && !stderr.includes("output exceeded")) {
|
|
163
|
+
stderr = `${stderr}\nProcess terminated because output exceeded max_output_bytes.`.trim();
|
|
164
|
+
}
|
|
165
|
+
finish(code, null);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
}
|