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