@agenshield/broker 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/LICENSE +201 -0
- package/README.md +141 -0
- package/audit/logger.d.ts +69 -0
- package/audit/logger.d.ts.map +1 -0
- package/client/broker-client.d.ts +91 -0
- package/client/broker-client.d.ts.map +1 -0
- package/client/index.d.ts +8 -0
- package/client/index.d.ts.map +1 -0
- package/client/index.js +222 -0
- package/client/shield-client.d.ts +8 -0
- package/client/shield-client.d.ts.map +1 -0
- package/client/shield-client.js +410 -0
- package/handlers/exec.d.ts +13 -0
- package/handlers/exec.d.ts.map +1 -0
- package/handlers/file.d.ts +20 -0
- package/handlers/file.d.ts.map +1 -0
- package/handlers/http.d.ts +9 -0
- package/handlers/http.d.ts.map +1 -0
- package/handlers/index.d.ts +12 -0
- package/handlers/index.d.ts.map +1 -0
- package/handlers/open-url.d.ts +9 -0
- package/handlers/open-url.d.ts.map +1 -0
- package/handlers/ping.d.ts +9 -0
- package/handlers/ping.d.ts.map +1 -0
- package/handlers/secret-inject.d.ts +9 -0
- package/handlers/secret-inject.d.ts.map +1 -0
- package/handlers/skill-install.d.ts +17 -0
- package/handlers/skill-install.d.ts.map +1 -0
- package/handlers/types.d.ts +28 -0
- package/handlers/types.d.ts.map +1 -0
- package/http-fallback.d.ts +54 -0
- package/http-fallback.d.ts.map +1 -0
- package/index.d.ts +18 -0
- package/index.d.ts.map +1 -0
- package/index.js +2636 -0
- package/main.d.ts +8 -0
- package/main.d.ts.map +1 -0
- package/main.js +2136 -0
- package/package.json +34 -0
- package/policies/builtin.d.ts +15 -0
- package/policies/builtin.d.ts.map +1 -0
- package/policies/command-allowlist.d.ts +62 -0
- package/policies/command-allowlist.d.ts.map +1 -0
- package/policies/enforcer.d.ts +98 -0
- package/policies/enforcer.d.ts.map +1 -0
- package/policies/index.d.ts +8 -0
- package/policies/index.d.ts.map +1 -0
- package/seatbelt/generator.d.ts +39 -0
- package/seatbelt/generator.d.ts.map +1 -0
- package/seatbelt/templates.d.ts +36 -0
- package/seatbelt/templates.d.ts.map +1 -0
- package/secrets/vault.d.ts +67 -0
- package/secrets/vault.d.ts.map +1 -0
- package/server.d.ts +54 -0
- package/server.d.ts.map +1 -0
- package/types.d.ts +285 -0
- package/types.d.ts.map +1 -0
package/index.js
ADDED
|
@@ -0,0 +1,2636 @@
|
|
|
1
|
+
// libs/shield-broker/src/server.ts
|
|
2
|
+
import * as net from "node:net";
|
|
3
|
+
import * as fs3 from "node:fs";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
|
|
6
|
+
// libs/shield-broker/src/handlers/http.ts
|
|
7
|
+
async function handleHttpRequest(params, context, deps) {
|
|
8
|
+
const startTime = Date.now();
|
|
9
|
+
try {
|
|
10
|
+
const {
|
|
11
|
+
url,
|
|
12
|
+
method = "GET",
|
|
13
|
+
headers = {},
|
|
14
|
+
body,
|
|
15
|
+
timeout = 3e4,
|
|
16
|
+
followRedirects = true
|
|
17
|
+
} = params;
|
|
18
|
+
if (!url) {
|
|
19
|
+
return {
|
|
20
|
+
success: false,
|
|
21
|
+
error: { code: 1003, message: "URL is required" }
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
let parsedUrl;
|
|
25
|
+
try {
|
|
26
|
+
parsedUrl = new URL(url);
|
|
27
|
+
} catch {
|
|
28
|
+
return {
|
|
29
|
+
success: false,
|
|
30
|
+
error: { code: 1003, message: "Invalid URL" }
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const controller = new AbortController();
|
|
34
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
35
|
+
try {
|
|
36
|
+
const response = await fetch(url, {
|
|
37
|
+
method,
|
|
38
|
+
headers,
|
|
39
|
+
body: body ? String(body) : void 0,
|
|
40
|
+
signal: controller.signal,
|
|
41
|
+
redirect: followRedirects ? "follow" : "manual"
|
|
42
|
+
});
|
|
43
|
+
clearTimeout(timeoutId);
|
|
44
|
+
const responseHeaders = {};
|
|
45
|
+
response.headers.forEach((value, key) => {
|
|
46
|
+
responseHeaders[key] = value;
|
|
47
|
+
});
|
|
48
|
+
const responseBody = await response.text();
|
|
49
|
+
return {
|
|
50
|
+
success: true,
|
|
51
|
+
data: {
|
|
52
|
+
status: response.status,
|
|
53
|
+
statusText: response.statusText,
|
|
54
|
+
headers: responseHeaders,
|
|
55
|
+
body: responseBody
|
|
56
|
+
},
|
|
57
|
+
audit: {
|
|
58
|
+
duration: Date.now() - startTime,
|
|
59
|
+
bytesTransferred: responseBody.length
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
} catch (error) {
|
|
63
|
+
clearTimeout(timeoutId);
|
|
64
|
+
if (error.name === "AbortError") {
|
|
65
|
+
return {
|
|
66
|
+
success: false,
|
|
67
|
+
error: { code: 1004, message: "Request timeout" }
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
success: false,
|
|
72
|
+
error: { code: 1004, message: `Network error: ${error.message}` }
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
return {
|
|
77
|
+
success: false,
|
|
78
|
+
error: { code: 1004, message: `Handler error: ${error.message}` }
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// libs/shield-broker/src/handlers/file.ts
|
|
84
|
+
import * as fs from "node:fs/promises";
|
|
85
|
+
import * as path from "node:path";
|
|
86
|
+
async function handleFileRead(params, context, deps) {
|
|
87
|
+
const startTime = Date.now();
|
|
88
|
+
try {
|
|
89
|
+
const { path: filePath, encoding = "utf-8" } = params;
|
|
90
|
+
if (!filePath) {
|
|
91
|
+
return {
|
|
92
|
+
success: false,
|
|
93
|
+
error: { code: 1003, message: "Path is required" }
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const absolutePath = path.resolve(filePath);
|
|
97
|
+
try {
|
|
98
|
+
await fs.access(absolutePath, fs.constants.R_OK);
|
|
99
|
+
} catch {
|
|
100
|
+
return {
|
|
101
|
+
success: false,
|
|
102
|
+
error: { code: 1005, message: `File not found or not readable: ${absolutePath}` }
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const stats = await fs.stat(absolutePath);
|
|
106
|
+
if (!stats.isFile()) {
|
|
107
|
+
return {
|
|
108
|
+
success: false,
|
|
109
|
+
error: { code: 1005, message: "Path is not a file" }
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const content = await fs.readFile(absolutePath, { encoding });
|
|
113
|
+
return {
|
|
114
|
+
success: true,
|
|
115
|
+
data: {
|
|
116
|
+
content,
|
|
117
|
+
size: stats.size,
|
|
118
|
+
mtime: stats.mtime.toISOString()
|
|
119
|
+
},
|
|
120
|
+
audit: {
|
|
121
|
+
duration: Date.now() - startTime,
|
|
122
|
+
bytesTransferred: stats.size
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
} catch (error) {
|
|
126
|
+
return {
|
|
127
|
+
success: false,
|
|
128
|
+
error: { code: 1005, message: `File read error: ${error.message}` }
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async function handleFileWrite(params, context, deps) {
|
|
133
|
+
const startTime = Date.now();
|
|
134
|
+
try {
|
|
135
|
+
const {
|
|
136
|
+
path: filePath,
|
|
137
|
+
content,
|
|
138
|
+
encoding = "utf-8",
|
|
139
|
+
mode
|
|
140
|
+
} = params;
|
|
141
|
+
if (!filePath) {
|
|
142
|
+
return {
|
|
143
|
+
success: false,
|
|
144
|
+
error: { code: 1003, message: "Path is required" }
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (content === void 0) {
|
|
148
|
+
return {
|
|
149
|
+
success: false,
|
|
150
|
+
error: { code: 1003, message: "Content is required" }
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const absolutePath = path.resolve(filePath);
|
|
154
|
+
const parentDir = path.dirname(absolutePath);
|
|
155
|
+
await fs.mkdir(parentDir, { recursive: true });
|
|
156
|
+
const buffer = Buffer.from(content, encoding);
|
|
157
|
+
await fs.writeFile(absolutePath, buffer, { mode });
|
|
158
|
+
return {
|
|
159
|
+
success: true,
|
|
160
|
+
data: {
|
|
161
|
+
bytesWritten: buffer.length,
|
|
162
|
+
path: absolutePath
|
|
163
|
+
},
|
|
164
|
+
audit: {
|
|
165
|
+
duration: Date.now() - startTime,
|
|
166
|
+
bytesTransferred: buffer.length
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
} catch (error) {
|
|
170
|
+
return {
|
|
171
|
+
success: false,
|
|
172
|
+
error: { code: 1005, message: `File write error: ${error.message}` }
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async function handleFileList(params, context, deps) {
|
|
177
|
+
const startTime = Date.now();
|
|
178
|
+
try {
|
|
179
|
+
const { path: dirPath, recursive = false, pattern } = params;
|
|
180
|
+
if (!dirPath) {
|
|
181
|
+
return {
|
|
182
|
+
success: false,
|
|
183
|
+
error: { code: 1003, message: "Path is required" }
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
const absolutePath = path.resolve(dirPath);
|
|
187
|
+
try {
|
|
188
|
+
await fs.access(absolutePath, fs.constants.R_OK);
|
|
189
|
+
} catch {
|
|
190
|
+
return {
|
|
191
|
+
success: false,
|
|
192
|
+
error: { code: 1005, message: `Directory not found or not readable: ${absolutePath}` }
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
const stats = await fs.stat(absolutePath);
|
|
196
|
+
if (!stats.isDirectory()) {
|
|
197
|
+
return {
|
|
198
|
+
success: false,
|
|
199
|
+
error: { code: 1005, message: "Path is not a directory" }
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
const entries = await listDirectory(absolutePath, recursive, pattern);
|
|
203
|
+
return {
|
|
204
|
+
success: true,
|
|
205
|
+
data: { entries },
|
|
206
|
+
audit: {
|
|
207
|
+
duration: Date.now() - startTime
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
} catch (error) {
|
|
211
|
+
return {
|
|
212
|
+
success: false,
|
|
213
|
+
error: { code: 1005, message: `File list error: ${error.message}` }
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async function listDirectory(dirPath, recursive, pattern) {
|
|
218
|
+
const entries = [];
|
|
219
|
+
const items = await fs.readdir(dirPath, { withFileTypes: true });
|
|
220
|
+
for (const item of items) {
|
|
221
|
+
const itemPath = path.join(dirPath, item.name);
|
|
222
|
+
if (pattern && !matchPattern(item.name, pattern)) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
const stats = await fs.stat(itemPath);
|
|
227
|
+
entries.push({
|
|
228
|
+
name: item.name,
|
|
229
|
+
path: itemPath,
|
|
230
|
+
type: item.isDirectory() ? "directory" : item.isSymbolicLink() ? "symlink" : "file",
|
|
231
|
+
size: stats.size,
|
|
232
|
+
mtime: stats.mtime.toISOString()
|
|
233
|
+
});
|
|
234
|
+
if (recursive && item.isDirectory()) {
|
|
235
|
+
const subEntries = await listDirectory(itemPath, recursive, pattern);
|
|
236
|
+
entries.push(...subEntries);
|
|
237
|
+
}
|
|
238
|
+
} catch {
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return entries;
|
|
242
|
+
}
|
|
243
|
+
function matchPattern(name, pattern) {
|
|
244
|
+
const regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
245
|
+
const regex = new RegExp(`^${regexPattern}$`, "i");
|
|
246
|
+
return regex.test(name);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// libs/shield-broker/src/handlers/exec.ts
|
|
250
|
+
import * as path2 from "node:path";
|
|
251
|
+
import { spawn } from "node:child_process";
|
|
252
|
+
var MAX_OUTPUT_SIZE = 10 * 1024 * 1024;
|
|
253
|
+
var DEFAULT_WORKSPACE = "/Users/clawagent/workspace";
|
|
254
|
+
var FS_COMMANDS = /* @__PURE__ */ new Set([
|
|
255
|
+
"rm",
|
|
256
|
+
"cp",
|
|
257
|
+
"mv",
|
|
258
|
+
"mkdir",
|
|
259
|
+
"touch",
|
|
260
|
+
"chmod",
|
|
261
|
+
"cat",
|
|
262
|
+
"ls",
|
|
263
|
+
"find",
|
|
264
|
+
"head",
|
|
265
|
+
"tail",
|
|
266
|
+
"tar",
|
|
267
|
+
"sed",
|
|
268
|
+
"awk",
|
|
269
|
+
"sort",
|
|
270
|
+
"uniq",
|
|
271
|
+
"wc",
|
|
272
|
+
"grep"
|
|
273
|
+
]);
|
|
274
|
+
var HTTP_EXEC_COMMANDS = /* @__PURE__ */ new Set(["curl", "wget"]);
|
|
275
|
+
var HTTP_FLAGS_WITH_VALUE = /* @__PURE__ */ new Set([
|
|
276
|
+
"-X",
|
|
277
|
+
"--request",
|
|
278
|
+
"-H",
|
|
279
|
+
"--header",
|
|
280
|
+
"-d",
|
|
281
|
+
"--data",
|
|
282
|
+
"--data-raw",
|
|
283
|
+
"--data-binary",
|
|
284
|
+
"--data-urlencode",
|
|
285
|
+
"-o",
|
|
286
|
+
"--output",
|
|
287
|
+
"-u",
|
|
288
|
+
"--user",
|
|
289
|
+
"-A",
|
|
290
|
+
"--user-agent",
|
|
291
|
+
"-e",
|
|
292
|
+
"--referer",
|
|
293
|
+
"-b",
|
|
294
|
+
"--cookie",
|
|
295
|
+
"-c",
|
|
296
|
+
"--cookie-jar",
|
|
297
|
+
"--connect-timeout",
|
|
298
|
+
"--max-time",
|
|
299
|
+
"-w",
|
|
300
|
+
"--write-out",
|
|
301
|
+
"-T",
|
|
302
|
+
"--upload-file",
|
|
303
|
+
"--resolve",
|
|
304
|
+
"--cacert",
|
|
305
|
+
"--cert",
|
|
306
|
+
"--key"
|
|
307
|
+
]);
|
|
308
|
+
function validateFsPaths(args, cwd, allowedPaths) {
|
|
309
|
+
for (let i = 0; i < args.length; i++) {
|
|
310
|
+
const arg = args[i];
|
|
311
|
+
if (arg.startsWith("-")) {
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
const resolved = path2.isAbsolute(arg) ? path2.resolve(arg) : path2.resolve(cwd, arg);
|
|
315
|
+
const isAllowed = allowedPaths.some((allowed) => resolved.startsWith(allowed));
|
|
316
|
+
if (!isAllowed) {
|
|
317
|
+
return {
|
|
318
|
+
valid: false,
|
|
319
|
+
reason: "Path not in allowed directories",
|
|
320
|
+
violatingPath: resolved
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return { valid: true };
|
|
325
|
+
}
|
|
326
|
+
function extractUrlFromArgs(args) {
|
|
327
|
+
for (let i = 0; i < args.length; i++) {
|
|
328
|
+
const arg = args[i];
|
|
329
|
+
if (arg.startsWith("-")) {
|
|
330
|
+
if (HTTP_FLAGS_WITH_VALUE.has(arg)) {
|
|
331
|
+
i++;
|
|
332
|
+
}
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
return arg;
|
|
336
|
+
}
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
async function handleExec(params, context, deps) {
|
|
340
|
+
const startTime = Date.now();
|
|
341
|
+
try {
|
|
342
|
+
const {
|
|
343
|
+
command,
|
|
344
|
+
args = [],
|
|
345
|
+
cwd,
|
|
346
|
+
env,
|
|
347
|
+
timeout = 3e4
|
|
348
|
+
} = params;
|
|
349
|
+
if (!command) {
|
|
350
|
+
return {
|
|
351
|
+
success: false,
|
|
352
|
+
error: { code: 1003, message: "Command is required" }
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
const resolvedCommand = deps.commandAllowlist.resolve(command);
|
|
356
|
+
if (!resolvedCommand) {
|
|
357
|
+
const reason = `Command not allowed: ${command}`;
|
|
358
|
+
deps.onExecDenied?.(command, reason);
|
|
359
|
+
return {
|
|
360
|
+
success: false,
|
|
361
|
+
error: { code: 1007, message: reason }
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
const commandBasename = path2.basename(resolvedCommand);
|
|
365
|
+
const effectiveCwd = cwd || DEFAULT_WORKSPACE;
|
|
366
|
+
if (FS_COMMANDS.has(commandBasename)) {
|
|
367
|
+
const policies = deps.policyEnforcer.getPolicies();
|
|
368
|
+
const allowedPaths = policies.fsConstraints?.allowedPaths || [DEFAULT_WORKSPACE];
|
|
369
|
+
const fsResult = validateFsPaths(args, effectiveCwd, allowedPaths);
|
|
370
|
+
if (!fsResult.valid) {
|
|
371
|
+
const reason = `${fsResult.reason}: ${fsResult.violatingPath}`;
|
|
372
|
+
deps.onExecDenied?.(command, reason);
|
|
373
|
+
return {
|
|
374
|
+
success: false,
|
|
375
|
+
error: { code: 1008, message: `Path not allowed: ${fsResult.violatingPath} - ${fsResult.reason}` }
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (HTTP_EXEC_COMMANDS.has(commandBasename)) {
|
|
380
|
+
const url = extractUrlFromArgs(args);
|
|
381
|
+
if (url) {
|
|
382
|
+
const networkCheck = await deps.policyEnforcer.check("http_request", { url }, context);
|
|
383
|
+
if (!networkCheck.allowed) {
|
|
384
|
+
const reason = `URL not allowed: ${url} - ${networkCheck.reason}`;
|
|
385
|
+
deps.onExecDenied?.(command, reason);
|
|
386
|
+
return {
|
|
387
|
+
success: false,
|
|
388
|
+
error: { code: 1009, message: reason }
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
const effectiveTimeout = HTTP_EXEC_COMMANDS.has(commandBasename) ? Math.max(timeout, 3e5) : timeout;
|
|
394
|
+
const result = await executeCommand({
|
|
395
|
+
command: resolvedCommand,
|
|
396
|
+
args,
|
|
397
|
+
cwd: effectiveCwd,
|
|
398
|
+
env,
|
|
399
|
+
timeout: effectiveTimeout,
|
|
400
|
+
shell: false
|
|
401
|
+
// Always force shell: false to prevent injection
|
|
402
|
+
});
|
|
403
|
+
const duration = Date.now() - startTime;
|
|
404
|
+
deps.onExecMonitor?.({
|
|
405
|
+
command: commandBasename,
|
|
406
|
+
args,
|
|
407
|
+
cwd: effectiveCwd,
|
|
408
|
+
exitCode: result.exitCode,
|
|
409
|
+
allowed: true,
|
|
410
|
+
duration,
|
|
411
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
412
|
+
});
|
|
413
|
+
return {
|
|
414
|
+
success: true,
|
|
415
|
+
data: result,
|
|
416
|
+
audit: {
|
|
417
|
+
duration
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
} catch (error) {
|
|
421
|
+
return {
|
|
422
|
+
success: false,
|
|
423
|
+
error: { code: 1006, message: `Exec error: ${error.message}` }
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
async function executeCommand(options) {
|
|
428
|
+
return new Promise((resolve3, reject) => {
|
|
429
|
+
const { command, args = [], cwd, env, timeout = 3e4 } = options;
|
|
430
|
+
const shell = false;
|
|
431
|
+
let stdout = "";
|
|
432
|
+
let stderr = "";
|
|
433
|
+
let stdoutSize = 0;
|
|
434
|
+
let stderrSize = 0;
|
|
435
|
+
const proc = spawn(command, args, {
|
|
436
|
+
cwd,
|
|
437
|
+
env: env ? { ...process.env, ...env } : process.env,
|
|
438
|
+
shell,
|
|
439
|
+
timeout
|
|
440
|
+
});
|
|
441
|
+
proc.stdout?.on("data", (data) => {
|
|
442
|
+
const chunk = data.toString();
|
|
443
|
+
if (stdoutSize + chunk.length <= MAX_OUTPUT_SIZE) {
|
|
444
|
+
stdout += chunk;
|
|
445
|
+
stdoutSize += chunk.length;
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
proc.stderr?.on("data", (data) => {
|
|
449
|
+
const chunk = data.toString();
|
|
450
|
+
if (stderrSize + chunk.length <= MAX_OUTPUT_SIZE) {
|
|
451
|
+
stderr += chunk;
|
|
452
|
+
stderrSize += chunk.length;
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
proc.on("error", (error) => {
|
|
456
|
+
reject(error);
|
|
457
|
+
});
|
|
458
|
+
proc.on("close", (code, signal) => {
|
|
459
|
+
resolve3({
|
|
460
|
+
exitCode: code ?? -1,
|
|
461
|
+
stdout,
|
|
462
|
+
stderr,
|
|
463
|
+
signal: signal ?? void 0
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
const timeoutId = setTimeout(() => {
|
|
467
|
+
proc.kill("SIGTERM");
|
|
468
|
+
setTimeout(() => {
|
|
469
|
+
if (!proc.killed) {
|
|
470
|
+
proc.kill("SIGKILL");
|
|
471
|
+
}
|
|
472
|
+
}, 5e3);
|
|
473
|
+
}, timeout);
|
|
474
|
+
proc.on("exit", () => {
|
|
475
|
+
clearTimeout(timeoutId);
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// libs/shield-broker/src/handlers/open-url.ts
|
|
481
|
+
import { exec } from "node:child_process";
|
|
482
|
+
import { promisify } from "node:util";
|
|
483
|
+
var execAsync = promisify(exec);
|
|
484
|
+
async function handleOpenUrl(params, context, deps) {
|
|
485
|
+
const startTime = Date.now();
|
|
486
|
+
try {
|
|
487
|
+
const { url, browser } = params;
|
|
488
|
+
if (!url) {
|
|
489
|
+
return {
|
|
490
|
+
success: false,
|
|
491
|
+
error: { code: 1003, message: "URL is required" }
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
try {
|
|
495
|
+
new URL(url);
|
|
496
|
+
} catch {
|
|
497
|
+
return {
|
|
498
|
+
success: false,
|
|
499
|
+
error: { code: 1003, message: "Invalid URL" }
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
const parsedUrl = new URL(url);
|
|
503
|
+
if (!["http:", "https:"].includes(parsedUrl.protocol)) {
|
|
504
|
+
return {
|
|
505
|
+
success: false,
|
|
506
|
+
error: { code: 1003, message: "Only http/https URLs are allowed" }
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
const command = browser ? `open -a "${browser}" "${url}"` : `open "${url}"`;
|
|
510
|
+
try {
|
|
511
|
+
await execAsync(command, { timeout: 1e4 });
|
|
512
|
+
return {
|
|
513
|
+
success: true,
|
|
514
|
+
data: { opened: true },
|
|
515
|
+
audit: {
|
|
516
|
+
duration: Date.now() - startTime
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
} catch (error) {
|
|
520
|
+
return {
|
|
521
|
+
success: false,
|
|
522
|
+
error: {
|
|
523
|
+
code: 1006,
|
|
524
|
+
message: `Failed to open URL: ${error.message}`
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
} catch (error) {
|
|
529
|
+
return {
|
|
530
|
+
success: false,
|
|
531
|
+
error: { code: 1006, message: `Handler error: ${error.message}` }
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// libs/shield-broker/src/handlers/secret-inject.ts
|
|
537
|
+
async function handleSecretInject(params, context, deps) {
|
|
538
|
+
const startTime = Date.now();
|
|
539
|
+
try {
|
|
540
|
+
const { name, targetEnv } = params;
|
|
541
|
+
if (!name) {
|
|
542
|
+
return {
|
|
543
|
+
success: false,
|
|
544
|
+
error: { code: 1003, message: "Secret name is required" }
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
if (context.channel !== "socket") {
|
|
548
|
+
return {
|
|
549
|
+
success: false,
|
|
550
|
+
error: { code: 1008, message: "Secret injection only allowed via Unix socket" }
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
const secret = await deps.secretVault.get(name);
|
|
554
|
+
if (!secret) {
|
|
555
|
+
return {
|
|
556
|
+
success: false,
|
|
557
|
+
error: { code: 1007, message: `Secret not found: ${name}` }
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
return {
|
|
561
|
+
success: true,
|
|
562
|
+
data: {
|
|
563
|
+
value: secret.value,
|
|
564
|
+
injected: true
|
|
565
|
+
},
|
|
566
|
+
audit: {
|
|
567
|
+
duration: Date.now() - startTime
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
} catch (error) {
|
|
571
|
+
return {
|
|
572
|
+
success: false,
|
|
573
|
+
error: { code: 1007, message: `Secret inject error: ${error.message}` }
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// libs/shield-broker/src/handlers/ping.ts
|
|
579
|
+
var VERSION = "0.1.0";
|
|
580
|
+
async function handlePing(params, context, deps) {
|
|
581
|
+
const { echo } = params;
|
|
582
|
+
return {
|
|
583
|
+
success: true,
|
|
584
|
+
data: {
|
|
585
|
+
pong: true,
|
|
586
|
+
echo,
|
|
587
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
588
|
+
version: VERSION
|
|
589
|
+
},
|
|
590
|
+
audit: {
|
|
591
|
+
duration: 0
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// libs/shield-broker/src/handlers/skill-install.ts
|
|
597
|
+
import * as fs2 from "node:fs/promises";
|
|
598
|
+
import * as path3 from "node:path";
|
|
599
|
+
import { execSync } from "node:child_process";
|
|
600
|
+
function isValidSlug(slug) {
|
|
601
|
+
const validPattern = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
|
|
602
|
+
return validPattern.test(slug) && !slug.includes("..") && !slug.includes("/");
|
|
603
|
+
}
|
|
604
|
+
function createWrapperContent(slug, skillDir) {
|
|
605
|
+
return `#!/bin/bash
|
|
606
|
+
# Auto-generated wrapper for skill: ${slug}
|
|
607
|
+
# This script runs the skill via openclaw-pkg
|
|
608
|
+
|
|
609
|
+
set -e
|
|
610
|
+
|
|
611
|
+
SKILL_DIR="${skillDir}"
|
|
612
|
+
|
|
613
|
+
# Check if skill directory exists
|
|
614
|
+
if [ ! -d "$SKILL_DIR" ]; then
|
|
615
|
+
echo "Error: Skill directory not found: $SKILL_DIR" >&2
|
|
616
|
+
exit 1
|
|
617
|
+
fi
|
|
618
|
+
|
|
619
|
+
# Find and execute the main skill file
|
|
620
|
+
if [ -f "$SKILL_DIR/skill.md" ]; then
|
|
621
|
+
exec openclaw-pkg run "$SKILL_DIR/skill.md" "$@"
|
|
622
|
+
elif [ -f "$SKILL_DIR/index.js" ]; then
|
|
623
|
+
exec node "$SKILL_DIR/index.js" "$@"
|
|
624
|
+
elif [ -f "$SKILL_DIR/main.py" ]; then
|
|
625
|
+
exec python3 "$SKILL_DIR/main.py" "$@"
|
|
626
|
+
else
|
|
627
|
+
echo "Error: No entry point found in $SKILL_DIR" >&2
|
|
628
|
+
exit 1
|
|
629
|
+
fi
|
|
630
|
+
`;
|
|
631
|
+
}
|
|
632
|
+
async function handleSkillInstall(params, context, deps) {
|
|
633
|
+
const startTime = Date.now();
|
|
634
|
+
try {
|
|
635
|
+
const {
|
|
636
|
+
slug,
|
|
637
|
+
files,
|
|
638
|
+
createWrapper = true,
|
|
639
|
+
agentHome = process.env["AGENSHIELD_AGENT_HOME"] || "/Users/ash_default_agent",
|
|
640
|
+
socketGroup = process.env["AGENSHIELD_SOCKET_GROUP"] || "clawshield"
|
|
641
|
+
} = params;
|
|
642
|
+
if (!slug || !isValidSlug(slug)) {
|
|
643
|
+
return {
|
|
644
|
+
success: false,
|
|
645
|
+
error: { code: 1003, message: `Invalid skill slug: ${slug}. Must be alphanumeric with dashes/underscores.` }
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
if (!Array.isArray(files) || files.length === 0) {
|
|
649
|
+
return {
|
|
650
|
+
success: false,
|
|
651
|
+
error: { code: 1003, message: "Files array is required and must not be empty" }
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
for (const file of files) {
|
|
655
|
+
if (!file.name || typeof file.name !== "string") {
|
|
656
|
+
return {
|
|
657
|
+
success: false,
|
|
658
|
+
error: { code: 1003, message: "Each file must have a name" }
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
if (file.name.includes("..") || file.name.startsWith("/")) {
|
|
662
|
+
return {
|
|
663
|
+
success: false,
|
|
664
|
+
error: { code: 1003, message: `Invalid file name: ${file.name}` }
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
const skillsDir = path3.join(agentHome, ".openclaw", "skills");
|
|
669
|
+
const skillDir = path3.join(skillsDir, slug);
|
|
670
|
+
const binDir = path3.join(agentHome, "bin");
|
|
671
|
+
await fs2.mkdir(skillDir, { recursive: true });
|
|
672
|
+
let filesWritten = 0;
|
|
673
|
+
for (const file of files) {
|
|
674
|
+
const filePath = path3.join(skillDir, file.name);
|
|
675
|
+
const fileDir = path3.dirname(filePath);
|
|
676
|
+
await fs2.mkdir(fileDir, { recursive: true });
|
|
677
|
+
const content = file.base64 ? Buffer.from(file.content, "base64") : file.content;
|
|
678
|
+
await fs2.writeFile(filePath, content, { mode: file.mode });
|
|
679
|
+
filesWritten++;
|
|
680
|
+
}
|
|
681
|
+
try {
|
|
682
|
+
execSync(`chown -R root:${socketGroup} "${skillDir}"`, { stdio: "pipe" });
|
|
683
|
+
execSync(`chmod -R a+rX,go-w "${skillDir}"`, { stdio: "pipe" });
|
|
684
|
+
} catch (err) {
|
|
685
|
+
console.warn(`[SkillInstall] chown failed (may be expected in dev): ${err.message}`);
|
|
686
|
+
}
|
|
687
|
+
let wrapperPath;
|
|
688
|
+
if (createWrapper) {
|
|
689
|
+
wrapperPath = path3.join(binDir, slug);
|
|
690
|
+
await fs2.mkdir(binDir, { recursive: true });
|
|
691
|
+
const wrapperContent = createWrapperContent(slug, skillDir);
|
|
692
|
+
await fs2.writeFile(wrapperPath, wrapperContent, { mode: 493 });
|
|
693
|
+
try {
|
|
694
|
+
execSync(`chown root:${socketGroup} "${wrapperPath}"`, { stdio: "pipe" });
|
|
695
|
+
execSync(`chmod 755 "${wrapperPath}"`, { stdio: "pipe" });
|
|
696
|
+
} catch (err) {
|
|
697
|
+
console.warn(`[SkillInstall] wrapper chown failed: ${err.message}`);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return {
|
|
701
|
+
success: true,
|
|
702
|
+
data: {
|
|
703
|
+
installed: true,
|
|
704
|
+
skillDir,
|
|
705
|
+
wrapperPath,
|
|
706
|
+
filesWritten
|
|
707
|
+
},
|
|
708
|
+
audit: {
|
|
709
|
+
duration: Date.now() - startTime,
|
|
710
|
+
bytesTransferred: files.reduce((sum, f) => sum + (f.content?.length || 0), 0)
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
} catch (error) {
|
|
714
|
+
return {
|
|
715
|
+
success: false,
|
|
716
|
+
error: { code: 1005, message: `Skill installation failed: ${error.message}` }
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
async function handleSkillUninstall(params, context, deps) {
|
|
721
|
+
const startTime = Date.now();
|
|
722
|
+
try {
|
|
723
|
+
const {
|
|
724
|
+
slug,
|
|
725
|
+
agentHome = process.env["AGENSHIELD_AGENT_HOME"] || "/Users/ash_default_agent",
|
|
726
|
+
removeWrapper = true
|
|
727
|
+
} = params;
|
|
728
|
+
if (!slug || !isValidSlug(slug)) {
|
|
729
|
+
return {
|
|
730
|
+
success: false,
|
|
731
|
+
error: { code: 1003, message: `Invalid skill slug: ${slug}` }
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
const skillsDir = path3.join(agentHome, ".openclaw", "skills");
|
|
735
|
+
const skillDir = path3.join(skillsDir, slug);
|
|
736
|
+
const binDir = path3.join(agentHome, "bin");
|
|
737
|
+
const wrapperPath = path3.join(binDir, slug);
|
|
738
|
+
let skillExists = false;
|
|
739
|
+
try {
|
|
740
|
+
await fs2.access(skillDir);
|
|
741
|
+
skillExists = true;
|
|
742
|
+
} catch {
|
|
743
|
+
}
|
|
744
|
+
if (skillExists) {
|
|
745
|
+
await fs2.rm(skillDir, { recursive: true, force: true });
|
|
746
|
+
}
|
|
747
|
+
let wrapperRemoved = false;
|
|
748
|
+
if (removeWrapper) {
|
|
749
|
+
try {
|
|
750
|
+
await fs2.access(wrapperPath);
|
|
751
|
+
await fs2.unlink(wrapperPath);
|
|
752
|
+
wrapperRemoved = true;
|
|
753
|
+
} catch {
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return {
|
|
757
|
+
success: true,
|
|
758
|
+
data: {
|
|
759
|
+
uninstalled: true,
|
|
760
|
+
skillDir,
|
|
761
|
+
wrapperRemoved
|
|
762
|
+
},
|
|
763
|
+
audit: {
|
|
764
|
+
duration: Date.now() - startTime
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
} catch (error) {
|
|
768
|
+
return {
|
|
769
|
+
success: false,
|
|
770
|
+
error: { code: 1005, message: `Skill uninstallation failed: ${error.message}` }
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// libs/shield-broker/src/server.ts
|
|
776
|
+
var UnixSocketServer = class {
|
|
777
|
+
server = null;
|
|
778
|
+
config;
|
|
779
|
+
policyEnforcer;
|
|
780
|
+
auditLogger;
|
|
781
|
+
secretVault;
|
|
782
|
+
connections = /* @__PURE__ */ new Set();
|
|
783
|
+
constructor(options) {
|
|
784
|
+
this.config = options.config;
|
|
785
|
+
this.policyEnforcer = options.policyEnforcer;
|
|
786
|
+
this.auditLogger = options.auditLogger;
|
|
787
|
+
this.secretVault = options.secretVault;
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Start the Unix socket server
|
|
791
|
+
*/
|
|
792
|
+
async start() {
|
|
793
|
+
if (fs3.existsSync(this.config.socketPath)) {
|
|
794
|
+
fs3.unlinkSync(this.config.socketPath);
|
|
795
|
+
}
|
|
796
|
+
return new Promise((resolve3, reject) => {
|
|
797
|
+
this.server = net.createServer((socket) => {
|
|
798
|
+
this.handleConnection(socket);
|
|
799
|
+
});
|
|
800
|
+
this.server.on("error", (error) => {
|
|
801
|
+
reject(error);
|
|
802
|
+
});
|
|
803
|
+
this.server.listen(this.config.socketPath, () => {
|
|
804
|
+
try {
|
|
805
|
+
fs3.chmodSync(this.config.socketPath, this.config.socketMode);
|
|
806
|
+
} catch (error) {
|
|
807
|
+
console.warn("Warning: Could not set socket permissions:", error);
|
|
808
|
+
}
|
|
809
|
+
resolve3();
|
|
810
|
+
});
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Stop the Unix socket server
|
|
815
|
+
*/
|
|
816
|
+
async stop() {
|
|
817
|
+
for (const socket of this.connections) {
|
|
818
|
+
socket.destroy();
|
|
819
|
+
}
|
|
820
|
+
this.connections.clear();
|
|
821
|
+
return new Promise((resolve3) => {
|
|
822
|
+
if (this.server) {
|
|
823
|
+
this.server.close(() => {
|
|
824
|
+
if (fs3.existsSync(this.config.socketPath)) {
|
|
825
|
+
try {
|
|
826
|
+
fs3.unlinkSync(this.config.socketPath);
|
|
827
|
+
} catch {
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
resolve3();
|
|
831
|
+
});
|
|
832
|
+
} else {
|
|
833
|
+
resolve3();
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Handle a new client connection
|
|
839
|
+
*/
|
|
840
|
+
handleConnection(socket) {
|
|
841
|
+
this.connections.add(socket);
|
|
842
|
+
let buffer = "";
|
|
843
|
+
socket.on("data", async (data) => {
|
|
844
|
+
buffer += data.toString();
|
|
845
|
+
let newlineIndex;
|
|
846
|
+
while ((newlineIndex = buffer.indexOf("\n")) !== -1) {
|
|
847
|
+
const line = buffer.slice(0, newlineIndex);
|
|
848
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
849
|
+
if (line.trim()) {
|
|
850
|
+
const response = await this.processRequest(line, socket);
|
|
851
|
+
socket.write(JSON.stringify(response) + "\n");
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
socket.on("close", () => {
|
|
856
|
+
this.connections.delete(socket);
|
|
857
|
+
});
|
|
858
|
+
socket.on("error", (error) => {
|
|
859
|
+
console.error("Socket error:", error);
|
|
860
|
+
this.connections.delete(socket);
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Process a JSON-RPC request
|
|
865
|
+
*/
|
|
866
|
+
async processRequest(line, socket) {
|
|
867
|
+
const requestId = randomUUID();
|
|
868
|
+
const startTime = Date.now();
|
|
869
|
+
try {
|
|
870
|
+
let request;
|
|
871
|
+
try {
|
|
872
|
+
request = JSON.parse(line);
|
|
873
|
+
} catch {
|
|
874
|
+
return this.errorResponse(null, -32700, "Parse error");
|
|
875
|
+
}
|
|
876
|
+
if (request.jsonrpc !== "2.0" || !request.method || request.id === void 0) {
|
|
877
|
+
return this.errorResponse(request.id, -32600, "Invalid Request");
|
|
878
|
+
}
|
|
879
|
+
const context = {
|
|
880
|
+
requestId,
|
|
881
|
+
channel: "socket",
|
|
882
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
883
|
+
config: this.config
|
|
884
|
+
// Socket credentials would be extracted here on supported platforms
|
|
885
|
+
};
|
|
886
|
+
const policyResult = await this.policyEnforcer.check(
|
|
887
|
+
request.method,
|
|
888
|
+
request.params,
|
|
889
|
+
context
|
|
890
|
+
);
|
|
891
|
+
if (!policyResult.allowed) {
|
|
892
|
+
await this.auditLogger.log({
|
|
893
|
+
id: requestId,
|
|
894
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
895
|
+
operation: request.method,
|
|
896
|
+
channel: "socket",
|
|
897
|
+
allowed: false,
|
|
898
|
+
policyId: policyResult.policyId,
|
|
899
|
+
target: this.extractTarget(request),
|
|
900
|
+
result: "denied",
|
|
901
|
+
errorMessage: policyResult.reason,
|
|
902
|
+
durationMs: Date.now() - startTime
|
|
903
|
+
});
|
|
904
|
+
return this.errorResponse(request.id, 1001, policyResult.reason || "Policy denied");
|
|
905
|
+
}
|
|
906
|
+
const handler = this.getHandler(request.method);
|
|
907
|
+
if (!handler) {
|
|
908
|
+
return this.errorResponse(request.id, -32601, "Method not found");
|
|
909
|
+
}
|
|
910
|
+
const result = await handler(request.params, context, {
|
|
911
|
+
policyEnforcer: this.policyEnforcer,
|
|
912
|
+
auditLogger: this.auditLogger,
|
|
913
|
+
secretVault: this.secretVault
|
|
914
|
+
});
|
|
915
|
+
await this.auditLogger.log({
|
|
916
|
+
id: requestId,
|
|
917
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
918
|
+
operation: request.method,
|
|
919
|
+
channel: "socket",
|
|
920
|
+
allowed: true,
|
|
921
|
+
policyId: policyResult.policyId,
|
|
922
|
+
target: this.extractTarget(request),
|
|
923
|
+
result: result.success ? "success" : "error",
|
|
924
|
+
errorMessage: result.error?.message,
|
|
925
|
+
durationMs: Date.now() - startTime,
|
|
926
|
+
metadata: result.audit
|
|
927
|
+
});
|
|
928
|
+
if (result.success) {
|
|
929
|
+
return {
|
|
930
|
+
jsonrpc: "2.0",
|
|
931
|
+
id: request.id,
|
|
932
|
+
result: result.data
|
|
933
|
+
};
|
|
934
|
+
} else {
|
|
935
|
+
return this.errorResponse(
|
|
936
|
+
request.id,
|
|
937
|
+
result.error?.code || -32e3,
|
|
938
|
+
result.error?.message || "Unknown error"
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
} catch (error) {
|
|
942
|
+
console.error("Request processing error:", error);
|
|
943
|
+
return this.errorResponse(null, -32603, "Internal error");
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Get the handler for an operation type
|
|
948
|
+
*/
|
|
949
|
+
getHandler(method) {
|
|
950
|
+
const handlerMap = {
|
|
951
|
+
http_request: handleHttpRequest,
|
|
952
|
+
file_read: handleFileRead,
|
|
953
|
+
file_write: handleFileWrite,
|
|
954
|
+
file_list: handleFileList,
|
|
955
|
+
exec: handleExec,
|
|
956
|
+
open_url: handleOpenUrl,
|
|
957
|
+
secret_inject: handleSecretInject,
|
|
958
|
+
ping: handlePing,
|
|
959
|
+
skill_install: handleSkillInstall,
|
|
960
|
+
skill_uninstall: handleSkillUninstall
|
|
961
|
+
};
|
|
962
|
+
return handlerMap[method];
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Extract target from request for audit logging
|
|
966
|
+
*/
|
|
967
|
+
extractTarget(request) {
|
|
968
|
+
const params = request.params || {};
|
|
969
|
+
return params["url"] || params["path"] || params["command"] || params["name"] || request.method;
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Create an error response
|
|
973
|
+
*/
|
|
974
|
+
errorResponse(id, code, message) {
|
|
975
|
+
return {
|
|
976
|
+
jsonrpc: "2.0",
|
|
977
|
+
id: id ?? 0,
|
|
978
|
+
error: { code, message }
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
};
|
|
982
|
+
|
|
983
|
+
// libs/shield-broker/src/http-fallback.ts
|
|
984
|
+
import * as http from "node:http";
|
|
985
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
986
|
+
var HTTP_ALLOWED_OPERATIONS = /* @__PURE__ */ new Set([
|
|
987
|
+
"http_request",
|
|
988
|
+
"file_read",
|
|
989
|
+
"file_list",
|
|
990
|
+
"open_url",
|
|
991
|
+
"ping"
|
|
992
|
+
]);
|
|
993
|
+
var HTTP_DENIED_OPERATIONS = /* @__PURE__ */ new Set([
|
|
994
|
+
"exec",
|
|
995
|
+
"file_write",
|
|
996
|
+
"secret_inject"
|
|
997
|
+
]);
|
|
998
|
+
var HttpFallbackServer = class {
|
|
999
|
+
server = null;
|
|
1000
|
+
config;
|
|
1001
|
+
policyEnforcer;
|
|
1002
|
+
auditLogger;
|
|
1003
|
+
constructor(options) {
|
|
1004
|
+
this.config = options.config;
|
|
1005
|
+
this.policyEnforcer = options.policyEnforcer;
|
|
1006
|
+
this.auditLogger = options.auditLogger;
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Start the HTTP fallback server
|
|
1010
|
+
*/
|
|
1011
|
+
async start() {
|
|
1012
|
+
return new Promise((resolve3, reject) => {
|
|
1013
|
+
this.server = http.createServer((req, res) => {
|
|
1014
|
+
this.handleRequest(req, res);
|
|
1015
|
+
});
|
|
1016
|
+
this.server.on("error", (error) => {
|
|
1017
|
+
reject(error);
|
|
1018
|
+
});
|
|
1019
|
+
const listenHost = this.config.httpHost === "localhost" ? "127.0.0.1" : this.config.httpHost;
|
|
1020
|
+
this.server.listen(this.config.httpPort, listenHost, () => {
|
|
1021
|
+
resolve3();
|
|
1022
|
+
});
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Stop the HTTP fallback server
|
|
1027
|
+
*/
|
|
1028
|
+
async stop() {
|
|
1029
|
+
return new Promise((resolve3) => {
|
|
1030
|
+
if (this.server) {
|
|
1031
|
+
this.server.close(() => {
|
|
1032
|
+
resolve3();
|
|
1033
|
+
});
|
|
1034
|
+
} else {
|
|
1035
|
+
resolve3();
|
|
1036
|
+
}
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Handle an HTTP request
|
|
1041
|
+
*/
|
|
1042
|
+
async handleRequest(req, res) {
|
|
1043
|
+
if (req.method !== "POST" || req.url !== "/rpc") {
|
|
1044
|
+
if (req.method === "GET" && req.url === "/health") {
|
|
1045
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1046
|
+
res.end(JSON.stringify({ status: "ok", version: "0.1.0" }));
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1050
|
+
res.end(JSON.stringify({ error: "Not found" }));
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
const remoteAddr = req.socket.remoteAddress;
|
|
1054
|
+
if (!this.isLocalhost(remoteAddr)) {
|
|
1055
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
1056
|
+
res.end(JSON.stringify({ error: "Access denied: localhost only" }));
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
let body = "";
|
|
1060
|
+
for await (const chunk of req) {
|
|
1061
|
+
body += chunk;
|
|
1062
|
+
if (body.length > 10 * 1024 * 1024) {
|
|
1063
|
+
res.writeHead(413, { "Content-Type": "application/json" });
|
|
1064
|
+
res.end(JSON.stringify({ error: "Request too large" }));
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
const response = await this.processRequest(body);
|
|
1069
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1070
|
+
res.end(JSON.stringify(response));
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* Check if address is localhost
|
|
1074
|
+
*/
|
|
1075
|
+
isLocalhost(address) {
|
|
1076
|
+
if (!address) return false;
|
|
1077
|
+
return address === "127.0.0.1" || address === "::1" || address === "::ffff:127.0.0.1" || address === "localhost";
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* Process a JSON-RPC request
|
|
1081
|
+
*/
|
|
1082
|
+
async processRequest(body) {
|
|
1083
|
+
const requestId = randomUUID2();
|
|
1084
|
+
const startTime = Date.now();
|
|
1085
|
+
try {
|
|
1086
|
+
let request;
|
|
1087
|
+
try {
|
|
1088
|
+
request = JSON.parse(body);
|
|
1089
|
+
} catch {
|
|
1090
|
+
return this.errorResponse(null, -32700, "Parse error");
|
|
1091
|
+
}
|
|
1092
|
+
if (request.jsonrpc !== "2.0" || !request.method || request.id === void 0) {
|
|
1093
|
+
return this.errorResponse(request.id, -32600, "Invalid Request");
|
|
1094
|
+
}
|
|
1095
|
+
if (HTTP_DENIED_OPERATIONS.has(request.method)) {
|
|
1096
|
+
await this.auditLogger.log({
|
|
1097
|
+
id: requestId,
|
|
1098
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1099
|
+
operation: request.method,
|
|
1100
|
+
channel: "http",
|
|
1101
|
+
allowed: false,
|
|
1102
|
+
target: this.extractTarget(request),
|
|
1103
|
+
result: "denied",
|
|
1104
|
+
errorMessage: "Operation not allowed over HTTP fallback",
|
|
1105
|
+
durationMs: Date.now() - startTime
|
|
1106
|
+
});
|
|
1107
|
+
return this.errorResponse(
|
|
1108
|
+
request.id,
|
|
1109
|
+
1008,
|
|
1110
|
+
`Operation '${request.method}' not allowed over HTTP. Use Unix socket.`
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
if (!HTTP_ALLOWED_OPERATIONS.has(request.method)) {
|
|
1114
|
+
return this.errorResponse(request.id, -32601, "Method not found");
|
|
1115
|
+
}
|
|
1116
|
+
const context = {
|
|
1117
|
+
requestId,
|
|
1118
|
+
channel: "http",
|
|
1119
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1120
|
+
config: this.config
|
|
1121
|
+
};
|
|
1122
|
+
const policyResult = await this.policyEnforcer.check(
|
|
1123
|
+
request.method,
|
|
1124
|
+
request.params,
|
|
1125
|
+
context
|
|
1126
|
+
);
|
|
1127
|
+
if (!policyResult.allowed) {
|
|
1128
|
+
await this.auditLogger.log({
|
|
1129
|
+
id: requestId,
|
|
1130
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1131
|
+
operation: request.method,
|
|
1132
|
+
channel: "http",
|
|
1133
|
+
allowed: false,
|
|
1134
|
+
policyId: policyResult.policyId,
|
|
1135
|
+
target: this.extractTarget(request),
|
|
1136
|
+
result: "denied",
|
|
1137
|
+
errorMessage: policyResult.reason,
|
|
1138
|
+
durationMs: Date.now() - startTime
|
|
1139
|
+
});
|
|
1140
|
+
return this.errorResponse(request.id, 1001, policyResult.reason || "Policy denied");
|
|
1141
|
+
}
|
|
1142
|
+
const handler = this.getHandler(request.method);
|
|
1143
|
+
if (!handler) {
|
|
1144
|
+
return this.errorResponse(request.id, -32601, "Method not found");
|
|
1145
|
+
}
|
|
1146
|
+
const result = await handler(request.params, context, {
|
|
1147
|
+
policyEnforcer: this.policyEnforcer,
|
|
1148
|
+
auditLogger: this.auditLogger,
|
|
1149
|
+
secretVault: null
|
|
1150
|
+
// Not available over HTTP
|
|
1151
|
+
});
|
|
1152
|
+
await this.auditLogger.log({
|
|
1153
|
+
id: requestId,
|
|
1154
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1155
|
+
operation: request.method,
|
|
1156
|
+
channel: "http",
|
|
1157
|
+
allowed: true,
|
|
1158
|
+
policyId: policyResult.policyId,
|
|
1159
|
+
target: this.extractTarget(request),
|
|
1160
|
+
result: result.success ? "success" : "error",
|
|
1161
|
+
errorMessage: result.error?.message,
|
|
1162
|
+
durationMs: Date.now() - startTime,
|
|
1163
|
+
metadata: result.audit
|
|
1164
|
+
});
|
|
1165
|
+
if (result.success) {
|
|
1166
|
+
return {
|
|
1167
|
+
jsonrpc: "2.0",
|
|
1168
|
+
id: request.id,
|
|
1169
|
+
result: result.data
|
|
1170
|
+
};
|
|
1171
|
+
} else {
|
|
1172
|
+
return this.errorResponse(
|
|
1173
|
+
request.id,
|
|
1174
|
+
result.error?.code || -32e3,
|
|
1175
|
+
result.error?.message || "Unknown error"
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
} catch (error) {
|
|
1179
|
+
console.error("Request processing error:", error);
|
|
1180
|
+
return this.errorResponse(null, -32603, "Internal error");
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* Get the handler for an operation type
|
|
1185
|
+
*/
|
|
1186
|
+
getHandler(method) {
|
|
1187
|
+
const handlerMap = {
|
|
1188
|
+
http_request: handleHttpRequest,
|
|
1189
|
+
file_read: handleFileRead,
|
|
1190
|
+
file_list: handleFileList,
|
|
1191
|
+
open_url: handleOpenUrl,
|
|
1192
|
+
ping: handlePing
|
|
1193
|
+
};
|
|
1194
|
+
return handlerMap[method];
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Extract target from request for audit logging
|
|
1198
|
+
*/
|
|
1199
|
+
extractTarget(request) {
|
|
1200
|
+
const params = request.params || {};
|
|
1201
|
+
return params["url"] || params["path"] || params["command"] || params["name"] || request.method;
|
|
1202
|
+
}
|
|
1203
|
+
/**
|
|
1204
|
+
* Create an error response
|
|
1205
|
+
*/
|
|
1206
|
+
errorResponse(id, code, message) {
|
|
1207
|
+
return {
|
|
1208
|
+
jsonrpc: "2.0",
|
|
1209
|
+
id: id ?? 0,
|
|
1210
|
+
error: { code, message }
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
};
|
|
1214
|
+
|
|
1215
|
+
// libs/shield-broker/src/policies/enforcer.ts
|
|
1216
|
+
import * as fs4 from "node:fs";
|
|
1217
|
+
import * as path4 from "node:path";
|
|
1218
|
+
var PolicyEnforcer = class {
|
|
1219
|
+
policies;
|
|
1220
|
+
policiesPath;
|
|
1221
|
+
failOpen;
|
|
1222
|
+
lastLoad = 0;
|
|
1223
|
+
reloadInterval = 6e4;
|
|
1224
|
+
// 1 minute
|
|
1225
|
+
constructor(options) {
|
|
1226
|
+
this.policiesPath = options.policiesPath;
|
|
1227
|
+
this.failOpen = options.failOpen;
|
|
1228
|
+
this.policies = options.defaultPolicies;
|
|
1229
|
+
this.loadPolicies();
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* Load policies from disk
|
|
1233
|
+
*/
|
|
1234
|
+
loadPolicies() {
|
|
1235
|
+
const configFile = path4.join(this.policiesPath, "default.json");
|
|
1236
|
+
if (fs4.existsSync(configFile)) {
|
|
1237
|
+
try {
|
|
1238
|
+
const content = fs4.readFileSync(configFile, "utf-8");
|
|
1239
|
+
const loaded = JSON.parse(content);
|
|
1240
|
+
this.policies = {
|
|
1241
|
+
...this.policies,
|
|
1242
|
+
...loaded,
|
|
1243
|
+
rules: [...this.policies.rules, ...loaded.rules || []]
|
|
1244
|
+
};
|
|
1245
|
+
this.lastLoad = Date.now();
|
|
1246
|
+
} catch (error) {
|
|
1247
|
+
console.warn("Warning: Failed to load policies:", error);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
const customDir = path4.join(this.policiesPath, "custom");
|
|
1251
|
+
if (fs4.existsSync(customDir)) {
|
|
1252
|
+
try {
|
|
1253
|
+
const files = fs4.readdirSync(customDir);
|
|
1254
|
+
for (const file of files) {
|
|
1255
|
+
if (file.endsWith(".json")) {
|
|
1256
|
+
const content = fs4.readFileSync(path4.join(customDir, file), "utf-8");
|
|
1257
|
+
const custom = JSON.parse(content);
|
|
1258
|
+
if (custom.rules) {
|
|
1259
|
+
this.policies.rules.push(...custom.rules);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
} catch (error) {
|
|
1264
|
+
console.warn("Warning: Failed to load custom policies:", error);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
this.policies.rules.sort((a, b) => (b.priority || 0) - (a.priority || 0));
|
|
1268
|
+
}
|
|
1269
|
+
/**
|
|
1270
|
+
* Maybe reload policies if stale
|
|
1271
|
+
*/
|
|
1272
|
+
maybeReload() {
|
|
1273
|
+
if (Date.now() - this.lastLoad > this.reloadInterval) {
|
|
1274
|
+
this.loadPolicies();
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* Check if an operation is allowed
|
|
1279
|
+
*/
|
|
1280
|
+
async check(operation, params, context) {
|
|
1281
|
+
this.maybeReload();
|
|
1282
|
+
try {
|
|
1283
|
+
const target = this.extractTarget(operation, params);
|
|
1284
|
+
for (const rule of this.policies.rules) {
|
|
1285
|
+
if (!rule.enabled) continue;
|
|
1286
|
+
if (!rule.operations.includes(operation) && !rule.operations.includes("*")) {
|
|
1287
|
+
continue;
|
|
1288
|
+
}
|
|
1289
|
+
const matches = this.matchesPatterns(target, rule.patterns);
|
|
1290
|
+
if (matches) {
|
|
1291
|
+
if (rule.action === "deny" || rule.action === "approval") {
|
|
1292
|
+
return {
|
|
1293
|
+
allowed: false,
|
|
1294
|
+
policyId: rule.id,
|
|
1295
|
+
reason: `Denied by policy: ${rule.name}`
|
|
1296
|
+
};
|
|
1297
|
+
} else if (rule.action === "allow") {
|
|
1298
|
+
return {
|
|
1299
|
+
allowed: true,
|
|
1300
|
+
policyId: rule.id
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
const constraintResult = this.checkConstraints(operation, params);
|
|
1306
|
+
if (!constraintResult.allowed) {
|
|
1307
|
+
return constraintResult;
|
|
1308
|
+
}
|
|
1309
|
+
return {
|
|
1310
|
+
allowed: this.policies.defaultAction === "allow",
|
|
1311
|
+
reason: this.policies.defaultAction === "deny" ? "No matching allow policy" : void 0
|
|
1312
|
+
};
|
|
1313
|
+
} catch (error) {
|
|
1314
|
+
console.error("Policy check error:", error);
|
|
1315
|
+
return {
|
|
1316
|
+
allowed: this.failOpen,
|
|
1317
|
+
reason: this.failOpen ? "Policy check failed, failing open" : "Policy check failed"
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Extract target from operation params
|
|
1323
|
+
*/
|
|
1324
|
+
extractTarget(operation, params) {
|
|
1325
|
+
switch (operation) {
|
|
1326
|
+
case "http_request":
|
|
1327
|
+
return params["url"] || "";
|
|
1328
|
+
case "file_read":
|
|
1329
|
+
case "file_write":
|
|
1330
|
+
case "file_list":
|
|
1331
|
+
return params["path"] || "";
|
|
1332
|
+
case "exec":
|
|
1333
|
+
return `${params["command"] || ""} ${(params["args"] || []).join(" ")}`;
|
|
1334
|
+
case "open_url":
|
|
1335
|
+
return params["url"] || "";
|
|
1336
|
+
case "secret_inject":
|
|
1337
|
+
return params["name"] || "";
|
|
1338
|
+
default:
|
|
1339
|
+
return "";
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
/**
|
|
1343
|
+
* Check if target matches any patterns
|
|
1344
|
+
*/
|
|
1345
|
+
matchesPatterns(target, patterns) {
|
|
1346
|
+
for (const pattern of patterns) {
|
|
1347
|
+
if (this.matchPattern(target, pattern)) {
|
|
1348
|
+
return true;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
return false;
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* Match a single pattern (supports glob-like matching)
|
|
1355
|
+
*/
|
|
1356
|
+
matchPattern(target, pattern) {
|
|
1357
|
+
const regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\?/g, ".").replace(/{{GLOBSTAR}}/g, ".*");
|
|
1358
|
+
const regex = new RegExp(`^${regexPattern}$`, "i");
|
|
1359
|
+
return regex.test(target);
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* Check operation-specific constraints
|
|
1363
|
+
*/
|
|
1364
|
+
checkConstraints(operation, params) {
|
|
1365
|
+
if (["file_read", "file_write", "file_list"].includes(operation)) {
|
|
1366
|
+
const filePath = params["path"];
|
|
1367
|
+
if (filePath && this.policies.fsConstraints) {
|
|
1368
|
+
const { allowedPaths, deniedPatterns } = this.policies.fsConstraints;
|
|
1369
|
+
for (const pattern of deniedPatterns || []) {
|
|
1370
|
+
if (this.matchPattern(filePath, pattern)) {
|
|
1371
|
+
return {
|
|
1372
|
+
allowed: false,
|
|
1373
|
+
reason: `File path matches denied pattern: ${pattern}`
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
if (allowedPaths && allowedPaths.length > 0) {
|
|
1378
|
+
const isAllowed = allowedPaths.some(
|
|
1379
|
+
(allowed) => filePath.startsWith(allowed)
|
|
1380
|
+
);
|
|
1381
|
+
if (!isAllowed) {
|
|
1382
|
+
return {
|
|
1383
|
+
allowed: false,
|
|
1384
|
+
reason: "File path not in allowed directories"
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
if (operation === "http_request") {
|
|
1391
|
+
const url = params["url"];
|
|
1392
|
+
if (url && this.policies.networkConstraints) {
|
|
1393
|
+
try {
|
|
1394
|
+
const parsedUrl = new URL(url);
|
|
1395
|
+
const host = parsedUrl.hostname;
|
|
1396
|
+
const port = parseInt(parsedUrl.port) || (parsedUrl.protocol === "https:" ? 443 : 80);
|
|
1397
|
+
const { allowedHosts, deniedHosts, allowedPorts } = this.policies.networkConstraints;
|
|
1398
|
+
for (const pattern of deniedHosts || []) {
|
|
1399
|
+
if (pattern === "*" || this.matchPattern(host, pattern)) {
|
|
1400
|
+
const isAllowed = (allowedHosts || []).some(
|
|
1401
|
+
(allowed) => this.matchPattern(host, allowed)
|
|
1402
|
+
);
|
|
1403
|
+
if (!isAllowed) {
|
|
1404
|
+
return {
|
|
1405
|
+
allowed: false,
|
|
1406
|
+
reason: `Host '${host}' is not allowed`
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
if (allowedPorts && allowedPorts.length > 0 && !allowedPorts.includes(port)) {
|
|
1412
|
+
return {
|
|
1413
|
+
allowed: false,
|
|
1414
|
+
reason: `Port ${port} is not in allowed ports`
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
} catch {
|
|
1418
|
+
return {
|
|
1419
|
+
allowed: false,
|
|
1420
|
+
reason: "Invalid URL"
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
if (operation === "exec") {
|
|
1426
|
+
const command = params["command"] || "";
|
|
1427
|
+
const args = params["args"] || [];
|
|
1428
|
+
const shellMetachars = /[;&|`$(){}[\]<>!\\]/;
|
|
1429
|
+
if (shellMetachars.test(command)) {
|
|
1430
|
+
return {
|
|
1431
|
+
allowed: false,
|
|
1432
|
+
reason: `Shell metacharacters not allowed in command: ${command}`
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
for (const arg of args) {
|
|
1436
|
+
if (typeof arg === "string" && shellMetachars.test(arg) && !arg.startsWith("-")) {
|
|
1437
|
+
if (arg.includes("|") || arg.includes(";") || arg.includes("`") || arg.includes("$(")) {
|
|
1438
|
+
return {
|
|
1439
|
+
allowed: false,
|
|
1440
|
+
reason: `Suspicious argument rejected: ${arg}`
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
return { allowed: true };
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* Get all configured policies
|
|
1450
|
+
*/
|
|
1451
|
+
getPolicies() {
|
|
1452
|
+
this.maybeReload();
|
|
1453
|
+
return this.policies;
|
|
1454
|
+
}
|
|
1455
|
+
/**
|
|
1456
|
+
* Add a policy rule at runtime
|
|
1457
|
+
*/
|
|
1458
|
+
addRule(rule) {
|
|
1459
|
+
this.policies.rules.push(rule);
|
|
1460
|
+
this.policies.rules.sort((a, b) => (b.priority || 0) - (a.priority || 0));
|
|
1461
|
+
}
|
|
1462
|
+
/**
|
|
1463
|
+
* Remove a policy rule
|
|
1464
|
+
*/
|
|
1465
|
+
removeRule(id) {
|
|
1466
|
+
const index = this.policies.rules.findIndex((r) => r.id === id);
|
|
1467
|
+
if (index >= 0) {
|
|
1468
|
+
this.policies.rules.splice(index, 1);
|
|
1469
|
+
return true;
|
|
1470
|
+
}
|
|
1471
|
+
return false;
|
|
1472
|
+
}
|
|
1473
|
+
};
|
|
1474
|
+
|
|
1475
|
+
// libs/shield-broker/src/policies/builtin.ts
|
|
1476
|
+
var BuiltinPolicies = [
|
|
1477
|
+
// Always allow ping (health check for broker availability)
|
|
1478
|
+
{
|
|
1479
|
+
id: "builtin-allow-ping",
|
|
1480
|
+
name: "Allow ping health checks",
|
|
1481
|
+
action: "allow",
|
|
1482
|
+
target: "command",
|
|
1483
|
+
operations: ["ping"],
|
|
1484
|
+
patterns: ["*"],
|
|
1485
|
+
enabled: true,
|
|
1486
|
+
priority: 1e3
|
|
1487
|
+
},
|
|
1488
|
+
// Allow skill installation/uninstallation (daemon management operations)
|
|
1489
|
+
{
|
|
1490
|
+
id: "builtin-allow-skill-management",
|
|
1491
|
+
name: "Allow skill management operations",
|
|
1492
|
+
action: "allow",
|
|
1493
|
+
target: "command",
|
|
1494
|
+
operations: ["skill_install", "skill_uninstall"],
|
|
1495
|
+
patterns: ["*"],
|
|
1496
|
+
enabled: true,
|
|
1497
|
+
priority: 1e3
|
|
1498
|
+
},
|
|
1499
|
+
// Allow localhost connections (for broker communication)
|
|
1500
|
+
{
|
|
1501
|
+
id: "builtin-allow-localhost",
|
|
1502
|
+
name: "Allow localhost connections",
|
|
1503
|
+
action: "allow",
|
|
1504
|
+
target: "url",
|
|
1505
|
+
operations: ["http_request"],
|
|
1506
|
+
patterns: [
|
|
1507
|
+
"http://localhost:*",
|
|
1508
|
+
"http://127.0.0.1:*",
|
|
1509
|
+
"https://localhost:*",
|
|
1510
|
+
"https://127.0.0.1:*"
|
|
1511
|
+
],
|
|
1512
|
+
enabled: true,
|
|
1513
|
+
priority: 100
|
|
1514
|
+
},
|
|
1515
|
+
// Deny access to sensitive files
|
|
1516
|
+
{
|
|
1517
|
+
id: "builtin-deny-secrets",
|
|
1518
|
+
name: "Deny access to secret files",
|
|
1519
|
+
action: "deny",
|
|
1520
|
+
target: "command",
|
|
1521
|
+
operations: ["file_read", "file_write"],
|
|
1522
|
+
patterns: [
|
|
1523
|
+
"**/.env",
|
|
1524
|
+
"**/.env.*",
|
|
1525
|
+
"**/secrets.json",
|
|
1526
|
+
"**/secrets.yaml",
|
|
1527
|
+
"**/secrets.yml",
|
|
1528
|
+
"**/*.key",
|
|
1529
|
+
"**/*.pem",
|
|
1530
|
+
"**/*.p12",
|
|
1531
|
+
"**/id_rsa",
|
|
1532
|
+
"**/id_ed25519",
|
|
1533
|
+
"**/.ssh/*",
|
|
1534
|
+
"**/credentials.json",
|
|
1535
|
+
"**/service-account*.json"
|
|
1536
|
+
],
|
|
1537
|
+
enabled: true,
|
|
1538
|
+
priority: 200
|
|
1539
|
+
},
|
|
1540
|
+
// Deny access to system files
|
|
1541
|
+
{
|
|
1542
|
+
id: "builtin-deny-system",
|
|
1543
|
+
name: "Deny access to system files",
|
|
1544
|
+
action: "deny",
|
|
1545
|
+
target: "command",
|
|
1546
|
+
operations: ["file_read", "file_write"],
|
|
1547
|
+
patterns: [
|
|
1548
|
+
"/etc/passwd",
|
|
1549
|
+
"/etc/shadow",
|
|
1550
|
+
"/etc/sudoers",
|
|
1551
|
+
"/etc/ssh/*",
|
|
1552
|
+
"/root/**",
|
|
1553
|
+
"/var/run/docker.sock"
|
|
1554
|
+
],
|
|
1555
|
+
enabled: true,
|
|
1556
|
+
priority: 200
|
|
1557
|
+
},
|
|
1558
|
+
// Deny dangerous commands
|
|
1559
|
+
{
|
|
1560
|
+
id: "builtin-deny-dangerous-commands",
|
|
1561
|
+
name: "Deny dangerous commands",
|
|
1562
|
+
action: "deny",
|
|
1563
|
+
target: "command",
|
|
1564
|
+
operations: ["exec"],
|
|
1565
|
+
patterns: [
|
|
1566
|
+
"rm -rf /*",
|
|
1567
|
+
"rm -rf /",
|
|
1568
|
+
"dd if=*",
|
|
1569
|
+
"mkfs.*",
|
|
1570
|
+
"chmod -R 777 /*",
|
|
1571
|
+
"curl * | sh",
|
|
1572
|
+
"curl * | bash",
|
|
1573
|
+
"wget * | sh",
|
|
1574
|
+
"wget * | bash",
|
|
1575
|
+
"* > /dev/sda*",
|
|
1576
|
+
"shutdown*",
|
|
1577
|
+
"reboot*",
|
|
1578
|
+
"init 0",
|
|
1579
|
+
"init 6"
|
|
1580
|
+
],
|
|
1581
|
+
enabled: true,
|
|
1582
|
+
priority: 300
|
|
1583
|
+
},
|
|
1584
|
+
// Deny network tools that bypass proxy
|
|
1585
|
+
{
|
|
1586
|
+
id: "builtin-deny-network-bypass",
|
|
1587
|
+
name: "Deny direct network tools",
|
|
1588
|
+
action: "deny",
|
|
1589
|
+
target: "command",
|
|
1590
|
+
operations: ["exec"],
|
|
1591
|
+
patterns: [
|
|
1592
|
+
"nc *",
|
|
1593
|
+
"netcat *",
|
|
1594
|
+
"ncat *",
|
|
1595
|
+
"socat *",
|
|
1596
|
+
"telnet *",
|
|
1597
|
+
"nmap *"
|
|
1598
|
+
],
|
|
1599
|
+
enabled: true,
|
|
1600
|
+
priority: 150
|
|
1601
|
+
},
|
|
1602
|
+
// Allow common AI API endpoints
|
|
1603
|
+
{
|
|
1604
|
+
id: "builtin-allow-ai-apis",
|
|
1605
|
+
name: "Allow common AI API endpoints",
|
|
1606
|
+
action: "allow",
|
|
1607
|
+
target: "url",
|
|
1608
|
+
operations: ["http_request"],
|
|
1609
|
+
patterns: [
|
|
1610
|
+
"https://api.anthropic.com/**",
|
|
1611
|
+
"https://api.openai.com/**",
|
|
1612
|
+
"https://api.cohere.ai/**",
|
|
1613
|
+
"https://generativelanguage.googleapis.com/**",
|
|
1614
|
+
"https://api.mistral.ai/**"
|
|
1615
|
+
],
|
|
1616
|
+
enabled: true,
|
|
1617
|
+
priority: 50
|
|
1618
|
+
},
|
|
1619
|
+
// Allow common package registries
|
|
1620
|
+
{
|
|
1621
|
+
id: "builtin-allow-registries",
|
|
1622
|
+
name: "Allow package registries",
|
|
1623
|
+
action: "allow",
|
|
1624
|
+
target: "url",
|
|
1625
|
+
operations: ["http_request"],
|
|
1626
|
+
patterns: [
|
|
1627
|
+
"https://registry.npmjs.org/**",
|
|
1628
|
+
"https://pypi.org/**",
|
|
1629
|
+
"https://files.pythonhosted.org/**",
|
|
1630
|
+
"https://crates.io/**",
|
|
1631
|
+
"https://rubygems.org/**"
|
|
1632
|
+
],
|
|
1633
|
+
enabled: true,
|
|
1634
|
+
priority: 50
|
|
1635
|
+
},
|
|
1636
|
+
// Allow GitHub
|
|
1637
|
+
{
|
|
1638
|
+
id: "builtin-allow-github",
|
|
1639
|
+
name: "Allow GitHub",
|
|
1640
|
+
action: "allow",
|
|
1641
|
+
target: "url",
|
|
1642
|
+
operations: ["http_request"],
|
|
1643
|
+
patterns: [
|
|
1644
|
+
"https://github.com/**",
|
|
1645
|
+
"https://api.github.com/**",
|
|
1646
|
+
"https://raw.githubusercontent.com/**",
|
|
1647
|
+
"https://gist.github.com/**"
|
|
1648
|
+
],
|
|
1649
|
+
enabled: true,
|
|
1650
|
+
priority: 50
|
|
1651
|
+
}
|
|
1652
|
+
];
|
|
1653
|
+
function getDefaultPolicies() {
|
|
1654
|
+
return {
|
|
1655
|
+
version: "1.0.0",
|
|
1656
|
+
defaultAction: "deny",
|
|
1657
|
+
rules: [...BuiltinPolicies],
|
|
1658
|
+
fsConstraints: {
|
|
1659
|
+
allowedPaths: [
|
|
1660
|
+
"/Users/clawagent/workspace",
|
|
1661
|
+
"/tmp/agenshield"
|
|
1662
|
+
],
|
|
1663
|
+
deniedPatterns: [
|
|
1664
|
+
"**/.env*",
|
|
1665
|
+
"**/secrets.*",
|
|
1666
|
+
"**/*.key",
|
|
1667
|
+
"**/*.pem"
|
|
1668
|
+
]
|
|
1669
|
+
},
|
|
1670
|
+
networkConstraints: {
|
|
1671
|
+
allowedHosts: [
|
|
1672
|
+
"localhost",
|
|
1673
|
+
"127.0.0.1",
|
|
1674
|
+
"api.anthropic.com",
|
|
1675
|
+
"api.openai.com",
|
|
1676
|
+
"registry.npmjs.org",
|
|
1677
|
+
"pypi.org",
|
|
1678
|
+
"github.com",
|
|
1679
|
+
"api.github.com"
|
|
1680
|
+
],
|
|
1681
|
+
deniedHosts: ["*"],
|
|
1682
|
+
allowedPorts: [80, 443, 5200]
|
|
1683
|
+
}
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
// libs/shield-broker/src/seatbelt/generator.ts
|
|
1688
|
+
import * as fs5 from "node:fs/promises";
|
|
1689
|
+
import * as path5 from "node:path";
|
|
1690
|
+
|
|
1691
|
+
// libs/shield-broker/src/seatbelt/templates.ts
|
|
1692
|
+
var SeatbeltTemplates = class {
|
|
1693
|
+
/**
|
|
1694
|
+
* Base profile with minimal permissions
|
|
1695
|
+
*/
|
|
1696
|
+
baseProfile() {
|
|
1697
|
+
return `
|
|
1698
|
+
(version 1)
|
|
1699
|
+
(deny default)
|
|
1700
|
+
|
|
1701
|
+
;; Allow reading system libraries
|
|
1702
|
+
(allow file-read*
|
|
1703
|
+
(subpath "/System")
|
|
1704
|
+
(subpath "/usr/lib")
|
|
1705
|
+
(subpath "/usr/share"))
|
|
1706
|
+
|
|
1707
|
+
;; Allow basic process operations
|
|
1708
|
+
(allow process-fork)
|
|
1709
|
+
(allow signal (target self))
|
|
1710
|
+
(allow sysctl-read)
|
|
1711
|
+
`.trim();
|
|
1712
|
+
}
|
|
1713
|
+
/**
|
|
1714
|
+
* Profile for file read operations
|
|
1715
|
+
*/
|
|
1716
|
+
fileReadProfile(targetPath) {
|
|
1717
|
+
return `
|
|
1718
|
+
(version 1)
|
|
1719
|
+
(deny default)
|
|
1720
|
+
|
|
1721
|
+
;; Allow reading system libraries
|
|
1722
|
+
(allow file-read*
|
|
1723
|
+
(subpath "/System")
|
|
1724
|
+
(subpath "/usr/lib")
|
|
1725
|
+
(subpath "/usr/share"))
|
|
1726
|
+
|
|
1727
|
+
;; Allow reading target path
|
|
1728
|
+
(allow file-read*
|
|
1729
|
+
(subpath "${targetPath}"))
|
|
1730
|
+
|
|
1731
|
+
;; Deny all network
|
|
1732
|
+
(deny network*)
|
|
1733
|
+
|
|
1734
|
+
;; Allow basic process operations
|
|
1735
|
+
(allow process-fork)
|
|
1736
|
+
(allow signal (target self))
|
|
1737
|
+
(allow sysctl-read)
|
|
1738
|
+
`.trim();
|
|
1739
|
+
}
|
|
1740
|
+
/**
|
|
1741
|
+
* Profile for file write operations
|
|
1742
|
+
*/
|
|
1743
|
+
fileWriteProfile(targetPath) {
|
|
1744
|
+
return `
|
|
1745
|
+
(version 1)
|
|
1746
|
+
(deny default)
|
|
1747
|
+
|
|
1748
|
+
;; Allow reading system libraries
|
|
1749
|
+
(allow file-read*
|
|
1750
|
+
(subpath "/System")
|
|
1751
|
+
(subpath "/usr/lib")
|
|
1752
|
+
(subpath "/usr/share"))
|
|
1753
|
+
|
|
1754
|
+
;; Allow reading and writing target path
|
|
1755
|
+
(allow file-read* file-write*
|
|
1756
|
+
(subpath "${targetPath}"))
|
|
1757
|
+
|
|
1758
|
+
;; Deny all network
|
|
1759
|
+
(deny network*)
|
|
1760
|
+
|
|
1761
|
+
;; Allow basic process operations
|
|
1762
|
+
(allow process-fork)
|
|
1763
|
+
(allow signal (target self))
|
|
1764
|
+
(allow sysctl-read)
|
|
1765
|
+
`.trim();
|
|
1766
|
+
}
|
|
1767
|
+
/**
|
|
1768
|
+
* Profile for HTTP request operations
|
|
1769
|
+
*/
|
|
1770
|
+
httpRequestProfile(host, port) {
|
|
1771
|
+
const networkRule = host && port ? `(allow network-outbound (remote tcp "${host}:${port}"))` : "(deny network*)";
|
|
1772
|
+
return `
|
|
1773
|
+
(version 1)
|
|
1774
|
+
(deny default)
|
|
1775
|
+
|
|
1776
|
+
;; Allow reading system libraries
|
|
1777
|
+
(allow file-read*
|
|
1778
|
+
(subpath "/System")
|
|
1779
|
+
(subpath "/usr/lib")
|
|
1780
|
+
(subpath "/usr/share")
|
|
1781
|
+
(subpath "/private/var/db")
|
|
1782
|
+
(subpath "/Library/Preferences"))
|
|
1783
|
+
|
|
1784
|
+
;; Network access
|
|
1785
|
+
${networkRule}
|
|
1786
|
+
|
|
1787
|
+
;; Allow DNS resolution
|
|
1788
|
+
(allow network-outbound
|
|
1789
|
+
(remote udp "*:53")
|
|
1790
|
+
(remote tcp "*:53"))
|
|
1791
|
+
|
|
1792
|
+
;; Allow basic process operations
|
|
1793
|
+
(allow process-fork)
|
|
1794
|
+
(allow signal (target self))
|
|
1795
|
+
(allow sysctl-read)
|
|
1796
|
+
|
|
1797
|
+
;; Allow mach lookups for network
|
|
1798
|
+
(allow mach-lookup
|
|
1799
|
+
(global-name "com.apple.system.opendirectoryd.libinfo")
|
|
1800
|
+
(global-name "com.apple.networkd")
|
|
1801
|
+
(global-name "com.apple.nsurlsessiond"))
|
|
1802
|
+
`.trim();
|
|
1803
|
+
}
|
|
1804
|
+
/**
|
|
1805
|
+
* Profile for command execution
|
|
1806
|
+
*/
|
|
1807
|
+
execProfile(binaryPath) {
|
|
1808
|
+
const execRule = binaryPath ? `(allow process-exec (literal "${binaryPath}"))` : "(deny process-exec)";
|
|
1809
|
+
return `
|
|
1810
|
+
(version 1)
|
|
1811
|
+
(deny default)
|
|
1812
|
+
|
|
1813
|
+
;; Allow reading system libraries and binaries
|
|
1814
|
+
(allow file-read*
|
|
1815
|
+
(subpath "/System")
|
|
1816
|
+
(subpath "/usr/lib")
|
|
1817
|
+
(subpath "/usr/bin")
|
|
1818
|
+
(subpath "/bin")
|
|
1819
|
+
(subpath "/usr/share"))
|
|
1820
|
+
|
|
1821
|
+
;; Execution permission
|
|
1822
|
+
${execRule}
|
|
1823
|
+
(allow process-exec
|
|
1824
|
+
(literal "/bin/sh")
|
|
1825
|
+
(literal "/bin/bash")
|
|
1826
|
+
(literal "/usr/bin/env"))
|
|
1827
|
+
|
|
1828
|
+
;; Deny all network
|
|
1829
|
+
(deny network*)
|
|
1830
|
+
|
|
1831
|
+
;; Allow process operations
|
|
1832
|
+
(allow process-fork)
|
|
1833
|
+
(allow signal)
|
|
1834
|
+
(allow sysctl-read)
|
|
1835
|
+
`.trim();
|
|
1836
|
+
}
|
|
1837
|
+
/**
|
|
1838
|
+
* Profile for broker daemon (has network)
|
|
1839
|
+
*/
|
|
1840
|
+
brokerProfile(socketPath) {
|
|
1841
|
+
return `
|
|
1842
|
+
(version 1)
|
|
1843
|
+
(deny default)
|
|
1844
|
+
|
|
1845
|
+
;; Allow reading system libraries
|
|
1846
|
+
(allow file-read*
|
|
1847
|
+
(subpath "/System")
|
|
1848
|
+
(subpath "/usr/lib")
|
|
1849
|
+
(subpath "/usr/share")
|
|
1850
|
+
(subpath "/private/var/db")
|
|
1851
|
+
(subpath "/Library/Preferences")
|
|
1852
|
+
(subpath "/opt/agenshield"))
|
|
1853
|
+
|
|
1854
|
+
;; Allow config and policy access
|
|
1855
|
+
(allow file-read* file-write*
|
|
1856
|
+
(subpath "/opt/agenshield")
|
|
1857
|
+
(subpath "/var/log/agenshield")
|
|
1858
|
+
(subpath "/etc/agenshield"))
|
|
1859
|
+
|
|
1860
|
+
;; Allow socket operations
|
|
1861
|
+
(allow file-read* file-write*
|
|
1862
|
+
(literal "${socketPath}")
|
|
1863
|
+
(subpath "/var/run/agenshield"))
|
|
1864
|
+
|
|
1865
|
+
;; Allow outbound network (broker needs it)
|
|
1866
|
+
(allow network*)
|
|
1867
|
+
|
|
1868
|
+
;; Allow process operations
|
|
1869
|
+
(allow process-fork)
|
|
1870
|
+
(allow process-exec)
|
|
1871
|
+
(allow signal)
|
|
1872
|
+
(allow sysctl-read)
|
|
1873
|
+
|
|
1874
|
+
;; Allow mach lookups
|
|
1875
|
+
(allow mach-lookup)
|
|
1876
|
+
`.trim();
|
|
1877
|
+
}
|
|
1878
|
+
/**
|
|
1879
|
+
* Deny-all profile for testing
|
|
1880
|
+
*/
|
|
1881
|
+
denyAllProfile() {
|
|
1882
|
+
return `
|
|
1883
|
+
(version 1)
|
|
1884
|
+
(deny default)
|
|
1885
|
+
`.trim();
|
|
1886
|
+
}
|
|
1887
|
+
};
|
|
1888
|
+
|
|
1889
|
+
// libs/shield-broker/src/seatbelt/generator.ts
|
|
1890
|
+
var SeatbeltGenerator = class {
|
|
1891
|
+
templates;
|
|
1892
|
+
constructor() {
|
|
1893
|
+
this.templates = new SeatbeltTemplates();
|
|
1894
|
+
}
|
|
1895
|
+
/**
|
|
1896
|
+
* Generate the main agent seatbelt profile
|
|
1897
|
+
*/
|
|
1898
|
+
generateAgentProfile(options) {
|
|
1899
|
+
const {
|
|
1900
|
+
workspacePath,
|
|
1901
|
+
socketPath,
|
|
1902
|
+
allowedBinPaths,
|
|
1903
|
+
allowedReadPaths,
|
|
1904
|
+
additionalRules = []
|
|
1905
|
+
} = options;
|
|
1906
|
+
const profile = `
|
|
1907
|
+
;; AgenShield Agent Sandbox Profile
|
|
1908
|
+
;; Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
1909
|
+
(version 1)
|
|
1910
|
+
(deny default)
|
|
1911
|
+
|
|
1912
|
+
;; ========================================
|
|
1913
|
+
;; System Libraries & Frameworks
|
|
1914
|
+
;; ========================================
|
|
1915
|
+
(allow file-read*
|
|
1916
|
+
(subpath "/System")
|
|
1917
|
+
(subpath "/usr/lib")
|
|
1918
|
+
(subpath "/usr/share")
|
|
1919
|
+
(subpath "/Library/Frameworks")
|
|
1920
|
+
(subpath "/Library/Preferences")
|
|
1921
|
+
(subpath "/private/var/db"))
|
|
1922
|
+
|
|
1923
|
+
;; ========================================
|
|
1924
|
+
;; Node.js / Python Runtimes
|
|
1925
|
+
;; ========================================
|
|
1926
|
+
(allow file-read*
|
|
1927
|
+
(subpath "/usr/local/lib/node_modules")
|
|
1928
|
+
(subpath "/opt/homebrew/lib/node_modules")
|
|
1929
|
+
(subpath "/usr/local/Cellar")
|
|
1930
|
+
(subpath "/opt/homebrew/Cellar")
|
|
1931
|
+
(subpath "/Library/Frameworks/Python.framework"))
|
|
1932
|
+
|
|
1933
|
+
;; ========================================
|
|
1934
|
+
;; Workspace Access (Read/Write)
|
|
1935
|
+
;; ========================================
|
|
1936
|
+
(allow file-read* file-write*
|
|
1937
|
+
(subpath "${workspacePath}"))
|
|
1938
|
+
|
|
1939
|
+
;; ========================================
|
|
1940
|
+
;; Additional Read-Only Paths
|
|
1941
|
+
;; ========================================
|
|
1942
|
+
${allowedReadPaths.map((p) => `(allow file-read* (subpath "${p}"))`).join("\n")}
|
|
1943
|
+
|
|
1944
|
+
;; ========================================
|
|
1945
|
+
;; Binary Execution
|
|
1946
|
+
;; ========================================
|
|
1947
|
+
(allow process-exec
|
|
1948
|
+
(literal "/bin/sh")
|
|
1949
|
+
(literal "/bin/bash")
|
|
1950
|
+
(literal "/usr/bin/env")
|
|
1951
|
+
${allowedBinPaths.map((p) => `(subpath "${p}")`).join("\n ")})
|
|
1952
|
+
|
|
1953
|
+
;; ========================================
|
|
1954
|
+
;; Unix Socket Access (Broker)
|
|
1955
|
+
;; ========================================
|
|
1956
|
+
(allow network-outbound
|
|
1957
|
+
(local unix-socket "${socketPath}"))
|
|
1958
|
+
|
|
1959
|
+
;; ========================================
|
|
1960
|
+
;; Network Denial (CRITICAL)
|
|
1961
|
+
;; ========================================
|
|
1962
|
+
(deny network*)
|
|
1963
|
+
|
|
1964
|
+
;; ========================================
|
|
1965
|
+
;; Process & Signal Handling
|
|
1966
|
+
;; ========================================
|
|
1967
|
+
(allow process-fork)
|
|
1968
|
+
(allow signal (target self))
|
|
1969
|
+
|
|
1970
|
+
;; ========================================
|
|
1971
|
+
;; Mach IPC (Limited)
|
|
1972
|
+
;; ========================================
|
|
1973
|
+
(allow mach-lookup
|
|
1974
|
+
(global-name "com.apple.system.opendirectoryd.libinfo")
|
|
1975
|
+
(global-name "com.apple.system.notification_center")
|
|
1976
|
+
(global-name "com.apple.CoreServices.coreservicesd"))
|
|
1977
|
+
|
|
1978
|
+
;; ========================================
|
|
1979
|
+
;; Sysctl (Limited)
|
|
1980
|
+
;; ========================================
|
|
1981
|
+
(allow sysctl-read)
|
|
1982
|
+
|
|
1983
|
+
;; ========================================
|
|
1984
|
+
;; Additional Rules
|
|
1985
|
+
;; ========================================
|
|
1986
|
+
${additionalRules.join("\n")}
|
|
1987
|
+
`;
|
|
1988
|
+
return profile.trim();
|
|
1989
|
+
}
|
|
1990
|
+
/**
|
|
1991
|
+
* Generate a per-operation seatbelt profile
|
|
1992
|
+
*/
|
|
1993
|
+
generateOperationProfile(options) {
|
|
1994
|
+
const { operation, targetPath, targetHost, targetPort } = options;
|
|
1995
|
+
switch (operation) {
|
|
1996
|
+
case "file_read":
|
|
1997
|
+
return this.templates.fileReadProfile(targetPath || "/");
|
|
1998
|
+
case "file_write":
|
|
1999
|
+
return this.templates.fileWriteProfile(targetPath || "/");
|
|
2000
|
+
case "http_request":
|
|
2001
|
+
return this.templates.httpRequestProfile(targetHost, targetPort);
|
|
2002
|
+
case "exec":
|
|
2003
|
+
return this.templates.execProfile(targetPath);
|
|
2004
|
+
default:
|
|
2005
|
+
return this.templates.baseProfile();
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
/**
|
|
2009
|
+
* Install seatbelt profiles to disk
|
|
2010
|
+
*/
|
|
2011
|
+
async installProfiles(outputDir, options) {
|
|
2012
|
+
await fs5.mkdir(outputDir, { recursive: true });
|
|
2013
|
+
const agentProfile = this.generateAgentProfile(options);
|
|
2014
|
+
await fs5.writeFile(
|
|
2015
|
+
path5.join(outputDir, "agent.sb"),
|
|
2016
|
+
agentProfile,
|
|
2017
|
+
{ mode: 420 }
|
|
2018
|
+
);
|
|
2019
|
+
const opsDir = path5.join(outputDir, "ops");
|
|
2020
|
+
await fs5.mkdir(opsDir, { recursive: true });
|
|
2021
|
+
const operations = ["file_read", "file_write", "http_request", "exec"];
|
|
2022
|
+
for (const op of operations) {
|
|
2023
|
+
const profile = this.generateOperationProfile({ operation: op });
|
|
2024
|
+
await fs5.writeFile(
|
|
2025
|
+
path5.join(opsDir, `${op}.sb`),
|
|
2026
|
+
profile,
|
|
2027
|
+
{ mode: 420 }
|
|
2028
|
+
);
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
/**
|
|
2032
|
+
* Verify a seatbelt profile is valid
|
|
2033
|
+
*/
|
|
2034
|
+
async verifyProfile(profilePath) {
|
|
2035
|
+
try {
|
|
2036
|
+
const content = await fs5.readFile(profilePath, "utf-8");
|
|
2037
|
+
if (!content.includes("(version 1)")) {
|
|
2038
|
+
return false;
|
|
2039
|
+
}
|
|
2040
|
+
let depth = 0;
|
|
2041
|
+
for (const char of content) {
|
|
2042
|
+
if (char === "(") depth++;
|
|
2043
|
+
if (char === ")") depth--;
|
|
2044
|
+
if (depth < 0) return false;
|
|
2045
|
+
}
|
|
2046
|
+
return depth === 0;
|
|
2047
|
+
} catch {
|
|
2048
|
+
return false;
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
};
|
|
2052
|
+
|
|
2053
|
+
// libs/shield-broker/src/secrets/vault.ts
|
|
2054
|
+
import * as fs6 from "node:fs/promises";
|
|
2055
|
+
import * as crypto from "node:crypto";
|
|
2056
|
+
var SecretVault = class {
|
|
2057
|
+
vaultPath;
|
|
2058
|
+
key = null;
|
|
2059
|
+
data = null;
|
|
2060
|
+
constructor(options) {
|
|
2061
|
+
this.vaultPath = options.vaultPath;
|
|
2062
|
+
}
|
|
2063
|
+
/**
|
|
2064
|
+
* Initialize the vault
|
|
2065
|
+
*/
|
|
2066
|
+
async initialize() {
|
|
2067
|
+
this.key = await this.loadOrCreateKey();
|
|
2068
|
+
await this.load();
|
|
2069
|
+
}
|
|
2070
|
+
/**
|
|
2071
|
+
* Load or create the encryption key
|
|
2072
|
+
*/
|
|
2073
|
+
async loadOrCreateKey() {
|
|
2074
|
+
const keyPath = this.vaultPath.replace(".enc", ".key");
|
|
2075
|
+
try {
|
|
2076
|
+
const keyData = await fs6.readFile(keyPath);
|
|
2077
|
+
return keyData;
|
|
2078
|
+
} catch {
|
|
2079
|
+
const key = crypto.randomBytes(32);
|
|
2080
|
+
await fs6.writeFile(keyPath, key, { mode: 384 });
|
|
2081
|
+
return key;
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
/**
|
|
2085
|
+
* Load vault data from disk
|
|
2086
|
+
*/
|
|
2087
|
+
async load() {
|
|
2088
|
+
try {
|
|
2089
|
+
const content = await fs6.readFile(this.vaultPath, "utf-8");
|
|
2090
|
+
this.data = JSON.parse(content);
|
|
2091
|
+
} catch {
|
|
2092
|
+
this.data = {
|
|
2093
|
+
version: "1.0.0",
|
|
2094
|
+
secrets: {}
|
|
2095
|
+
};
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
/**
|
|
2099
|
+
* Save vault data to disk
|
|
2100
|
+
*/
|
|
2101
|
+
async save() {
|
|
2102
|
+
if (!this.data) return;
|
|
2103
|
+
await fs6.writeFile(
|
|
2104
|
+
this.vaultPath,
|
|
2105
|
+
JSON.stringify(this.data, null, 2),
|
|
2106
|
+
{ mode: 384 }
|
|
2107
|
+
);
|
|
2108
|
+
}
|
|
2109
|
+
/**
|
|
2110
|
+
* Encrypt a value
|
|
2111
|
+
*/
|
|
2112
|
+
encrypt(value) {
|
|
2113
|
+
if (!this.key) {
|
|
2114
|
+
throw new Error("Vault not initialized");
|
|
2115
|
+
}
|
|
2116
|
+
const iv = crypto.randomBytes(12);
|
|
2117
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", this.key, iv);
|
|
2118
|
+
let encrypted = cipher.update(value, "utf-8", "base64");
|
|
2119
|
+
encrypted += cipher.final("base64");
|
|
2120
|
+
const tag = cipher.getAuthTag();
|
|
2121
|
+
return {
|
|
2122
|
+
encrypted,
|
|
2123
|
+
iv: iv.toString("base64"),
|
|
2124
|
+
tag: tag.toString("base64")
|
|
2125
|
+
};
|
|
2126
|
+
}
|
|
2127
|
+
/**
|
|
2128
|
+
* Decrypt a value
|
|
2129
|
+
*/
|
|
2130
|
+
decrypt(encrypted, iv, tag) {
|
|
2131
|
+
if (!this.key) {
|
|
2132
|
+
throw new Error("Vault not initialized");
|
|
2133
|
+
}
|
|
2134
|
+
const decipher = crypto.createDecipheriv(
|
|
2135
|
+
"aes-256-gcm",
|
|
2136
|
+
this.key,
|
|
2137
|
+
Buffer.from(iv, "base64")
|
|
2138
|
+
);
|
|
2139
|
+
decipher.setAuthTag(Buffer.from(tag, "base64"));
|
|
2140
|
+
let decrypted = decipher.update(encrypted, "base64", "utf-8");
|
|
2141
|
+
decrypted += decipher.final("utf-8");
|
|
2142
|
+
return decrypted;
|
|
2143
|
+
}
|
|
2144
|
+
/**
|
|
2145
|
+
* Get a secret by name
|
|
2146
|
+
*/
|
|
2147
|
+
async get(name) {
|
|
2148
|
+
if (!this.data) {
|
|
2149
|
+
await this.initialize();
|
|
2150
|
+
}
|
|
2151
|
+
const entry = this.data.secrets[name];
|
|
2152
|
+
if (!entry) {
|
|
2153
|
+
return null;
|
|
2154
|
+
}
|
|
2155
|
+
try {
|
|
2156
|
+
const value = this.decrypt(entry.encrypted, entry.iv, entry.tag);
|
|
2157
|
+
entry.accessCount++;
|
|
2158
|
+
await this.save();
|
|
2159
|
+
return {
|
|
2160
|
+
name,
|
|
2161
|
+
value,
|
|
2162
|
+
createdAt: new Date(entry.createdAt),
|
|
2163
|
+
lastAccessedAt: /* @__PURE__ */ new Date(),
|
|
2164
|
+
accessCount: entry.accessCount
|
|
2165
|
+
};
|
|
2166
|
+
} catch (error) {
|
|
2167
|
+
console.error(`Failed to decrypt secret ${name}:`, error);
|
|
2168
|
+
return null;
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
/**
|
|
2172
|
+
* Set a secret
|
|
2173
|
+
*/
|
|
2174
|
+
async set(name, value) {
|
|
2175
|
+
if (!this.data) {
|
|
2176
|
+
await this.initialize();
|
|
2177
|
+
}
|
|
2178
|
+
const encrypted = this.encrypt(value);
|
|
2179
|
+
this.data.secrets[name] = {
|
|
2180
|
+
...encrypted,
|
|
2181
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2182
|
+
accessCount: 0
|
|
2183
|
+
};
|
|
2184
|
+
await this.save();
|
|
2185
|
+
}
|
|
2186
|
+
/**
|
|
2187
|
+
* Delete a secret
|
|
2188
|
+
*/
|
|
2189
|
+
async delete(name) {
|
|
2190
|
+
if (!this.data) {
|
|
2191
|
+
await this.initialize();
|
|
2192
|
+
}
|
|
2193
|
+
if (this.data.secrets[name]) {
|
|
2194
|
+
delete this.data.secrets[name];
|
|
2195
|
+
await this.save();
|
|
2196
|
+
return true;
|
|
2197
|
+
}
|
|
2198
|
+
return false;
|
|
2199
|
+
}
|
|
2200
|
+
/**
|
|
2201
|
+
* List all secret names
|
|
2202
|
+
*/
|
|
2203
|
+
async list() {
|
|
2204
|
+
if (!this.data) {
|
|
2205
|
+
await this.initialize();
|
|
2206
|
+
}
|
|
2207
|
+
return Object.keys(this.data.secrets);
|
|
2208
|
+
}
|
|
2209
|
+
/**
|
|
2210
|
+
* Check if a secret exists
|
|
2211
|
+
*/
|
|
2212
|
+
async has(name) {
|
|
2213
|
+
if (!this.data) {
|
|
2214
|
+
await this.initialize();
|
|
2215
|
+
}
|
|
2216
|
+
return name in this.data.secrets;
|
|
2217
|
+
}
|
|
2218
|
+
};
|
|
2219
|
+
|
|
2220
|
+
// libs/shield-broker/src/audit/logger.ts
|
|
2221
|
+
import * as fs7 from "node:fs";
|
|
2222
|
+
import * as path6 from "node:path";
|
|
2223
|
+
var AuditLogger = class {
|
|
2224
|
+
logPath;
|
|
2225
|
+
logLevel;
|
|
2226
|
+
maxFileSize;
|
|
2227
|
+
maxFiles;
|
|
2228
|
+
writeStream = null;
|
|
2229
|
+
currentSize = 0;
|
|
2230
|
+
levelPriority = {
|
|
2231
|
+
debug: 0,
|
|
2232
|
+
info: 1,
|
|
2233
|
+
warn: 2,
|
|
2234
|
+
error: 3
|
|
2235
|
+
};
|
|
2236
|
+
constructor(options) {
|
|
2237
|
+
this.logPath = options.logPath;
|
|
2238
|
+
this.logLevel = options.logLevel;
|
|
2239
|
+
this.maxFileSize = options.maxFileSize || 10 * 1024 * 1024;
|
|
2240
|
+
this.maxFiles = options.maxFiles || 5;
|
|
2241
|
+
this.initializeStream();
|
|
2242
|
+
}
|
|
2243
|
+
/**
|
|
2244
|
+
* Initialize the write stream
|
|
2245
|
+
*/
|
|
2246
|
+
initializeStream() {
|
|
2247
|
+
const dir = path6.dirname(this.logPath);
|
|
2248
|
+
if (!fs7.existsSync(dir)) {
|
|
2249
|
+
fs7.mkdirSync(dir, { recursive: true });
|
|
2250
|
+
}
|
|
2251
|
+
if (fs7.existsSync(this.logPath)) {
|
|
2252
|
+
const stats = fs7.statSync(this.logPath);
|
|
2253
|
+
this.currentSize = stats.size;
|
|
2254
|
+
}
|
|
2255
|
+
this.writeStream = fs7.createWriteStream(this.logPath, {
|
|
2256
|
+
flags: "a",
|
|
2257
|
+
encoding: "utf-8"
|
|
2258
|
+
});
|
|
2259
|
+
}
|
|
2260
|
+
/**
|
|
2261
|
+
* Rotate log files if needed
|
|
2262
|
+
*/
|
|
2263
|
+
async maybeRotate() {
|
|
2264
|
+
if (this.currentSize < this.maxFileSize) {
|
|
2265
|
+
return;
|
|
2266
|
+
}
|
|
2267
|
+
if (this.writeStream) {
|
|
2268
|
+
this.writeStream.end();
|
|
2269
|
+
this.writeStream = null;
|
|
2270
|
+
}
|
|
2271
|
+
for (let i = this.maxFiles - 1; i >= 1; i--) {
|
|
2272
|
+
const oldPath = `${this.logPath}.${i}`;
|
|
2273
|
+
const newPath = `${this.logPath}.${i + 1}`;
|
|
2274
|
+
if (fs7.existsSync(oldPath)) {
|
|
2275
|
+
if (i === this.maxFiles - 1) {
|
|
2276
|
+
fs7.unlinkSync(oldPath);
|
|
2277
|
+
} else {
|
|
2278
|
+
fs7.renameSync(oldPath, newPath);
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
if (fs7.existsSync(this.logPath)) {
|
|
2283
|
+
fs7.renameSync(this.logPath, `${this.logPath}.1`);
|
|
2284
|
+
}
|
|
2285
|
+
this.currentSize = 0;
|
|
2286
|
+
this.initializeStream();
|
|
2287
|
+
}
|
|
2288
|
+
/**
|
|
2289
|
+
* Log an audit entry
|
|
2290
|
+
*/
|
|
2291
|
+
async log(entry) {
|
|
2292
|
+
await this.maybeRotate();
|
|
2293
|
+
const logLine = JSON.stringify({
|
|
2294
|
+
...entry,
|
|
2295
|
+
timestamp: entry.timestamp.toISOString()
|
|
2296
|
+
}) + "\n";
|
|
2297
|
+
if (this.writeStream) {
|
|
2298
|
+
this.writeStream.write(logLine);
|
|
2299
|
+
this.currentSize += Buffer.byteLength(logLine);
|
|
2300
|
+
}
|
|
2301
|
+
const level = entry.allowed ? "info" : "warn";
|
|
2302
|
+
if (this.shouldLog(level)) {
|
|
2303
|
+
const prefix = entry.allowed ? "\u2713" : "\u2717";
|
|
2304
|
+
const message = `[${entry.operation}] ${prefix} ${entry.target}`;
|
|
2305
|
+
if (level === "info") {
|
|
2306
|
+
console.info(message);
|
|
2307
|
+
} else {
|
|
2308
|
+
console.warn(message);
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
/**
|
|
2313
|
+
* Check if we should log at the given level
|
|
2314
|
+
*/
|
|
2315
|
+
shouldLog(level) {
|
|
2316
|
+
return this.levelPriority[level] >= this.levelPriority[this.logLevel];
|
|
2317
|
+
}
|
|
2318
|
+
/**
|
|
2319
|
+
* Log a debug message
|
|
2320
|
+
*/
|
|
2321
|
+
debug(message, data) {
|
|
2322
|
+
if (this.shouldLog("debug")) {
|
|
2323
|
+
console.debug(`[DEBUG] ${message}`, data || "");
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
/**
|
|
2327
|
+
* Log an info message
|
|
2328
|
+
*/
|
|
2329
|
+
info(message, data) {
|
|
2330
|
+
if (this.shouldLog("info")) {
|
|
2331
|
+
console.info(`[INFO] ${message}`, data || "");
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
/**
|
|
2335
|
+
* Log a warning message
|
|
2336
|
+
*/
|
|
2337
|
+
warn(message, data) {
|
|
2338
|
+
if (this.shouldLog("warn")) {
|
|
2339
|
+
console.warn(`[WARN] ${message}`, data || "");
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
/**
|
|
2343
|
+
* Log an error message
|
|
2344
|
+
*/
|
|
2345
|
+
error(message, data) {
|
|
2346
|
+
if (this.shouldLog("error")) {
|
|
2347
|
+
console.error(`[ERROR] ${message}`, data || "");
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
/**
|
|
2351
|
+
* Query audit logs
|
|
2352
|
+
*/
|
|
2353
|
+
async query(options) {
|
|
2354
|
+
const results = [];
|
|
2355
|
+
const limit = options.limit || 1e3;
|
|
2356
|
+
if (!fs7.existsSync(this.logPath)) {
|
|
2357
|
+
return results;
|
|
2358
|
+
}
|
|
2359
|
+
const content = fs7.readFileSync(this.logPath, "utf-8");
|
|
2360
|
+
const lines = content.trim().split("\n");
|
|
2361
|
+
for (const line of lines.reverse()) {
|
|
2362
|
+
if (results.length >= limit) break;
|
|
2363
|
+
try {
|
|
2364
|
+
const parsed = JSON.parse(line);
|
|
2365
|
+
const entry = {
|
|
2366
|
+
...parsed,
|
|
2367
|
+
timestamp: new Date(parsed.timestamp)
|
|
2368
|
+
};
|
|
2369
|
+
if (options.startTime && entry.timestamp < options.startTime) continue;
|
|
2370
|
+
if (options.endTime && entry.timestamp > options.endTime) continue;
|
|
2371
|
+
if (options.operation && entry.operation !== options.operation) continue;
|
|
2372
|
+
if (options.allowed !== void 0 && entry.allowed !== options.allowed) continue;
|
|
2373
|
+
results.push(entry);
|
|
2374
|
+
} catch {
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
return results;
|
|
2378
|
+
}
|
|
2379
|
+
/**
|
|
2380
|
+
* Close the logger
|
|
2381
|
+
*/
|
|
2382
|
+
async close() {
|
|
2383
|
+
return new Promise((resolve3) => {
|
|
2384
|
+
if (this.writeStream) {
|
|
2385
|
+
this.writeStream.end(() => {
|
|
2386
|
+
this.writeStream = null;
|
|
2387
|
+
resolve3();
|
|
2388
|
+
});
|
|
2389
|
+
} else {
|
|
2390
|
+
resolve3();
|
|
2391
|
+
}
|
|
2392
|
+
});
|
|
2393
|
+
}
|
|
2394
|
+
};
|
|
2395
|
+
|
|
2396
|
+
// libs/shield-broker/src/client/broker-client.ts
|
|
2397
|
+
import * as net2 from "node:net";
|
|
2398
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
2399
|
+
var BrokerClient = class {
|
|
2400
|
+
socketPath;
|
|
2401
|
+
httpHost;
|
|
2402
|
+
httpPort;
|
|
2403
|
+
timeout;
|
|
2404
|
+
preferSocket;
|
|
2405
|
+
constructor(options = {}) {
|
|
2406
|
+
this.socketPath = options.socketPath || "/var/run/agenshield/agenshield.sock";
|
|
2407
|
+
this.httpHost = options.httpHost || "localhost";
|
|
2408
|
+
this.httpPort = options.httpPort || 5201;
|
|
2409
|
+
this.timeout = options.timeout || 3e4;
|
|
2410
|
+
this.preferSocket = options.preferSocket ?? true;
|
|
2411
|
+
}
|
|
2412
|
+
/**
|
|
2413
|
+
* Make an HTTP request through the broker
|
|
2414
|
+
*/
|
|
2415
|
+
async httpRequest(params, options) {
|
|
2416
|
+
return this.request("http_request", params, options);
|
|
2417
|
+
}
|
|
2418
|
+
/**
|
|
2419
|
+
* Read a file through the broker
|
|
2420
|
+
*/
|
|
2421
|
+
async fileRead(params, options) {
|
|
2422
|
+
return this.request("file_read", params, options);
|
|
2423
|
+
}
|
|
2424
|
+
/**
|
|
2425
|
+
* Write a file through the broker
|
|
2426
|
+
*/
|
|
2427
|
+
async fileWrite(params, options) {
|
|
2428
|
+
return this.request("file_write", params, {
|
|
2429
|
+
...options,
|
|
2430
|
+
channel: "socket"
|
|
2431
|
+
// file_write only allowed via socket
|
|
2432
|
+
});
|
|
2433
|
+
}
|
|
2434
|
+
/**
|
|
2435
|
+
* List files through the broker
|
|
2436
|
+
*/
|
|
2437
|
+
async fileList(params, options) {
|
|
2438
|
+
return this.request("file_list", params, options);
|
|
2439
|
+
}
|
|
2440
|
+
/**
|
|
2441
|
+
* Execute a command through the broker
|
|
2442
|
+
*/
|
|
2443
|
+
async exec(params, options) {
|
|
2444
|
+
return this.request("exec", params, {
|
|
2445
|
+
...options,
|
|
2446
|
+
channel: "socket"
|
|
2447
|
+
// exec only allowed via socket
|
|
2448
|
+
});
|
|
2449
|
+
}
|
|
2450
|
+
/**
|
|
2451
|
+
* Open a URL through the broker
|
|
2452
|
+
*/
|
|
2453
|
+
async openUrl(params, options) {
|
|
2454
|
+
return this.request("open_url", params, options);
|
|
2455
|
+
}
|
|
2456
|
+
/**
|
|
2457
|
+
* Inject a secret through the broker
|
|
2458
|
+
*/
|
|
2459
|
+
async secretInject(params, options) {
|
|
2460
|
+
return this.request("secret_inject", params, {
|
|
2461
|
+
...options,
|
|
2462
|
+
channel: "socket"
|
|
2463
|
+
// secret_inject only allowed via socket
|
|
2464
|
+
});
|
|
2465
|
+
}
|
|
2466
|
+
/**
|
|
2467
|
+
* Ping the broker
|
|
2468
|
+
*/
|
|
2469
|
+
async ping(echo, options) {
|
|
2470
|
+
return this.request("ping", { echo }, options);
|
|
2471
|
+
}
|
|
2472
|
+
/**
|
|
2473
|
+
* Install a skill through the broker
|
|
2474
|
+
* Socket-only operation due to privileged file operations
|
|
2475
|
+
*/
|
|
2476
|
+
async skillInstall(params, options) {
|
|
2477
|
+
return this.request("skill_install", params, {
|
|
2478
|
+
...options,
|
|
2479
|
+
channel: "socket"
|
|
2480
|
+
// skill_install only allowed via socket
|
|
2481
|
+
});
|
|
2482
|
+
}
|
|
2483
|
+
/**
|
|
2484
|
+
* Uninstall a skill through the broker
|
|
2485
|
+
* Socket-only operation due to privileged file operations
|
|
2486
|
+
*/
|
|
2487
|
+
async skillUninstall(params, options) {
|
|
2488
|
+
return this.request("skill_uninstall", params, {
|
|
2489
|
+
...options,
|
|
2490
|
+
channel: "socket"
|
|
2491
|
+
// skill_uninstall only allowed via socket
|
|
2492
|
+
});
|
|
2493
|
+
}
|
|
2494
|
+
/**
|
|
2495
|
+
* Check if the broker is available
|
|
2496
|
+
*/
|
|
2497
|
+
async isAvailable() {
|
|
2498
|
+
try {
|
|
2499
|
+
await this.ping();
|
|
2500
|
+
return true;
|
|
2501
|
+
} catch {
|
|
2502
|
+
return false;
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
/**
|
|
2506
|
+
* Make a request to the broker
|
|
2507
|
+
*/
|
|
2508
|
+
async request(method, params, options) {
|
|
2509
|
+
const channel = options?.channel || (this.preferSocket ? "socket" : "http");
|
|
2510
|
+
const timeout = options?.timeout || this.timeout;
|
|
2511
|
+
if (channel === "socket") {
|
|
2512
|
+
try {
|
|
2513
|
+
return await this.socketRequest(method, params, timeout);
|
|
2514
|
+
} catch (error) {
|
|
2515
|
+
if (!options?.channel) {
|
|
2516
|
+
return await this.httpRequest_internal(method, params, timeout);
|
|
2517
|
+
}
|
|
2518
|
+
throw error;
|
|
2519
|
+
}
|
|
2520
|
+
} else {
|
|
2521
|
+
return await this.httpRequest_internal(method, params, timeout);
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
/**
|
|
2525
|
+
* Make a request via Unix socket
|
|
2526
|
+
*/
|
|
2527
|
+
async socketRequest(method, params, timeout) {
|
|
2528
|
+
return new Promise((resolve3, reject) => {
|
|
2529
|
+
const socket = net2.createConnection(this.socketPath);
|
|
2530
|
+
const id = randomUUID3();
|
|
2531
|
+
let responseData = "";
|
|
2532
|
+
let timeoutId;
|
|
2533
|
+
socket.on("connect", () => {
|
|
2534
|
+
const request = {
|
|
2535
|
+
jsonrpc: "2.0",
|
|
2536
|
+
id,
|
|
2537
|
+
method,
|
|
2538
|
+
params
|
|
2539
|
+
};
|
|
2540
|
+
socket.write(JSON.stringify(request) + "\n");
|
|
2541
|
+
timeoutId = setTimeout(() => {
|
|
2542
|
+
socket.destroy();
|
|
2543
|
+
reject(new Error("Request timeout"));
|
|
2544
|
+
}, timeout);
|
|
2545
|
+
});
|
|
2546
|
+
socket.on("data", (data) => {
|
|
2547
|
+
responseData += data.toString();
|
|
2548
|
+
const newlineIndex = responseData.indexOf("\n");
|
|
2549
|
+
if (newlineIndex !== -1) {
|
|
2550
|
+
clearTimeout(timeoutId);
|
|
2551
|
+
socket.end();
|
|
2552
|
+
try {
|
|
2553
|
+
const response = JSON.parse(
|
|
2554
|
+
responseData.slice(0, newlineIndex)
|
|
2555
|
+
);
|
|
2556
|
+
if (response.error) {
|
|
2557
|
+
const error = new Error(response.error.message);
|
|
2558
|
+
error.code = response.error.code;
|
|
2559
|
+
reject(error);
|
|
2560
|
+
} else {
|
|
2561
|
+
resolve3(response.result);
|
|
2562
|
+
}
|
|
2563
|
+
} catch (error) {
|
|
2564
|
+
reject(new Error("Invalid response from broker"));
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
});
|
|
2568
|
+
socket.on("error", (error) => {
|
|
2569
|
+
clearTimeout(timeoutId);
|
|
2570
|
+
reject(error);
|
|
2571
|
+
});
|
|
2572
|
+
});
|
|
2573
|
+
}
|
|
2574
|
+
/**
|
|
2575
|
+
* Make a request via HTTP
|
|
2576
|
+
*/
|
|
2577
|
+
async httpRequest_internal(method, params, timeout) {
|
|
2578
|
+
const url = `http://${this.httpHost}:${this.httpPort}/rpc`;
|
|
2579
|
+
const id = randomUUID3();
|
|
2580
|
+
const request = {
|
|
2581
|
+
jsonrpc: "2.0",
|
|
2582
|
+
id,
|
|
2583
|
+
method,
|
|
2584
|
+
params
|
|
2585
|
+
};
|
|
2586
|
+
const controller = new AbortController();
|
|
2587
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
2588
|
+
try {
|
|
2589
|
+
const response = await fetch(url, {
|
|
2590
|
+
method: "POST",
|
|
2591
|
+
headers: { "Content-Type": "application/json" },
|
|
2592
|
+
body: JSON.stringify(request),
|
|
2593
|
+
signal: controller.signal
|
|
2594
|
+
});
|
|
2595
|
+
clearTimeout(timeoutId);
|
|
2596
|
+
if (!response.ok) {
|
|
2597
|
+
throw new Error(`HTTP error: ${response.status}`);
|
|
2598
|
+
}
|
|
2599
|
+
const jsonResponse = await response.json();
|
|
2600
|
+
if (jsonResponse.error) {
|
|
2601
|
+
const error = new Error(jsonResponse.error.message);
|
|
2602
|
+
error.code = jsonResponse.error.code;
|
|
2603
|
+
throw error;
|
|
2604
|
+
}
|
|
2605
|
+
return jsonResponse.result;
|
|
2606
|
+
} catch (error) {
|
|
2607
|
+
clearTimeout(timeoutId);
|
|
2608
|
+
if (error.name === "AbortError") {
|
|
2609
|
+
throw new Error("Request timeout");
|
|
2610
|
+
}
|
|
2611
|
+
throw error;
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
};
|
|
2615
|
+
export {
|
|
2616
|
+
AuditLogger,
|
|
2617
|
+
BrokerClient,
|
|
2618
|
+
BuiltinPolicies,
|
|
2619
|
+
HttpFallbackServer,
|
|
2620
|
+
PolicyEnforcer,
|
|
2621
|
+
SeatbeltGenerator,
|
|
2622
|
+
SeatbeltTemplates,
|
|
2623
|
+
SecretVault,
|
|
2624
|
+
UnixSocketServer,
|
|
2625
|
+
getDefaultPolicies,
|
|
2626
|
+
handleExec,
|
|
2627
|
+
handleFileList,
|
|
2628
|
+
handleFileRead,
|
|
2629
|
+
handleFileWrite,
|
|
2630
|
+
handleHttpRequest,
|
|
2631
|
+
handleOpenUrl,
|
|
2632
|
+
handlePing,
|
|
2633
|
+
handleSecretInject,
|
|
2634
|
+
handleSkillInstall,
|
|
2635
|
+
handleSkillUninstall
|
|
2636
|
+
};
|