@agenshield/interceptor 0.6.2 → 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 +677 -166
- 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 +677 -166
- package/require.js +677 -166
- package/seatbelt/profile-manager.d.ts +36 -0
- package/seatbelt/profile-manager.d.ts.map +1 -0
package/index.js
CHANGED
|
@@ -61,7 +61,12 @@ function createConfig(overrides) {
|
|
|
61
61
|
interceptWs: env["AGENSHIELD_INTERCEPT_WS"] !== "false",
|
|
62
62
|
interceptFs: false,
|
|
63
63
|
interceptExec: env["AGENSHIELD_INTERCEPT_EXEC"] !== "false",
|
|
64
|
-
timeout: parseInt(env["AGENSHIELD_TIMEOUT"] || "
|
|
64
|
+
timeout: parseInt(env["AGENSHIELD_TIMEOUT"] || "5000", 10),
|
|
65
|
+
contextType: env["AGENSHIELD_CONTEXT_TYPE"] || "agent",
|
|
66
|
+
contextSkillSlug: env["AGENSHIELD_SKILL_SLUG"],
|
|
67
|
+
contextAgentId: env["AGENSHIELD_AGENT_ID"],
|
|
68
|
+
enableSeatbelt: env["AGENSHIELD_SEATBELT"] !== "false" && process.platform === "darwin",
|
|
69
|
+
seatbeltProfileDir: env["AGENSHIELD_SEATBELT_DIR"] || "/tmp/agenshield-profiles",
|
|
65
70
|
...overrides
|
|
66
71
|
};
|
|
67
72
|
}
|
|
@@ -105,13 +110,34 @@ var TimeoutError = class extends AgenShieldError {
|
|
|
105
110
|
// libs/shield-interceptor/src/debug-log.ts
|
|
106
111
|
var fs = __toESM(require("node:fs"), 1);
|
|
107
112
|
var _appendFileSync = fs.appendFileSync.bind(fs);
|
|
113
|
+
var _writeSync = fs.writeSync.bind(fs);
|
|
108
114
|
var LOG_PATH = "/var/log/agenshield/interceptor.log";
|
|
115
|
+
var FALLBACK_LOG_PATH = "/tmp/agenshield-interceptor.log";
|
|
116
|
+
var resolvedLogPath = null;
|
|
117
|
+
function getLogPath() {
|
|
118
|
+
if (resolvedLogPath !== null) return resolvedLogPath;
|
|
119
|
+
try {
|
|
120
|
+
_appendFileSync(LOG_PATH, "");
|
|
121
|
+
resolvedLogPath = LOG_PATH;
|
|
122
|
+
} catch {
|
|
123
|
+
resolvedLogPath = FALLBACK_LOG_PATH;
|
|
124
|
+
}
|
|
125
|
+
return resolvedLogPath;
|
|
126
|
+
}
|
|
109
127
|
function debugLog(msg) {
|
|
128
|
+
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [pid:${process.pid}] ${msg}
|
|
129
|
+
`;
|
|
110
130
|
try {
|
|
111
|
-
_appendFileSync(
|
|
112
|
-
`);
|
|
131
|
+
_appendFileSync(getLogPath(), line);
|
|
113
132
|
} catch {
|
|
114
133
|
}
|
|
134
|
+
if (process.env["AGENSHIELD_LOG_LEVEL"] === "debug") {
|
|
135
|
+
try {
|
|
136
|
+
_writeSync(2, `[AgenShield:debug] ${msg}
|
|
137
|
+
`);
|
|
138
|
+
} catch {
|
|
139
|
+
}
|
|
140
|
+
}
|
|
115
141
|
}
|
|
116
142
|
|
|
117
143
|
// libs/shield-interceptor/src/interceptors/base.ts
|
|
@@ -121,6 +147,7 @@ var BaseInterceptor = class {
|
|
|
121
147
|
eventReporter;
|
|
122
148
|
failOpen;
|
|
123
149
|
installed = false;
|
|
150
|
+
interceptorConfig;
|
|
124
151
|
brokerHttpPort;
|
|
125
152
|
constructor(options) {
|
|
126
153
|
this.client = options.client;
|
|
@@ -128,6 +155,7 @@ var BaseInterceptor = class {
|
|
|
128
155
|
this.eventReporter = options.eventReporter;
|
|
129
156
|
this.failOpen = options.failOpen;
|
|
130
157
|
this.brokerHttpPort = options.brokerHttpPort ?? 5201;
|
|
158
|
+
this.interceptorConfig = options.config;
|
|
131
159
|
}
|
|
132
160
|
/**
|
|
133
161
|
* Check if a URL targets the broker or daemon (should not be intercepted)
|
|
@@ -152,15 +180,28 @@ var BaseInterceptor = class {
|
|
|
152
180
|
isInstalled() {
|
|
153
181
|
return this.installed;
|
|
154
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* Build execution context from config
|
|
185
|
+
*/
|
|
186
|
+
getBasePolicyExecutionContext() {
|
|
187
|
+
const config = this.interceptorConfig;
|
|
188
|
+
if (!config) return void 0;
|
|
189
|
+
return {
|
|
190
|
+
callerType: config.contextType || "agent",
|
|
191
|
+
skillSlug: config.contextSkillSlug,
|
|
192
|
+
agentId: config.contextAgentId,
|
|
193
|
+
depth: 0
|
|
194
|
+
};
|
|
195
|
+
}
|
|
155
196
|
/**
|
|
156
197
|
* Check policy and handle the result
|
|
157
198
|
*/
|
|
158
|
-
async checkPolicy(operation, target) {
|
|
199
|
+
async checkPolicy(operation, target, context) {
|
|
159
200
|
const startTime = Date.now();
|
|
160
201
|
debugLog(`base.checkPolicy START op=${operation} target=${target}`);
|
|
161
202
|
try {
|
|
162
203
|
this.eventReporter.intercept(operation, target);
|
|
163
|
-
const result = await this.policyEvaluator.check(operation, target);
|
|
204
|
+
const result = await this.policyEvaluator.check(operation, target, context);
|
|
164
205
|
debugLog(`base.checkPolicy evaluator result op=${operation} target=${target} allowed=${result.allowed} policyId=${result.policyId}`);
|
|
165
206
|
if (!result.allowed) {
|
|
166
207
|
this.eventReporter.deny(operation, target, result.policyId, result.reason);
|
|
@@ -207,6 +248,19 @@ var FetchInterceptor = class extends BaseInterceptor {
|
|
|
207
248
|
constructor(options) {
|
|
208
249
|
super(options);
|
|
209
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* Build execution context from config
|
|
253
|
+
*/
|
|
254
|
+
getPolicyExecutionContext() {
|
|
255
|
+
const config = this.interceptorConfig;
|
|
256
|
+
if (!config) return void 0;
|
|
257
|
+
return {
|
|
258
|
+
callerType: config.contextType || "agent",
|
|
259
|
+
skillSlug: config.contextSkillSlug,
|
|
260
|
+
agentId: config.contextAgentId,
|
|
261
|
+
depth: 0
|
|
262
|
+
};
|
|
263
|
+
}
|
|
210
264
|
install() {
|
|
211
265
|
if (this.installed) return;
|
|
212
266
|
this.originalFetch = globalThis.fetch;
|
|
@@ -237,48 +291,10 @@ var FetchInterceptor = class extends BaseInterceptor {
|
|
|
237
291
|
return this.originalFetch(input, init);
|
|
238
292
|
}
|
|
239
293
|
debugLog(`fetch checkPolicy START url=${url}`);
|
|
240
|
-
await this.checkPolicy("http_request", url);
|
|
241
|
-
debugLog(`fetch checkPolicy DONE url=${url}`);
|
|
242
294
|
try {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if (init?.headers) {
|
|
246
|
-
if (init.headers instanceof Headers) {
|
|
247
|
-
init.headers.forEach((value, key) => {
|
|
248
|
-
headers[key] = value;
|
|
249
|
-
});
|
|
250
|
-
} else if (Array.isArray(init.headers)) {
|
|
251
|
-
for (const [key, value] of init.headers) {
|
|
252
|
-
headers[key] = value;
|
|
253
|
-
}
|
|
254
|
-
} else {
|
|
255
|
-
Object.assign(headers, init.headers);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
let body;
|
|
259
|
-
if (init?.body) {
|
|
260
|
-
if (typeof init.body === "string") {
|
|
261
|
-
body = init.body;
|
|
262
|
-
} else if (init.body instanceof ArrayBuffer) {
|
|
263
|
-
body = Buffer.from(init.body).toString("base64");
|
|
264
|
-
} else {
|
|
265
|
-
body = String(init.body);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
const result = await this.client.request("http_request", {
|
|
269
|
-
url,
|
|
270
|
-
method,
|
|
271
|
-
headers,
|
|
272
|
-
body
|
|
273
|
-
});
|
|
274
|
-
const responseHeaders = new Headers(result.headers);
|
|
275
|
-
return new Response(result.body, {
|
|
276
|
-
status: result.status,
|
|
277
|
-
statusText: result.statusText,
|
|
278
|
-
headers: responseHeaders
|
|
279
|
-
});
|
|
295
|
+
await this.checkPolicy("http_request", url, this.getPolicyExecutionContext());
|
|
296
|
+
debugLog(`fetch checkPolicy DONE url=${url}`);
|
|
280
297
|
} catch (error) {
|
|
281
|
-
debugLog(`fetch ERROR url=${url} error=${error.message}`);
|
|
282
298
|
if (error.name === "PolicyDeniedError") {
|
|
283
299
|
throw error;
|
|
284
300
|
}
|
|
@@ -288,6 +304,7 @@ var FetchInterceptor = class extends BaseInterceptor {
|
|
|
288
304
|
}
|
|
289
305
|
throw error;
|
|
290
306
|
}
|
|
307
|
+
return this.originalFetch(input, init);
|
|
291
308
|
}
|
|
292
309
|
};
|
|
293
310
|
|
|
@@ -432,6 +449,8 @@ var import_node_crypto = require("node:crypto");
|
|
|
432
449
|
var _existsSync = fs2.existsSync.bind(fs2);
|
|
433
450
|
var _readFileSync = fs2.readFileSync.bind(fs2);
|
|
434
451
|
var _unlinkSync = fs2.unlinkSync.bind(fs2);
|
|
452
|
+
var _readdirSync = fs2.readdirSync.bind(fs2);
|
|
453
|
+
var _statSync = fs2.statSync.bind(fs2);
|
|
435
454
|
var _spawnSync = import_node_child_process.spawnSync;
|
|
436
455
|
var _execSync = import_node_child_process.execSync;
|
|
437
456
|
var SyncClient = class {
|
|
@@ -439,22 +458,59 @@ var SyncClient = class {
|
|
|
439
458
|
httpHost;
|
|
440
459
|
httpPort;
|
|
441
460
|
timeout;
|
|
461
|
+
socketFailCount = 0;
|
|
462
|
+
socketSkipUntil = 0;
|
|
442
463
|
constructor(options) {
|
|
443
464
|
this.socketPath = options.socketPath;
|
|
444
465
|
this.httpHost = options.httpHost;
|
|
445
466
|
this.httpPort = options.httpPort;
|
|
446
467
|
this.timeout = options.timeout;
|
|
468
|
+
this.cleanupStaleTmpFiles();
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Remove stale /tmp/agenshield-sync-*.json files from previous runs
|
|
472
|
+
*/
|
|
473
|
+
cleanupStaleTmpFiles() {
|
|
474
|
+
try {
|
|
475
|
+
const tmpDir = "/tmp";
|
|
476
|
+
const files = _readdirSync(tmpDir);
|
|
477
|
+
const cutoff = Date.now() - 5 * 60 * 1e3;
|
|
478
|
+
for (const f of files) {
|
|
479
|
+
if (f.startsWith("agenshield-sync-") && f.endsWith(".json")) {
|
|
480
|
+
const fp = `${tmpDir}/${f}`;
|
|
481
|
+
try {
|
|
482
|
+
const stat = _statSync(fp);
|
|
483
|
+
if (stat.mtimeMs < cutoff) _unlinkSync(fp);
|
|
484
|
+
} catch {
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
} catch {
|
|
489
|
+
}
|
|
447
490
|
}
|
|
448
491
|
/**
|
|
449
492
|
* Send a synchronous request to the broker
|
|
450
493
|
*/
|
|
451
494
|
request(method, params) {
|
|
452
495
|
debugLog(`syncClient.request START method=${method}`);
|
|
496
|
+
const now = Date.now();
|
|
497
|
+
if (now < this.socketSkipUntil) {
|
|
498
|
+
debugLog(`syncClient.request SKIP socket (circuit open for ${this.socketSkipUntil - now}ms), using HTTP`);
|
|
499
|
+
const result = this.httpRequestSync(method, params);
|
|
500
|
+
debugLog(`syncClient.request http OK method=${method}`);
|
|
501
|
+
return result;
|
|
502
|
+
}
|
|
453
503
|
try {
|
|
454
504
|
const result = this.socketRequestSync(method, params);
|
|
505
|
+
this.socketFailCount = 0;
|
|
455
506
|
debugLog(`syncClient.request socket OK method=${method}`);
|
|
456
507
|
return result;
|
|
457
508
|
} catch (socketErr) {
|
|
509
|
+
this.socketFailCount++;
|
|
510
|
+
if (this.socketFailCount >= 2) {
|
|
511
|
+
this.socketSkipUntil = Date.now() + 6e4;
|
|
512
|
+
debugLog(`syncClient.request socket circuit OPEN (${this.socketFailCount} failures)`);
|
|
513
|
+
}
|
|
458
514
|
debugLog(`syncClient.request socket FAILED: ${socketErr.message}, trying HTTP`);
|
|
459
515
|
const result = this.httpRequestSync(method, params);
|
|
460
516
|
debugLog(`syncClient.request http OK method=${method}`);
|
|
@@ -479,29 +535,40 @@ var SyncClient = class {
|
|
|
479
535
|
const net = require('net');
|
|
480
536
|
const fs = require('fs');
|
|
481
537
|
|
|
538
|
+
let done = false;
|
|
482
539
|
const socket = net.createConnection('${this.socketPath}');
|
|
483
540
|
let data = '';
|
|
484
541
|
|
|
542
|
+
const timer = setTimeout(() => {
|
|
543
|
+
if (done) return;
|
|
544
|
+
done = true;
|
|
545
|
+
socket.destroy();
|
|
546
|
+
fs.writeFileSync('${tmpFile}', JSON.stringify({ error: 'timeout' }));
|
|
547
|
+
process.exit(1);
|
|
548
|
+
}, ${this.timeout});
|
|
549
|
+
|
|
485
550
|
socket.on('connect', () => {
|
|
486
551
|
socket.write(${JSON.stringify(request)});
|
|
487
552
|
});
|
|
488
553
|
|
|
489
554
|
socket.on('data', (chunk) => {
|
|
490
555
|
data += chunk.toString();
|
|
491
|
-
if (data.includes('\\n')) {
|
|
556
|
+
if (data.includes('\\n') && !done) {
|
|
557
|
+
done = true;
|
|
558
|
+
clearTimeout(timer);
|
|
492
559
|
socket.end();
|
|
493
560
|
fs.writeFileSync('${tmpFile}', data.split('\\n')[0]);
|
|
561
|
+
process.exit(0);
|
|
494
562
|
}
|
|
495
563
|
});
|
|
496
564
|
|
|
497
565
|
socket.on('error', (err) => {
|
|
566
|
+
if (done) return;
|
|
567
|
+
done = true;
|
|
568
|
+
clearTimeout(timer);
|
|
498
569
|
fs.writeFileSync('${tmpFile}', JSON.stringify({ error: err.message }));
|
|
570
|
+
process.exit(1);
|
|
499
571
|
});
|
|
500
|
-
|
|
501
|
-
setTimeout(() => {
|
|
502
|
-
socket.destroy();
|
|
503
|
-
fs.writeFileSync('${tmpFile}', JSON.stringify({ error: 'timeout' }));
|
|
504
|
-
}, ${this.timeout});
|
|
505
572
|
`;
|
|
506
573
|
try {
|
|
507
574
|
debugLog(`syncClient.socketRequestSync _spawnSync START node-bin method=${method}`);
|
|
@@ -578,11 +645,227 @@ var SyncClient = class {
|
|
|
578
645
|
}
|
|
579
646
|
};
|
|
580
647
|
|
|
648
|
+
// libs/shield-interceptor/src/seatbelt/profile-manager.ts
|
|
649
|
+
var fs3 = __toESM(require("node:fs"), 1);
|
|
650
|
+
var crypto = __toESM(require("node:crypto"), 1);
|
|
651
|
+
var path = __toESM(require("node:path"), 1);
|
|
652
|
+
var _mkdirSync = fs3.mkdirSync.bind(fs3);
|
|
653
|
+
var _writeFileSync = fs3.writeFileSync.bind(fs3);
|
|
654
|
+
var _existsSync2 = fs3.existsSync.bind(fs3);
|
|
655
|
+
var _readFileSync2 = fs3.readFileSync.bind(fs3);
|
|
656
|
+
var _readdirSync2 = fs3.readdirSync.bind(fs3);
|
|
657
|
+
var _statSync2 = fs3.statSync.bind(fs3);
|
|
658
|
+
var _unlinkSync2 = fs3.unlinkSync.bind(fs3);
|
|
659
|
+
var _chmodSync = fs3.chmodSync.bind(fs3);
|
|
660
|
+
var ProfileManager = class {
|
|
661
|
+
profileDir;
|
|
662
|
+
ensuredDir = false;
|
|
663
|
+
constructor(profileDir) {
|
|
664
|
+
this.profileDir = profileDir;
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Get or create a profile file on disk. Returns the absolute path.
|
|
668
|
+
* Uses content-hash naming so identical configs reuse the same file.
|
|
669
|
+
*/
|
|
670
|
+
getOrCreateProfile(content) {
|
|
671
|
+
this.ensureDir();
|
|
672
|
+
const hash = crypto.createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
673
|
+
const profilePath = path.join(this.profileDir, `sb-${hash}.sb`);
|
|
674
|
+
if (!_existsSync2(profilePath)) {
|
|
675
|
+
debugLog(`profile-manager: writing new profile ${profilePath} (${content.length} bytes)`);
|
|
676
|
+
_writeFileSync(profilePath, content, { mode: 420 });
|
|
677
|
+
}
|
|
678
|
+
return profilePath;
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Generate an SBPL profile from a SandboxConfig.
|
|
682
|
+
*/
|
|
683
|
+
generateProfile(sandbox) {
|
|
684
|
+
if (sandbox.profileContent) {
|
|
685
|
+
return sandbox.profileContent;
|
|
686
|
+
}
|
|
687
|
+
const lines = [
|
|
688
|
+
";; AgenShield dynamic seatbelt profile",
|
|
689
|
+
`;; Generated: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
690
|
+
"(version 1)",
|
|
691
|
+
"(deny default)",
|
|
692
|
+
""
|
|
693
|
+
];
|
|
694
|
+
lines.push(
|
|
695
|
+
";; Filesystem: reads allowed, writes restricted",
|
|
696
|
+
"(allow file-read*)",
|
|
697
|
+
""
|
|
698
|
+
);
|
|
699
|
+
const writePaths = ["/tmp", "/private/tmp", "/var/folders"];
|
|
700
|
+
if (sandbox.allowedWritePaths.length > 0) {
|
|
701
|
+
writePaths.push(...sandbox.allowedWritePaths);
|
|
702
|
+
}
|
|
703
|
+
lines.push("(allow file-write*");
|
|
704
|
+
for (const p of writePaths) {
|
|
705
|
+
lines.push(` (subpath "${this.escapeSbpl(p)}")`);
|
|
706
|
+
}
|
|
707
|
+
lines.push(")");
|
|
708
|
+
lines.push("");
|
|
709
|
+
lines.push("(allow file-write*");
|
|
710
|
+
lines.push(' (literal "/dev/null")');
|
|
711
|
+
lines.push(' (literal "/dev/zero")');
|
|
712
|
+
lines.push(' (literal "/dev/random")');
|
|
713
|
+
lines.push(' (literal "/dev/urandom")');
|
|
714
|
+
lines.push(")");
|
|
715
|
+
lines.push("");
|
|
716
|
+
if (sandbox.deniedPaths.length > 0) {
|
|
717
|
+
lines.push(";; Denied paths");
|
|
718
|
+
for (const p of sandbox.deniedPaths) {
|
|
719
|
+
lines.push(`(deny file-read* file-write* (subpath "${this.escapeSbpl(p)}"))`);
|
|
720
|
+
}
|
|
721
|
+
lines.push("");
|
|
722
|
+
}
|
|
723
|
+
lines.push(";; Binary execution (system directories allowed as subpaths)");
|
|
724
|
+
lines.push("(allow process-exec");
|
|
725
|
+
lines.push(' (subpath "/bin")');
|
|
726
|
+
lines.push(' (subpath "/sbin")');
|
|
727
|
+
lines.push(' (subpath "/usr/bin")');
|
|
728
|
+
lines.push(' (subpath "/usr/sbin")');
|
|
729
|
+
lines.push(' (subpath "/usr/local/bin")');
|
|
730
|
+
lines.push(' (subpath "/opt/agenshield/bin")');
|
|
731
|
+
const coveredSubpaths = ["/bin/", "/sbin/", "/usr/bin/", "/usr/sbin/", "/usr/local/bin/", "/opt/agenshield/bin/"];
|
|
732
|
+
const home = process.env["HOME"];
|
|
733
|
+
if (home) {
|
|
734
|
+
lines.push(` (subpath "${this.escapeSbpl(home)}/bin")`);
|
|
735
|
+
lines.push(` (subpath "${this.escapeSbpl(home)}/homebrew")`);
|
|
736
|
+
coveredSubpaths.push(`${home}/bin/`, `${home}/homebrew/`);
|
|
737
|
+
}
|
|
738
|
+
const nvmDir = process.env["NVM_DIR"] || (home ? `${home}/.nvm` : null);
|
|
739
|
+
if (nvmDir) {
|
|
740
|
+
lines.push(` (subpath "${this.escapeSbpl(nvmDir)}")`);
|
|
741
|
+
coveredSubpaths.push(`${nvmDir}/`);
|
|
742
|
+
}
|
|
743
|
+
const brewPrefix = process.env["HOMEBREW_PREFIX"];
|
|
744
|
+
if (brewPrefix && (!home || !brewPrefix.startsWith(home))) {
|
|
745
|
+
lines.push(` (subpath "${this.escapeSbpl(brewPrefix)}/bin")`);
|
|
746
|
+
lines.push(` (subpath "${this.escapeSbpl(brewPrefix)}/lib")`);
|
|
747
|
+
coveredSubpaths.push(`${brewPrefix}/bin/`, `${brewPrefix}/lib/`);
|
|
748
|
+
}
|
|
749
|
+
const uniqueBinaries = [...new Set(sandbox.allowedBinaries)];
|
|
750
|
+
for (const bin of uniqueBinaries) {
|
|
751
|
+
if (coveredSubpaths.some((dir) => bin === dir || bin.startsWith(dir))) continue;
|
|
752
|
+
if (bin.endsWith("/")) {
|
|
753
|
+
lines.push(` (subpath "${this.escapeSbpl(bin)}")`);
|
|
754
|
+
} else {
|
|
755
|
+
lines.push(` (literal "${this.escapeSbpl(bin)}")`);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
lines.push(")");
|
|
759
|
+
lines.push("");
|
|
760
|
+
const uniqueDenied = [...new Set(sandbox.deniedBinaries)];
|
|
761
|
+
if (uniqueDenied.length > 0) {
|
|
762
|
+
lines.push(";; Denied binaries");
|
|
763
|
+
for (const bin of uniqueDenied) {
|
|
764
|
+
lines.push(`(deny process-exec (literal "${this.escapeSbpl(bin)}"))`);
|
|
765
|
+
}
|
|
766
|
+
lines.push("");
|
|
767
|
+
}
|
|
768
|
+
lines.push(";; Network");
|
|
769
|
+
if (sandbox.networkAllowed) {
|
|
770
|
+
if (sandbox.allowedHosts.length > 0 || sandbox.allowedPorts.length > 0) {
|
|
771
|
+
lines.push(";; Allow specific network targets");
|
|
772
|
+
for (const host of sandbox.allowedHosts) {
|
|
773
|
+
lines.push(`(allow network-outbound (remote tcp "${this.escapeSbpl(host)}:*"))`);
|
|
774
|
+
}
|
|
775
|
+
for (const port of sandbox.allowedPorts) {
|
|
776
|
+
lines.push(`(allow network-outbound (remote tcp "*:${port}"))`);
|
|
777
|
+
}
|
|
778
|
+
const isLocalhostOnly = sandbox.allowedHosts.length > 0 && sandbox.allowedHosts.every((h) => h === "localhost" || h === "127.0.0.1");
|
|
779
|
+
if (!isLocalhostOnly) {
|
|
780
|
+
lines.push('(allow network-outbound (remote udp "*:53") (remote tcp "*:53"))');
|
|
781
|
+
}
|
|
782
|
+
} else {
|
|
783
|
+
lines.push("(allow network*)");
|
|
784
|
+
}
|
|
785
|
+
} else {
|
|
786
|
+
lines.push("(deny network*)");
|
|
787
|
+
}
|
|
788
|
+
lines.push("");
|
|
789
|
+
lines.push(
|
|
790
|
+
";; Broker / local unix sockets",
|
|
791
|
+
"(allow network-outbound (remote unix))",
|
|
792
|
+
"(allow network-inbound (local unix))",
|
|
793
|
+
"(allow file-read* file-write*",
|
|
794
|
+
' (subpath "/var/run/agenshield")',
|
|
795
|
+
' (subpath "/private/var/run/agenshield"))',
|
|
796
|
+
""
|
|
797
|
+
);
|
|
798
|
+
lines.push(
|
|
799
|
+
";; Process management",
|
|
800
|
+
"(allow process-fork)",
|
|
801
|
+
"(allow signal (target self))",
|
|
802
|
+
"(allow sysctl-read)",
|
|
803
|
+
""
|
|
804
|
+
);
|
|
805
|
+
lines.push(
|
|
806
|
+
";; Mach IPC",
|
|
807
|
+
"(allow mach-lookup)",
|
|
808
|
+
""
|
|
809
|
+
);
|
|
810
|
+
return lines.join("\n");
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Remove stale profile files older than maxAgeMs.
|
|
814
|
+
*/
|
|
815
|
+
cleanup(maxAgeMs) {
|
|
816
|
+
if (!_existsSync2(this.profileDir)) return;
|
|
817
|
+
try {
|
|
818
|
+
const now = Date.now();
|
|
819
|
+
const entries = _readdirSync2(this.profileDir);
|
|
820
|
+
for (const entry of entries) {
|
|
821
|
+
if (!entry.endsWith(".sb")) continue;
|
|
822
|
+
const filePath = path.join(this.profileDir, entry);
|
|
823
|
+
try {
|
|
824
|
+
const stat = _statSync2(filePath);
|
|
825
|
+
if (now - stat.mtimeMs > maxAgeMs) {
|
|
826
|
+
_unlinkSync2(filePath);
|
|
827
|
+
debugLog(`profile-manager: cleaned up stale profile ${filePath}`);
|
|
828
|
+
}
|
|
829
|
+
} catch {
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
} catch {
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Escape a string for safe inclusion in SBPL
|
|
837
|
+
*/
|
|
838
|
+
escapeSbpl(s) {
|
|
839
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Ensure the profile directory exists
|
|
843
|
+
*/
|
|
844
|
+
ensureDir() {
|
|
845
|
+
if (this.ensuredDir) return;
|
|
846
|
+
if (!_existsSync2(this.profileDir)) {
|
|
847
|
+
_mkdirSync(this.profileDir, { recursive: true, mode: 1023 });
|
|
848
|
+
} else {
|
|
849
|
+
try {
|
|
850
|
+
const stat = _statSync2(this.profileDir);
|
|
851
|
+
if ((stat.mode & 511) !== 511) {
|
|
852
|
+
_chmodSync(this.profileDir, 1023);
|
|
853
|
+
}
|
|
854
|
+
} catch {
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
this.ensuredDir = true;
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
|
|
581
861
|
// libs/shield-interceptor/src/interceptors/child-process.ts
|
|
582
862
|
var childProcessModule = require("node:child_process");
|
|
583
863
|
var ChildProcessInterceptor = class extends BaseInterceptor {
|
|
584
864
|
syncClient;
|
|
585
865
|
_checking = false;
|
|
866
|
+
_executing = false;
|
|
867
|
+
// Guards exec→execFile re-entrancy
|
|
868
|
+
profileManager = null;
|
|
586
869
|
originalExec = null;
|
|
587
870
|
originalExecSync = null;
|
|
588
871
|
originalSpawn = null;
|
|
@@ -591,13 +874,18 @@ var ChildProcessInterceptor = class extends BaseInterceptor {
|
|
|
591
874
|
originalFork = null;
|
|
592
875
|
constructor(options) {
|
|
593
876
|
super(options);
|
|
877
|
+
const config = this.interceptorConfig;
|
|
594
878
|
this.syncClient = new SyncClient({
|
|
595
|
-
socketPath: "/var/run/agenshield/agenshield.sock",
|
|
596
|
-
httpHost: "localhost",
|
|
597
|
-
httpPort: 5201,
|
|
598
|
-
|
|
599
|
-
timeout: 3e4
|
|
879
|
+
socketPath: config?.socketPath || "/var/run/agenshield/agenshield.sock",
|
|
880
|
+
httpHost: config?.httpHost || "localhost",
|
|
881
|
+
httpPort: config?.httpPort || 5201,
|
|
882
|
+
timeout: config?.timeout || 3e4
|
|
600
883
|
});
|
|
884
|
+
if (config?.enableSeatbelt && process.platform === "darwin") {
|
|
885
|
+
this.profileManager = new ProfileManager(
|
|
886
|
+
config.seatbeltProfileDir || "/tmp/agenshield-profiles"
|
|
887
|
+
);
|
|
888
|
+
}
|
|
601
889
|
}
|
|
602
890
|
install() {
|
|
603
891
|
if (this.installed) return;
|
|
@@ -631,64 +919,207 @@ var ChildProcessInterceptor = class extends BaseInterceptor {
|
|
|
631
919
|
this.originalFork = null;
|
|
632
920
|
this.installed = false;
|
|
633
921
|
}
|
|
922
|
+
/**
|
|
923
|
+
* Build execution context from config for RPC calls
|
|
924
|
+
*/
|
|
925
|
+
getPolicyExecutionContext() {
|
|
926
|
+
const config = this.interceptorConfig;
|
|
927
|
+
return {
|
|
928
|
+
callerType: config?.contextType || "agent",
|
|
929
|
+
skillSlug: config?.contextSkillSlug,
|
|
930
|
+
agentId: config?.contextAgentId,
|
|
931
|
+
depth: 0
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Synchronous policy check via SyncClient.
|
|
936
|
+
* Returns the full policy result (with sandbox config) or null if broker
|
|
937
|
+
* is unavailable and failOpen is true.
|
|
938
|
+
*/
|
|
939
|
+
syncPolicyCheck(fullCommand) {
|
|
940
|
+
this._checking = true;
|
|
941
|
+
try {
|
|
942
|
+
debugLog(`cp.syncPolicyCheck START command=${fullCommand}`);
|
|
943
|
+
const context = this.getPolicyExecutionContext();
|
|
944
|
+
const result = this.syncClient.request(
|
|
945
|
+
"policy_check",
|
|
946
|
+
{ operation: "exec", target: fullCommand, context }
|
|
947
|
+
);
|
|
948
|
+
debugLog(`cp.syncPolicyCheck DONE allowed=${result.allowed} command=${fullCommand}`);
|
|
949
|
+
if (!result.allowed) {
|
|
950
|
+
throw new PolicyDeniedError(result.reason || "Operation denied by policy", {
|
|
951
|
+
operation: "exec",
|
|
952
|
+
target: fullCommand,
|
|
953
|
+
policyId: result.policyId
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
return result;
|
|
957
|
+
} catch (error) {
|
|
958
|
+
if (error instanceof PolicyDeniedError) {
|
|
959
|
+
throw error;
|
|
960
|
+
}
|
|
961
|
+
debugLog(`cp.syncPolicyCheck ERROR: ${error.message} command=${fullCommand}`);
|
|
962
|
+
if (!this.failOpen) {
|
|
963
|
+
throw error;
|
|
964
|
+
}
|
|
965
|
+
return null;
|
|
966
|
+
} finally {
|
|
967
|
+
this._checking = false;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Create a restrictive default sandbox config for fail-open scenarios.
|
|
972
|
+
* No network, minimal fs — better than running completely unsandboxed.
|
|
973
|
+
*/
|
|
974
|
+
getFailOpenSandbox() {
|
|
975
|
+
return {
|
|
976
|
+
enabled: true,
|
|
977
|
+
allowedReadPaths: [],
|
|
978
|
+
allowedWritePaths: [],
|
|
979
|
+
deniedPaths: [],
|
|
980
|
+
networkAllowed: false,
|
|
981
|
+
allowedHosts: [],
|
|
982
|
+
allowedPorts: [],
|
|
983
|
+
allowedBinaries: [],
|
|
984
|
+
deniedBinaries: [],
|
|
985
|
+
envInjection: {},
|
|
986
|
+
envDeny: []
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Resolve the sandbox config to use: from policy result, fail-open default, or null.
|
|
991
|
+
*/
|
|
992
|
+
resolveSandbox(policyResult) {
|
|
993
|
+
if (policyResult?.sandbox?.enabled) {
|
|
994
|
+
return policyResult.sandbox;
|
|
995
|
+
}
|
|
996
|
+
if (policyResult === null && this.profileManager) {
|
|
997
|
+
return this.getFailOpenSandbox();
|
|
998
|
+
}
|
|
999
|
+
return null;
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Wrap a command with sandbox-exec if seatbelt is enabled and sandbox config is present.
|
|
1003
|
+
* Returns modified { command, args, options } for spawn-style calls.
|
|
1004
|
+
*/
|
|
1005
|
+
wrapWithSeatbelt(command, args, options, policyResult) {
|
|
1006
|
+
const sandbox = this.resolveSandbox(policyResult);
|
|
1007
|
+
if (!this.profileManager || !sandbox || process.platform !== "darwin") {
|
|
1008
|
+
return { command, args, options };
|
|
1009
|
+
}
|
|
1010
|
+
if (command === "/opt/agenshield/bin/node-bin" || command.endsWith("/node-bin")) {
|
|
1011
|
+
debugLog(`cp.wrapWithSeatbelt: SKIP node-bin (already intercepted) command=${command}`);
|
|
1012
|
+
return { command, args, options };
|
|
1013
|
+
}
|
|
1014
|
+
if (command === "/usr/bin/sandbox-exec" || command.endsWith("/sandbox-exec")) {
|
|
1015
|
+
debugLog(`cp.wrapWithSeatbelt: SKIP already sandbox-exec command=${command}`);
|
|
1016
|
+
return { command, args, options };
|
|
1017
|
+
}
|
|
1018
|
+
debugLog(`cp.wrapWithSeatbelt: wrapping command=${command}`);
|
|
1019
|
+
const profileContent = this.profileManager.generateProfile(sandbox);
|
|
1020
|
+
const profilePath = this.profileManager.getOrCreateProfile(profileContent);
|
|
1021
|
+
const env = { ...options?.env || process.env };
|
|
1022
|
+
if (sandbox.envInjection) {
|
|
1023
|
+
Object.assign(env, sandbox.envInjection);
|
|
1024
|
+
}
|
|
1025
|
+
if (sandbox.envDeny) {
|
|
1026
|
+
for (const key of sandbox.envDeny) {
|
|
1027
|
+
delete env[key];
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
return {
|
|
1031
|
+
command: "/usr/bin/sandbox-exec",
|
|
1032
|
+
args: ["-f", profilePath, command, ...args],
|
|
1033
|
+
options: { ...options, env }
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Wrap a shell command string with sandbox-exec.
|
|
1038
|
+
* For exec/execSync which take a full command string.
|
|
1039
|
+
*/
|
|
1040
|
+
wrapCommandStringWithSeatbelt(command, options, policyResult) {
|
|
1041
|
+
const sandbox = this.resolveSandbox(policyResult);
|
|
1042
|
+
if (!this.profileManager || !sandbox || process.platform !== "darwin") {
|
|
1043
|
+
return { command, options };
|
|
1044
|
+
}
|
|
1045
|
+
if (command.startsWith("/usr/bin/sandbox-exec ") || command.startsWith("sandbox-exec ")) {
|
|
1046
|
+
debugLog(`cp.wrapCommandStringWithSeatbelt: SKIP already sandbox-exec command=${command}`);
|
|
1047
|
+
return { command, options };
|
|
1048
|
+
}
|
|
1049
|
+
debugLog(`cp.wrapCommandStringWithSeatbelt: wrapping command=${command}`);
|
|
1050
|
+
const profileContent = this.profileManager.generateProfile(sandbox);
|
|
1051
|
+
const profilePath = this.profileManager.getOrCreateProfile(profileContent);
|
|
1052
|
+
const env = { ...options?.env || process.env };
|
|
1053
|
+
if (sandbox.envInjection) {
|
|
1054
|
+
Object.assign(env, sandbox.envInjection);
|
|
1055
|
+
}
|
|
1056
|
+
if (sandbox.envDeny) {
|
|
1057
|
+
for (const key of sandbox.envDeny) {
|
|
1058
|
+
delete env[key];
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
return {
|
|
1062
|
+
command: `/usr/bin/sandbox-exec -f ${profilePath} ${command}`,
|
|
1063
|
+
options: { ...options, env }
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
634
1066
|
createInterceptedExec() {
|
|
635
1067
|
const self = this;
|
|
636
1068
|
const original = this.originalExec;
|
|
637
1069
|
return function interceptedExec(command, ...args) {
|
|
638
1070
|
const callback = typeof args[args.length - 1] === "function" ? args.pop() : void 0;
|
|
639
|
-
|
|
640
|
-
|
|
1071
|
+
const options = args[0];
|
|
1072
|
+
debugLog(`cp.exec ENTER command=${command} _checking=${self._checking} _executing=${self._executing}`);
|
|
1073
|
+
if (self._checking || self._executing) {
|
|
641
1074
|
debugLog(`cp.exec SKIP (re-entrancy) command=${command}`);
|
|
642
1075
|
return original(command, ...args, callback);
|
|
643
1076
|
}
|
|
644
1077
|
self.eventReporter.intercept("exec", command);
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
1078
|
+
let policyResult = null;
|
|
1079
|
+
try {
|
|
1080
|
+
policyResult = self.syncPolicyCheck(command);
|
|
1081
|
+
} catch (error) {
|
|
648
1082
|
if (callback) {
|
|
649
|
-
callback(error, "", "");
|
|
1083
|
+
process.nextTick(() => callback(error, "", ""));
|
|
650
1084
|
}
|
|
651
|
-
|
|
652
|
-
|
|
1085
|
+
return original('echo ""');
|
|
1086
|
+
}
|
|
1087
|
+
const wrapped = self.wrapCommandStringWithSeatbelt(command, options, policyResult);
|
|
1088
|
+
debugLog(`cp.exec calling original command=${wrapped.command}`);
|
|
1089
|
+
self._executing = true;
|
|
1090
|
+
try {
|
|
1091
|
+
if (wrapped.options) {
|
|
1092
|
+
return original(wrapped.command, wrapped.options, callback);
|
|
1093
|
+
}
|
|
1094
|
+
return original(wrapped.command, callback);
|
|
1095
|
+
} finally {
|
|
1096
|
+
self._executing = false;
|
|
1097
|
+
}
|
|
653
1098
|
};
|
|
654
1099
|
}
|
|
655
1100
|
createInterceptedExecSync() {
|
|
656
1101
|
const self = this;
|
|
657
1102
|
const original = this.originalExecSync;
|
|
658
1103
|
const interceptedExecSync = function(command, options) {
|
|
659
|
-
debugLog(`cp.execSync ENTER command=${command} _checking=${self._checking}`);
|
|
660
|
-
if (self._checking) {
|
|
1104
|
+
debugLog(`cp.execSync ENTER command=${command} _checking=${self._checking} _executing=${self._executing}`);
|
|
1105
|
+
if (self._checking || self._executing) {
|
|
661
1106
|
debugLog(`cp.execSync SKIP (re-entrancy) command=${command}`);
|
|
662
1107
|
return original(command, options);
|
|
663
1108
|
}
|
|
664
|
-
self.
|
|
1109
|
+
self.eventReporter.intercept("exec", command);
|
|
1110
|
+
const policyResult = self.syncPolicyCheck(command);
|
|
1111
|
+
const wrapped = self.wrapCommandStringWithSeatbelt(
|
|
1112
|
+
command,
|
|
1113
|
+
options,
|
|
1114
|
+
policyResult
|
|
1115
|
+
);
|
|
1116
|
+
debugLog(`cp.execSync calling original command=${wrapped.command}`);
|
|
1117
|
+
self._executing = true;
|
|
665
1118
|
try {
|
|
666
|
-
|
|
667
|
-
debugLog(`cp.execSync policy_check START command=${command}`);
|
|
668
|
-
const result = self.syncClient.request(
|
|
669
|
-
"policy_check",
|
|
670
|
-
{ operation: "exec", target: command }
|
|
671
|
-
);
|
|
672
|
-
debugLog(`cp.execSync policy_check DONE allowed=${result.allowed} command=${command}`);
|
|
673
|
-
if (!result.allowed) {
|
|
674
|
-
throw new PolicyDeniedError(result.reason || "Operation denied by policy", {
|
|
675
|
-
operation: "exec",
|
|
676
|
-
target: command
|
|
677
|
-
});
|
|
678
|
-
}
|
|
679
|
-
} catch (error) {
|
|
680
|
-
debugLog(`cp.execSync policy_check ERROR: ${error.message} command=${command}`);
|
|
681
|
-
if (error instanceof PolicyDeniedError) {
|
|
682
|
-
throw error;
|
|
683
|
-
}
|
|
684
|
-
if (!self.failOpen) {
|
|
685
|
-
throw error;
|
|
686
|
-
}
|
|
1119
|
+
return original(wrapped.command, wrapped.options);
|
|
687
1120
|
} finally {
|
|
688
|
-
self.
|
|
1121
|
+
self._executing = false;
|
|
689
1122
|
}
|
|
690
|
-
debugLog(`cp.execSync calling original command=${command}`);
|
|
691
|
-
return original(command, options);
|
|
692
1123
|
};
|
|
693
1124
|
return interceptedExecSync;
|
|
694
1125
|
}
|
|
@@ -697,17 +1128,31 @@ var ChildProcessInterceptor = class extends BaseInterceptor {
|
|
|
697
1128
|
const original = this.originalSpawn;
|
|
698
1129
|
const interceptedSpawn = function(command, args, options) {
|
|
699
1130
|
const fullCmd = args ? `${command} ${args.join(" ")}` : command;
|
|
700
|
-
debugLog(`cp.spawn ENTER command=${fullCmd} _checking=${self._checking}`);
|
|
701
|
-
if (self._checking) {
|
|
1131
|
+
debugLog(`cp.spawn ENTER command=${fullCmd} _checking=${self._checking} _executing=${self._executing}`);
|
|
1132
|
+
if (self._checking || self._executing) {
|
|
702
1133
|
debugLog(`cp.spawn SKIP (re-entrancy) command=${fullCmd}`);
|
|
703
1134
|
return original(command, args, options || {});
|
|
704
1135
|
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
self.
|
|
709
|
-
})
|
|
710
|
-
|
|
1136
|
+
self.eventReporter.intercept("exec", fullCmd);
|
|
1137
|
+
let policyResult = null;
|
|
1138
|
+
try {
|
|
1139
|
+
policyResult = self.syncPolicyCheck(fullCmd);
|
|
1140
|
+
} catch (error) {
|
|
1141
|
+
debugLog(`cp.spawn DENIED command=${fullCmd}`);
|
|
1142
|
+
const denied = original("false", [], { stdio: "pipe" });
|
|
1143
|
+
process.nextTick(() => {
|
|
1144
|
+
denied.emit("error", error);
|
|
1145
|
+
});
|
|
1146
|
+
return denied;
|
|
1147
|
+
}
|
|
1148
|
+
const wrapped = self.wrapWithSeatbelt(
|
|
1149
|
+
command,
|
|
1150
|
+
Array.from(args || []),
|
|
1151
|
+
options,
|
|
1152
|
+
policyResult
|
|
1153
|
+
);
|
|
1154
|
+
debugLog(`cp.spawn calling original command=${wrapped.command} args=${wrapped.args.join(" ")}`);
|
|
1155
|
+
return original(wrapped.command, wrapped.args, wrapped.options || {});
|
|
711
1156
|
};
|
|
712
1157
|
return interceptedSpawn;
|
|
713
1158
|
}
|
|
@@ -716,77 +1161,130 @@ var ChildProcessInterceptor = class extends BaseInterceptor {
|
|
|
716
1161
|
const original = this.originalSpawnSync;
|
|
717
1162
|
return function interceptedSpawnSync(command, args, options) {
|
|
718
1163
|
const fullCommand = args ? `${command} ${args.join(" ")}` : command;
|
|
719
|
-
debugLog(`cp.spawnSync ENTER command=${fullCommand} _checking=${self._checking}`);
|
|
720
|
-
if (self._checking) {
|
|
1164
|
+
debugLog(`cp.spawnSync ENTER command=${fullCommand} _checking=${self._checking} _executing=${self._executing}`);
|
|
1165
|
+
if (self._checking || self._executing) {
|
|
721
1166
|
debugLog(`cp.spawnSync SKIP (re-entrancy) command=${fullCommand}`);
|
|
722
1167
|
return original(command, args, options);
|
|
723
1168
|
}
|
|
724
|
-
self.
|
|
1169
|
+
self.eventReporter.intercept("exec", fullCommand);
|
|
1170
|
+
let policyResult = null;
|
|
725
1171
|
try {
|
|
726
|
-
self.
|
|
727
|
-
debugLog(`cp.spawnSync policy_check START command=${fullCommand}`);
|
|
728
|
-
const result = self.syncClient.request(
|
|
729
|
-
"policy_check",
|
|
730
|
-
{ operation: "exec", target: fullCommand }
|
|
731
|
-
);
|
|
732
|
-
debugLog(`cp.spawnSync policy_check DONE allowed=${result.allowed} command=${fullCommand}`);
|
|
733
|
-
if (!result.allowed) {
|
|
734
|
-
return {
|
|
735
|
-
pid: -1,
|
|
736
|
-
output: [],
|
|
737
|
-
stdout: Buffer.alloc(0),
|
|
738
|
-
stderr: Buffer.from(result.reason || "Policy denied"),
|
|
739
|
-
status: 1,
|
|
740
|
-
signal: null,
|
|
741
|
-
error: new PolicyDeniedError(result.reason || "Policy denied")
|
|
742
|
-
};
|
|
743
|
-
}
|
|
1172
|
+
policyResult = self.syncPolicyCheck(fullCommand);
|
|
744
1173
|
} catch (error) {
|
|
745
|
-
debugLog(`cp.spawnSync
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
}
|
|
757
|
-
} finally {
|
|
758
|
-
self._checking = false;
|
|
1174
|
+
debugLog(`cp.spawnSync DENIED command=${fullCommand}`);
|
|
1175
|
+
return {
|
|
1176
|
+
pid: -1,
|
|
1177
|
+
output: [],
|
|
1178
|
+
stdout: Buffer.alloc(0),
|
|
1179
|
+
stderr: Buffer.from(
|
|
1180
|
+
error instanceof PolicyDeniedError ? error.message || "Policy denied" : error.message
|
|
1181
|
+
),
|
|
1182
|
+
status: 1,
|
|
1183
|
+
signal: null,
|
|
1184
|
+
error
|
|
1185
|
+
};
|
|
759
1186
|
}
|
|
760
|
-
|
|
761
|
-
|
|
1187
|
+
const wrapped = self.wrapWithSeatbelt(
|
|
1188
|
+
command,
|
|
1189
|
+
Array.from(args || []),
|
|
1190
|
+
options,
|
|
1191
|
+
policyResult
|
|
1192
|
+
);
|
|
1193
|
+
debugLog(`cp.spawnSync calling original command=${wrapped.command}`);
|
|
1194
|
+
return original(
|
|
1195
|
+
wrapped.command,
|
|
1196
|
+
wrapped.args,
|
|
1197
|
+
wrapped.options
|
|
1198
|
+
);
|
|
762
1199
|
};
|
|
763
1200
|
}
|
|
764
1201
|
createInterceptedExecFile() {
|
|
765
1202
|
const self = this;
|
|
766
1203
|
const original = this.originalExecFile;
|
|
767
|
-
return function interceptedExecFile(file, ...
|
|
768
|
-
if (self._checking) {
|
|
769
|
-
return original(file, ...
|
|
1204
|
+
return function interceptedExecFile(file, ...rest) {
|
|
1205
|
+
if (self._checking || self._executing) {
|
|
1206
|
+
return original(file, ...rest);
|
|
770
1207
|
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
1208
|
+
let args = [];
|
|
1209
|
+
let options;
|
|
1210
|
+
let callback;
|
|
1211
|
+
for (const arg of rest) {
|
|
1212
|
+
if (typeof arg === "function") {
|
|
1213
|
+
callback = arg;
|
|
1214
|
+
} else if (Array.isArray(arg)) {
|
|
1215
|
+
args = arg;
|
|
1216
|
+
} else if (typeof arg === "object" && arg !== null) {
|
|
1217
|
+
options = arg;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
const fullCommand = args.length > 0 ? `${file} ${args.join(" ")}` : file;
|
|
1221
|
+
debugLog(`cp.execFile ENTER command=${fullCommand}`);
|
|
1222
|
+
self.eventReporter.intercept("exec", fullCommand);
|
|
1223
|
+
let policyResult = null;
|
|
1224
|
+
try {
|
|
1225
|
+
policyResult = self.syncPolicyCheck(fullCommand);
|
|
1226
|
+
} catch (error) {
|
|
1227
|
+
if (callback) {
|
|
1228
|
+
process.nextTick(() => callback(error, "", ""));
|
|
1229
|
+
}
|
|
1230
|
+
return original("false");
|
|
1231
|
+
}
|
|
1232
|
+
const wrapped = self.wrapWithSeatbelt(file, args, options, policyResult);
|
|
1233
|
+
debugLog(`cp.execFile calling original command=${wrapped.command}`);
|
|
1234
|
+
if (callback) {
|
|
1235
|
+
return original(
|
|
1236
|
+
wrapped.command,
|
|
1237
|
+
wrapped.args,
|
|
1238
|
+
wrapped.options,
|
|
1239
|
+
callback
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1242
|
+
return original(
|
|
1243
|
+
wrapped.command,
|
|
1244
|
+
wrapped.args,
|
|
1245
|
+
wrapped.options || {},
|
|
1246
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
1247
|
+
() => {
|
|
1248
|
+
}
|
|
1249
|
+
);
|
|
776
1250
|
};
|
|
777
1251
|
}
|
|
778
1252
|
createInterceptedFork() {
|
|
779
1253
|
const self = this;
|
|
780
1254
|
const original = this.originalFork;
|
|
781
1255
|
const interceptedFork = function(modulePath, args, options) {
|
|
782
|
-
if (self._checking) {
|
|
1256
|
+
if (self._checking || self._executing) {
|
|
783
1257
|
return original(modulePath, args, options);
|
|
784
1258
|
}
|
|
785
1259
|
const pathStr = modulePath.toString();
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
1260
|
+
const fullCommand = `fork:${pathStr}`;
|
|
1261
|
+
debugLog(`cp.fork ENTER command=${fullCommand}`);
|
|
1262
|
+
self.eventReporter.intercept("exec", fullCommand);
|
|
1263
|
+
let policyResult = null;
|
|
1264
|
+
try {
|
|
1265
|
+
policyResult = self.syncPolicyCheck(fullCommand);
|
|
1266
|
+
} catch (error) {
|
|
1267
|
+
debugLog(`cp.fork DENIED command=${fullCommand}`);
|
|
1268
|
+
const denied = self.originalSpawn("false", [], { stdio: "pipe" });
|
|
1269
|
+
process.nextTick(() => {
|
|
1270
|
+
denied.emit("error", error);
|
|
1271
|
+
});
|
|
1272
|
+
return denied;
|
|
1273
|
+
}
|
|
1274
|
+
if (policyResult?.sandbox) {
|
|
1275
|
+
const sandbox = policyResult.sandbox;
|
|
1276
|
+
const env = { ...options?.env || process.env };
|
|
1277
|
+
if (sandbox.envInjection) {
|
|
1278
|
+
Object.assign(env, sandbox.envInjection);
|
|
1279
|
+
}
|
|
1280
|
+
if (sandbox.envDeny) {
|
|
1281
|
+
for (const key of sandbox.envDeny) {
|
|
1282
|
+
delete env[key];
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
options = { ...options, env };
|
|
1286
|
+
}
|
|
1287
|
+
debugLog(`cp.fork calling original module=${pathStr}`);
|
|
790
1288
|
return original(modulePath, args, options);
|
|
791
1289
|
};
|
|
792
1290
|
return interceptedFork;
|
|
@@ -880,19 +1378,19 @@ var FsInterceptor = class extends BaseInterceptor {
|
|
|
880
1378
|
const key = `fs:${methodName}`;
|
|
881
1379
|
this.originals.set(key, original);
|
|
882
1380
|
const self = this;
|
|
883
|
-
safeOverride(module2, methodName, function intercepted(
|
|
884
|
-
const pathString = normalizePathArg(
|
|
1381
|
+
safeOverride(module2, methodName, function intercepted(path2, ...args) {
|
|
1382
|
+
const pathString = normalizePathArg(path2);
|
|
885
1383
|
const callback = typeof args[args.length - 1] === "function" ? args.pop() : void 0;
|
|
886
1384
|
debugLog(`fs.${methodName} ENTER (async) path=${pathString} _checking=${self._checking}`);
|
|
887
1385
|
if (self._checking) {
|
|
888
1386
|
debugLog(`fs.${methodName} SKIP (re-entrancy, async) path=${pathString}`);
|
|
889
|
-
original.call(module2,
|
|
1387
|
+
original.call(module2, path2, ...args, callback);
|
|
890
1388
|
return;
|
|
891
1389
|
}
|
|
892
1390
|
self.eventReporter.intercept(operation, pathString);
|
|
893
1391
|
self.checkPolicy(operation, pathString).then(() => {
|
|
894
1392
|
debugLog(`fs.${methodName} policy OK (async) path=${pathString}`);
|
|
895
|
-
original.call(module2,
|
|
1393
|
+
original.call(module2, path2, ...args, callback);
|
|
896
1394
|
}).catch((error) => {
|
|
897
1395
|
debugLog(`fs.${methodName} policy ERROR (async): ${error.message} path=${pathString}`);
|
|
898
1396
|
if (callback) {
|
|
@@ -907,12 +1405,12 @@ var FsInterceptor = class extends BaseInterceptor {
|
|
|
907
1405
|
const key = `fs:${methodName}`;
|
|
908
1406
|
this.originals.set(key, original);
|
|
909
1407
|
const self = this;
|
|
910
|
-
safeOverride(module2, methodName, function interceptedSync(
|
|
911
|
-
const pathString = normalizePathArg(
|
|
1408
|
+
safeOverride(module2, methodName, function interceptedSync(path2, ...args) {
|
|
1409
|
+
const pathString = normalizePathArg(path2);
|
|
912
1410
|
debugLog(`fs.${methodName} ENTER path=${pathString} _checking=${self._checking}`);
|
|
913
1411
|
if (self._checking) {
|
|
914
1412
|
debugLog(`fs.${methodName} SKIP (re-entrancy) path=${pathString}`);
|
|
915
|
-
return original.call(module2,
|
|
1413
|
+
return original.call(module2, path2, ...args);
|
|
916
1414
|
}
|
|
917
1415
|
self._checking = true;
|
|
918
1416
|
try {
|
|
@@ -941,7 +1439,7 @@ var FsInterceptor = class extends BaseInterceptor {
|
|
|
941
1439
|
self._checking = false;
|
|
942
1440
|
}
|
|
943
1441
|
debugLog(`fs.${methodName} calling original path=${pathString}`);
|
|
944
|
-
return original.call(module2,
|
|
1442
|
+
return original.call(module2, path2, ...args);
|
|
945
1443
|
});
|
|
946
1444
|
}
|
|
947
1445
|
interceptPromiseMethod(module2, methodName, operation) {
|
|
@@ -950,17 +1448,17 @@ var FsInterceptor = class extends BaseInterceptor {
|
|
|
950
1448
|
const key = `fsPromises:${methodName}`;
|
|
951
1449
|
this.originals.set(key, original);
|
|
952
1450
|
const self = this;
|
|
953
|
-
safeOverride(module2, methodName, async function interceptedPromise(
|
|
954
|
-
const pathString = normalizePathArg(
|
|
1451
|
+
safeOverride(module2, methodName, async function interceptedPromise(path2, ...args) {
|
|
1452
|
+
const pathString = normalizePathArg(path2);
|
|
955
1453
|
debugLog(`fsPromises.${methodName} ENTER path=${pathString} _checking=${self._checking}`);
|
|
956
1454
|
if (self._checking) {
|
|
957
1455
|
debugLog(`fsPromises.${methodName} SKIP (re-entrancy) path=${pathString}`);
|
|
958
|
-
return original.call(module2,
|
|
1456
|
+
return original.call(module2, path2, ...args);
|
|
959
1457
|
}
|
|
960
1458
|
self.eventReporter.intercept(operation, pathString);
|
|
961
1459
|
await self.checkPolicy(operation, pathString);
|
|
962
1460
|
debugLog(`fsPromises.${methodName} policy OK path=${pathString}`);
|
|
963
|
-
return original.call(module2,
|
|
1461
|
+
return original.call(module2, path2, ...args);
|
|
964
1462
|
});
|
|
965
1463
|
}
|
|
966
1464
|
};
|
|
@@ -1104,11 +1602,11 @@ var PolicyEvaluator = class {
|
|
|
1104
1602
|
* Check if an operation is allowed
|
|
1105
1603
|
* Always queries the daemon for fresh policy decisions
|
|
1106
1604
|
*/
|
|
1107
|
-
async check(operation, target) {
|
|
1605
|
+
async check(operation, target, context) {
|
|
1108
1606
|
try {
|
|
1109
1607
|
const result = await this.client.request(
|
|
1110
1608
|
"policy_check",
|
|
1111
|
-
{ operation, target }
|
|
1609
|
+
{ operation, target, context }
|
|
1112
1610
|
);
|
|
1113
1611
|
return result;
|
|
1114
1612
|
} catch (error) {
|
|
@@ -1152,7 +1650,11 @@ var EventReporter = class _EventReporter {
|
|
|
1152
1650
|
const level = this.getLogLevel(event);
|
|
1153
1651
|
if (this.shouldLog(level)) {
|
|
1154
1652
|
const prefix = event.type === "allow" ? "\u2713" : event.type === "deny" ? "\u2717" : "\u2022";
|
|
1155
|
-
|
|
1653
|
+
let detail = `${prefix} ${event.operation}: ${event.target}`;
|
|
1654
|
+
if (event.policyId) detail += ` [policy:${event.policyId}]`;
|
|
1655
|
+
if (event.error) detail += ` [reason:${event.error}]`;
|
|
1656
|
+
if (event.duration) detail += ` [${event.duration}ms]`;
|
|
1657
|
+
console[level](`[AgenShield] ${detail}`);
|
|
1156
1658
|
}
|
|
1157
1659
|
if (this.queue.length >= 100) {
|
|
1158
1660
|
this.flush();
|
|
@@ -1272,6 +1774,13 @@ function installInterceptors(configOverrides) {
|
|
|
1272
1774
|
return;
|
|
1273
1775
|
}
|
|
1274
1776
|
const config = createConfig(configOverrides);
|
|
1777
|
+
if (config.logLevel === "debug") {
|
|
1778
|
+
try {
|
|
1779
|
+
const safeConfig = { ...config };
|
|
1780
|
+
console.error("[AgenShield:config]", JSON.stringify(safeConfig, null, 2));
|
|
1781
|
+
} catch {
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1275
1784
|
client = new AsyncClient({
|
|
1276
1785
|
socketPath: config.socketPath,
|
|
1277
1786
|
httpHost: config.httpHost,
|
|
@@ -1292,7 +1801,8 @@ function installInterceptors(configOverrides) {
|
|
|
1292
1801
|
policyEvaluator,
|
|
1293
1802
|
eventReporter,
|
|
1294
1803
|
failOpen: config.failOpen,
|
|
1295
|
-
brokerHttpPort: config.httpPort
|
|
1804
|
+
brokerHttpPort: config.httpPort,
|
|
1805
|
+
config
|
|
1296
1806
|
});
|
|
1297
1807
|
installed.fetch.install();
|
|
1298
1808
|
log(config, "debug", "Installed fetch interceptor");
|
|
@@ -1325,7 +1835,8 @@ function installInterceptors(configOverrides) {
|
|
|
1325
1835
|
policyEvaluator,
|
|
1326
1836
|
eventReporter,
|
|
1327
1837
|
failOpen: config.failOpen,
|
|
1328
|
-
brokerHttpPort: config.httpPort
|
|
1838
|
+
brokerHttpPort: config.httpPort,
|
|
1839
|
+
config
|
|
1329
1840
|
});
|
|
1330
1841
|
installed.childProcess.install();
|
|
1331
1842
|
log(config, "debug", "Installed child_process interceptor");
|