lynkr 0.1.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/.eslintrc.cjs +12 -0
- package/CLAUDE.md +39 -0
- package/LICENSE +21 -0
- package/README.md +417 -0
- package/bin/cli.js +3 -0
- package/index.js +3 -0
- package/package.json +54 -0
- package/src/api/middleware/logging.js +37 -0
- package/src/api/middleware/session.js +55 -0
- package/src/api/router.js +80 -0
- package/src/cache/prompt.js +183 -0
- package/src/clients/databricks.js +72 -0
- package/src/config/index.js +301 -0
- package/src/db/index.js +192 -0
- package/src/diff/comments.js +153 -0
- package/src/edits/index.js +171 -0
- package/src/indexer/index.js +1610 -0
- package/src/indexer/navigation/index.js +32 -0
- package/src/indexer/navigation/providers/treeSitter.js +36 -0
- package/src/indexer/parser.js +324 -0
- package/src/logger/index.js +27 -0
- package/src/mcp/client.js +194 -0
- package/src/mcp/index.js +34 -0
- package/src/mcp/permissions.js +69 -0
- package/src/mcp/registry.js +225 -0
- package/src/mcp/sandbox.js +238 -0
- package/src/metrics/index.js +38 -0
- package/src/orchestrator/index.js +1492 -0
- package/src/policy/index.js +212 -0
- package/src/policy/web-fallback.js +33 -0
- package/src/server.js +73 -0
- package/src/sessions/index.js +15 -0
- package/src/sessions/record.js +31 -0
- package/src/sessions/store.js +179 -0
- package/src/tasks/store.js +349 -0
- package/src/tests/coverage.js +173 -0
- package/src/tests/index.js +171 -0
- package/src/tests/store.js +213 -0
- package/src/tools/edits.js +94 -0
- package/src/tools/execution.js +169 -0
- package/src/tools/git.js +1346 -0
- package/src/tools/index.js +258 -0
- package/src/tools/indexer.js +360 -0
- package/src/tools/mcp-remote.js +81 -0
- package/src/tools/mcp.js +116 -0
- package/src/tools/process.js +151 -0
- package/src/tools/stubs.js +55 -0
- package/src/tools/tasks.js +260 -0
- package/src/tools/tests.js +132 -0
- package/src/tools/web.js +286 -0
- package/src/tools/workspace.js +173 -0
- package/src/workspace/index.js +95 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const config = require("../config");
|
|
2
|
+
const logger = require("../logger");
|
|
3
|
+
|
|
4
|
+
function normaliseValue(value) {
|
|
5
|
+
if (typeof value !== "string") return "";
|
|
6
|
+
return value.trim().toLowerCase();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function matchesPattern(pattern, target) {
|
|
10
|
+
if (!pattern) return false;
|
|
11
|
+
if (pattern === "*") return true;
|
|
12
|
+
if (pattern.endsWith("*")) {
|
|
13
|
+
const prefix = pattern.slice(0, -1);
|
|
14
|
+
return target.startsWith(prefix);
|
|
15
|
+
}
|
|
16
|
+
return pattern === target;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function evaluateSandboxRequest({ sessionId, command }) {
|
|
20
|
+
const permissions = config.mcp?.permissions ?? {};
|
|
21
|
+
const mode = typeof permissions.mode === "string" ? permissions.mode : "auto";
|
|
22
|
+
const allowList = Array.isArray(permissions.allow)
|
|
23
|
+
? permissions.allow.map(normaliseValue)
|
|
24
|
+
: [];
|
|
25
|
+
const denyList = Array.isArray(permissions.deny)
|
|
26
|
+
? permissions.deny.map(normaliseValue)
|
|
27
|
+
: [];
|
|
28
|
+
const target = normaliseValue(command);
|
|
29
|
+
|
|
30
|
+
if (denyList.some((pattern) => matchesPattern(pattern, target))) {
|
|
31
|
+
const reason = `Command "${command}" denied by sandbox deny list.`;
|
|
32
|
+
logger.warn({ sessionId, command }, reason);
|
|
33
|
+
return { allowed: false, reason, source: "deny_list" };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (mode === "deny") {
|
|
37
|
+
const reason = "Sandbox execution disabled by configuration.";
|
|
38
|
+
logger.warn({ sessionId, command }, reason);
|
|
39
|
+
return { allowed: false, reason, source: "mode_deny" };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (mode === "require") {
|
|
43
|
+
if (allowList.length === 0) {
|
|
44
|
+
const reason =
|
|
45
|
+
"Sandbox permission mode 'require' is set but no allow list entries are configured.";
|
|
46
|
+
logger.warn({ sessionId, command }, reason);
|
|
47
|
+
return { allowed: false, reason, source: "mode_require" };
|
|
48
|
+
}
|
|
49
|
+
if (!allowList.some((pattern) => matchesPattern(pattern, target))) {
|
|
50
|
+
const reason = `Command "${command}" is not permitted by sandbox allow list.`;
|
|
51
|
+
logger.warn({ sessionId, command }, reason);
|
|
52
|
+
return { allowed: false, reason, source: "mode_require" };
|
|
53
|
+
}
|
|
54
|
+
return { allowed: true, reason: "Allow list match", source: "allow_list" };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (allowList.length > 0 && !allowList.some((pattern) => matchesPattern(pattern, target))) {
|
|
58
|
+
logger.debug(
|
|
59
|
+
{ sessionId, command },
|
|
60
|
+
"Sandbox command not in allow list; proceeding because mode is not 'require'.",
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { allowed: true, source: mode === "auto" ? "auto" : "implicit_allow" };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
evaluateSandboxRequest,
|
|
69
|
+
};
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const config = require("../config");
|
|
4
|
+
const logger = require("../logger");
|
|
5
|
+
const McpClient = require("./client");
|
|
6
|
+
|
|
7
|
+
const servers = new Map();
|
|
8
|
+
const clients = new Map();
|
|
9
|
+
let manifestLoaded = false;
|
|
10
|
+
|
|
11
|
+
function normaliseServer(entry) {
|
|
12
|
+
if (!entry || typeof entry !== "object") return null;
|
|
13
|
+
const id = entry.id ?? entry.name ?? entry.label;
|
|
14
|
+
if (!id) return null;
|
|
15
|
+
return {
|
|
16
|
+
id: String(id),
|
|
17
|
+
name: entry.name ?? String(id),
|
|
18
|
+
description: entry.description ?? null,
|
|
19
|
+
command: entry.command ?? null,
|
|
20
|
+
args: Array.isArray(entry.args) ? entry.args.map(String) : [],
|
|
21
|
+
env: entry.env && typeof entry.env === "object" ? entry.env : {},
|
|
22
|
+
transport: entry.transport ?? "stdio",
|
|
23
|
+
metadata: entry.metadata && typeof entry.metadata === "object" ? entry.metadata : {},
|
|
24
|
+
raw: entry,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function registerServer(entry) {
|
|
29
|
+
const server = normaliseServer(entry);
|
|
30
|
+
if (!server) return null;
|
|
31
|
+
servers.set(server.id, server);
|
|
32
|
+
return server;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function listServers() {
|
|
36
|
+
return Array.from(servers.values());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getServer(id) {
|
|
40
|
+
if (!id) return null;
|
|
41
|
+
return servers.get(String(id)) ?? null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function clearServers() {
|
|
45
|
+
servers.clear();
|
|
46
|
+
manifestLoaded = false;
|
|
47
|
+
clients.forEach((client) => {
|
|
48
|
+
client.close().catch((err) => {
|
|
49
|
+
logger.debug({ err }, "Error closing MCP client");
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
clients.clear();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function loadServersFromEntries(entries, { source } = {}) {
|
|
56
|
+
let registeredCount = 0;
|
|
57
|
+
entries.forEach((entry) => {
|
|
58
|
+
const registered = registerServer(entry);
|
|
59
|
+
if (registered) {
|
|
60
|
+
registeredCount += 1;
|
|
61
|
+
logger.debug(
|
|
62
|
+
{ mcpServer: registered.id, source },
|
|
63
|
+
"Registered MCP server",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
if (registeredCount > 0) {
|
|
68
|
+
logger.info(
|
|
69
|
+
{ count: registeredCount, source },
|
|
70
|
+
"Loaded MCP servers",
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return registeredCount;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readManifest(manifestPath) {
|
|
77
|
+
const resolved = path.resolve(manifestPath);
|
|
78
|
+
try {
|
|
79
|
+
const raw = fs.readFileSync(resolved, "utf8");
|
|
80
|
+
const parsed = JSON.parse(raw);
|
|
81
|
+
const entries = Array.isArray(parsed?.servers)
|
|
82
|
+
? parsed.servers
|
|
83
|
+
: Array.isArray(parsed)
|
|
84
|
+
? parsed
|
|
85
|
+
: [];
|
|
86
|
+
return { entries, path: resolved };
|
|
87
|
+
} catch (err) {
|
|
88
|
+
logger.warn({ err, manifest: resolved }, "Failed to load MCP server manifest");
|
|
89
|
+
return { entries: [], path: resolved, error: err };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function loadFromManifest(manifestPath, { clear = true } = {}) {
|
|
94
|
+
const { entries, path: resolved } = readManifest(manifestPath);
|
|
95
|
+
if (clear) {
|
|
96
|
+
clearServers();
|
|
97
|
+
}
|
|
98
|
+
const registeredCount = loadServersFromEntries(entries, { source: resolved });
|
|
99
|
+
manifestLoaded = manifestLoaded || registeredCount > 0;
|
|
100
|
+
return listServers();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function discoverManifestFiles(directories = []) {
|
|
104
|
+
const files = [];
|
|
105
|
+
directories.forEach((dir) => {
|
|
106
|
+
if (typeof dir !== "string" || dir.length === 0) return;
|
|
107
|
+
let stats = null;
|
|
108
|
+
try {
|
|
109
|
+
stats = fs.statSync(dir);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
logger.debug({ dir, err }, "Manifest directory not accessible");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (!stats.isDirectory()) {
|
|
115
|
+
logger.debug({ dir }, "Manifest path is not a directory");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
120
|
+
entries.forEach((entry) => {
|
|
121
|
+
if (!entry.isFile()) return;
|
|
122
|
+
if (!entry.name.toLowerCase().endsWith(".json")) return;
|
|
123
|
+
files.push(path.join(dir, entry.name));
|
|
124
|
+
});
|
|
125
|
+
} catch (err) {
|
|
126
|
+
logger.debug({ dir, err }, "Failed to read manifest directory");
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
return files;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function loadConfiguredServers() {
|
|
133
|
+
const manifestPath = config.mcp?.servers?.manifestPath;
|
|
134
|
+
const manifestDirs = Array.isArray(config.mcp?.servers?.manifestDirs)
|
|
135
|
+
? config.mcp.servers.manifestDirs
|
|
136
|
+
: [];
|
|
137
|
+
|
|
138
|
+
clearServers();
|
|
139
|
+
const seen = new Set();
|
|
140
|
+
|
|
141
|
+
if (manifestPath) {
|
|
142
|
+
const resolved = path.resolve(manifestPath);
|
|
143
|
+
seen.add(resolved);
|
|
144
|
+
loadFromManifest(resolved, { clear: false });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const discovered = discoverManifestFiles(manifestDirs);
|
|
148
|
+
discovered.forEach((filePath) => {
|
|
149
|
+
const resolved = path.resolve(filePath);
|
|
150
|
+
if (seen.has(resolved)) return;
|
|
151
|
+
seen.add(resolved);
|
|
152
|
+
const { entries } = readManifest(resolved);
|
|
153
|
+
loadServersFromEntries(entries, { source: resolved });
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
manifestLoaded = true;
|
|
157
|
+
listServers().forEach((server) => {
|
|
158
|
+
ensureClient(server.id).catch((err) => {
|
|
159
|
+
logger.warn({ err, server: server.id }, "Failed to start MCP client");
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
return listServers();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function hasLoadedManifest() {
|
|
166
|
+
return manifestLoaded;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function ensureClient(serverId) {
|
|
170
|
+
if (!serverId) return null;
|
|
171
|
+
const existing = clients.get(serverId);
|
|
172
|
+
if (existing) return existing;
|
|
173
|
+
const server = getServer(serverId);
|
|
174
|
+
if (!server) return null;
|
|
175
|
+
if (server.transport && server.transport !== "stdio") {
|
|
176
|
+
logger.warn(
|
|
177
|
+
{ server: server.id, transport: server.transport },
|
|
178
|
+
"Unsupported MCP transport; only 'stdio' is implemented",
|
|
179
|
+
);
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const client = new McpClient(server);
|
|
184
|
+
clients.set(serverId, client);
|
|
185
|
+
try {
|
|
186
|
+
await client.start();
|
|
187
|
+
logger.info({ server: server.id }, "MCP client ready");
|
|
188
|
+
} catch (err) {
|
|
189
|
+
clients.delete(serverId);
|
|
190
|
+
logger.error({ err, server: server.id }, "Failed to start MCP client");
|
|
191
|
+
throw err;
|
|
192
|
+
}
|
|
193
|
+
return client;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function getClient(serverId) {
|
|
197
|
+
return clients.get(serverId) ?? null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function listClients() {
|
|
201
|
+
return Array.from(clients.keys());
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
module.exports = {
|
|
205
|
+
registerServer,
|
|
206
|
+
listServers,
|
|
207
|
+
getServer,
|
|
208
|
+
clearServers,
|
|
209
|
+
loadFromManifest,
|
|
210
|
+
loadConfiguredServers,
|
|
211
|
+
hasLoadedManifest,
|
|
212
|
+
ensureClient,
|
|
213
|
+
getClient,
|
|
214
|
+
listClients,
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
module.exports = {
|
|
218
|
+
registerServer,
|
|
219
|
+
listServers,
|
|
220
|
+
getServer,
|
|
221
|
+
clearServers,
|
|
222
|
+
loadFromManifest,
|
|
223
|
+
loadConfiguredServers,
|
|
224
|
+
hasLoadedManifest,
|
|
225
|
+
};
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
const { spawn } = require("child_process");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const config = require("../config");
|
|
4
|
+
const logger = require("../logger");
|
|
5
|
+
const { workspaceRoot } = require("../workspace");
|
|
6
|
+
const { evaluateSandboxRequest } = require("./permissions");
|
|
7
|
+
|
|
8
|
+
const DEFAULT_MAX_BUFFER = 1024 * 1024;
|
|
9
|
+
const sessionStore = new Map();
|
|
10
|
+
|
|
11
|
+
function isSandboxEnabled() {
|
|
12
|
+
return Boolean(config.mcp?.sandbox?.enabled);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normaliseSessionId(sessionId) {
|
|
16
|
+
if (!sessionId) return "shared";
|
|
17
|
+
return String(sessionId);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function ensureSession(sessionId) {
|
|
21
|
+
const key = normaliseSessionId(sessionId);
|
|
22
|
+
if (!sessionStore.has(key)) {
|
|
23
|
+
sessionStore.set(key, {
|
|
24
|
+
id: key,
|
|
25
|
+
createdAt: Date.now(),
|
|
26
|
+
lastUsedAt: null,
|
|
27
|
+
runCount: 0,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return sessionStore.get(key);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function listSessions() {
|
|
34
|
+
return Array.from(sessionStore.values()).map((session) => ({
|
|
35
|
+
id: session.id,
|
|
36
|
+
createdAt: session.createdAt,
|
|
37
|
+
lastUsedAt: session.lastUsedAt,
|
|
38
|
+
runCount: session.runCount,
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function releaseSession(sessionId) {
|
|
43
|
+
const key = normaliseSessionId(sessionId);
|
|
44
|
+
sessionStore.delete(key);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function toContainerPath(hostPath) {
|
|
48
|
+
const relative = path.relative(workspaceRoot, hostPath);
|
|
49
|
+
if (relative && relative.startsWith("..")) {
|
|
50
|
+
throw new Error(`Path "${hostPath}" is outside of the workspace root and cannot be mounted.`);
|
|
51
|
+
}
|
|
52
|
+
const containerRoot = config.mcp?.sandbox?.containerWorkspace ?? "/workspace";
|
|
53
|
+
if (!relative || relative === "") {
|
|
54
|
+
return containerRoot;
|
|
55
|
+
}
|
|
56
|
+
const segments = relative.split(path.sep).filter(Boolean);
|
|
57
|
+
return [containerRoot, ...segments].join("/").replace(/\/+/g, "/");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function buildRuntimeArgs({ session, command, args, cwd, env }) {
|
|
61
|
+
const sandboxConfig = config.mcp?.sandbox ?? {};
|
|
62
|
+
const runtimeArgs = ["run", "--rm"];
|
|
63
|
+
|
|
64
|
+
if (!sandboxConfig.allowNetworking) {
|
|
65
|
+
runtimeArgs.push("--network", "none");
|
|
66
|
+
} else if (sandboxConfig.networkMode && sandboxConfig.networkMode !== "none") {
|
|
67
|
+
runtimeArgs.push("--network", sandboxConfig.networkMode);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (sandboxConfig.mountWorkspace !== false) {
|
|
71
|
+
runtimeArgs.push(
|
|
72
|
+
"-v",
|
|
73
|
+
`${workspaceRoot}:${sandboxConfig.containerWorkspace ?? "/workspace"}:rw`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (const mount of sandboxConfig.extraMounts ?? []) {
|
|
78
|
+
runtimeArgs.push("-v", `${mount.host}:${mount.container}:${mount.mode}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const containerCwd = toContainerPath(cwd ?? workspaceRoot);
|
|
82
|
+
runtimeArgs.push("-w", containerCwd);
|
|
83
|
+
|
|
84
|
+
if (sandboxConfig.user) {
|
|
85
|
+
runtimeArgs.push("-u", sandboxConfig.user);
|
|
86
|
+
}
|
|
87
|
+
if (sandboxConfig.entrypoint) {
|
|
88
|
+
runtimeArgs.push("--entrypoint", sandboxConfig.entrypoint);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const passthroughEnv = new Set(
|
|
92
|
+
Array.isArray(sandboxConfig.passthroughEnv)
|
|
93
|
+
? sandboxConfig.passthroughEnv.map((name) => String(name).toUpperCase())
|
|
94
|
+
: [],
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const envArgs = [];
|
|
98
|
+
if (env && typeof env === "object") {
|
|
99
|
+
for (const [key, value] of Object.entries(env)) {
|
|
100
|
+
if (passthroughEnv.size > 0 && !passthroughEnv.has(String(key).toUpperCase())) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (value === undefined || value === null) continue;
|
|
104
|
+
envArgs.push("-e", `${key}=${value}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
envArgs.push("-e", `MCP_SANDBOX_SESSION=${session.id}`);
|
|
109
|
+
runtimeArgs.push(...envArgs);
|
|
110
|
+
|
|
111
|
+
const commandArgs = Array.isArray(args) ? args.map(String) : [];
|
|
112
|
+
runtimeArgs.push(sandboxConfig.image, command, ...commandArgs);
|
|
113
|
+
return runtimeArgs;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function appendBuffer(current, chunk, maxBuffer) {
|
|
117
|
+
if (current.length >= maxBuffer) {
|
|
118
|
+
return { value: current, overflow: true };
|
|
119
|
+
}
|
|
120
|
+
const next = current + chunk;
|
|
121
|
+
if (next.length > maxBuffer) {
|
|
122
|
+
return { value: next.slice(0, maxBuffer), overflow: true };
|
|
123
|
+
}
|
|
124
|
+
return { value: next, overflow: false };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function runSandboxProcess({
|
|
128
|
+
sessionId,
|
|
129
|
+
command,
|
|
130
|
+
args = [],
|
|
131
|
+
input,
|
|
132
|
+
cwd,
|
|
133
|
+
env,
|
|
134
|
+
timeoutMs,
|
|
135
|
+
maxBuffer = DEFAULT_MAX_BUFFER,
|
|
136
|
+
}) {
|
|
137
|
+
if (!isSandboxEnabled()) {
|
|
138
|
+
throw new Error("Sandbox execution requested but the sandbox is not enabled.");
|
|
139
|
+
}
|
|
140
|
+
const sandboxConfig = config.mcp?.sandbox ?? {};
|
|
141
|
+
const session = ensureSession(sessionId);
|
|
142
|
+
|
|
143
|
+
const permission = evaluateSandboxRequest({ sessionId: session.id, command });
|
|
144
|
+
if (!permission.allowed) {
|
|
145
|
+
const error = new Error(`Sandbox permission denied: ${permission.reason}`);
|
|
146
|
+
error.code = "SANDBOX_PERMISSION_DENIED";
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const runtimeCommand = sandboxConfig.runtime ?? "docker";
|
|
151
|
+
const runtimeArgs = buildRuntimeArgs({
|
|
152
|
+
session,
|
|
153
|
+
command,
|
|
154
|
+
args,
|
|
155
|
+
cwd,
|
|
156
|
+
env,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
logger.debug(
|
|
160
|
+
{
|
|
161
|
+
sessionId: session.id,
|
|
162
|
+
runtime: runtimeCommand,
|
|
163
|
+
args: runtimeArgs,
|
|
164
|
+
permissionSource: permission.source,
|
|
165
|
+
},
|
|
166
|
+
"Launching sandboxed process",
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const timeout = Number.isFinite(timeoutMs) && timeoutMs > 0
|
|
170
|
+
? timeoutMs
|
|
171
|
+
: sandboxConfig.defaultTimeoutMs ?? 20000;
|
|
172
|
+
|
|
173
|
+
return new Promise((resolve, reject) => {
|
|
174
|
+
const child = spawn(runtimeCommand, runtimeArgs, {
|
|
175
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
let stdout = "";
|
|
179
|
+
let stderr = "";
|
|
180
|
+
let stdoutOverflow = false;
|
|
181
|
+
let stderrOverflow = false;
|
|
182
|
+
let timedOut = false;
|
|
183
|
+
const start = Date.now();
|
|
184
|
+
|
|
185
|
+
const timer = setTimeout(() => {
|
|
186
|
+
timedOut = true;
|
|
187
|
+
child.kill("SIGKILL");
|
|
188
|
+
}, timeout);
|
|
189
|
+
|
|
190
|
+
child.stdout.on("data", (chunk) => {
|
|
191
|
+
const { value, overflow } = appendBuffer(stdout, chunk.toString(), maxBuffer);
|
|
192
|
+
stdout = value;
|
|
193
|
+
if (overflow) stdoutOverflow = true;
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
child.stderr.on("data", (chunk) => {
|
|
197
|
+
const { value, overflow } = appendBuffer(stderr, chunk.toString(), maxBuffer);
|
|
198
|
+
stderr = value;
|
|
199
|
+
if (overflow) stderrOverflow = true;
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
child.on("error", (err) => {
|
|
203
|
+
clearTimeout(timer);
|
|
204
|
+
reject(err);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
child.on("close", (code, signal) => {
|
|
208
|
+
clearTimeout(timer);
|
|
209
|
+
session.lastUsedAt = Date.now();
|
|
210
|
+
session.runCount += 1;
|
|
211
|
+
resolve({
|
|
212
|
+
exitCode: code,
|
|
213
|
+
signal,
|
|
214
|
+
stdout,
|
|
215
|
+
stderr,
|
|
216
|
+
stdoutOverflow,
|
|
217
|
+
stderrOverflow,
|
|
218
|
+
timedOut,
|
|
219
|
+
durationMs: Date.now() - start,
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
if (typeof input === "string" && input.length > 0 && child.stdin.writable) {
|
|
224
|
+
child.stdin.write(input);
|
|
225
|
+
child.stdin.end();
|
|
226
|
+
} else if (child.stdin.writable) {
|
|
227
|
+
child.stdin.end();
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
module.exports = {
|
|
233
|
+
isSandboxEnabled,
|
|
234
|
+
runSandboxProcess,
|
|
235
|
+
ensureSession,
|
|
236
|
+
listSessions,
|
|
237
|
+
releaseSession,
|
|
238
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const metrics = {
|
|
2
|
+
requestsTotal: 0,
|
|
3
|
+
responses: {
|
|
4
|
+
success: 0,
|
|
5
|
+
error: 0,
|
|
6
|
+
},
|
|
7
|
+
streamingSessions: 0,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function recordRequest() {
|
|
11
|
+
metrics.requestsTotal += 1;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function recordStreamingStart() {
|
|
15
|
+
metrics.streamingSessions += 1;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function recordResponse(status) {
|
|
19
|
+
if (status >= 200 && status < 400) {
|
|
20
|
+
metrics.responses.success += 1;
|
|
21
|
+
} else {
|
|
22
|
+
metrics.responses.error += 1;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function snapshot() {
|
|
27
|
+
return {
|
|
28
|
+
...metrics,
|
|
29
|
+
timestamp: Date.now(),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = {
|
|
34
|
+
recordRequest,
|
|
35
|
+
recordResponse,
|
|
36
|
+
recordStreamingStart,
|
|
37
|
+
snapshot,
|
|
38
|
+
};
|