@agenshield/interceptor 0.6.1 → 0.7.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/client/sync-client.d.ts +6 -0
- package/client/sync-client.d.ts.map +1 -1
- package/config.d.ts +10 -0
- package/config.d.ts.map +1 -1
- package/debug-log.d.ts +1 -0
- package/debug-log.d.ts.map +1 -1
- package/events/reporter.d.ts.map +1 -1
- package/index.js +678 -167
- package/installer.d.ts.map +1 -1
- package/interceptors/base.d.ts +9 -1
- package/interceptors/base.d.ts.map +1 -1
- package/interceptors/child-process.d.ts +37 -1
- package/interceptors/child-process.d.ts.map +1 -1
- package/interceptors/fetch.d.ts +4 -0
- package/interceptors/fetch.d.ts.map +1 -1
- package/package.json +2 -2
- package/policy/evaluator.d.ts +4 -1
- package/policy/evaluator.d.ts.map +1 -1
- package/register.js +678 -167
- package/require.js +678 -167
- package/seatbelt/profile-manager.d.ts +36 -0
- package/seatbelt/profile-manager.d.ts.map +1 -0
package/require.js
CHANGED
|
@@ -34,9 +34,14 @@ function createConfig(overrides) {
|
|
|
34
34
|
interceptFetch: env["AGENSHIELD_INTERCEPT_FETCH"] !== "false",
|
|
35
35
|
interceptHttp: env["AGENSHIELD_INTERCEPT_HTTP"] !== "false",
|
|
36
36
|
interceptWs: env["AGENSHIELD_INTERCEPT_WS"] !== "false",
|
|
37
|
-
interceptFs:
|
|
37
|
+
interceptFs: false,
|
|
38
38
|
interceptExec: env["AGENSHIELD_INTERCEPT_EXEC"] !== "false",
|
|
39
|
-
timeout: parseInt(env["AGENSHIELD_TIMEOUT"] || "
|
|
39
|
+
timeout: parseInt(env["AGENSHIELD_TIMEOUT"] || "5000", 10),
|
|
40
|
+
contextType: env["AGENSHIELD_CONTEXT_TYPE"] || "agent",
|
|
41
|
+
contextSkillSlug: env["AGENSHIELD_SKILL_SLUG"],
|
|
42
|
+
contextAgentId: env["AGENSHIELD_AGENT_ID"],
|
|
43
|
+
enableSeatbelt: env["AGENSHIELD_SEATBELT"] !== "false" && process.platform === "darwin",
|
|
44
|
+
seatbeltProfileDir: env["AGENSHIELD_SEATBELT_DIR"] || "/tmp/agenshield-profiles",
|
|
40
45
|
...overrides
|
|
41
46
|
};
|
|
42
47
|
}
|
|
@@ -80,13 +85,34 @@ var TimeoutError = class extends AgenShieldError {
|
|
|
80
85
|
// libs/shield-interceptor/src/debug-log.ts
|
|
81
86
|
var fs = __toESM(require("node:fs"), 1);
|
|
82
87
|
var _appendFileSync = fs.appendFileSync.bind(fs);
|
|
88
|
+
var _writeSync = fs.writeSync.bind(fs);
|
|
83
89
|
var LOG_PATH = "/var/log/agenshield/interceptor.log";
|
|
90
|
+
var FALLBACK_LOG_PATH = "/tmp/agenshield-interceptor.log";
|
|
91
|
+
var resolvedLogPath = null;
|
|
92
|
+
function getLogPath() {
|
|
93
|
+
if (resolvedLogPath !== null) return resolvedLogPath;
|
|
94
|
+
try {
|
|
95
|
+
_appendFileSync(LOG_PATH, "");
|
|
96
|
+
resolvedLogPath = LOG_PATH;
|
|
97
|
+
} catch {
|
|
98
|
+
resolvedLogPath = FALLBACK_LOG_PATH;
|
|
99
|
+
}
|
|
100
|
+
return resolvedLogPath;
|
|
101
|
+
}
|
|
84
102
|
function debugLog(msg) {
|
|
103
|
+
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [pid:${process.pid}] ${msg}
|
|
104
|
+
`;
|
|
85
105
|
try {
|
|
86
|
-
_appendFileSync(
|
|
87
|
-
`);
|
|
106
|
+
_appendFileSync(getLogPath(), line);
|
|
88
107
|
} catch {
|
|
89
108
|
}
|
|
109
|
+
if (process.env["AGENSHIELD_LOG_LEVEL"] === "debug") {
|
|
110
|
+
try {
|
|
111
|
+
_writeSync(2, `[AgenShield:debug] ${msg}
|
|
112
|
+
`);
|
|
113
|
+
} catch {
|
|
114
|
+
}
|
|
115
|
+
}
|
|
90
116
|
}
|
|
91
117
|
|
|
92
118
|
// libs/shield-interceptor/src/interceptors/base.ts
|
|
@@ -96,6 +122,7 @@ var BaseInterceptor = class {
|
|
|
96
122
|
eventReporter;
|
|
97
123
|
failOpen;
|
|
98
124
|
installed = false;
|
|
125
|
+
interceptorConfig;
|
|
99
126
|
brokerHttpPort;
|
|
100
127
|
constructor(options) {
|
|
101
128
|
this.client = options.client;
|
|
@@ -103,6 +130,7 @@ var BaseInterceptor = class {
|
|
|
103
130
|
this.eventReporter = options.eventReporter;
|
|
104
131
|
this.failOpen = options.failOpen;
|
|
105
132
|
this.brokerHttpPort = options.brokerHttpPort ?? 5201;
|
|
133
|
+
this.interceptorConfig = options.config;
|
|
106
134
|
}
|
|
107
135
|
/**
|
|
108
136
|
* Check if a URL targets the broker or daemon (should not be intercepted)
|
|
@@ -127,15 +155,28 @@ var BaseInterceptor = class {
|
|
|
127
155
|
isInstalled() {
|
|
128
156
|
return this.installed;
|
|
129
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Build execution context from config
|
|
160
|
+
*/
|
|
161
|
+
getBasePolicyExecutionContext() {
|
|
162
|
+
const config = this.interceptorConfig;
|
|
163
|
+
if (!config) return void 0;
|
|
164
|
+
return {
|
|
165
|
+
callerType: config.contextType || "agent",
|
|
166
|
+
skillSlug: config.contextSkillSlug,
|
|
167
|
+
agentId: config.contextAgentId,
|
|
168
|
+
depth: 0
|
|
169
|
+
};
|
|
170
|
+
}
|
|
130
171
|
/**
|
|
131
172
|
* Check policy and handle the result
|
|
132
173
|
*/
|
|
133
|
-
async checkPolicy(operation, target) {
|
|
174
|
+
async checkPolicy(operation, target, context) {
|
|
134
175
|
const startTime = Date.now();
|
|
135
176
|
debugLog(`base.checkPolicy START op=${operation} target=${target}`);
|
|
136
177
|
try {
|
|
137
178
|
this.eventReporter.intercept(operation, target);
|
|
138
|
-
const result = await this.policyEvaluator.check(operation, target);
|
|
179
|
+
const result = await this.policyEvaluator.check(operation, target, context);
|
|
139
180
|
debugLog(`base.checkPolicy evaluator result op=${operation} target=${target} allowed=${result.allowed} policyId=${result.policyId}`);
|
|
140
181
|
if (!result.allowed) {
|
|
141
182
|
this.eventReporter.deny(operation, target, result.policyId, result.reason);
|
|
@@ -182,6 +223,19 @@ var FetchInterceptor = class extends BaseInterceptor {
|
|
|
182
223
|
constructor(options) {
|
|
183
224
|
super(options);
|
|
184
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* Build execution context from config
|
|
228
|
+
*/
|
|
229
|
+
getPolicyExecutionContext() {
|
|
230
|
+
const config = this.interceptorConfig;
|
|
231
|
+
if (!config) return void 0;
|
|
232
|
+
return {
|
|
233
|
+
callerType: config.contextType || "agent",
|
|
234
|
+
skillSlug: config.contextSkillSlug,
|
|
235
|
+
agentId: config.contextAgentId,
|
|
236
|
+
depth: 0
|
|
237
|
+
};
|
|
238
|
+
}
|
|
185
239
|
install() {
|
|
186
240
|
if (this.installed) return;
|
|
187
241
|
this.originalFetch = globalThis.fetch;
|
|
@@ -212,48 +266,10 @@ var FetchInterceptor = class extends BaseInterceptor {
|
|
|
212
266
|
return this.originalFetch(input, init);
|
|
213
267
|
}
|
|
214
268
|
debugLog(`fetch checkPolicy START url=${url}`);
|
|
215
|
-
await this.checkPolicy("http_request", url);
|
|
216
|
-
debugLog(`fetch checkPolicy DONE url=${url}`);
|
|
217
269
|
try {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if (init?.headers) {
|
|
221
|
-
if (init.headers instanceof Headers) {
|
|
222
|
-
init.headers.forEach((value, key) => {
|
|
223
|
-
headers[key] = value;
|
|
224
|
-
});
|
|
225
|
-
} else if (Array.isArray(init.headers)) {
|
|
226
|
-
for (const [key, value] of init.headers) {
|
|
227
|
-
headers[key] = value;
|
|
228
|
-
}
|
|
229
|
-
} else {
|
|
230
|
-
Object.assign(headers, init.headers);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
let body;
|
|
234
|
-
if (init?.body) {
|
|
235
|
-
if (typeof init.body === "string") {
|
|
236
|
-
body = init.body;
|
|
237
|
-
} else if (init.body instanceof ArrayBuffer) {
|
|
238
|
-
body = Buffer.from(init.body).toString("base64");
|
|
239
|
-
} else {
|
|
240
|
-
body = String(init.body);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
const result = await this.client.request("http_request", {
|
|
244
|
-
url,
|
|
245
|
-
method,
|
|
246
|
-
headers,
|
|
247
|
-
body
|
|
248
|
-
});
|
|
249
|
-
const responseHeaders = new Headers(result.headers);
|
|
250
|
-
return new Response(result.body, {
|
|
251
|
-
status: result.status,
|
|
252
|
-
statusText: result.statusText,
|
|
253
|
-
headers: responseHeaders
|
|
254
|
-
});
|
|
270
|
+
await this.checkPolicy("http_request", url, this.getPolicyExecutionContext());
|
|
271
|
+
debugLog(`fetch checkPolicy DONE url=${url}`);
|
|
255
272
|
} catch (error) {
|
|
256
|
-
debugLog(`fetch ERROR url=${url} error=${error.message}`);
|
|
257
273
|
if (error.name === "PolicyDeniedError") {
|
|
258
274
|
throw error;
|
|
259
275
|
}
|
|
@@ -263,6 +279,7 @@ var FetchInterceptor = class extends BaseInterceptor {
|
|
|
263
279
|
}
|
|
264
280
|
throw error;
|
|
265
281
|
}
|
|
282
|
+
return this.originalFetch(input, init);
|
|
266
283
|
}
|
|
267
284
|
};
|
|
268
285
|
|
|
@@ -407,6 +424,8 @@ var import_node_crypto = require("node:crypto");
|
|
|
407
424
|
var _existsSync = fs2.existsSync.bind(fs2);
|
|
408
425
|
var _readFileSync = fs2.readFileSync.bind(fs2);
|
|
409
426
|
var _unlinkSync = fs2.unlinkSync.bind(fs2);
|
|
427
|
+
var _readdirSync = fs2.readdirSync.bind(fs2);
|
|
428
|
+
var _statSync = fs2.statSync.bind(fs2);
|
|
410
429
|
var _spawnSync = import_node_child_process.spawnSync;
|
|
411
430
|
var _execSync = import_node_child_process.execSync;
|
|
412
431
|
var SyncClient = class {
|
|
@@ -414,22 +433,59 @@ var SyncClient = class {
|
|
|
414
433
|
httpHost;
|
|
415
434
|
httpPort;
|
|
416
435
|
timeout;
|
|
436
|
+
socketFailCount = 0;
|
|
437
|
+
socketSkipUntil = 0;
|
|
417
438
|
constructor(options) {
|
|
418
439
|
this.socketPath = options.socketPath;
|
|
419
440
|
this.httpHost = options.httpHost;
|
|
420
441
|
this.httpPort = options.httpPort;
|
|
421
442
|
this.timeout = options.timeout;
|
|
443
|
+
this.cleanupStaleTmpFiles();
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Remove stale /tmp/agenshield-sync-*.json files from previous runs
|
|
447
|
+
*/
|
|
448
|
+
cleanupStaleTmpFiles() {
|
|
449
|
+
try {
|
|
450
|
+
const tmpDir = "/tmp";
|
|
451
|
+
const files = _readdirSync(tmpDir);
|
|
452
|
+
const cutoff = Date.now() - 5 * 60 * 1e3;
|
|
453
|
+
for (const f of files) {
|
|
454
|
+
if (f.startsWith("agenshield-sync-") && f.endsWith(".json")) {
|
|
455
|
+
const fp = `${tmpDir}/${f}`;
|
|
456
|
+
try {
|
|
457
|
+
const stat = _statSync(fp);
|
|
458
|
+
if (stat.mtimeMs < cutoff) _unlinkSync(fp);
|
|
459
|
+
} catch {
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
} catch {
|
|
464
|
+
}
|
|
422
465
|
}
|
|
423
466
|
/**
|
|
424
467
|
* Send a synchronous request to the broker
|
|
425
468
|
*/
|
|
426
469
|
request(method, params) {
|
|
427
470
|
debugLog(`syncClient.request START method=${method}`);
|
|
471
|
+
const now = Date.now();
|
|
472
|
+
if (now < this.socketSkipUntil) {
|
|
473
|
+
debugLog(`syncClient.request SKIP socket (circuit open for ${this.socketSkipUntil - now}ms), using HTTP`);
|
|
474
|
+
const result = this.httpRequestSync(method, params);
|
|
475
|
+
debugLog(`syncClient.request http OK method=${method}`);
|
|
476
|
+
return result;
|
|
477
|
+
}
|
|
428
478
|
try {
|
|
429
479
|
const result = this.socketRequestSync(method, params);
|
|
480
|
+
this.socketFailCount = 0;
|
|
430
481
|
debugLog(`syncClient.request socket OK method=${method}`);
|
|
431
482
|
return result;
|
|
432
483
|
} catch (socketErr) {
|
|
484
|
+
this.socketFailCount++;
|
|
485
|
+
if (this.socketFailCount >= 2) {
|
|
486
|
+
this.socketSkipUntil = Date.now() + 6e4;
|
|
487
|
+
debugLog(`syncClient.request socket circuit OPEN (${this.socketFailCount} failures)`);
|
|
488
|
+
}
|
|
433
489
|
debugLog(`syncClient.request socket FAILED: ${socketErr.message}, trying HTTP`);
|
|
434
490
|
const result = this.httpRequestSync(method, params);
|
|
435
491
|
debugLog(`syncClient.request http OK method=${method}`);
|
|
@@ -454,29 +510,40 @@ var SyncClient = class {
|
|
|
454
510
|
const net = require('net');
|
|
455
511
|
const fs = require('fs');
|
|
456
512
|
|
|
513
|
+
let done = false;
|
|
457
514
|
const socket = net.createConnection('${this.socketPath}');
|
|
458
515
|
let data = '';
|
|
459
516
|
|
|
517
|
+
const timer = setTimeout(() => {
|
|
518
|
+
if (done) return;
|
|
519
|
+
done = true;
|
|
520
|
+
socket.destroy();
|
|
521
|
+
fs.writeFileSync('${tmpFile}', JSON.stringify({ error: 'timeout' }));
|
|
522
|
+
process.exit(1);
|
|
523
|
+
}, ${this.timeout});
|
|
524
|
+
|
|
460
525
|
socket.on('connect', () => {
|
|
461
526
|
socket.write(${JSON.stringify(request)});
|
|
462
527
|
});
|
|
463
528
|
|
|
464
529
|
socket.on('data', (chunk) => {
|
|
465
530
|
data += chunk.toString();
|
|
466
|
-
if (data.includes('\\n')) {
|
|
531
|
+
if (data.includes('\\n') && !done) {
|
|
532
|
+
done = true;
|
|
533
|
+
clearTimeout(timer);
|
|
467
534
|
socket.end();
|
|
468
535
|
fs.writeFileSync('${tmpFile}', data.split('\\n')[0]);
|
|
536
|
+
process.exit(0);
|
|
469
537
|
}
|
|
470
538
|
});
|
|
471
539
|
|
|
472
540
|
socket.on('error', (err) => {
|
|
541
|
+
if (done) return;
|
|
542
|
+
done = true;
|
|
543
|
+
clearTimeout(timer);
|
|
473
544
|
fs.writeFileSync('${tmpFile}', JSON.stringify({ error: err.message }));
|
|
545
|
+
process.exit(1);
|
|
474
546
|
});
|
|
475
|
-
|
|
476
|
-
setTimeout(() => {
|
|
477
|
-
socket.destroy();
|
|
478
|
-
fs.writeFileSync('${tmpFile}', JSON.stringify({ error: 'timeout' }));
|
|
479
|
-
}, ${this.timeout});
|
|
480
547
|
`;
|
|
481
548
|
try {
|
|
482
549
|
debugLog(`syncClient.socketRequestSync _spawnSync START node-bin method=${method}`);
|
|
@@ -553,11 +620,227 @@ var SyncClient = class {
|
|
|
553
620
|
}
|
|
554
621
|
};
|
|
555
622
|
|
|
623
|
+
// libs/shield-interceptor/src/seatbelt/profile-manager.ts
|
|
624
|
+
var fs3 = __toESM(require("node:fs"), 1);
|
|
625
|
+
var crypto = __toESM(require("node:crypto"), 1);
|
|
626
|
+
var path = __toESM(require("node:path"), 1);
|
|
627
|
+
var _mkdirSync = fs3.mkdirSync.bind(fs3);
|
|
628
|
+
var _writeFileSync = fs3.writeFileSync.bind(fs3);
|
|
629
|
+
var _existsSync2 = fs3.existsSync.bind(fs3);
|
|
630
|
+
var _readFileSync2 = fs3.readFileSync.bind(fs3);
|
|
631
|
+
var _readdirSync2 = fs3.readdirSync.bind(fs3);
|
|
632
|
+
var _statSync2 = fs3.statSync.bind(fs3);
|
|
633
|
+
var _unlinkSync2 = fs3.unlinkSync.bind(fs3);
|
|
634
|
+
var _chmodSync = fs3.chmodSync.bind(fs3);
|
|
635
|
+
var ProfileManager = class {
|
|
636
|
+
profileDir;
|
|
637
|
+
ensuredDir = false;
|
|
638
|
+
constructor(profileDir) {
|
|
639
|
+
this.profileDir = profileDir;
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Get or create a profile file on disk. Returns the absolute path.
|
|
643
|
+
* Uses content-hash naming so identical configs reuse the same file.
|
|
644
|
+
*/
|
|
645
|
+
getOrCreateProfile(content) {
|
|
646
|
+
this.ensureDir();
|
|
647
|
+
const hash = crypto.createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
648
|
+
const profilePath = path.join(this.profileDir, `sb-${hash}.sb`);
|
|
649
|
+
if (!_existsSync2(profilePath)) {
|
|
650
|
+
debugLog(`profile-manager: writing new profile ${profilePath} (${content.length} bytes)`);
|
|
651
|
+
_writeFileSync(profilePath, content, { mode: 420 });
|
|
652
|
+
}
|
|
653
|
+
return profilePath;
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Generate an SBPL profile from a SandboxConfig.
|
|
657
|
+
*/
|
|
658
|
+
generateProfile(sandbox) {
|
|
659
|
+
if (sandbox.profileContent) {
|
|
660
|
+
return sandbox.profileContent;
|
|
661
|
+
}
|
|
662
|
+
const lines = [
|
|
663
|
+
";; AgenShield dynamic seatbelt profile",
|
|
664
|
+
`;; Generated: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
665
|
+
"(version 1)",
|
|
666
|
+
"(deny default)",
|
|
667
|
+
""
|
|
668
|
+
];
|
|
669
|
+
lines.push(
|
|
670
|
+
";; Filesystem: reads allowed, writes restricted",
|
|
671
|
+
"(allow file-read*)",
|
|
672
|
+
""
|
|
673
|
+
);
|
|
674
|
+
const writePaths = ["/tmp", "/private/tmp", "/var/folders"];
|
|
675
|
+
if (sandbox.allowedWritePaths.length > 0) {
|
|
676
|
+
writePaths.push(...sandbox.allowedWritePaths);
|
|
677
|
+
}
|
|
678
|
+
lines.push("(allow file-write*");
|
|
679
|
+
for (const p of writePaths) {
|
|
680
|
+
lines.push(` (subpath "${this.escapeSbpl(p)}")`);
|
|
681
|
+
}
|
|
682
|
+
lines.push(")");
|
|
683
|
+
lines.push("");
|
|
684
|
+
lines.push("(allow file-write*");
|
|
685
|
+
lines.push(' (literal "/dev/null")');
|
|
686
|
+
lines.push(' (literal "/dev/zero")');
|
|
687
|
+
lines.push(' (literal "/dev/random")');
|
|
688
|
+
lines.push(' (literal "/dev/urandom")');
|
|
689
|
+
lines.push(")");
|
|
690
|
+
lines.push("");
|
|
691
|
+
if (sandbox.deniedPaths.length > 0) {
|
|
692
|
+
lines.push(";; Denied paths");
|
|
693
|
+
for (const p of sandbox.deniedPaths) {
|
|
694
|
+
lines.push(`(deny file-read* file-write* (subpath "${this.escapeSbpl(p)}"))`);
|
|
695
|
+
}
|
|
696
|
+
lines.push("");
|
|
697
|
+
}
|
|
698
|
+
lines.push(";; Binary execution (system directories allowed as subpaths)");
|
|
699
|
+
lines.push("(allow process-exec");
|
|
700
|
+
lines.push(' (subpath "/bin")');
|
|
701
|
+
lines.push(' (subpath "/sbin")');
|
|
702
|
+
lines.push(' (subpath "/usr/bin")');
|
|
703
|
+
lines.push(' (subpath "/usr/sbin")');
|
|
704
|
+
lines.push(' (subpath "/usr/local/bin")');
|
|
705
|
+
lines.push(' (subpath "/opt/agenshield/bin")');
|
|
706
|
+
const coveredSubpaths = ["/bin/", "/sbin/", "/usr/bin/", "/usr/sbin/", "/usr/local/bin/", "/opt/agenshield/bin/"];
|
|
707
|
+
const home = process.env["HOME"];
|
|
708
|
+
if (home) {
|
|
709
|
+
lines.push(` (subpath "${this.escapeSbpl(home)}/bin")`);
|
|
710
|
+
lines.push(` (subpath "${this.escapeSbpl(home)}/homebrew")`);
|
|
711
|
+
coveredSubpaths.push(`${home}/bin/`, `${home}/homebrew/`);
|
|
712
|
+
}
|
|
713
|
+
const nvmDir = process.env["NVM_DIR"] || (home ? `${home}/.nvm` : null);
|
|
714
|
+
if (nvmDir) {
|
|
715
|
+
lines.push(` (subpath "${this.escapeSbpl(nvmDir)}")`);
|
|
716
|
+
coveredSubpaths.push(`${nvmDir}/`);
|
|
717
|
+
}
|
|
718
|
+
const brewPrefix = process.env["HOMEBREW_PREFIX"];
|
|
719
|
+
if (brewPrefix && (!home || !brewPrefix.startsWith(home))) {
|
|
720
|
+
lines.push(` (subpath "${this.escapeSbpl(brewPrefix)}/bin")`);
|
|
721
|
+
lines.push(` (subpath "${this.escapeSbpl(brewPrefix)}/lib")`);
|
|
722
|
+
coveredSubpaths.push(`${brewPrefix}/bin/`, `${brewPrefix}/lib/`);
|
|
723
|
+
}
|
|
724
|
+
const uniqueBinaries = [...new Set(sandbox.allowedBinaries)];
|
|
725
|
+
for (const bin of uniqueBinaries) {
|
|
726
|
+
if (coveredSubpaths.some((dir) => bin === dir || bin.startsWith(dir))) continue;
|
|
727
|
+
if (bin.endsWith("/")) {
|
|
728
|
+
lines.push(` (subpath "${this.escapeSbpl(bin)}")`);
|
|
729
|
+
} else {
|
|
730
|
+
lines.push(` (literal "${this.escapeSbpl(bin)}")`);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
lines.push(")");
|
|
734
|
+
lines.push("");
|
|
735
|
+
const uniqueDenied = [...new Set(sandbox.deniedBinaries)];
|
|
736
|
+
if (uniqueDenied.length > 0) {
|
|
737
|
+
lines.push(";; Denied binaries");
|
|
738
|
+
for (const bin of uniqueDenied) {
|
|
739
|
+
lines.push(`(deny process-exec (literal "${this.escapeSbpl(bin)}"))`);
|
|
740
|
+
}
|
|
741
|
+
lines.push("");
|
|
742
|
+
}
|
|
743
|
+
lines.push(";; Network");
|
|
744
|
+
if (sandbox.networkAllowed) {
|
|
745
|
+
if (sandbox.allowedHosts.length > 0 || sandbox.allowedPorts.length > 0) {
|
|
746
|
+
lines.push(";; Allow specific network targets");
|
|
747
|
+
for (const host of sandbox.allowedHosts) {
|
|
748
|
+
lines.push(`(allow network-outbound (remote tcp "${this.escapeSbpl(host)}:*"))`);
|
|
749
|
+
}
|
|
750
|
+
for (const port of sandbox.allowedPorts) {
|
|
751
|
+
lines.push(`(allow network-outbound (remote tcp "*:${port}"))`);
|
|
752
|
+
}
|
|
753
|
+
const isLocalhostOnly = sandbox.allowedHosts.length > 0 && sandbox.allowedHosts.every((h) => h === "localhost" || h === "127.0.0.1");
|
|
754
|
+
if (!isLocalhostOnly) {
|
|
755
|
+
lines.push('(allow network-outbound (remote udp "*:53") (remote tcp "*:53"))');
|
|
756
|
+
}
|
|
757
|
+
} else {
|
|
758
|
+
lines.push("(allow network*)");
|
|
759
|
+
}
|
|
760
|
+
} else {
|
|
761
|
+
lines.push("(deny network*)");
|
|
762
|
+
}
|
|
763
|
+
lines.push("");
|
|
764
|
+
lines.push(
|
|
765
|
+
";; Broker / local unix sockets",
|
|
766
|
+
"(allow network-outbound (remote unix))",
|
|
767
|
+
"(allow network-inbound (local unix))",
|
|
768
|
+
"(allow file-read* file-write*",
|
|
769
|
+
' (subpath "/var/run/agenshield")',
|
|
770
|
+
' (subpath "/private/var/run/agenshield"))',
|
|
771
|
+
""
|
|
772
|
+
);
|
|
773
|
+
lines.push(
|
|
774
|
+
";; Process management",
|
|
775
|
+
"(allow process-fork)",
|
|
776
|
+
"(allow signal (target self))",
|
|
777
|
+
"(allow sysctl-read)",
|
|
778
|
+
""
|
|
779
|
+
);
|
|
780
|
+
lines.push(
|
|
781
|
+
";; Mach IPC",
|
|
782
|
+
"(allow mach-lookup)",
|
|
783
|
+
""
|
|
784
|
+
);
|
|
785
|
+
return lines.join("\n");
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Remove stale profile files older than maxAgeMs.
|
|
789
|
+
*/
|
|
790
|
+
cleanup(maxAgeMs) {
|
|
791
|
+
if (!_existsSync2(this.profileDir)) return;
|
|
792
|
+
try {
|
|
793
|
+
const now = Date.now();
|
|
794
|
+
const entries = _readdirSync2(this.profileDir);
|
|
795
|
+
for (const entry of entries) {
|
|
796
|
+
if (!entry.endsWith(".sb")) continue;
|
|
797
|
+
const filePath = path.join(this.profileDir, entry);
|
|
798
|
+
try {
|
|
799
|
+
const stat = _statSync2(filePath);
|
|
800
|
+
if (now - stat.mtimeMs > maxAgeMs) {
|
|
801
|
+
_unlinkSync2(filePath);
|
|
802
|
+
debugLog(`profile-manager: cleaned up stale profile ${filePath}`);
|
|
803
|
+
}
|
|
804
|
+
} catch {
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
} catch {
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Escape a string for safe inclusion in SBPL
|
|
812
|
+
*/
|
|
813
|
+
escapeSbpl(s) {
|
|
814
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Ensure the profile directory exists
|
|
818
|
+
*/
|
|
819
|
+
ensureDir() {
|
|
820
|
+
if (this.ensuredDir) return;
|
|
821
|
+
if (!_existsSync2(this.profileDir)) {
|
|
822
|
+
_mkdirSync(this.profileDir, { recursive: true, mode: 1023 });
|
|
823
|
+
} else {
|
|
824
|
+
try {
|
|
825
|
+
const stat = _statSync2(this.profileDir);
|
|
826
|
+
if ((stat.mode & 511) !== 511) {
|
|
827
|
+
_chmodSync(this.profileDir, 1023);
|
|
828
|
+
}
|
|
829
|
+
} catch {
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
this.ensuredDir = true;
|
|
833
|
+
}
|
|
834
|
+
};
|
|
835
|
+
|
|
556
836
|
// libs/shield-interceptor/src/interceptors/child-process.ts
|
|
557
837
|
var childProcessModule = require("node:child_process");
|
|
558
838
|
var ChildProcessInterceptor = class extends BaseInterceptor {
|
|
559
839
|
syncClient;
|
|
560
840
|
_checking = false;
|
|
841
|
+
_executing = false;
|
|
842
|
+
// Guards exec→execFile re-entrancy
|
|
843
|
+
profileManager = null;
|
|
561
844
|
originalExec = null;
|
|
562
845
|
originalExecSync = null;
|
|
563
846
|
originalSpawn = null;
|
|
@@ -566,13 +849,18 @@ var ChildProcessInterceptor = class extends BaseInterceptor {
|
|
|
566
849
|
originalFork = null;
|
|
567
850
|
constructor(options) {
|
|
568
851
|
super(options);
|
|
852
|
+
const config = this.interceptorConfig;
|
|
569
853
|
this.syncClient = new SyncClient({
|
|
570
|
-
socketPath: "/var/run/agenshield/agenshield.sock",
|
|
571
|
-
httpHost: "localhost",
|
|
572
|
-
httpPort: 5201,
|
|
573
|
-
|
|
574
|
-
timeout: 3e4
|
|
854
|
+
socketPath: config?.socketPath || "/var/run/agenshield/agenshield.sock",
|
|
855
|
+
httpHost: config?.httpHost || "localhost",
|
|
856
|
+
httpPort: config?.httpPort || 5201,
|
|
857
|
+
timeout: config?.timeout || 3e4
|
|
575
858
|
});
|
|
859
|
+
if (config?.enableSeatbelt && process.platform === "darwin") {
|
|
860
|
+
this.profileManager = new ProfileManager(
|
|
861
|
+
config.seatbeltProfileDir || "/tmp/agenshield-profiles"
|
|
862
|
+
);
|
|
863
|
+
}
|
|
576
864
|
}
|
|
577
865
|
install() {
|
|
578
866
|
if (this.installed) return;
|
|
@@ -606,64 +894,207 @@ var ChildProcessInterceptor = class extends BaseInterceptor {
|
|
|
606
894
|
this.originalFork = null;
|
|
607
895
|
this.installed = false;
|
|
608
896
|
}
|
|
897
|
+
/**
|
|
898
|
+
* Build execution context from config for RPC calls
|
|
899
|
+
*/
|
|
900
|
+
getPolicyExecutionContext() {
|
|
901
|
+
const config = this.interceptorConfig;
|
|
902
|
+
return {
|
|
903
|
+
callerType: config?.contextType || "agent",
|
|
904
|
+
skillSlug: config?.contextSkillSlug,
|
|
905
|
+
agentId: config?.contextAgentId,
|
|
906
|
+
depth: 0
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* Synchronous policy check via SyncClient.
|
|
911
|
+
* Returns the full policy result (with sandbox config) or null if broker
|
|
912
|
+
* is unavailable and failOpen is true.
|
|
913
|
+
*/
|
|
914
|
+
syncPolicyCheck(fullCommand) {
|
|
915
|
+
this._checking = true;
|
|
916
|
+
try {
|
|
917
|
+
debugLog(`cp.syncPolicyCheck START command=${fullCommand}`);
|
|
918
|
+
const context = this.getPolicyExecutionContext();
|
|
919
|
+
const result = this.syncClient.request(
|
|
920
|
+
"policy_check",
|
|
921
|
+
{ operation: "exec", target: fullCommand, context }
|
|
922
|
+
);
|
|
923
|
+
debugLog(`cp.syncPolicyCheck DONE allowed=${result.allowed} command=${fullCommand}`);
|
|
924
|
+
if (!result.allowed) {
|
|
925
|
+
throw new PolicyDeniedError(result.reason || "Operation denied by policy", {
|
|
926
|
+
operation: "exec",
|
|
927
|
+
target: fullCommand,
|
|
928
|
+
policyId: result.policyId
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
return result;
|
|
932
|
+
} catch (error) {
|
|
933
|
+
if (error instanceof PolicyDeniedError) {
|
|
934
|
+
throw error;
|
|
935
|
+
}
|
|
936
|
+
debugLog(`cp.syncPolicyCheck ERROR: ${error.message} command=${fullCommand}`);
|
|
937
|
+
if (!this.failOpen) {
|
|
938
|
+
throw error;
|
|
939
|
+
}
|
|
940
|
+
return null;
|
|
941
|
+
} finally {
|
|
942
|
+
this._checking = false;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Create a restrictive default sandbox config for fail-open scenarios.
|
|
947
|
+
* No network, minimal fs — better than running completely unsandboxed.
|
|
948
|
+
*/
|
|
949
|
+
getFailOpenSandbox() {
|
|
950
|
+
return {
|
|
951
|
+
enabled: true,
|
|
952
|
+
allowedReadPaths: [],
|
|
953
|
+
allowedWritePaths: [],
|
|
954
|
+
deniedPaths: [],
|
|
955
|
+
networkAllowed: false,
|
|
956
|
+
allowedHosts: [],
|
|
957
|
+
allowedPorts: [],
|
|
958
|
+
allowedBinaries: [],
|
|
959
|
+
deniedBinaries: [],
|
|
960
|
+
envInjection: {},
|
|
961
|
+
envDeny: []
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Resolve the sandbox config to use: from policy result, fail-open default, or null.
|
|
966
|
+
*/
|
|
967
|
+
resolveSandbox(policyResult) {
|
|
968
|
+
if (policyResult?.sandbox?.enabled) {
|
|
969
|
+
return policyResult.sandbox;
|
|
970
|
+
}
|
|
971
|
+
if (policyResult === null && this.profileManager) {
|
|
972
|
+
return this.getFailOpenSandbox();
|
|
973
|
+
}
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Wrap a command with sandbox-exec if seatbelt is enabled and sandbox config is present.
|
|
978
|
+
* Returns modified { command, args, options } for spawn-style calls.
|
|
979
|
+
*/
|
|
980
|
+
wrapWithSeatbelt(command, args, options, policyResult) {
|
|
981
|
+
const sandbox = this.resolveSandbox(policyResult);
|
|
982
|
+
if (!this.profileManager || !sandbox || process.platform !== "darwin") {
|
|
983
|
+
return { command, args, options };
|
|
984
|
+
}
|
|
985
|
+
if (command === "/opt/agenshield/bin/node-bin" || command.endsWith("/node-bin")) {
|
|
986
|
+
debugLog(`cp.wrapWithSeatbelt: SKIP node-bin (already intercepted) command=${command}`);
|
|
987
|
+
return { command, args, options };
|
|
988
|
+
}
|
|
989
|
+
if (command === "/usr/bin/sandbox-exec" || command.endsWith("/sandbox-exec")) {
|
|
990
|
+
debugLog(`cp.wrapWithSeatbelt: SKIP already sandbox-exec command=${command}`);
|
|
991
|
+
return { command, args, options };
|
|
992
|
+
}
|
|
993
|
+
debugLog(`cp.wrapWithSeatbelt: wrapping command=${command}`);
|
|
994
|
+
const profileContent = this.profileManager.generateProfile(sandbox);
|
|
995
|
+
const profilePath = this.profileManager.getOrCreateProfile(profileContent);
|
|
996
|
+
const env = { ...options?.env || process.env };
|
|
997
|
+
if (sandbox.envInjection) {
|
|
998
|
+
Object.assign(env, sandbox.envInjection);
|
|
999
|
+
}
|
|
1000
|
+
if (sandbox.envDeny) {
|
|
1001
|
+
for (const key of sandbox.envDeny) {
|
|
1002
|
+
delete env[key];
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
return {
|
|
1006
|
+
command: "/usr/bin/sandbox-exec",
|
|
1007
|
+
args: ["-f", profilePath, command, ...args],
|
|
1008
|
+
options: { ...options, env }
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Wrap a shell command string with sandbox-exec.
|
|
1013
|
+
* For exec/execSync which take a full command string.
|
|
1014
|
+
*/
|
|
1015
|
+
wrapCommandStringWithSeatbelt(command, options, policyResult) {
|
|
1016
|
+
const sandbox = this.resolveSandbox(policyResult);
|
|
1017
|
+
if (!this.profileManager || !sandbox || process.platform !== "darwin") {
|
|
1018
|
+
return { command, options };
|
|
1019
|
+
}
|
|
1020
|
+
if (command.startsWith("/usr/bin/sandbox-exec ") || command.startsWith("sandbox-exec ")) {
|
|
1021
|
+
debugLog(`cp.wrapCommandStringWithSeatbelt: SKIP already sandbox-exec command=${command}`);
|
|
1022
|
+
return { command, options };
|
|
1023
|
+
}
|
|
1024
|
+
debugLog(`cp.wrapCommandStringWithSeatbelt: wrapping command=${command}`);
|
|
1025
|
+
const profileContent = this.profileManager.generateProfile(sandbox);
|
|
1026
|
+
const profilePath = this.profileManager.getOrCreateProfile(profileContent);
|
|
1027
|
+
const env = { ...options?.env || process.env };
|
|
1028
|
+
if (sandbox.envInjection) {
|
|
1029
|
+
Object.assign(env, sandbox.envInjection);
|
|
1030
|
+
}
|
|
1031
|
+
if (sandbox.envDeny) {
|
|
1032
|
+
for (const key of sandbox.envDeny) {
|
|
1033
|
+
delete env[key];
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
return {
|
|
1037
|
+
command: `/usr/bin/sandbox-exec -f ${profilePath} ${command}`,
|
|
1038
|
+
options: { ...options, env }
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
609
1041
|
createInterceptedExec() {
|
|
610
1042
|
const self = this;
|
|
611
1043
|
const original = this.originalExec;
|
|
612
1044
|
return function interceptedExec(command, ...args) {
|
|
613
1045
|
const callback = typeof args[args.length - 1] === "function" ? args.pop() : void 0;
|
|
614
|
-
|
|
615
|
-
|
|
1046
|
+
const options = args[0];
|
|
1047
|
+
debugLog(`cp.exec ENTER command=${command} _checking=${self._checking} _executing=${self._executing}`);
|
|
1048
|
+
if (self._checking || self._executing) {
|
|
616
1049
|
debugLog(`cp.exec SKIP (re-entrancy) command=${command}`);
|
|
617
1050
|
return original(command, ...args, callback);
|
|
618
1051
|
}
|
|
619
1052
|
self.eventReporter.intercept("exec", command);
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
1053
|
+
let policyResult = null;
|
|
1054
|
+
try {
|
|
1055
|
+
policyResult = self.syncPolicyCheck(command);
|
|
1056
|
+
} catch (error) {
|
|
623
1057
|
if (callback) {
|
|
624
|
-
callback(error, "", "");
|
|
1058
|
+
process.nextTick(() => callback(error, "", ""));
|
|
625
1059
|
}
|
|
626
|
-
|
|
627
|
-
|
|
1060
|
+
return original('echo ""');
|
|
1061
|
+
}
|
|
1062
|
+
const wrapped = self.wrapCommandStringWithSeatbelt(command, options, policyResult);
|
|
1063
|
+
debugLog(`cp.exec calling original command=${wrapped.command}`);
|
|
1064
|
+
self._executing = true;
|
|
1065
|
+
try {
|
|
1066
|
+
if (wrapped.options) {
|
|
1067
|
+
return original(wrapped.command, wrapped.options, callback);
|
|
1068
|
+
}
|
|
1069
|
+
return original(wrapped.command, callback);
|
|
1070
|
+
} finally {
|
|
1071
|
+
self._executing = false;
|
|
1072
|
+
}
|
|
628
1073
|
};
|
|
629
1074
|
}
|
|
630
1075
|
createInterceptedExecSync() {
|
|
631
1076
|
const self = this;
|
|
632
1077
|
const original = this.originalExecSync;
|
|
633
1078
|
const interceptedExecSync = function(command, options) {
|
|
634
|
-
debugLog(`cp.execSync ENTER command=${command} _checking=${self._checking}`);
|
|
635
|
-
if (self._checking) {
|
|
1079
|
+
debugLog(`cp.execSync ENTER command=${command} _checking=${self._checking} _executing=${self._executing}`);
|
|
1080
|
+
if (self._checking || self._executing) {
|
|
636
1081
|
debugLog(`cp.execSync SKIP (re-entrancy) command=${command}`);
|
|
637
1082
|
return original(command, options);
|
|
638
1083
|
}
|
|
639
|
-
self.
|
|
1084
|
+
self.eventReporter.intercept("exec", command);
|
|
1085
|
+
const policyResult = self.syncPolicyCheck(command);
|
|
1086
|
+
const wrapped = self.wrapCommandStringWithSeatbelt(
|
|
1087
|
+
command,
|
|
1088
|
+
options,
|
|
1089
|
+
policyResult
|
|
1090
|
+
);
|
|
1091
|
+
debugLog(`cp.execSync calling original command=${wrapped.command}`);
|
|
1092
|
+
self._executing = true;
|
|
640
1093
|
try {
|
|
641
|
-
|
|
642
|
-
debugLog(`cp.execSync policy_check START command=${command}`);
|
|
643
|
-
const result = self.syncClient.request(
|
|
644
|
-
"policy_check",
|
|
645
|
-
{ operation: "exec", target: command }
|
|
646
|
-
);
|
|
647
|
-
debugLog(`cp.execSync policy_check DONE allowed=${result.allowed} command=${command}`);
|
|
648
|
-
if (!result.allowed) {
|
|
649
|
-
throw new PolicyDeniedError(result.reason || "Operation denied by policy", {
|
|
650
|
-
operation: "exec",
|
|
651
|
-
target: command
|
|
652
|
-
});
|
|
653
|
-
}
|
|
654
|
-
} catch (error) {
|
|
655
|
-
debugLog(`cp.execSync policy_check ERROR: ${error.message} command=${command}`);
|
|
656
|
-
if (error instanceof PolicyDeniedError) {
|
|
657
|
-
throw error;
|
|
658
|
-
}
|
|
659
|
-
if (!self.failOpen) {
|
|
660
|
-
throw error;
|
|
661
|
-
}
|
|
1094
|
+
return original(wrapped.command, wrapped.options);
|
|
662
1095
|
} finally {
|
|
663
|
-
self.
|
|
1096
|
+
self._executing = false;
|
|
664
1097
|
}
|
|
665
|
-
debugLog(`cp.execSync calling original command=${command}`);
|
|
666
|
-
return original(command, options);
|
|
667
1098
|
};
|
|
668
1099
|
return interceptedExecSync;
|
|
669
1100
|
}
|
|
@@ -672,17 +1103,31 @@ var ChildProcessInterceptor = class extends BaseInterceptor {
|
|
|
672
1103
|
const original = this.originalSpawn;
|
|
673
1104
|
const interceptedSpawn = function(command, args, options) {
|
|
674
1105
|
const fullCmd = args ? `${command} ${args.join(" ")}` : command;
|
|
675
|
-
debugLog(`cp.spawn ENTER command=${fullCmd} _checking=${self._checking}`);
|
|
676
|
-
if (self._checking) {
|
|
1106
|
+
debugLog(`cp.spawn ENTER command=${fullCmd} _checking=${self._checking} _executing=${self._executing}`);
|
|
1107
|
+
if (self._checking || self._executing) {
|
|
677
1108
|
debugLog(`cp.spawn SKIP (re-entrancy) command=${fullCmd}`);
|
|
678
1109
|
return original(command, args, options || {});
|
|
679
1110
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
self.
|
|
684
|
-
})
|
|
685
|
-
|
|
1111
|
+
self.eventReporter.intercept("exec", fullCmd);
|
|
1112
|
+
let policyResult = null;
|
|
1113
|
+
try {
|
|
1114
|
+
policyResult = self.syncPolicyCheck(fullCmd);
|
|
1115
|
+
} catch (error) {
|
|
1116
|
+
debugLog(`cp.spawn DENIED command=${fullCmd}`);
|
|
1117
|
+
const denied = original("false", [], { stdio: "pipe" });
|
|
1118
|
+
process.nextTick(() => {
|
|
1119
|
+
denied.emit("error", error);
|
|
1120
|
+
});
|
|
1121
|
+
return denied;
|
|
1122
|
+
}
|
|
1123
|
+
const wrapped = self.wrapWithSeatbelt(
|
|
1124
|
+
command,
|
|
1125
|
+
Array.from(args || []),
|
|
1126
|
+
options,
|
|
1127
|
+
policyResult
|
|
1128
|
+
);
|
|
1129
|
+
debugLog(`cp.spawn calling original command=${wrapped.command} args=${wrapped.args.join(" ")}`);
|
|
1130
|
+
return original(wrapped.command, wrapped.args, wrapped.options || {});
|
|
686
1131
|
};
|
|
687
1132
|
return interceptedSpawn;
|
|
688
1133
|
}
|
|
@@ -691,77 +1136,130 @@ var ChildProcessInterceptor = class extends BaseInterceptor {
|
|
|
691
1136
|
const original = this.originalSpawnSync;
|
|
692
1137
|
return function interceptedSpawnSync(command, args, options) {
|
|
693
1138
|
const fullCommand = args ? `${command} ${args.join(" ")}` : command;
|
|
694
|
-
debugLog(`cp.spawnSync ENTER command=${fullCommand} _checking=${self._checking}`);
|
|
695
|
-
if (self._checking) {
|
|
1139
|
+
debugLog(`cp.spawnSync ENTER command=${fullCommand} _checking=${self._checking} _executing=${self._executing}`);
|
|
1140
|
+
if (self._checking || self._executing) {
|
|
696
1141
|
debugLog(`cp.spawnSync SKIP (re-entrancy) command=${fullCommand}`);
|
|
697
1142
|
return original(command, args, options);
|
|
698
1143
|
}
|
|
699
|
-
self.
|
|
1144
|
+
self.eventReporter.intercept("exec", fullCommand);
|
|
1145
|
+
let policyResult = null;
|
|
700
1146
|
try {
|
|
701
|
-
self.
|
|
702
|
-
debugLog(`cp.spawnSync policy_check START command=${fullCommand}`);
|
|
703
|
-
const result = self.syncClient.request(
|
|
704
|
-
"policy_check",
|
|
705
|
-
{ operation: "exec", target: fullCommand }
|
|
706
|
-
);
|
|
707
|
-
debugLog(`cp.spawnSync policy_check DONE allowed=${result.allowed} command=${fullCommand}`);
|
|
708
|
-
if (!result.allowed) {
|
|
709
|
-
return {
|
|
710
|
-
pid: -1,
|
|
711
|
-
output: [],
|
|
712
|
-
stdout: Buffer.alloc(0),
|
|
713
|
-
stderr: Buffer.from(result.reason || "Policy denied"),
|
|
714
|
-
status: 1,
|
|
715
|
-
signal: null,
|
|
716
|
-
error: new PolicyDeniedError(result.reason || "Policy denied")
|
|
717
|
-
};
|
|
718
|
-
}
|
|
1147
|
+
policyResult = self.syncPolicyCheck(fullCommand);
|
|
719
1148
|
} catch (error) {
|
|
720
|
-
debugLog(`cp.spawnSync
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
}
|
|
732
|
-
} finally {
|
|
733
|
-
self._checking = false;
|
|
1149
|
+
debugLog(`cp.spawnSync DENIED command=${fullCommand}`);
|
|
1150
|
+
return {
|
|
1151
|
+
pid: -1,
|
|
1152
|
+
output: [],
|
|
1153
|
+
stdout: Buffer.alloc(0),
|
|
1154
|
+
stderr: Buffer.from(
|
|
1155
|
+
error instanceof PolicyDeniedError ? error.message || "Policy denied" : error.message
|
|
1156
|
+
),
|
|
1157
|
+
status: 1,
|
|
1158
|
+
signal: null,
|
|
1159
|
+
error
|
|
1160
|
+
};
|
|
734
1161
|
}
|
|
735
|
-
|
|
736
|
-
|
|
1162
|
+
const wrapped = self.wrapWithSeatbelt(
|
|
1163
|
+
command,
|
|
1164
|
+
Array.from(args || []),
|
|
1165
|
+
options,
|
|
1166
|
+
policyResult
|
|
1167
|
+
);
|
|
1168
|
+
debugLog(`cp.spawnSync calling original command=${wrapped.command}`);
|
|
1169
|
+
return original(
|
|
1170
|
+
wrapped.command,
|
|
1171
|
+
wrapped.args,
|
|
1172
|
+
wrapped.options
|
|
1173
|
+
);
|
|
737
1174
|
};
|
|
738
1175
|
}
|
|
739
1176
|
createInterceptedExecFile() {
|
|
740
1177
|
const self = this;
|
|
741
1178
|
const original = this.originalExecFile;
|
|
742
|
-
return function interceptedExecFile(file, ...
|
|
743
|
-
if (self._checking) {
|
|
744
|
-
return original(file, ...
|
|
1179
|
+
return function interceptedExecFile(file, ...rest) {
|
|
1180
|
+
if (self._checking || self._executing) {
|
|
1181
|
+
return original(file, ...rest);
|
|
745
1182
|
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
1183
|
+
let args = [];
|
|
1184
|
+
let options;
|
|
1185
|
+
let callback;
|
|
1186
|
+
for (const arg of rest) {
|
|
1187
|
+
if (typeof arg === "function") {
|
|
1188
|
+
callback = arg;
|
|
1189
|
+
} else if (Array.isArray(arg)) {
|
|
1190
|
+
args = arg;
|
|
1191
|
+
} else if (typeof arg === "object" && arg !== null) {
|
|
1192
|
+
options = arg;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
const fullCommand = args.length > 0 ? `${file} ${args.join(" ")}` : file;
|
|
1196
|
+
debugLog(`cp.execFile ENTER command=${fullCommand}`);
|
|
1197
|
+
self.eventReporter.intercept("exec", fullCommand);
|
|
1198
|
+
let policyResult = null;
|
|
1199
|
+
try {
|
|
1200
|
+
policyResult = self.syncPolicyCheck(fullCommand);
|
|
1201
|
+
} catch (error) {
|
|
1202
|
+
if (callback) {
|
|
1203
|
+
process.nextTick(() => callback(error, "", ""));
|
|
1204
|
+
}
|
|
1205
|
+
return original("false");
|
|
1206
|
+
}
|
|
1207
|
+
const wrapped = self.wrapWithSeatbelt(file, args, options, policyResult);
|
|
1208
|
+
debugLog(`cp.execFile calling original command=${wrapped.command}`);
|
|
1209
|
+
if (callback) {
|
|
1210
|
+
return original(
|
|
1211
|
+
wrapped.command,
|
|
1212
|
+
wrapped.args,
|
|
1213
|
+
wrapped.options,
|
|
1214
|
+
callback
|
|
1215
|
+
);
|
|
1216
|
+
}
|
|
1217
|
+
return original(
|
|
1218
|
+
wrapped.command,
|
|
1219
|
+
wrapped.args,
|
|
1220
|
+
wrapped.options || {},
|
|
1221
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
1222
|
+
() => {
|
|
1223
|
+
}
|
|
1224
|
+
);
|
|
751
1225
|
};
|
|
752
1226
|
}
|
|
753
1227
|
createInterceptedFork() {
|
|
754
1228
|
const self = this;
|
|
755
1229
|
const original = this.originalFork;
|
|
756
1230
|
const interceptedFork = function(modulePath, args, options) {
|
|
757
|
-
if (self._checking) {
|
|
1231
|
+
if (self._checking || self._executing) {
|
|
758
1232
|
return original(modulePath, args, options);
|
|
759
1233
|
}
|
|
760
1234
|
const pathStr = modulePath.toString();
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
1235
|
+
const fullCommand = `fork:${pathStr}`;
|
|
1236
|
+
debugLog(`cp.fork ENTER command=${fullCommand}`);
|
|
1237
|
+
self.eventReporter.intercept("exec", fullCommand);
|
|
1238
|
+
let policyResult = null;
|
|
1239
|
+
try {
|
|
1240
|
+
policyResult = self.syncPolicyCheck(fullCommand);
|
|
1241
|
+
} catch (error) {
|
|
1242
|
+
debugLog(`cp.fork DENIED command=${fullCommand}`);
|
|
1243
|
+
const denied = self.originalSpawn("false", [], { stdio: "pipe" });
|
|
1244
|
+
process.nextTick(() => {
|
|
1245
|
+
denied.emit("error", error);
|
|
1246
|
+
});
|
|
1247
|
+
return denied;
|
|
1248
|
+
}
|
|
1249
|
+
if (policyResult?.sandbox) {
|
|
1250
|
+
const sandbox = policyResult.sandbox;
|
|
1251
|
+
const env = { ...options?.env || process.env };
|
|
1252
|
+
if (sandbox.envInjection) {
|
|
1253
|
+
Object.assign(env, sandbox.envInjection);
|
|
1254
|
+
}
|
|
1255
|
+
if (sandbox.envDeny) {
|
|
1256
|
+
for (const key of sandbox.envDeny) {
|
|
1257
|
+
delete env[key];
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
options = { ...options, env };
|
|
1261
|
+
}
|
|
1262
|
+
debugLog(`cp.fork calling original module=${pathStr}`);
|
|
765
1263
|
return original(modulePath, args, options);
|
|
766
1264
|
};
|
|
767
1265
|
return interceptedFork;
|
|
@@ -855,19 +1353,19 @@ var FsInterceptor = class extends BaseInterceptor {
|
|
|
855
1353
|
const key = `fs:${methodName}`;
|
|
856
1354
|
this.originals.set(key, original);
|
|
857
1355
|
const self = this;
|
|
858
|
-
safeOverride(module2, methodName, function intercepted(
|
|
859
|
-
const pathString = normalizePathArg(
|
|
1356
|
+
safeOverride(module2, methodName, function intercepted(path2, ...args) {
|
|
1357
|
+
const pathString = normalizePathArg(path2);
|
|
860
1358
|
const callback = typeof args[args.length - 1] === "function" ? args.pop() : void 0;
|
|
861
1359
|
debugLog(`fs.${methodName} ENTER (async) path=${pathString} _checking=${self._checking}`);
|
|
862
1360
|
if (self._checking) {
|
|
863
1361
|
debugLog(`fs.${methodName} SKIP (re-entrancy, async) path=${pathString}`);
|
|
864
|
-
original.call(module2,
|
|
1362
|
+
original.call(module2, path2, ...args, callback);
|
|
865
1363
|
return;
|
|
866
1364
|
}
|
|
867
1365
|
self.eventReporter.intercept(operation, pathString);
|
|
868
1366
|
self.checkPolicy(operation, pathString).then(() => {
|
|
869
1367
|
debugLog(`fs.${methodName} policy OK (async) path=${pathString}`);
|
|
870
|
-
original.call(module2,
|
|
1368
|
+
original.call(module2, path2, ...args, callback);
|
|
871
1369
|
}).catch((error) => {
|
|
872
1370
|
debugLog(`fs.${methodName} policy ERROR (async): ${error.message} path=${pathString}`);
|
|
873
1371
|
if (callback) {
|
|
@@ -882,12 +1380,12 @@ var FsInterceptor = class extends BaseInterceptor {
|
|
|
882
1380
|
const key = `fs:${methodName}`;
|
|
883
1381
|
this.originals.set(key, original);
|
|
884
1382
|
const self = this;
|
|
885
|
-
safeOverride(module2, methodName, function interceptedSync(
|
|
886
|
-
const pathString = normalizePathArg(
|
|
1383
|
+
safeOverride(module2, methodName, function interceptedSync(path2, ...args) {
|
|
1384
|
+
const pathString = normalizePathArg(path2);
|
|
887
1385
|
debugLog(`fs.${methodName} ENTER path=${pathString} _checking=${self._checking}`);
|
|
888
1386
|
if (self._checking) {
|
|
889
1387
|
debugLog(`fs.${methodName} SKIP (re-entrancy) path=${pathString}`);
|
|
890
|
-
return original.call(module2,
|
|
1388
|
+
return original.call(module2, path2, ...args);
|
|
891
1389
|
}
|
|
892
1390
|
self._checking = true;
|
|
893
1391
|
try {
|
|
@@ -916,7 +1414,7 @@ var FsInterceptor = class extends BaseInterceptor {
|
|
|
916
1414
|
self._checking = false;
|
|
917
1415
|
}
|
|
918
1416
|
debugLog(`fs.${methodName} calling original path=${pathString}`);
|
|
919
|
-
return original.call(module2,
|
|
1417
|
+
return original.call(module2, path2, ...args);
|
|
920
1418
|
});
|
|
921
1419
|
}
|
|
922
1420
|
interceptPromiseMethod(module2, methodName, operation) {
|
|
@@ -925,17 +1423,17 @@ var FsInterceptor = class extends BaseInterceptor {
|
|
|
925
1423
|
const key = `fsPromises:${methodName}`;
|
|
926
1424
|
this.originals.set(key, original);
|
|
927
1425
|
const self = this;
|
|
928
|
-
safeOverride(module2, methodName, async function interceptedPromise(
|
|
929
|
-
const pathString = normalizePathArg(
|
|
1426
|
+
safeOverride(module2, methodName, async function interceptedPromise(path2, ...args) {
|
|
1427
|
+
const pathString = normalizePathArg(path2);
|
|
930
1428
|
debugLog(`fsPromises.${methodName} ENTER path=${pathString} _checking=${self._checking}`);
|
|
931
1429
|
if (self._checking) {
|
|
932
1430
|
debugLog(`fsPromises.${methodName} SKIP (re-entrancy) path=${pathString}`);
|
|
933
|
-
return original.call(module2,
|
|
1431
|
+
return original.call(module2, path2, ...args);
|
|
934
1432
|
}
|
|
935
1433
|
self.eventReporter.intercept(operation, pathString);
|
|
936
1434
|
await self.checkPolicy(operation, pathString);
|
|
937
1435
|
debugLog(`fsPromises.${methodName} policy OK path=${pathString}`);
|
|
938
|
-
return original.call(module2,
|
|
1436
|
+
return original.call(module2, path2, ...args);
|
|
939
1437
|
});
|
|
940
1438
|
}
|
|
941
1439
|
};
|
|
@@ -1079,11 +1577,11 @@ var PolicyEvaluator = class {
|
|
|
1079
1577
|
* Check if an operation is allowed
|
|
1080
1578
|
* Always queries the daemon for fresh policy decisions
|
|
1081
1579
|
*/
|
|
1082
|
-
async check(operation, target) {
|
|
1580
|
+
async check(operation, target, context) {
|
|
1083
1581
|
try {
|
|
1084
1582
|
const result = await this.client.request(
|
|
1085
1583
|
"policy_check",
|
|
1086
|
-
{ operation, target }
|
|
1584
|
+
{ operation, target, context }
|
|
1087
1585
|
);
|
|
1088
1586
|
return result;
|
|
1089
1587
|
} catch (error) {
|
|
@@ -1127,7 +1625,11 @@ var EventReporter = class _EventReporter {
|
|
|
1127
1625
|
const level = this.getLogLevel(event);
|
|
1128
1626
|
if (this.shouldLog(level)) {
|
|
1129
1627
|
const prefix = event.type === "allow" ? "\u2713" : event.type === "deny" ? "\u2717" : "\u2022";
|
|
1130
|
-
|
|
1628
|
+
let detail = `${prefix} ${event.operation}: ${event.target}`;
|
|
1629
|
+
if (event.policyId) detail += ` [policy:${event.policyId}]`;
|
|
1630
|
+
if (event.error) detail += ` [reason:${event.error}]`;
|
|
1631
|
+
if (event.duration) detail += ` [${event.duration}ms]`;
|
|
1632
|
+
console[level](`[AgenShield] ${detail}`);
|
|
1131
1633
|
}
|
|
1132
1634
|
if (this.queue.length >= 100) {
|
|
1133
1635
|
this.flush();
|
|
@@ -1247,6 +1749,13 @@ function installInterceptors(configOverrides) {
|
|
|
1247
1749
|
return;
|
|
1248
1750
|
}
|
|
1249
1751
|
const config = createConfig(configOverrides);
|
|
1752
|
+
if (config.logLevel === "debug") {
|
|
1753
|
+
try {
|
|
1754
|
+
const safeConfig = { ...config };
|
|
1755
|
+
console.error("[AgenShield:config]", JSON.stringify(safeConfig, null, 2));
|
|
1756
|
+
} catch {
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1250
1759
|
client = new AsyncClient({
|
|
1251
1760
|
socketPath: config.socketPath,
|
|
1252
1761
|
httpHost: config.httpHost,
|
|
@@ -1267,7 +1776,8 @@ function installInterceptors(configOverrides) {
|
|
|
1267
1776
|
policyEvaluator,
|
|
1268
1777
|
eventReporter,
|
|
1269
1778
|
failOpen: config.failOpen,
|
|
1270
|
-
brokerHttpPort: config.httpPort
|
|
1779
|
+
brokerHttpPort: config.httpPort,
|
|
1780
|
+
config
|
|
1271
1781
|
});
|
|
1272
1782
|
installed.fetch.install();
|
|
1273
1783
|
log(config, "debug", "Installed fetch interceptor");
|
|
@@ -1300,7 +1810,8 @@ function installInterceptors(configOverrides) {
|
|
|
1300
1810
|
policyEvaluator,
|
|
1301
1811
|
eventReporter,
|
|
1302
1812
|
failOpen: config.failOpen,
|
|
1303
|
-
brokerHttpPort: config.httpPort
|
|
1813
|
+
brokerHttpPort: config.httpPort,
|
|
1814
|
+
config
|
|
1304
1815
|
});
|
|
1305
1816
|
installed.childProcess.install();
|
|
1306
1817
|
log(config, "debug", "Installed child_process interceptor");
|