@agenshield/interceptor
Advanced tools
| /** | ||
| * Seatbelt Profile Manager | ||
| * | ||
| * Generates macOS seatbelt (sandbox-exec) SBPL profiles from SandboxConfig, | ||
| * writes them to disk with content-hash naming for caching, and manages cleanup. | ||
| * | ||
| * Uses captured-original fs functions to avoid interception loops. | ||
| */ | ||
| import type { SandboxConfig } from '@agenshield/ipc'; | ||
| export declare class ProfileManager { | ||
| private profileDir; | ||
| private ensuredDir; | ||
| constructor(profileDir: string); | ||
| /** | ||
| * Get or create a profile file on disk. Returns the absolute path. | ||
| * Uses content-hash naming so identical configs reuse the same file. | ||
| */ | ||
| getOrCreateProfile(content: string): string; | ||
| /** | ||
| * Generate an SBPL profile from a SandboxConfig. | ||
| */ | ||
| generateProfile(sandbox: SandboxConfig): string; | ||
| /** | ||
| * Remove stale profile files older than maxAgeMs. | ||
| */ | ||
| cleanup(maxAgeMs: number): void; | ||
| /** | ||
| * Escape a string for safe inclusion in SBPL | ||
| */ | ||
| private escapeSbpl; | ||
| /** | ||
| * Ensure the profile directory exists | ||
| */ | ||
| private ensureDir; | ||
| } | ||
| //# sourceMappingURL=profile-manager.d.ts.map |
| {"version":3,"file":"profile-manager.d.ts","sourceRoot":"","sources":["../../src/seatbelt/profile-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAarD,qBAAa,cAAc;IACzB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,UAAU,CAAS;gBAEf,UAAU,EAAE,MAAM;IAI9B;;;OAGG;IACH,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM;IAc3C;;OAEG;IACH,eAAe,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM;IAgL/C;;OAEG;IACH,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAwB/B;;OAEG;IACH,OAAO,CAAC,UAAU;IAIlB;;OAEG;IACH,OAAO,CAAC,SAAS;CAgBlB"} |
@@ -18,4 +18,10 @@ /** | ||
| private timeout; | ||
| private socketFailCount; | ||
| private socketSkipUntil; | ||
| constructor(options: SyncClientOptions); | ||
| /** | ||
| * Remove stale /tmp/agenshield-sync-*.json files from previous runs | ||
| */ | ||
| private cleanupStaleTmpFiles; | ||
| /** | ||
| * Send a synchronous request to the broker | ||
@@ -22,0 +28,0 @@ */ |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"sync-client.d.ts","sourceRoot":"","sources":["../../src/client/sync-client.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAiBH,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,UAAU;IACrB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,OAAO,CAAS;gBAEZ,OAAO,EAAE,iBAAiB;IAOtC;;OAEG;IACH,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC;IAgB9D;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IAsFzB;;OAEG;IACH,OAAO,CAAC,eAAe;IAwCvB;;OAEG;IACH,IAAI,IAAI,OAAO;CAQhB"} | ||
| {"version":3,"file":"sync-client.d.ts","sourceRoot":"","sources":["../../src/client/sync-client.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAmBH,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,UAAU;IACrB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,eAAe,CAAK;gBAEhB,OAAO,EAAE,iBAAiB;IAQtC;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAiB5B;;OAEG;IACH,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC;IAiC9D;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IAkGzB;;OAEG;IACH,OAAO,CAAC,eAAe;IAwCvB;;OAEG;IACH,IAAI,IAAI,OAAO;CAQhB"} |
+10
-0
@@ -29,2 +29,12 @@ /** | ||
| timeout: number; | ||
| /** Execution context type: 'agent' or 'skill' */ | ||
| contextType: 'agent' | 'skill'; | ||
| /** Skill slug (when contextType is 'skill') */ | ||
| contextSkillSlug?: string; | ||
| /** Agent identifier */ | ||
| contextAgentId?: string; | ||
| /** Enable macOS seatbelt wrapping for exec operations */ | ||
| enableSeatbelt: boolean; | ||
| /** Directory for generated seatbelt profiles */ | ||
| seatbeltProfileDir: string; | ||
| } | ||
@@ -31,0 +41,0 @@ /** |
+1
-1
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,iBAAiB;IAChC,gDAAgD;IAChD,UAAU,EAAE,MAAM,CAAC;IAEnB,yBAAyB;IACzB,QAAQ,EAAE,MAAM,CAAC;IAEjB,yBAAyB;IACzB,QAAQ,EAAE,MAAM,CAAC;IAEjB,oDAAoD;IACpD,QAAQ,EAAE,OAAO,CAAC;IAElB,gBAAgB;IAChB,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;IAE9C,gCAAgC;IAChC,cAAc,EAAE,OAAO,CAAC;IAExB,4CAA4C;IAC5C,aAAa,EAAE,OAAO,CAAC;IAEvB,oCAAoC;IACpC,WAAW,EAAE,OAAO,CAAC;IAErB,oCAAoC;IACpC,WAAW,EAAE,OAAO,CAAC;IAErB,wCAAwC;IACxC,aAAa,EAAE,OAAO,CAAC;IAEvB,sCAAsC;IACtC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAAG,iBAAiB,CAiBtF;AAED;;GAEG;AACH,eAAO,MAAM,aAAa,EAAE,iBAAkC,CAAC"} | ||
| {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,iBAAiB;IAChC,gDAAgD;IAChD,UAAU,EAAE,MAAM,CAAC;IAEnB,yBAAyB;IACzB,QAAQ,EAAE,MAAM,CAAC;IAEjB,yBAAyB;IACzB,QAAQ,EAAE,MAAM,CAAC;IAEjB,oDAAoD;IACpD,QAAQ,EAAE,OAAO,CAAC;IAElB,gBAAgB;IAChB,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;IAE9C,gCAAgC;IAChC,cAAc,EAAE,OAAO,CAAC;IAExB,4CAA4C;IAC5C,aAAa,EAAE,OAAO,CAAC;IAEvB,oCAAoC;IACpC,WAAW,EAAE,OAAO,CAAC;IAErB,oCAAoC;IACpC,WAAW,EAAE,OAAO,CAAC;IAErB,wCAAwC;IACxC,aAAa,EAAE,OAAO,CAAC;IAEvB,sCAAsC;IACtC,OAAO,EAAE,MAAM,CAAC;IAEhB,iDAAiD;IACjD,WAAW,EAAE,OAAO,GAAG,OAAO,CAAC;IAE/B,+CAA+C;IAC/C,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B,uBAAuB;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB,yDAAyD;IACzD,cAAc,EAAE,OAAO,CAAC;IAExB,gDAAgD;IAChD,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAAG,iBAAiB,CAsBtF;AAED;;GAEG;AACH,eAAO,MAAM,aAAa,EAAE,iBAAkC,CAAC"} |
+1
-0
@@ -6,4 +6,5 @@ /** | ||
| * a captured (pre-patch) appendFileSync. Safe from interception. | ||
| * Falls back to /tmp/agenshield-interceptor.log if primary path is not writable. | ||
| */ | ||
| export declare function debugLog(msg: string): void; | ||
| //# sourceMappingURL=debug-log.d.ts.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"debug-log.d.ts","sourceRoot":"","sources":["../src/debug-log.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AASH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAM1C"} | ||
| {"version":3,"file":"debug-log.d.ts","sourceRoot":"","sources":["../src/debug-log.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAwBH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAO1C"} |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"reporter.d.ts","sourceRoot":"","sources":["../../src/events/reporter.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAE5D,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;CAC/C;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,WAAW,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC;IAC/C,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,IAAI,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,QAAQ,CAAmC;IACnD,OAAO,CAAC,KAAK,CAA0B;IACvC,OAAO,CAAC,aAAa,CAA+B;IACpD,OAAO,CAAC,gBAAgB,CAAK;IAE7B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAO;IAC7C,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAK;IAExC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAK5B;gBAEU,OAAO,EAAE,oBAAoB;IASzC;;OAEG;IACH,MAAM,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI;IAqBrC;;OAEG;IACH,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IASlD;;OAEG;IACH,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI;IAWpF;;OAEG;IACH,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;IAWjF;;OAEG;IACH,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAU7D;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAsB5B;;OAEG;IACH,IAAI,IAAI,IAAI;IAUZ;;OAEG;IACH,OAAO,CAAC,WAAW;IAenB;;OAEG;IACH,OAAO,CAAC,SAAS;CAGlB"} | ||
| {"version":3,"file":"reporter.d.ts","sourceRoot":"","sources":["../../src/events/reporter.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAE5D,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;CAC/C;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,WAAW,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC;IAC/C,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,IAAI,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,QAAQ,CAAmC;IACnD,OAAO,CAAC,KAAK,CAA0B;IACvC,OAAO,CAAC,aAAa,CAA+B;IACpD,OAAO,CAAC,gBAAgB,CAAK;IAE7B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAO;IAC7C,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAK;IAExC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAK5B;gBAEU,OAAO,EAAE,oBAAoB;IASzC;;OAEG;IACH,MAAM,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI;IAyBrC;;OAEG;IACH,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IASlD;;OAEG;IACH,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI;IAWpF;;OAEG;IACH,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;IAWjF;;OAEG;IACH,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAU7D;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAsB5B;;OAEG;IACH,IAAI,IAAI,IAAI;IAUZ;;OAEG;IACH,OAAO,CAAC,WAAW;IAenB;;OAEG;IACH,OAAO,CAAC,SAAS;CAGlB"} |
+677
-166
@@ -64,3 +64,8 @@ "use strict"; | ||
| interceptExec: env["AGENSHIELD_INTERCEPT_EXEC"] !== "false", | ||
| timeout: parseInt(env["AGENSHIELD_TIMEOUT"] || "30000", 10), | ||
| timeout: parseInt(env["AGENSHIELD_TIMEOUT"] || "5000", 10), | ||
| contextType: env["AGENSHIELD_CONTEXT_TYPE"] || "agent", | ||
| contextSkillSlug: env["AGENSHIELD_SKILL_SLUG"], | ||
| contextAgentId: env["AGENSHIELD_AGENT_ID"], | ||
| enableSeatbelt: env["AGENSHIELD_SEATBELT"] !== "false" && process.platform === "darwin", | ||
| seatbeltProfileDir: env["AGENSHIELD_SEATBELT_DIR"] || "/tmp/agenshield-profiles", | ||
| ...overrides | ||
@@ -109,9 +114,30 @@ }; | ||
| var _appendFileSync = fs.appendFileSync.bind(fs); | ||
| var _writeSync = fs.writeSync.bind(fs); | ||
| var LOG_PATH = "/var/log/agenshield/interceptor.log"; | ||
| var FALLBACK_LOG_PATH = "/tmp/agenshield-interceptor.log"; | ||
| var resolvedLogPath = null; | ||
| function getLogPath() { | ||
| if (resolvedLogPath !== null) return resolvedLogPath; | ||
| try { | ||
| _appendFileSync(LOG_PATH, ""); | ||
| resolvedLogPath = LOG_PATH; | ||
| } catch { | ||
| resolvedLogPath = FALLBACK_LOG_PATH; | ||
| } | ||
| return resolvedLogPath; | ||
| } | ||
| function debugLog(msg) { | ||
| const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [pid:${process.pid}] ${msg} | ||
| `; | ||
| try { | ||
| _appendFileSync(LOG_PATH, `[${(/* @__PURE__ */ new Date()).toISOString()}] [pid:${process.pid}] ${msg} | ||
| `); | ||
| _appendFileSync(getLogPath(), line); | ||
| } catch { | ||
| } | ||
| if (process.env["AGENSHIELD_LOG_LEVEL"] === "debug") { | ||
| try { | ||
| _writeSync(2, `[AgenShield:debug] ${msg} | ||
| `); | ||
| } catch { | ||
| } | ||
| } | ||
| } | ||
@@ -126,2 +152,3 @@ | ||
| installed = false; | ||
| interceptorConfig; | ||
| brokerHttpPort; | ||
@@ -134,2 +161,3 @@ constructor(options) { | ||
| this.brokerHttpPort = options.brokerHttpPort ?? 5201; | ||
| this.interceptorConfig = options.config; | ||
| } | ||
@@ -160,5 +188,18 @@ /** | ||
| /** | ||
| * Build execution context from config | ||
| */ | ||
| getBasePolicyExecutionContext() { | ||
| const config = this.interceptorConfig; | ||
| if (!config) return void 0; | ||
| return { | ||
| callerType: config.contextType || "agent", | ||
| skillSlug: config.contextSkillSlug, | ||
| agentId: config.contextAgentId, | ||
| depth: 0 | ||
| }; | ||
| } | ||
| /** | ||
| * Check policy and handle the result | ||
| */ | ||
| async checkPolicy(operation, target) { | ||
| async checkPolicy(operation, target, context) { | ||
| const startTime = Date.now(); | ||
@@ -168,3 +209,3 @@ debugLog(`base.checkPolicy START op=${operation} target=${target}`); | ||
| this.eventReporter.intercept(operation, target); | ||
| const result = await this.policyEvaluator.check(operation, target); | ||
| const result = await this.policyEvaluator.check(operation, target, context); | ||
| debugLog(`base.checkPolicy evaluator result op=${operation} target=${target} allowed=${result.allowed} policyId=${result.policyId}`); | ||
@@ -216,2 +257,15 @@ if (!result.allowed) { | ||
| } | ||
| /** | ||
| * Build execution context from config | ||
| */ | ||
| getPolicyExecutionContext() { | ||
| const config = this.interceptorConfig; | ||
| if (!config) return void 0; | ||
| return { | ||
| callerType: config.contextType || "agent", | ||
| skillSlug: config.contextSkillSlug, | ||
| agentId: config.contextAgentId, | ||
| depth: 0 | ||
| }; | ||
| } | ||
| install() { | ||
@@ -247,44 +301,6 @@ if (this.installed) return; | ||
| debugLog(`fetch checkPolicy START url=${url}`); | ||
| await this.checkPolicy("http_request", url); | ||
| debugLog(`fetch checkPolicy DONE url=${url}`); | ||
| try { | ||
| const method = init?.method || "GET"; | ||
| const headers = {}; | ||
| if (init?.headers) { | ||
| if (init.headers instanceof Headers) { | ||
| init.headers.forEach((value, key) => { | ||
| headers[key] = value; | ||
| }); | ||
| } else if (Array.isArray(init.headers)) { | ||
| for (const [key, value] of init.headers) { | ||
| headers[key] = value; | ||
| } | ||
| } else { | ||
| Object.assign(headers, init.headers); | ||
| } | ||
| } | ||
| let body; | ||
| if (init?.body) { | ||
| if (typeof init.body === "string") { | ||
| body = init.body; | ||
| } else if (init.body instanceof ArrayBuffer) { | ||
| body = Buffer.from(init.body).toString("base64"); | ||
| } else { | ||
| body = String(init.body); | ||
| } | ||
| } | ||
| const result = await this.client.request("http_request", { | ||
| url, | ||
| method, | ||
| headers, | ||
| body | ||
| }); | ||
| const responseHeaders = new Headers(result.headers); | ||
| return new Response(result.body, { | ||
| status: result.status, | ||
| statusText: result.statusText, | ||
| headers: responseHeaders | ||
| }); | ||
| await this.checkPolicy("http_request", url, this.getPolicyExecutionContext()); | ||
| debugLog(`fetch checkPolicy DONE url=${url}`); | ||
| } catch (error) { | ||
| debugLog(`fetch ERROR url=${url} error=${error.message}`); | ||
| if (error.name === "PolicyDeniedError") { | ||
@@ -299,2 +315,3 @@ throw error; | ||
| } | ||
| return this.originalFetch(input, init); | ||
| } | ||
@@ -444,2 +461,4 @@ }; | ||
| var _unlinkSync = fs2.unlinkSync.bind(fs2); | ||
| var _readdirSync = fs2.readdirSync.bind(fs2); | ||
| var _statSync = fs2.statSync.bind(fs2); | ||
| var _spawnSync = import_node_child_process.spawnSync; | ||
@@ -452,2 +471,4 @@ var _execSync = import_node_child_process.execSync; | ||
| timeout; | ||
| socketFailCount = 0; | ||
| socketSkipUntil = 0; | ||
| constructor(options) { | ||
@@ -458,4 +479,26 @@ this.socketPath = options.socketPath; | ||
| this.timeout = options.timeout; | ||
| this.cleanupStaleTmpFiles(); | ||
| } | ||
| /** | ||
| * Remove stale /tmp/agenshield-sync-*.json files from previous runs | ||
| */ | ||
| cleanupStaleTmpFiles() { | ||
| try { | ||
| const tmpDir = "/tmp"; | ||
| const files = _readdirSync(tmpDir); | ||
| const cutoff = Date.now() - 5 * 60 * 1e3; | ||
| for (const f of files) { | ||
| if (f.startsWith("agenshield-sync-") && f.endsWith(".json")) { | ||
| const fp = `${tmpDir}/${f}`; | ||
| try { | ||
| const stat = _statSync(fp); | ||
| if (stat.mtimeMs < cutoff) _unlinkSync(fp); | ||
| } catch { | ||
| } | ||
| } | ||
| } | ||
| } catch { | ||
| } | ||
| } | ||
| /** | ||
| * Send a synchronous request to the broker | ||
@@ -465,7 +508,20 @@ */ | ||
| debugLog(`syncClient.request START method=${method}`); | ||
| const now = Date.now(); | ||
| if (now < this.socketSkipUntil) { | ||
| debugLog(`syncClient.request SKIP socket (circuit open for ${this.socketSkipUntil - now}ms), using HTTP`); | ||
| const result = this.httpRequestSync(method, params); | ||
| debugLog(`syncClient.request http OK method=${method}`); | ||
| return result; | ||
| } | ||
| try { | ||
| const result = this.socketRequestSync(method, params); | ||
| this.socketFailCount = 0; | ||
| debugLog(`syncClient.request socket OK method=${method}`); | ||
| return result; | ||
| } catch (socketErr) { | ||
| this.socketFailCount++; | ||
| if (this.socketFailCount >= 2) { | ||
| this.socketSkipUntil = Date.now() + 6e4; | ||
| debugLog(`syncClient.request socket circuit OPEN (${this.socketFailCount} failures)`); | ||
| } | ||
| debugLog(`syncClient.request socket FAILED: ${socketErr.message}, trying HTTP`); | ||
@@ -495,5 +551,14 @@ const result = this.httpRequestSync(method, params); | ||
| let done = false; | ||
| const socket = net.createConnection('${this.socketPath}'); | ||
| let data = ''; | ||
| const timer = setTimeout(() => { | ||
| if (done) return; | ||
| done = true; | ||
| socket.destroy(); | ||
| fs.writeFileSync('${tmpFile}', JSON.stringify({ error: 'timeout' })); | ||
| process.exit(1); | ||
| }, ${this.timeout}); | ||
| socket.on('connect', () => { | ||
@@ -505,5 +570,8 @@ socket.write(${JSON.stringify(request)}); | ||
| data += chunk.toString(); | ||
| if (data.includes('\\n')) { | ||
| if (data.includes('\\n') && !done) { | ||
| done = true; | ||
| clearTimeout(timer); | ||
| socket.end(); | ||
| fs.writeFileSync('${tmpFile}', data.split('\\n')[0]); | ||
| process.exit(0); | ||
| } | ||
@@ -513,9 +581,8 @@ }); | ||
| socket.on('error', (err) => { | ||
| if (done) return; | ||
| done = true; | ||
| clearTimeout(timer); | ||
| fs.writeFileSync('${tmpFile}', JSON.stringify({ error: err.message })); | ||
| process.exit(1); | ||
| }); | ||
| setTimeout(() => { | ||
| socket.destroy(); | ||
| fs.writeFileSync('${tmpFile}', JSON.stringify({ error: 'timeout' })); | ||
| }, ${this.timeout}); | ||
| `; | ||
@@ -597,2 +664,215 @@ try { | ||
| // libs/shield-interceptor/src/seatbelt/profile-manager.ts | ||
| var fs3 = __toESM(require("node:fs"), 1); | ||
| var crypto = __toESM(require("node:crypto"), 1); | ||
| var path = __toESM(require("node:path"), 1); | ||
| var _mkdirSync = fs3.mkdirSync.bind(fs3); | ||
| var _writeFileSync = fs3.writeFileSync.bind(fs3); | ||
| var _existsSync2 = fs3.existsSync.bind(fs3); | ||
| var _readFileSync2 = fs3.readFileSync.bind(fs3); | ||
| var _readdirSync2 = fs3.readdirSync.bind(fs3); | ||
| var _statSync2 = fs3.statSync.bind(fs3); | ||
| var _unlinkSync2 = fs3.unlinkSync.bind(fs3); | ||
| var _chmodSync = fs3.chmodSync.bind(fs3); | ||
| var ProfileManager = class { | ||
| profileDir; | ||
| ensuredDir = false; | ||
| constructor(profileDir) { | ||
| this.profileDir = profileDir; | ||
| } | ||
| /** | ||
| * Get or create a profile file on disk. Returns the absolute path. | ||
| * Uses content-hash naming so identical configs reuse the same file. | ||
| */ | ||
| getOrCreateProfile(content) { | ||
| this.ensureDir(); | ||
| const hash = crypto.createHash("sha256").update(content).digest("hex").slice(0, 16); | ||
| const profilePath = path.join(this.profileDir, `sb-${hash}.sb`); | ||
| if (!_existsSync2(profilePath)) { | ||
| debugLog(`profile-manager: writing new profile ${profilePath} (${content.length} bytes)`); | ||
| _writeFileSync(profilePath, content, { mode: 420 }); | ||
| } | ||
| return profilePath; | ||
| } | ||
| /** | ||
| * Generate an SBPL profile from a SandboxConfig. | ||
| */ | ||
| generateProfile(sandbox) { | ||
| if (sandbox.profileContent) { | ||
| return sandbox.profileContent; | ||
| } | ||
| const lines = [ | ||
| ";; AgenShield dynamic seatbelt profile", | ||
| `;; Generated: ${(/* @__PURE__ */ new Date()).toISOString()}`, | ||
| "(version 1)", | ||
| "(deny default)", | ||
| "" | ||
| ]; | ||
| lines.push( | ||
| ";; Filesystem: reads allowed, writes restricted", | ||
| "(allow file-read*)", | ||
| "" | ||
| ); | ||
| const writePaths = ["/tmp", "/private/tmp", "/var/folders"]; | ||
| if (sandbox.allowedWritePaths.length > 0) { | ||
| writePaths.push(...sandbox.allowedWritePaths); | ||
| } | ||
| lines.push("(allow file-write*"); | ||
| for (const p of writePaths) { | ||
| lines.push(` (subpath "${this.escapeSbpl(p)}")`); | ||
| } | ||
| lines.push(")"); | ||
| lines.push(""); | ||
| lines.push("(allow file-write*"); | ||
| lines.push(' (literal "/dev/null")'); | ||
| lines.push(' (literal "/dev/zero")'); | ||
| lines.push(' (literal "/dev/random")'); | ||
| lines.push(' (literal "/dev/urandom")'); | ||
| lines.push(")"); | ||
| lines.push(""); | ||
| if (sandbox.deniedPaths.length > 0) { | ||
| lines.push(";; Denied paths"); | ||
| for (const p of sandbox.deniedPaths) { | ||
| lines.push(`(deny file-read* file-write* (subpath "${this.escapeSbpl(p)}"))`); | ||
| } | ||
| lines.push(""); | ||
| } | ||
| lines.push(";; Binary execution (system directories allowed as subpaths)"); | ||
| lines.push("(allow process-exec"); | ||
| lines.push(' (subpath "/bin")'); | ||
| lines.push(' (subpath "/sbin")'); | ||
| lines.push(' (subpath "/usr/bin")'); | ||
| lines.push(' (subpath "/usr/sbin")'); | ||
| lines.push(' (subpath "/usr/local/bin")'); | ||
| lines.push(' (subpath "/opt/agenshield/bin")'); | ||
| const coveredSubpaths = ["/bin/", "/sbin/", "/usr/bin/", "/usr/sbin/", "/usr/local/bin/", "/opt/agenshield/bin/"]; | ||
| const home = process.env["HOME"]; | ||
| if (home) { | ||
| lines.push(` (subpath "${this.escapeSbpl(home)}/bin")`); | ||
| lines.push(` (subpath "${this.escapeSbpl(home)}/homebrew")`); | ||
| coveredSubpaths.push(`${home}/bin/`, `${home}/homebrew/`); | ||
| } | ||
| const nvmDir = process.env["NVM_DIR"] || (home ? `${home}/.nvm` : null); | ||
| if (nvmDir) { | ||
| lines.push(` (subpath "${this.escapeSbpl(nvmDir)}")`); | ||
| coveredSubpaths.push(`${nvmDir}/`); | ||
| } | ||
| const brewPrefix = process.env["HOMEBREW_PREFIX"]; | ||
| if (brewPrefix && (!home || !brewPrefix.startsWith(home))) { | ||
| lines.push(` (subpath "${this.escapeSbpl(brewPrefix)}/bin")`); | ||
| lines.push(` (subpath "${this.escapeSbpl(brewPrefix)}/lib")`); | ||
| coveredSubpaths.push(`${brewPrefix}/bin/`, `${brewPrefix}/lib/`); | ||
| } | ||
| const uniqueBinaries = [...new Set(sandbox.allowedBinaries)]; | ||
| for (const bin of uniqueBinaries) { | ||
| if (coveredSubpaths.some((dir) => bin === dir || bin.startsWith(dir))) continue; | ||
| if (bin.endsWith("/")) { | ||
| lines.push(` (subpath "${this.escapeSbpl(bin)}")`); | ||
| } else { | ||
| lines.push(` (literal "${this.escapeSbpl(bin)}")`); | ||
| } | ||
| } | ||
| lines.push(")"); | ||
| lines.push(""); | ||
| const uniqueDenied = [...new Set(sandbox.deniedBinaries)]; | ||
| if (uniqueDenied.length > 0) { | ||
| lines.push(";; Denied binaries"); | ||
| for (const bin of uniqueDenied) { | ||
| lines.push(`(deny process-exec (literal "${this.escapeSbpl(bin)}"))`); | ||
| } | ||
| lines.push(""); | ||
| } | ||
| lines.push(";; Network"); | ||
| if (sandbox.networkAllowed) { | ||
| if (sandbox.allowedHosts.length > 0 || sandbox.allowedPorts.length > 0) { | ||
| lines.push(";; Allow specific network targets"); | ||
| for (const host of sandbox.allowedHosts) { | ||
| lines.push(`(allow network-outbound (remote tcp "${this.escapeSbpl(host)}:*"))`); | ||
| } | ||
| for (const port of sandbox.allowedPorts) { | ||
| lines.push(`(allow network-outbound (remote tcp "*:${port}"))`); | ||
| } | ||
| const isLocalhostOnly = sandbox.allowedHosts.length > 0 && sandbox.allowedHosts.every((h) => h === "localhost" || h === "127.0.0.1"); | ||
| if (!isLocalhostOnly) { | ||
| lines.push('(allow network-outbound (remote udp "*:53") (remote tcp "*:53"))'); | ||
| } | ||
| } else { | ||
| lines.push("(allow network*)"); | ||
| } | ||
| } else { | ||
| lines.push("(deny network*)"); | ||
| } | ||
| lines.push(""); | ||
| lines.push( | ||
| ";; Broker / local unix sockets", | ||
| "(allow network-outbound (remote unix))", | ||
| "(allow network-inbound (local unix))", | ||
| "(allow file-read* file-write*", | ||
| ' (subpath "/var/run/agenshield")', | ||
| ' (subpath "/private/var/run/agenshield"))', | ||
| "" | ||
| ); | ||
| lines.push( | ||
| ";; Process management", | ||
| "(allow process-fork)", | ||
| "(allow signal (target self))", | ||
| "(allow sysctl-read)", | ||
| "" | ||
| ); | ||
| lines.push( | ||
| ";; Mach IPC", | ||
| "(allow mach-lookup)", | ||
| "" | ||
| ); | ||
| return lines.join("\n"); | ||
| } | ||
| /** | ||
| * Remove stale profile files older than maxAgeMs. | ||
| */ | ||
| cleanup(maxAgeMs) { | ||
| if (!_existsSync2(this.profileDir)) return; | ||
| try { | ||
| const now = Date.now(); | ||
| const entries = _readdirSync2(this.profileDir); | ||
| for (const entry of entries) { | ||
| if (!entry.endsWith(".sb")) continue; | ||
| const filePath = path.join(this.profileDir, entry); | ||
| try { | ||
| const stat = _statSync2(filePath); | ||
| if (now - stat.mtimeMs > maxAgeMs) { | ||
| _unlinkSync2(filePath); | ||
| debugLog(`profile-manager: cleaned up stale profile ${filePath}`); | ||
| } | ||
| } catch { | ||
| } | ||
| } | ||
| } catch { | ||
| } | ||
| } | ||
| /** | ||
| * Escape a string for safe inclusion in SBPL | ||
| */ | ||
| escapeSbpl(s) { | ||
| return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); | ||
| } | ||
| /** | ||
| * Ensure the profile directory exists | ||
| */ | ||
| ensureDir() { | ||
| if (this.ensuredDir) return; | ||
| if (!_existsSync2(this.profileDir)) { | ||
| _mkdirSync(this.profileDir, { recursive: true, mode: 1023 }); | ||
| } else { | ||
| try { | ||
| const stat = _statSync2(this.profileDir); | ||
| if ((stat.mode & 511) !== 511) { | ||
| _chmodSync(this.profileDir, 1023); | ||
| } | ||
| } catch { | ||
| } | ||
| } | ||
| this.ensuredDir = true; | ||
| } | ||
| }; | ||
| // libs/shield-interceptor/src/interceptors/child-process.ts | ||
@@ -603,2 +883,5 @@ var childProcessModule = require("node:child_process"); | ||
| _checking = false; | ||
| _executing = false; | ||
| // Guards exec→execFile re-entrancy | ||
| profileManager = null; | ||
| originalExec = null; | ||
@@ -612,9 +895,14 @@ originalExecSync = null; | ||
| super(options); | ||
| const config = this.interceptorConfig; | ||
| this.syncClient = new SyncClient({ | ||
| socketPath: "/var/run/agenshield/agenshield.sock", | ||
| httpHost: "localhost", | ||
| httpPort: 5201, | ||
| // Broker uses 5201 | ||
| timeout: 3e4 | ||
| socketPath: config?.socketPath || "/var/run/agenshield/agenshield.sock", | ||
| httpHost: config?.httpHost || "localhost", | ||
| httpPort: config?.httpPort || 5201, | ||
| timeout: config?.timeout || 3e4 | ||
| }); | ||
| if (config?.enableSeatbelt && process.platform === "darwin") { | ||
| this.profileManager = new ProfileManager( | ||
| config.seatbeltProfileDir || "/tmp/agenshield-profiles" | ||
| ); | ||
| } | ||
| } | ||
@@ -653,2 +941,146 @@ install() { | ||
| } | ||
| /** | ||
| * Build execution context from config for RPC calls | ||
| */ | ||
| getPolicyExecutionContext() { | ||
| const config = this.interceptorConfig; | ||
| return { | ||
| callerType: config?.contextType || "agent", | ||
| skillSlug: config?.contextSkillSlug, | ||
| agentId: config?.contextAgentId, | ||
| depth: 0 | ||
| }; | ||
| } | ||
| /** | ||
| * Synchronous policy check via SyncClient. | ||
| * Returns the full policy result (with sandbox config) or null if broker | ||
| * is unavailable and failOpen is true. | ||
| */ | ||
| syncPolicyCheck(fullCommand) { | ||
| this._checking = true; | ||
| try { | ||
| debugLog(`cp.syncPolicyCheck START command=${fullCommand}`); | ||
| const context = this.getPolicyExecutionContext(); | ||
| const result = this.syncClient.request( | ||
| "policy_check", | ||
| { operation: "exec", target: fullCommand, context } | ||
| ); | ||
| debugLog(`cp.syncPolicyCheck DONE allowed=${result.allowed} command=${fullCommand}`); | ||
| if (!result.allowed) { | ||
| throw new PolicyDeniedError(result.reason || "Operation denied by policy", { | ||
| operation: "exec", | ||
| target: fullCommand, | ||
| policyId: result.policyId | ||
| }); | ||
| } | ||
| return result; | ||
| } catch (error) { | ||
| if (error instanceof PolicyDeniedError) { | ||
| throw error; | ||
| } | ||
| debugLog(`cp.syncPolicyCheck ERROR: ${error.message} command=${fullCommand}`); | ||
| if (!this.failOpen) { | ||
| throw error; | ||
| } | ||
| return null; | ||
| } finally { | ||
| this._checking = false; | ||
| } | ||
| } | ||
| /** | ||
| * Create a restrictive default sandbox config for fail-open scenarios. | ||
| * No network, minimal fs — better than running completely unsandboxed. | ||
| */ | ||
| getFailOpenSandbox() { | ||
| return { | ||
| enabled: true, | ||
| allowedReadPaths: [], | ||
| allowedWritePaths: [], | ||
| deniedPaths: [], | ||
| networkAllowed: false, | ||
| allowedHosts: [], | ||
| allowedPorts: [], | ||
| allowedBinaries: [], | ||
| deniedBinaries: [], | ||
| envInjection: {}, | ||
| envDeny: [] | ||
| }; | ||
| } | ||
| /** | ||
| * Resolve the sandbox config to use: from policy result, fail-open default, or null. | ||
| */ | ||
| resolveSandbox(policyResult) { | ||
| if (policyResult?.sandbox?.enabled) { | ||
| return policyResult.sandbox; | ||
| } | ||
| if (policyResult === null && this.profileManager) { | ||
| return this.getFailOpenSandbox(); | ||
| } | ||
| return null; | ||
| } | ||
| /** | ||
| * Wrap a command with sandbox-exec if seatbelt is enabled and sandbox config is present. | ||
| * Returns modified { command, args, options } for spawn-style calls. | ||
| */ | ||
| wrapWithSeatbelt(command, args, options, policyResult) { | ||
| const sandbox = this.resolveSandbox(policyResult); | ||
| if (!this.profileManager || !sandbox || process.platform !== "darwin") { | ||
| return { command, args, options }; | ||
| } | ||
| if (command === "/opt/agenshield/bin/node-bin" || command.endsWith("/node-bin")) { | ||
| debugLog(`cp.wrapWithSeatbelt: SKIP node-bin (already intercepted) command=${command}`); | ||
| return { command, args, options }; | ||
| } | ||
| if (command === "/usr/bin/sandbox-exec" || command.endsWith("/sandbox-exec")) { | ||
| debugLog(`cp.wrapWithSeatbelt: SKIP already sandbox-exec command=${command}`); | ||
| return { command, args, options }; | ||
| } | ||
| debugLog(`cp.wrapWithSeatbelt: wrapping command=${command}`); | ||
| const profileContent = this.profileManager.generateProfile(sandbox); | ||
| const profilePath = this.profileManager.getOrCreateProfile(profileContent); | ||
| const env = { ...options?.env || process.env }; | ||
| if (sandbox.envInjection) { | ||
| Object.assign(env, sandbox.envInjection); | ||
| } | ||
| if (sandbox.envDeny) { | ||
| for (const key of sandbox.envDeny) { | ||
| delete env[key]; | ||
| } | ||
| } | ||
| return { | ||
| command: "/usr/bin/sandbox-exec", | ||
| args: ["-f", profilePath, command, ...args], | ||
| options: { ...options, env } | ||
| }; | ||
| } | ||
| /** | ||
| * Wrap a shell command string with sandbox-exec. | ||
| * For exec/execSync which take a full command string. | ||
| */ | ||
| wrapCommandStringWithSeatbelt(command, options, policyResult) { | ||
| const sandbox = this.resolveSandbox(policyResult); | ||
| if (!this.profileManager || !sandbox || process.platform !== "darwin") { | ||
| return { command, options }; | ||
| } | ||
| if (command.startsWith("/usr/bin/sandbox-exec ") || command.startsWith("sandbox-exec ")) { | ||
| debugLog(`cp.wrapCommandStringWithSeatbelt: SKIP already sandbox-exec command=${command}`); | ||
| return { command, options }; | ||
| } | ||
| debugLog(`cp.wrapCommandStringWithSeatbelt: wrapping command=${command}`); | ||
| const profileContent = this.profileManager.generateProfile(sandbox); | ||
| const profilePath = this.profileManager.getOrCreateProfile(profileContent); | ||
| const env = { ...options?.env || process.env }; | ||
| if (sandbox.envInjection) { | ||
| Object.assign(env, sandbox.envInjection); | ||
| } | ||
| if (sandbox.envDeny) { | ||
| for (const key of sandbox.envDeny) { | ||
| delete env[key]; | ||
| } | ||
| } | ||
| return { | ||
| command: `/usr/bin/sandbox-exec -f ${profilePath} ${command}`, | ||
| options: { ...options, env } | ||
| }; | ||
| } | ||
| createInterceptedExec() { | ||
@@ -659,4 +1091,5 @@ const self = this; | ||
| const callback = typeof args[args.length - 1] === "function" ? args.pop() : void 0; | ||
| debugLog(`cp.exec ENTER command=${command} _checking=${self._checking}`); | ||
| if (self._checking) { | ||
| const options = args[0]; | ||
| debugLog(`cp.exec ENTER command=${command} _checking=${self._checking} _executing=${self._executing}`); | ||
| if (self._checking || self._executing) { | ||
| debugLog(`cp.exec SKIP (re-entrancy) command=${command}`); | ||
@@ -666,10 +1099,22 @@ return original(command, ...args, callback); | ||
| self.eventReporter.intercept("exec", command); | ||
| self.checkPolicy("exec", command).then(() => { | ||
| original(command, ...args, callback); | ||
| }).catch((error) => { | ||
| let policyResult = null; | ||
| try { | ||
| policyResult = self.syncPolicyCheck(command); | ||
| } catch (error) { | ||
| if (callback) { | ||
| callback(error, "", ""); | ||
| process.nextTick(() => callback(error, "", "")); | ||
| } | ||
| }); | ||
| return original('echo ""'); | ||
| return original('echo ""'); | ||
| } | ||
| const wrapped = self.wrapCommandStringWithSeatbelt(command, options, policyResult); | ||
| debugLog(`cp.exec calling original command=${wrapped.command}`); | ||
| self._executing = true; | ||
| try { | ||
| if (wrapped.options) { | ||
| return original(wrapped.command, wrapped.options, callback); | ||
| } | ||
| return original(wrapped.command, callback); | ||
| } finally { | ||
| self._executing = false; | ||
| } | ||
| }; | ||
@@ -681,35 +1126,21 @@ } | ||
| const interceptedExecSync = function(command, options) { | ||
| debugLog(`cp.execSync ENTER command=${command} _checking=${self._checking}`); | ||
| if (self._checking) { | ||
| debugLog(`cp.execSync ENTER command=${command} _checking=${self._checking} _executing=${self._executing}`); | ||
| if (self._checking || self._executing) { | ||
| debugLog(`cp.execSync SKIP (re-entrancy) command=${command}`); | ||
| return original(command, options); | ||
| } | ||
| self._checking = true; | ||
| self.eventReporter.intercept("exec", command); | ||
| const policyResult = self.syncPolicyCheck(command); | ||
| const wrapped = self.wrapCommandStringWithSeatbelt( | ||
| command, | ||
| options, | ||
| policyResult | ||
| ); | ||
| debugLog(`cp.execSync calling original command=${wrapped.command}`); | ||
| self._executing = true; | ||
| try { | ||
| self.eventReporter.intercept("exec", command); | ||
| debugLog(`cp.execSync policy_check START command=${command}`); | ||
| const result = self.syncClient.request( | ||
| "policy_check", | ||
| { operation: "exec", target: command } | ||
| ); | ||
| debugLog(`cp.execSync policy_check DONE allowed=${result.allowed} command=${command}`); | ||
| if (!result.allowed) { | ||
| throw new PolicyDeniedError(result.reason || "Operation denied by policy", { | ||
| operation: "exec", | ||
| target: command | ||
| }); | ||
| } | ||
| } catch (error) { | ||
| debugLog(`cp.execSync policy_check ERROR: ${error.message} command=${command}`); | ||
| if (error instanceof PolicyDeniedError) { | ||
| throw error; | ||
| } | ||
| if (!self.failOpen) { | ||
| throw error; | ||
| } | ||
| return original(wrapped.command, wrapped.options); | ||
| } finally { | ||
| self._checking = false; | ||
| self._executing = false; | ||
| } | ||
| debugLog(`cp.execSync calling original command=${command}`); | ||
| return original(command, options); | ||
| }; | ||
@@ -723,13 +1154,27 @@ return interceptedExecSync; | ||
| const fullCmd = args ? `${command} ${args.join(" ")}` : command; | ||
| debugLog(`cp.spawn ENTER command=${fullCmd} _checking=${self._checking}`); | ||
| if (self._checking) { | ||
| debugLog(`cp.spawn ENTER command=${fullCmd} _checking=${self._checking} _executing=${self._executing}`); | ||
| if (self._checking || self._executing) { | ||
| debugLog(`cp.spawn SKIP (re-entrancy) command=${fullCmd}`); | ||
| return original(command, args, options || {}); | ||
| } | ||
| const fullCommand = args ? `${command} ${args.join(" ")}` : command; | ||
| self.eventReporter.intercept("exec", fullCommand); | ||
| self.checkPolicy("exec", fullCommand).catch((error) => { | ||
| self.eventReporter.error("exec", fullCommand, error.message); | ||
| }); | ||
| return original(command, args, options || {}); | ||
| self.eventReporter.intercept("exec", fullCmd); | ||
| let policyResult = null; | ||
| try { | ||
| policyResult = self.syncPolicyCheck(fullCmd); | ||
| } catch (error) { | ||
| debugLog(`cp.spawn DENIED command=${fullCmd}`); | ||
| const denied = original("false", [], { stdio: "pipe" }); | ||
| process.nextTick(() => { | ||
| denied.emit("error", error); | ||
| }); | ||
| return denied; | ||
| } | ||
| const wrapped = self.wrapWithSeatbelt( | ||
| command, | ||
| Array.from(args || []), | ||
| options, | ||
| policyResult | ||
| ); | ||
| debugLog(`cp.spawn calling original command=${wrapped.command} args=${wrapped.args.join(" ")}`); | ||
| return original(wrapped.command, wrapped.args, wrapped.options || {}); | ||
| }; | ||
@@ -743,45 +1188,37 @@ return interceptedSpawn; | ||
| const fullCommand = args ? `${command} ${args.join(" ")}` : command; | ||
| debugLog(`cp.spawnSync ENTER command=${fullCommand} _checking=${self._checking}`); | ||
| if (self._checking) { | ||
| debugLog(`cp.spawnSync ENTER command=${fullCommand} _checking=${self._checking} _executing=${self._executing}`); | ||
| if (self._checking || self._executing) { | ||
| debugLog(`cp.spawnSync SKIP (re-entrancy) command=${fullCommand}`); | ||
| return original(command, args, options); | ||
| } | ||
| self._checking = true; | ||
| self.eventReporter.intercept("exec", fullCommand); | ||
| let policyResult = null; | ||
| try { | ||
| self.eventReporter.intercept("exec", fullCommand); | ||
| debugLog(`cp.spawnSync policy_check START command=${fullCommand}`); | ||
| const result = self.syncClient.request( | ||
| "policy_check", | ||
| { operation: "exec", target: fullCommand } | ||
| ); | ||
| debugLog(`cp.spawnSync policy_check DONE allowed=${result.allowed} command=${fullCommand}`); | ||
| if (!result.allowed) { | ||
| return { | ||
| pid: -1, | ||
| output: [], | ||
| stdout: Buffer.alloc(0), | ||
| stderr: Buffer.from(result.reason || "Policy denied"), | ||
| status: 1, | ||
| signal: null, | ||
| error: new PolicyDeniedError(result.reason || "Policy denied") | ||
| }; | ||
| } | ||
| policyResult = self.syncPolicyCheck(fullCommand); | ||
| } catch (error) { | ||
| debugLog(`cp.spawnSync policy_check ERROR: ${error.message} command=${fullCommand}`); | ||
| if (!self.failOpen) { | ||
| return { | ||
| pid: -1, | ||
| output: [], | ||
| stdout: Buffer.alloc(0), | ||
| stderr: Buffer.from(error.message), | ||
| status: 1, | ||
| signal: null, | ||
| error | ||
| }; | ||
| } | ||
| } finally { | ||
| self._checking = false; | ||
| debugLog(`cp.spawnSync DENIED command=${fullCommand}`); | ||
| return { | ||
| pid: -1, | ||
| output: [], | ||
| stdout: Buffer.alloc(0), | ||
| stderr: Buffer.from( | ||
| error instanceof PolicyDeniedError ? error.message || "Policy denied" : error.message | ||
| ), | ||
| status: 1, | ||
| signal: null, | ||
| error | ||
| }; | ||
| } | ||
| debugLog(`cp.spawnSync calling original command=${fullCommand}`); | ||
| return original(command, args, options); | ||
| const wrapped = self.wrapWithSeatbelt( | ||
| command, | ||
| Array.from(args || []), | ||
| options, | ||
| policyResult | ||
| ); | ||
| debugLog(`cp.spawnSync calling original command=${wrapped.command}`); | ||
| return original( | ||
| wrapped.command, | ||
| wrapped.args, | ||
| wrapped.options | ||
| ); | ||
| }; | ||
@@ -792,11 +1229,48 @@ } | ||
| const original = this.originalExecFile; | ||
| return function interceptedExecFile(file, ...args) { | ||
| if (self._checking) { | ||
| return original(file, ...args); | ||
| return function interceptedExecFile(file, ...rest) { | ||
| if (self._checking || self._executing) { | ||
| return original(file, ...rest); | ||
| } | ||
| self.eventReporter.intercept("exec", file); | ||
| self.checkPolicy("exec", file).catch((error) => { | ||
| self.eventReporter.error("exec", file, error.message); | ||
| }); | ||
| return original(file, ...args); | ||
| let args = []; | ||
| let options; | ||
| let callback; | ||
| for (const arg of rest) { | ||
| if (typeof arg === "function") { | ||
| callback = arg; | ||
| } else if (Array.isArray(arg)) { | ||
| args = arg; | ||
| } else if (typeof arg === "object" && arg !== null) { | ||
| options = arg; | ||
| } | ||
| } | ||
| const fullCommand = args.length > 0 ? `${file} ${args.join(" ")}` : file; | ||
| debugLog(`cp.execFile ENTER command=${fullCommand}`); | ||
| self.eventReporter.intercept("exec", fullCommand); | ||
| let policyResult = null; | ||
| try { | ||
| policyResult = self.syncPolicyCheck(fullCommand); | ||
| } catch (error) { | ||
| if (callback) { | ||
| process.nextTick(() => callback(error, "", "")); | ||
| } | ||
| return original("false"); | ||
| } | ||
| const wrapped = self.wrapWithSeatbelt(file, args, options, policyResult); | ||
| debugLog(`cp.execFile calling original command=${wrapped.command}`); | ||
| if (callback) { | ||
| return original( | ||
| wrapped.command, | ||
| wrapped.args, | ||
| wrapped.options, | ||
| callback | ||
| ); | ||
| } | ||
| return original( | ||
| wrapped.command, | ||
| wrapped.args, | ||
| wrapped.options || {}, | ||
| // eslint-disable-next-line @typescript-eslint/no-empty-function | ||
| () => { | ||
| } | ||
| ); | ||
| }; | ||
@@ -808,10 +1282,34 @@ } | ||
| const interceptedFork = function(modulePath, args, options) { | ||
| if (self._checking) { | ||
| if (self._checking || self._executing) { | ||
| return original(modulePath, args, options); | ||
| } | ||
| const pathStr = modulePath.toString(); | ||
| self.eventReporter.intercept("exec", `fork:${pathStr}`); | ||
| self.checkPolicy("exec", `fork:${pathStr}`).catch((error) => { | ||
| self.eventReporter.error("exec", pathStr, error.message); | ||
| }); | ||
| const fullCommand = `fork:${pathStr}`; | ||
| debugLog(`cp.fork ENTER command=${fullCommand}`); | ||
| self.eventReporter.intercept("exec", fullCommand); | ||
| let policyResult = null; | ||
| try { | ||
| policyResult = self.syncPolicyCheck(fullCommand); | ||
| } catch (error) { | ||
| debugLog(`cp.fork DENIED command=${fullCommand}`); | ||
| const denied = self.originalSpawn("false", [], { stdio: "pipe" }); | ||
| process.nextTick(() => { | ||
| denied.emit("error", error); | ||
| }); | ||
| return denied; | ||
| } | ||
| if (policyResult?.sandbox) { | ||
| const sandbox = policyResult.sandbox; | ||
| const env = { ...options?.env || process.env }; | ||
| if (sandbox.envInjection) { | ||
| Object.assign(env, sandbox.envInjection); | ||
| } | ||
| if (sandbox.envDeny) { | ||
| for (const key of sandbox.envDeny) { | ||
| delete env[key]; | ||
| } | ||
| } | ||
| options = { ...options, env }; | ||
| } | ||
| debugLog(`cp.fork calling original module=${pathStr}`); | ||
| return original(modulePath, args, options); | ||
@@ -910,4 +1408,4 @@ }; | ||
| const self = this; | ||
| safeOverride(module2, methodName, function intercepted(path, ...args) { | ||
| const pathString = normalizePathArg(path); | ||
| safeOverride(module2, methodName, function intercepted(path2, ...args) { | ||
| const pathString = normalizePathArg(path2); | ||
| const callback = typeof args[args.length - 1] === "function" ? args.pop() : void 0; | ||
@@ -917,3 +1415,3 @@ debugLog(`fs.${methodName} ENTER (async) path=${pathString} _checking=${self._checking}`); | ||
| debugLog(`fs.${methodName} SKIP (re-entrancy, async) path=${pathString}`); | ||
| original.call(module2, path, ...args, callback); | ||
| original.call(module2, path2, ...args, callback); | ||
| return; | ||
@@ -924,3 +1422,3 @@ } | ||
| debugLog(`fs.${methodName} policy OK (async) path=${pathString}`); | ||
| original.call(module2, path, ...args, callback); | ||
| original.call(module2, path2, ...args, callback); | ||
| }).catch((error) => { | ||
@@ -940,8 +1438,8 @@ debugLog(`fs.${methodName} policy ERROR (async): ${error.message} path=${pathString}`); | ||
| const self = this; | ||
| safeOverride(module2, methodName, function interceptedSync(path, ...args) { | ||
| const pathString = normalizePathArg(path); | ||
| safeOverride(module2, methodName, function interceptedSync(path2, ...args) { | ||
| const pathString = normalizePathArg(path2); | ||
| debugLog(`fs.${methodName} ENTER path=${pathString} _checking=${self._checking}`); | ||
| if (self._checking) { | ||
| debugLog(`fs.${methodName} SKIP (re-entrancy) path=${pathString}`); | ||
| return original.call(module2, path, ...args); | ||
| return original.call(module2, path2, ...args); | ||
| } | ||
@@ -975,3 +1473,3 @@ self._checking = true; | ||
| debugLog(`fs.${methodName} calling original path=${pathString}`); | ||
| return original.call(module2, path, ...args); | ||
| return original.call(module2, path2, ...args); | ||
| }); | ||
@@ -985,8 +1483,8 @@ } | ||
| const self = this; | ||
| safeOverride(module2, methodName, async function interceptedPromise(path, ...args) { | ||
| const pathString = normalizePathArg(path); | ||
| safeOverride(module2, methodName, async function interceptedPromise(path2, ...args) { | ||
| const pathString = normalizePathArg(path2); | ||
| debugLog(`fsPromises.${methodName} ENTER path=${pathString} _checking=${self._checking}`); | ||
| if (self._checking) { | ||
| debugLog(`fsPromises.${methodName} SKIP (re-entrancy) path=${pathString}`); | ||
| return original.call(module2, path, ...args); | ||
| return original.call(module2, path2, ...args); | ||
| } | ||
@@ -996,3 +1494,3 @@ self.eventReporter.intercept(operation, pathString); | ||
| debugLog(`fsPromises.${methodName} policy OK path=${pathString}`); | ||
| return original.call(module2, path, ...args); | ||
| return original.call(module2, path2, ...args); | ||
| }); | ||
@@ -1141,7 +1639,7 @@ } | ||
| */ | ||
| async check(operation, target) { | ||
| async check(operation, target, context) { | ||
| try { | ||
| const result = await this.client.request( | ||
| "policy_check", | ||
| { operation, target } | ||
| { operation, target, context } | ||
| ); | ||
@@ -1190,3 +1688,7 @@ return result; | ||
| const prefix = event.type === "allow" ? "\u2713" : event.type === "deny" ? "\u2717" : "\u2022"; | ||
| console[level](`[AgenShield] ${prefix} ${event.operation}: ${event.target}`); | ||
| let detail = `${prefix} ${event.operation}: ${event.target}`; | ||
| if (event.policyId) detail += ` [policy:${event.policyId}]`; | ||
| if (event.error) detail += ` [reason:${event.error}]`; | ||
| if (event.duration) detail += ` [${event.duration}ms]`; | ||
| console[level](`[AgenShield] ${detail}`); | ||
| } | ||
@@ -1311,2 +1813,9 @@ if (this.queue.length >= 100) { | ||
| const config = createConfig(configOverrides); | ||
| if (config.logLevel === "debug") { | ||
| try { | ||
| const safeConfig = { ...config }; | ||
| console.error("[AgenShield:config]", JSON.stringify(safeConfig, null, 2)); | ||
| } catch { | ||
| } | ||
| } | ||
| client = new AsyncClient({ | ||
@@ -1332,3 +1841,4 @@ socketPath: config.socketPath, | ||
| failOpen: config.failOpen, | ||
| brokerHttpPort: config.httpPort | ||
| brokerHttpPort: config.httpPort, | ||
| config | ||
| }); | ||
@@ -1366,3 +1876,4 @@ installed.fetch.install(); | ||
| failOpen: config.failOpen, | ||
| brokerHttpPort: config.httpPort | ||
| brokerHttpPort: config.httpPort, | ||
| config | ||
| }); | ||
@@ -1369,0 +1880,0 @@ installed.childProcess.install(); |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../src/installer.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAOrD,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAiBtD;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,eAAe,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAC3C,IAAI,CA6FN;AAED;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,IAAI,CA6B5C;AAED;;GAEG;AACH,wBAAgB,WAAW,IAAI,OAAO,CAErC;AAED;;GAEG;AACH,wBAAgB,SAAS,IAAI,WAAW,GAAG,IAAI,CAE9C"} | ||
| {"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../src/installer.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAOrD,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAiBtD;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,eAAe,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAC3C,IAAI,CAuGN;AAED;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,IAAI,CA6B5C;AAED;;GAEG;AACH,wBAAgB,WAAW,IAAI,OAAO,CAErC;AAED;;GAEG;AACH,wBAAgB,SAAS,IAAI,WAAW,GAAG,IAAI,CAE9C"} |
@@ -9,2 +9,3 @@ /** | ||
| import type { EventReporter } from '../events/reporter.js'; | ||
| import type { InterceptorConfig } from '../config.js'; | ||
| export interface BaseInterceptorOptions { | ||
@@ -17,2 +18,4 @@ client: AsyncClient; | ||
| brokerHttpPort?: number; | ||
| /** Full interceptor config (for seatbelt + context) */ | ||
| config?: InterceptorConfig; | ||
| } | ||
@@ -25,2 +28,3 @@ export declare abstract class BaseInterceptor { | ||
| protected installed: boolean; | ||
| protected interceptorConfig?: InterceptorConfig; | ||
| private brokerHttpPort; | ||
@@ -45,5 +49,9 @@ constructor(options: BaseInterceptorOptions); | ||
| /** | ||
| * Build execution context from config | ||
| */ | ||
| protected getBasePolicyExecutionContext(): import('@agenshield/ipc').PolicyExecutionContext | undefined; | ||
| /** | ||
| * Check policy and handle the result | ||
| */ | ||
| protected checkPolicy(operation: string, target: string): Promise<void>; | ||
| protected checkPolicy(operation: string, target: string, context?: import('@agenshield/ipc').PolicyExecutionContext): Promise<void>; | ||
| /** | ||
@@ -50,0 +58,0 @@ * Log a debug message |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../src/interceptors/base.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAI3D,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,WAAW,CAAC;IACpB,eAAe,EAAE,eAAe,CAAC;IACjC,aAAa,EAAE,aAAa,CAAC;IAC7B,QAAQ,EAAE,OAAO,CAAC;IAClB,4EAA4E;IAC5E,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,8BAAsB,eAAe;IACnC,SAAS,CAAC,MAAM,EAAE,WAAW,CAAC;IAC9B,SAAS,CAAC,eAAe,EAAE,eAAe,CAAC;IAC3C,SAAS,CAAC,aAAa,EAAE,aAAa,CAAC;IACvC,SAAS,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC5B,SAAS,CAAC,SAAS,EAAE,OAAO,CAAS;IACrC,OAAO,CAAC,cAAc,CAAS;gBAEnB,OAAO,EAAE,sBAAsB;IAQ3C;;OAEG;IACH,SAAS,CAAC,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAe3C;;OAEG;IACH,QAAQ,CAAC,OAAO,IAAI,IAAI;IAExB;;OAEG;IACH,QAAQ,CAAC,SAAS,IAAI,IAAI;IAE1B;;OAEG;IACH,WAAW,IAAI,OAAO;IAItB;;OAEG;cACa,WAAW,CACzB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC;IA8ChB;;OAEG;IACH,SAAS,CAAC,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;CAGvC"} | ||
| {"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../src/interceptors/base.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAItD,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,WAAW,CAAC;IACpB,eAAe,EAAE,eAAe,CAAC;IACjC,aAAa,EAAE,aAAa,CAAC;IAC7B,QAAQ,EAAE,OAAO,CAAC;IAClB,4EAA4E;IAC5E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,uDAAuD;IACvD,MAAM,CAAC,EAAE,iBAAiB,CAAC;CAC5B;AAED,8BAAsB,eAAe;IACnC,SAAS,CAAC,MAAM,EAAE,WAAW,CAAC;IAC9B,SAAS,CAAC,eAAe,EAAE,eAAe,CAAC;IAC3C,SAAS,CAAC,aAAa,EAAE,aAAa,CAAC;IACvC,SAAS,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC5B,SAAS,CAAC,SAAS,EAAE,OAAO,CAAS;IACrC,SAAS,CAAC,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IAChD,OAAO,CAAC,cAAc,CAAS;gBAEnB,OAAO,EAAE,sBAAsB;IAS3C;;OAEG;IACH,SAAS,CAAC,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAe3C;;OAEG;IACH,QAAQ,CAAC,OAAO,IAAI,IAAI;IAExB;;OAEG;IACH,QAAQ,CAAC,SAAS,IAAI,IAAI;IAE1B;;OAEG;IACH,WAAW,IAAI,OAAO;IAItB;;OAEG;IACH,SAAS,CAAC,6BAA6B,IAAI,OAAO,iBAAiB,EAAE,sBAAsB,GAAG,SAAS;IAWvG;;OAEG;cACa,WAAW,CACzB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,OAAO,iBAAiB,EAAE,sBAAsB,GACzD,OAAO,CAAC,IAAI,CAAC;IA8ChB;;OAEG;IACH,SAAS,CAAC,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;CAGvC"} |
| /** | ||
| * Child Process Interceptor | ||
| * | ||
| * Intercepts child_process module calls. | ||
| * Intercepts child_process module calls with synchronous policy checking | ||
| * and optional macOS seatbelt (sandbox-exec) wrapping for approved commands. | ||
| * | ||
| * ALL methods (spawn, exec, execFile, fork) now perform synchronous policy | ||
| * checks before execution. Previously, async methods would fire the original | ||
| * call immediately while the policy check ran in the background. | ||
| */ | ||
@@ -10,2 +15,4 @@ import { BaseInterceptor, type BaseInterceptorOptions } from './base.js'; | ||
| private _checking; | ||
| private _executing; | ||
| private profileManager; | ||
| private originalExec; | ||
@@ -20,2 +27,31 @@ private originalExecSync; | ||
| uninstall(): void; | ||
| /** | ||
| * Build execution context from config for RPC calls | ||
| */ | ||
| private getPolicyExecutionContext; | ||
| /** | ||
| * Synchronous policy check via SyncClient. | ||
| * Returns the full policy result (with sandbox config) or null if broker | ||
| * is unavailable and failOpen is true. | ||
| */ | ||
| private syncPolicyCheck; | ||
| /** | ||
| * Create a restrictive default sandbox config for fail-open scenarios. | ||
| * No network, minimal fs — better than running completely unsandboxed. | ||
| */ | ||
| private getFailOpenSandbox; | ||
| /** | ||
| * Resolve the sandbox config to use: from policy result, fail-open default, or null. | ||
| */ | ||
| private resolveSandbox; | ||
| /** | ||
| * Wrap a command with sandbox-exec if seatbelt is enabled and sandbox config is present. | ||
| * Returns modified { command, args, options } for spawn-style calls. | ||
| */ | ||
| private wrapWithSeatbelt; | ||
| /** | ||
| * Wrap a shell command string with sandbox-exec. | ||
| * For exec/execSync which take a full command string. | ||
| */ | ||
| private wrapCommandStringWithSeatbelt; | ||
| private createInterceptedExec; | ||
@@ -22,0 +58,0 @@ private createInterceptedExecSync; |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"child-process.d.ts","sourceRoot":"","sources":["../../src/interceptors/child-process.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,WAAW,CAAC;AASzE,qBAAa,uBAAwB,SAAQ,eAAe;IAC1D,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,YAAY,CAAyC;IAC7D,OAAO,CAAC,gBAAgB,CAA6C;IACrE,OAAO,CAAC,aAAa,CAA0C;IAC/D,OAAO,CAAC,iBAAiB,CAA8C;IACvE,OAAO,CAAC,gBAAgB,CAA6C;IACrE,OAAO,CAAC,YAAY,CAAyC;gBAEjD,OAAO,EAAE,sBAAsB;IAU3C,OAAO,IAAI,IAAI;IAsBf,SAAS,IAAI,IAAI;IAmBjB,OAAO,CAAC,qBAAqB;IAwC7B,OAAO,CAAC,yBAAyB;IAsDjC,OAAO,CAAC,sBAAsB;IAkC9B,OAAO,CAAC,0BAA0B;IA+DlC,OAAO,CAAC,yBAAyB;IAwBjC,OAAO,CAAC,qBAAqB;CA2B9B"} | ||
| {"version":3,"file":"child-process.d.ts","sourceRoot":"","sources":["../../src/interceptors/child-process.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,WAAW,CAAC;AAYzE,qBAAa,uBAAwB,SAAQ,eAAe;IAC1D,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,YAAY,CAAyC;IAC7D,OAAO,CAAC,gBAAgB,CAA6C;IACrE,OAAO,CAAC,aAAa,CAA0C;IAC/D,OAAO,CAAC,iBAAiB,CAA8C;IACvE,OAAO,CAAC,gBAAgB,CAA6C;IACrE,OAAO,CAAC,YAAY,CAAyC;gBAEjD,OAAO,EAAE,sBAAsB;IAkB3C,OAAO,IAAI,IAAI;IAsBf,SAAS,IAAI,IAAI;IAmBjB;;OAEG;IACH,OAAO,CAAC,yBAAyB;IAUjC;;;;OAIG;IACH,OAAO,CAAC,eAAe;IAkCvB;;;OAGG;IACH,OAAO,CAAC,kBAAkB;IAgB1B;;OAEG;IACH,OAAO,CAAC,cAAc;IAWtB;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IA+CxB;;;OAGG;IACH,OAAO,CAAC,6BAA6B;IAsCrC,OAAO,CAAC,qBAAqB;IAsD7B,OAAO,CAAC,yBAAyB;IAwCjC,OAAO,CAAC,sBAAsB;IAiD9B,OAAO,CAAC,0BAA0B;IA0DlC,OAAO,CAAC,yBAAyB;IAoEjC,OAAO,CAAC,qBAAqB;CAyD9B"} |
@@ -10,2 +10,6 @@ /** | ||
| constructor(options: BaseInterceptorOptions); | ||
| /** | ||
| * Build execution context from config | ||
| */ | ||
| private getPolicyExecutionContext; | ||
| install(): void; | ||
@@ -12,0 +16,0 @@ uninstall(): void; |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../src/interceptors/fetch.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,WAAW,CAAC;AAGzE,qBAAa,gBAAiB,SAAQ,eAAe;IACnD,OAAO,CAAC,aAAa,CAA6B;gBAEtC,OAAO,EAAE,sBAAsB;IAI3C,OAAO,IAAI,IAAI;IAYf,SAAS,IAAI,IAAI;YAQH,gBAAgB;CAiG/B"} | ||
| {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../src/interceptors/fetch.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,WAAW,CAAC;AAIzE,qBAAa,gBAAiB,SAAQ,eAAe;IACnD,OAAO,CAAC,aAAa,CAA6B;gBAEtC,OAAO,EAAE,sBAAsB;IAI3C;;OAEG;IACH,OAAO,CAAC,yBAAyB;IAWjC,OAAO,IAAI,IAAI;IAYf,SAAS,IAAI,IAAI;YAQH,gBAAgB;CA+C/B"} |
+2
-2
| { | ||
| "name": "@agenshield/interceptor", | ||
| "version": "0.6.2", | ||
| "version": "0.7.0", | ||
| "type": "module", | ||
@@ -28,3 +28,3 @@ "description": "Node.js runtime interception via ESM loader and CJS preload", | ||
| "dependencies": { | ||
| "@agenshield/ipc": "0.6.2" | ||
| "@agenshield/ipc": "0.7.0" | ||
| }, | ||
@@ -31,0 +31,0 @@ "devDependencies": { |
@@ -8,2 +8,3 @@ /** | ||
| import type { AsyncClient } from '../client/http-client.js'; | ||
| import type { SandboxConfig, PolicyExecutionContext } from '@agenshield/ipc'; | ||
| export interface PolicyEvaluatorOptions { | ||
@@ -16,2 +17,4 @@ client: AsyncClient; | ||
| reason?: string; | ||
| sandbox?: SandboxConfig; | ||
| executionContext?: PolicyExecutionContext; | ||
| } | ||
@@ -25,4 +28,4 @@ export declare class PolicyEvaluator { | ||
| */ | ||
| check(operation: string, target: string): Promise<PolicyCheckResult>; | ||
| check(operation: string, target: string, context?: PolicyExecutionContext): Promise<PolicyCheckResult>; | ||
| } | ||
| //# sourceMappingURL=evaluator.d.ts.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"evaluator.d.ts","sourceRoot":"","sources":["../../src/policy/evaluator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAE5D,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,WAAW,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAAc;gBAEhB,OAAO,EAAE,sBAAsB;IAI3C;;;OAGG;IACG,KAAK,CACT,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,iBAAiB,CAAC;CAgB9B"} | ||
| {"version":3,"file":"evaluator.d.ts","sourceRoot":"","sources":["../../src/policy/evaluator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,KAAK,EAAE,aAAa,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAE7E,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,WAAW,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,gBAAgB,CAAC,EAAE,sBAAsB,CAAC;CAC3C;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAAc;gBAEhB,OAAO,EAAE,sBAAsB;IAI3C;;;OAGG;IACG,KAAK,CACT,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,iBAAiB,CAAC;CAgB9B"} |
+677
-166
@@ -39,3 +39,8 @@ "use strict"; | ||
| interceptExec: env["AGENSHIELD_INTERCEPT_EXEC"] !== "false", | ||
| timeout: parseInt(env["AGENSHIELD_TIMEOUT"] || "30000", 10), | ||
| timeout: parseInt(env["AGENSHIELD_TIMEOUT"] || "5000", 10), | ||
| contextType: env["AGENSHIELD_CONTEXT_TYPE"] || "agent", | ||
| contextSkillSlug: env["AGENSHIELD_SKILL_SLUG"], | ||
| contextAgentId: env["AGENSHIELD_AGENT_ID"], | ||
| enableSeatbelt: env["AGENSHIELD_SEATBELT"] !== "false" && process.platform === "darwin", | ||
| seatbeltProfileDir: env["AGENSHIELD_SEATBELT_DIR"] || "/tmp/agenshield-profiles", | ||
| ...overrides | ||
@@ -84,9 +89,30 @@ }; | ||
| var _appendFileSync = fs.appendFileSync.bind(fs); | ||
| var _writeSync = fs.writeSync.bind(fs); | ||
| var LOG_PATH = "/var/log/agenshield/interceptor.log"; | ||
| var FALLBACK_LOG_PATH = "/tmp/agenshield-interceptor.log"; | ||
| var resolvedLogPath = null; | ||
| function getLogPath() { | ||
| if (resolvedLogPath !== null) return resolvedLogPath; | ||
| try { | ||
| _appendFileSync(LOG_PATH, ""); | ||
| resolvedLogPath = LOG_PATH; | ||
| } catch { | ||
| resolvedLogPath = FALLBACK_LOG_PATH; | ||
| } | ||
| return resolvedLogPath; | ||
| } | ||
| function debugLog(msg) { | ||
| const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [pid:${process.pid}] ${msg} | ||
| `; | ||
| try { | ||
| _appendFileSync(LOG_PATH, `[${(/* @__PURE__ */ new Date()).toISOString()}] [pid:${process.pid}] ${msg} | ||
| `); | ||
| _appendFileSync(getLogPath(), line); | ||
| } catch { | ||
| } | ||
| if (process.env["AGENSHIELD_LOG_LEVEL"] === "debug") { | ||
| try { | ||
| _writeSync(2, `[AgenShield:debug] ${msg} | ||
| `); | ||
| } catch { | ||
| } | ||
| } | ||
| } | ||
@@ -101,2 +127,3 @@ | ||
| installed = false; | ||
| interceptorConfig; | ||
| brokerHttpPort; | ||
@@ -109,2 +136,3 @@ constructor(options) { | ||
| this.brokerHttpPort = options.brokerHttpPort ?? 5201; | ||
| this.interceptorConfig = options.config; | ||
| } | ||
@@ -135,5 +163,18 @@ /** | ||
| /** | ||
| * Build execution context from config | ||
| */ | ||
| getBasePolicyExecutionContext() { | ||
| const config = this.interceptorConfig; | ||
| if (!config) return void 0; | ||
| return { | ||
| callerType: config.contextType || "agent", | ||
| skillSlug: config.contextSkillSlug, | ||
| agentId: config.contextAgentId, | ||
| depth: 0 | ||
| }; | ||
| } | ||
| /** | ||
| * Check policy and handle the result | ||
| */ | ||
| async checkPolicy(operation, target) { | ||
| async checkPolicy(operation, target, context) { | ||
| const startTime = Date.now(); | ||
@@ -143,3 +184,3 @@ debugLog(`base.checkPolicy START op=${operation} target=${target}`); | ||
| this.eventReporter.intercept(operation, target); | ||
| const result = await this.policyEvaluator.check(operation, target); | ||
| const result = await this.policyEvaluator.check(operation, target, context); | ||
| debugLog(`base.checkPolicy evaluator result op=${operation} target=${target} allowed=${result.allowed} policyId=${result.policyId}`); | ||
@@ -191,2 +232,15 @@ if (!result.allowed) { | ||
| } | ||
| /** | ||
| * Build execution context from config | ||
| */ | ||
| getPolicyExecutionContext() { | ||
| const config = this.interceptorConfig; | ||
| if (!config) return void 0; | ||
| return { | ||
| callerType: config.contextType || "agent", | ||
| skillSlug: config.contextSkillSlug, | ||
| agentId: config.contextAgentId, | ||
| depth: 0 | ||
| }; | ||
| } | ||
| install() { | ||
@@ -222,44 +276,6 @@ if (this.installed) return; | ||
| debugLog(`fetch checkPolicy START url=${url}`); | ||
| await this.checkPolicy("http_request", url); | ||
| debugLog(`fetch checkPolicy DONE url=${url}`); | ||
| try { | ||
| const method = init?.method || "GET"; | ||
| const headers = {}; | ||
| if (init?.headers) { | ||
| if (init.headers instanceof Headers) { | ||
| init.headers.forEach((value, key) => { | ||
| headers[key] = value; | ||
| }); | ||
| } else if (Array.isArray(init.headers)) { | ||
| for (const [key, value] of init.headers) { | ||
| headers[key] = value; | ||
| } | ||
| } else { | ||
| Object.assign(headers, init.headers); | ||
| } | ||
| } | ||
| let body; | ||
| if (init?.body) { | ||
| if (typeof init.body === "string") { | ||
| body = init.body; | ||
| } else if (init.body instanceof ArrayBuffer) { | ||
| body = Buffer.from(init.body).toString("base64"); | ||
| } else { | ||
| body = String(init.body); | ||
| } | ||
| } | ||
| const result = await this.client.request("http_request", { | ||
| url, | ||
| method, | ||
| headers, | ||
| body | ||
| }); | ||
| const responseHeaders = new Headers(result.headers); | ||
| return new Response(result.body, { | ||
| status: result.status, | ||
| statusText: result.statusText, | ||
| headers: responseHeaders | ||
| }); | ||
| await this.checkPolicy("http_request", url, this.getPolicyExecutionContext()); | ||
| debugLog(`fetch checkPolicy DONE url=${url}`); | ||
| } catch (error) { | ||
| debugLog(`fetch ERROR url=${url} error=${error.message}`); | ||
| if (error.name === "PolicyDeniedError") { | ||
@@ -274,2 +290,3 @@ throw error; | ||
| } | ||
| return this.originalFetch(input, init); | ||
| } | ||
@@ -419,2 +436,4 @@ }; | ||
| var _unlinkSync = fs2.unlinkSync.bind(fs2); | ||
| var _readdirSync = fs2.readdirSync.bind(fs2); | ||
| var _statSync = fs2.statSync.bind(fs2); | ||
| var _spawnSync = import_node_child_process.spawnSync; | ||
@@ -427,2 +446,4 @@ var _execSync = import_node_child_process.execSync; | ||
| timeout; | ||
| socketFailCount = 0; | ||
| socketSkipUntil = 0; | ||
| constructor(options) { | ||
@@ -433,4 +454,26 @@ this.socketPath = options.socketPath; | ||
| this.timeout = options.timeout; | ||
| this.cleanupStaleTmpFiles(); | ||
| } | ||
| /** | ||
| * Remove stale /tmp/agenshield-sync-*.json files from previous runs | ||
| */ | ||
| cleanupStaleTmpFiles() { | ||
| try { | ||
| const tmpDir = "/tmp"; | ||
| const files = _readdirSync(tmpDir); | ||
| const cutoff = Date.now() - 5 * 60 * 1e3; | ||
| for (const f of files) { | ||
| if (f.startsWith("agenshield-sync-") && f.endsWith(".json")) { | ||
| const fp = `${tmpDir}/${f}`; | ||
| try { | ||
| const stat = _statSync(fp); | ||
| if (stat.mtimeMs < cutoff) _unlinkSync(fp); | ||
| } catch { | ||
| } | ||
| } | ||
| } | ||
| } catch { | ||
| } | ||
| } | ||
| /** | ||
| * Send a synchronous request to the broker | ||
@@ -440,7 +483,20 @@ */ | ||
| debugLog(`syncClient.request START method=${method}`); | ||
| const now = Date.now(); | ||
| if (now < this.socketSkipUntil) { | ||
| debugLog(`syncClient.request SKIP socket (circuit open for ${this.socketSkipUntil - now}ms), using HTTP`); | ||
| const result = this.httpRequestSync(method, params); | ||
| debugLog(`syncClient.request http OK method=${method}`); | ||
| return result; | ||
| } | ||
| try { | ||
| const result = this.socketRequestSync(method, params); | ||
| this.socketFailCount = 0; | ||
| debugLog(`syncClient.request socket OK method=${method}`); | ||
| return result; | ||
| } catch (socketErr) { | ||
| this.socketFailCount++; | ||
| if (this.socketFailCount >= 2) { | ||
| this.socketSkipUntil = Date.now() + 6e4; | ||
| debugLog(`syncClient.request socket circuit OPEN (${this.socketFailCount} failures)`); | ||
| } | ||
| debugLog(`syncClient.request socket FAILED: ${socketErr.message}, trying HTTP`); | ||
@@ -470,5 +526,14 @@ const result = this.httpRequestSync(method, params); | ||
| let done = false; | ||
| const socket = net.createConnection('${this.socketPath}'); | ||
| let data = ''; | ||
| const timer = setTimeout(() => { | ||
| if (done) return; | ||
| done = true; | ||
| socket.destroy(); | ||
| fs.writeFileSync('${tmpFile}', JSON.stringify({ error: 'timeout' })); | ||
| process.exit(1); | ||
| }, ${this.timeout}); | ||
| socket.on('connect', () => { | ||
@@ -480,5 +545,8 @@ socket.write(${JSON.stringify(request)}); | ||
| data += chunk.toString(); | ||
| if (data.includes('\\n')) { | ||
| if (data.includes('\\n') && !done) { | ||
| done = true; | ||
| clearTimeout(timer); | ||
| socket.end(); | ||
| fs.writeFileSync('${tmpFile}', data.split('\\n')[0]); | ||
| process.exit(0); | ||
| } | ||
@@ -488,9 +556,8 @@ }); | ||
| socket.on('error', (err) => { | ||
| if (done) return; | ||
| done = true; | ||
| clearTimeout(timer); | ||
| fs.writeFileSync('${tmpFile}', JSON.stringify({ error: err.message })); | ||
| process.exit(1); | ||
| }); | ||
| setTimeout(() => { | ||
| socket.destroy(); | ||
| fs.writeFileSync('${tmpFile}', JSON.stringify({ error: 'timeout' })); | ||
| }, ${this.timeout}); | ||
| `; | ||
@@ -572,2 +639,215 @@ try { | ||
| // libs/shield-interceptor/src/seatbelt/profile-manager.ts | ||
| var fs3 = __toESM(require("node:fs"), 1); | ||
| var crypto = __toESM(require("node:crypto"), 1); | ||
| var path = __toESM(require("node:path"), 1); | ||
| var _mkdirSync = fs3.mkdirSync.bind(fs3); | ||
| var _writeFileSync = fs3.writeFileSync.bind(fs3); | ||
| var _existsSync2 = fs3.existsSync.bind(fs3); | ||
| var _readFileSync2 = fs3.readFileSync.bind(fs3); | ||
| var _readdirSync2 = fs3.readdirSync.bind(fs3); | ||
| var _statSync2 = fs3.statSync.bind(fs3); | ||
| var _unlinkSync2 = fs3.unlinkSync.bind(fs3); | ||
| var _chmodSync = fs3.chmodSync.bind(fs3); | ||
| var ProfileManager = class { | ||
| profileDir; | ||
| ensuredDir = false; | ||
| constructor(profileDir) { | ||
| this.profileDir = profileDir; | ||
| } | ||
| /** | ||
| * Get or create a profile file on disk. Returns the absolute path. | ||
| * Uses content-hash naming so identical configs reuse the same file. | ||
| */ | ||
| getOrCreateProfile(content) { | ||
| this.ensureDir(); | ||
| const hash = crypto.createHash("sha256").update(content).digest("hex").slice(0, 16); | ||
| const profilePath = path.join(this.profileDir, `sb-${hash}.sb`); | ||
| if (!_existsSync2(profilePath)) { | ||
| debugLog(`profile-manager: writing new profile ${profilePath} (${content.length} bytes)`); | ||
| _writeFileSync(profilePath, content, { mode: 420 }); | ||
| } | ||
| return profilePath; | ||
| } | ||
| /** | ||
| * Generate an SBPL profile from a SandboxConfig. | ||
| */ | ||
| generateProfile(sandbox) { | ||
| if (sandbox.profileContent) { | ||
| return sandbox.profileContent; | ||
| } | ||
| const lines = [ | ||
| ";; AgenShield dynamic seatbelt profile", | ||
| `;; Generated: ${(/* @__PURE__ */ new Date()).toISOString()}`, | ||
| "(version 1)", | ||
| "(deny default)", | ||
| "" | ||
| ]; | ||
| lines.push( | ||
| ";; Filesystem: reads allowed, writes restricted", | ||
| "(allow file-read*)", | ||
| "" | ||
| ); | ||
| const writePaths = ["/tmp", "/private/tmp", "/var/folders"]; | ||
| if (sandbox.allowedWritePaths.length > 0) { | ||
| writePaths.push(...sandbox.allowedWritePaths); | ||
| } | ||
| lines.push("(allow file-write*"); | ||
| for (const p of writePaths) { | ||
| lines.push(` (subpath "${this.escapeSbpl(p)}")`); | ||
| } | ||
| lines.push(")"); | ||
| lines.push(""); | ||
| lines.push("(allow file-write*"); | ||
| lines.push(' (literal "/dev/null")'); | ||
| lines.push(' (literal "/dev/zero")'); | ||
| lines.push(' (literal "/dev/random")'); | ||
| lines.push(' (literal "/dev/urandom")'); | ||
| lines.push(")"); | ||
| lines.push(""); | ||
| if (sandbox.deniedPaths.length > 0) { | ||
| lines.push(";; Denied paths"); | ||
| for (const p of sandbox.deniedPaths) { | ||
| lines.push(`(deny file-read* file-write* (subpath "${this.escapeSbpl(p)}"))`); | ||
| } | ||
| lines.push(""); | ||
| } | ||
| lines.push(";; Binary execution (system directories allowed as subpaths)"); | ||
| lines.push("(allow process-exec"); | ||
| lines.push(' (subpath "/bin")'); | ||
| lines.push(' (subpath "/sbin")'); | ||
| lines.push(' (subpath "/usr/bin")'); | ||
| lines.push(' (subpath "/usr/sbin")'); | ||
| lines.push(' (subpath "/usr/local/bin")'); | ||
| lines.push(' (subpath "/opt/agenshield/bin")'); | ||
| const coveredSubpaths = ["/bin/", "/sbin/", "/usr/bin/", "/usr/sbin/", "/usr/local/bin/", "/opt/agenshield/bin/"]; | ||
| const home = process.env["HOME"]; | ||
| if (home) { | ||
| lines.push(` (subpath "${this.escapeSbpl(home)}/bin")`); | ||
| lines.push(` (subpath "${this.escapeSbpl(home)}/homebrew")`); | ||
| coveredSubpaths.push(`${home}/bin/`, `${home}/homebrew/`); | ||
| } | ||
| const nvmDir = process.env["NVM_DIR"] || (home ? `${home}/.nvm` : null); | ||
| if (nvmDir) { | ||
| lines.push(` (subpath "${this.escapeSbpl(nvmDir)}")`); | ||
| coveredSubpaths.push(`${nvmDir}/`); | ||
| } | ||
| const brewPrefix = process.env["HOMEBREW_PREFIX"]; | ||
| if (brewPrefix && (!home || !brewPrefix.startsWith(home))) { | ||
| lines.push(` (subpath "${this.escapeSbpl(brewPrefix)}/bin")`); | ||
| lines.push(` (subpath "${this.escapeSbpl(brewPrefix)}/lib")`); | ||
| coveredSubpaths.push(`${brewPrefix}/bin/`, `${brewPrefix}/lib/`); | ||
| } | ||
| const uniqueBinaries = [...new Set(sandbox.allowedBinaries)]; | ||
| for (const bin of uniqueBinaries) { | ||
| if (coveredSubpaths.some((dir) => bin === dir || bin.startsWith(dir))) continue; | ||
| if (bin.endsWith("/")) { | ||
| lines.push(` (subpath "${this.escapeSbpl(bin)}")`); | ||
| } else { | ||
| lines.push(` (literal "${this.escapeSbpl(bin)}")`); | ||
| } | ||
| } | ||
| lines.push(")"); | ||
| lines.push(""); | ||
| const uniqueDenied = [...new Set(sandbox.deniedBinaries)]; | ||
| if (uniqueDenied.length > 0) { | ||
| lines.push(";; Denied binaries"); | ||
| for (const bin of uniqueDenied) { | ||
| lines.push(`(deny process-exec (literal "${this.escapeSbpl(bin)}"))`); | ||
| } | ||
| lines.push(""); | ||
| } | ||
| lines.push(";; Network"); | ||
| if (sandbox.networkAllowed) { | ||
| if (sandbox.allowedHosts.length > 0 || sandbox.allowedPorts.length > 0) { | ||
| lines.push(";; Allow specific network targets"); | ||
| for (const host of sandbox.allowedHosts) { | ||
| lines.push(`(allow network-outbound (remote tcp "${this.escapeSbpl(host)}:*"))`); | ||
| } | ||
| for (const port of sandbox.allowedPorts) { | ||
| lines.push(`(allow network-outbound (remote tcp "*:${port}"))`); | ||
| } | ||
| const isLocalhostOnly = sandbox.allowedHosts.length > 0 && sandbox.allowedHosts.every((h) => h === "localhost" || h === "127.0.0.1"); | ||
| if (!isLocalhostOnly) { | ||
| lines.push('(allow network-outbound (remote udp "*:53") (remote tcp "*:53"))'); | ||
| } | ||
| } else { | ||
| lines.push("(allow network*)"); | ||
| } | ||
| } else { | ||
| lines.push("(deny network*)"); | ||
| } | ||
| lines.push(""); | ||
| lines.push( | ||
| ";; Broker / local unix sockets", | ||
| "(allow network-outbound (remote unix))", | ||
| "(allow network-inbound (local unix))", | ||
| "(allow file-read* file-write*", | ||
| ' (subpath "/var/run/agenshield")', | ||
| ' (subpath "/private/var/run/agenshield"))', | ||
| "" | ||
| ); | ||
| lines.push( | ||
| ";; Process management", | ||
| "(allow process-fork)", | ||
| "(allow signal (target self))", | ||
| "(allow sysctl-read)", | ||
| "" | ||
| ); | ||
| lines.push( | ||
| ";; Mach IPC", | ||
| "(allow mach-lookup)", | ||
| "" | ||
| ); | ||
| return lines.join("\n"); | ||
| } | ||
| /** | ||
| * Remove stale profile files older than maxAgeMs. | ||
| */ | ||
| cleanup(maxAgeMs) { | ||
| if (!_existsSync2(this.profileDir)) return; | ||
| try { | ||
| const now = Date.now(); | ||
| const entries = _readdirSync2(this.profileDir); | ||
| for (const entry of entries) { | ||
| if (!entry.endsWith(".sb")) continue; | ||
| const filePath = path.join(this.profileDir, entry); | ||
| try { | ||
| const stat = _statSync2(filePath); | ||
| if (now - stat.mtimeMs > maxAgeMs) { | ||
| _unlinkSync2(filePath); | ||
| debugLog(`profile-manager: cleaned up stale profile ${filePath}`); | ||
| } | ||
| } catch { | ||
| } | ||
| } | ||
| } catch { | ||
| } | ||
| } | ||
| /** | ||
| * Escape a string for safe inclusion in SBPL | ||
| */ | ||
| escapeSbpl(s) { | ||
| return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); | ||
| } | ||
| /** | ||
| * Ensure the profile directory exists | ||
| */ | ||
| ensureDir() { | ||
| if (this.ensuredDir) return; | ||
| if (!_existsSync2(this.profileDir)) { | ||
| _mkdirSync(this.profileDir, { recursive: true, mode: 1023 }); | ||
| } else { | ||
| try { | ||
| const stat = _statSync2(this.profileDir); | ||
| if ((stat.mode & 511) !== 511) { | ||
| _chmodSync(this.profileDir, 1023); | ||
| } | ||
| } catch { | ||
| } | ||
| } | ||
| this.ensuredDir = true; | ||
| } | ||
| }; | ||
| // libs/shield-interceptor/src/interceptors/child-process.ts | ||
@@ -578,2 +858,5 @@ var childProcessModule = require("node:child_process"); | ||
| _checking = false; | ||
| _executing = false; | ||
| // Guards exec→execFile re-entrancy | ||
| profileManager = null; | ||
| originalExec = null; | ||
@@ -587,9 +870,14 @@ originalExecSync = null; | ||
| super(options); | ||
| const config = this.interceptorConfig; | ||
| this.syncClient = new SyncClient({ | ||
| socketPath: "/var/run/agenshield/agenshield.sock", | ||
| httpHost: "localhost", | ||
| httpPort: 5201, | ||
| // Broker uses 5201 | ||
| timeout: 3e4 | ||
| socketPath: config?.socketPath || "/var/run/agenshield/agenshield.sock", | ||
| httpHost: config?.httpHost || "localhost", | ||
| httpPort: config?.httpPort || 5201, | ||
| timeout: config?.timeout || 3e4 | ||
| }); | ||
| if (config?.enableSeatbelt && process.platform === "darwin") { | ||
| this.profileManager = new ProfileManager( | ||
| config.seatbeltProfileDir || "/tmp/agenshield-profiles" | ||
| ); | ||
| } | ||
| } | ||
@@ -628,2 +916,146 @@ install() { | ||
| } | ||
| /** | ||
| * Build execution context from config for RPC calls | ||
| */ | ||
| getPolicyExecutionContext() { | ||
| const config = this.interceptorConfig; | ||
| return { | ||
| callerType: config?.contextType || "agent", | ||
| skillSlug: config?.contextSkillSlug, | ||
| agentId: config?.contextAgentId, | ||
| depth: 0 | ||
| }; | ||
| } | ||
| /** | ||
| * Synchronous policy check via SyncClient. | ||
| * Returns the full policy result (with sandbox config) or null if broker | ||
| * is unavailable and failOpen is true. | ||
| */ | ||
| syncPolicyCheck(fullCommand) { | ||
| this._checking = true; | ||
| try { | ||
| debugLog(`cp.syncPolicyCheck START command=${fullCommand}`); | ||
| const context = this.getPolicyExecutionContext(); | ||
| const result = this.syncClient.request( | ||
| "policy_check", | ||
| { operation: "exec", target: fullCommand, context } | ||
| ); | ||
| debugLog(`cp.syncPolicyCheck DONE allowed=${result.allowed} command=${fullCommand}`); | ||
| if (!result.allowed) { | ||
| throw new PolicyDeniedError(result.reason || "Operation denied by policy", { | ||
| operation: "exec", | ||
| target: fullCommand, | ||
| policyId: result.policyId | ||
| }); | ||
| } | ||
| return result; | ||
| } catch (error) { | ||
| if (error instanceof PolicyDeniedError) { | ||
| throw error; | ||
| } | ||
| debugLog(`cp.syncPolicyCheck ERROR: ${error.message} command=${fullCommand}`); | ||
| if (!this.failOpen) { | ||
| throw error; | ||
| } | ||
| return null; | ||
| } finally { | ||
| this._checking = false; | ||
| } | ||
| } | ||
| /** | ||
| * Create a restrictive default sandbox config for fail-open scenarios. | ||
| * No network, minimal fs — better than running completely unsandboxed. | ||
| */ | ||
| getFailOpenSandbox() { | ||
| return { | ||
| enabled: true, | ||
| allowedReadPaths: [], | ||
| allowedWritePaths: [], | ||
| deniedPaths: [], | ||
| networkAllowed: false, | ||
| allowedHosts: [], | ||
| allowedPorts: [], | ||
| allowedBinaries: [], | ||
| deniedBinaries: [], | ||
| envInjection: {}, | ||
| envDeny: [] | ||
| }; | ||
| } | ||
| /** | ||
| * Resolve the sandbox config to use: from policy result, fail-open default, or null. | ||
| */ | ||
| resolveSandbox(policyResult) { | ||
| if (policyResult?.sandbox?.enabled) { | ||
| return policyResult.sandbox; | ||
| } | ||
| if (policyResult === null && this.profileManager) { | ||
| return this.getFailOpenSandbox(); | ||
| } | ||
| return null; | ||
| } | ||
| /** | ||
| * Wrap a command with sandbox-exec if seatbelt is enabled and sandbox config is present. | ||
| * Returns modified { command, args, options } for spawn-style calls. | ||
| */ | ||
| wrapWithSeatbelt(command, args, options, policyResult) { | ||
| const sandbox = this.resolveSandbox(policyResult); | ||
| if (!this.profileManager || !sandbox || process.platform !== "darwin") { | ||
| return { command, args, options }; | ||
| } | ||
| if (command === "/opt/agenshield/bin/node-bin" || command.endsWith("/node-bin")) { | ||
| debugLog(`cp.wrapWithSeatbelt: SKIP node-bin (already intercepted) command=${command}`); | ||
| return { command, args, options }; | ||
| } | ||
| if (command === "/usr/bin/sandbox-exec" || command.endsWith("/sandbox-exec")) { | ||
| debugLog(`cp.wrapWithSeatbelt: SKIP already sandbox-exec command=${command}`); | ||
| return { command, args, options }; | ||
| } | ||
| debugLog(`cp.wrapWithSeatbelt: wrapping command=${command}`); | ||
| const profileContent = this.profileManager.generateProfile(sandbox); | ||
| const profilePath = this.profileManager.getOrCreateProfile(profileContent); | ||
| const env = { ...options?.env || process.env }; | ||
| if (sandbox.envInjection) { | ||
| Object.assign(env, sandbox.envInjection); | ||
| } | ||
| if (sandbox.envDeny) { | ||
| for (const key of sandbox.envDeny) { | ||
| delete env[key]; | ||
| } | ||
| } | ||
| return { | ||
| command: "/usr/bin/sandbox-exec", | ||
| args: ["-f", profilePath, command, ...args], | ||
| options: { ...options, env } | ||
| }; | ||
| } | ||
| /** | ||
| * Wrap a shell command string with sandbox-exec. | ||
| * For exec/execSync which take a full command string. | ||
| */ | ||
| wrapCommandStringWithSeatbelt(command, options, policyResult) { | ||
| const sandbox = this.resolveSandbox(policyResult); | ||
| if (!this.profileManager || !sandbox || process.platform !== "darwin") { | ||
| return { command, options }; | ||
| } | ||
| if (command.startsWith("/usr/bin/sandbox-exec ") || command.startsWith("sandbox-exec ")) { | ||
| debugLog(`cp.wrapCommandStringWithSeatbelt: SKIP already sandbox-exec command=${command}`); | ||
| return { command, options }; | ||
| } | ||
| debugLog(`cp.wrapCommandStringWithSeatbelt: wrapping command=${command}`); | ||
| const profileContent = this.profileManager.generateProfile(sandbox); | ||
| const profilePath = this.profileManager.getOrCreateProfile(profileContent); | ||
| const env = { ...options?.env || process.env }; | ||
| if (sandbox.envInjection) { | ||
| Object.assign(env, sandbox.envInjection); | ||
| } | ||
| if (sandbox.envDeny) { | ||
| for (const key of sandbox.envDeny) { | ||
| delete env[key]; | ||
| } | ||
| } | ||
| return { | ||
| command: `/usr/bin/sandbox-exec -f ${profilePath} ${command}`, | ||
| options: { ...options, env } | ||
| }; | ||
| } | ||
| createInterceptedExec() { | ||
@@ -634,4 +1066,5 @@ const self = this; | ||
| const callback = typeof args[args.length - 1] === "function" ? args.pop() : void 0; | ||
| debugLog(`cp.exec ENTER command=${command} _checking=${self._checking}`); | ||
| if (self._checking) { | ||
| const options = args[0]; | ||
| debugLog(`cp.exec ENTER command=${command} _checking=${self._checking} _executing=${self._executing}`); | ||
| if (self._checking || self._executing) { | ||
| debugLog(`cp.exec SKIP (re-entrancy) command=${command}`); | ||
@@ -641,10 +1074,22 @@ return original(command, ...args, callback); | ||
| self.eventReporter.intercept("exec", command); | ||
| self.checkPolicy("exec", command).then(() => { | ||
| original(command, ...args, callback); | ||
| }).catch((error) => { | ||
| let policyResult = null; | ||
| try { | ||
| policyResult = self.syncPolicyCheck(command); | ||
| } catch (error) { | ||
| if (callback) { | ||
| callback(error, "", ""); | ||
| process.nextTick(() => callback(error, "", "")); | ||
| } | ||
| }); | ||
| return original('echo ""'); | ||
| return original('echo ""'); | ||
| } | ||
| const wrapped = self.wrapCommandStringWithSeatbelt(command, options, policyResult); | ||
| debugLog(`cp.exec calling original command=${wrapped.command}`); | ||
| self._executing = true; | ||
| try { | ||
| if (wrapped.options) { | ||
| return original(wrapped.command, wrapped.options, callback); | ||
| } | ||
| return original(wrapped.command, callback); | ||
| } finally { | ||
| self._executing = false; | ||
| } | ||
| }; | ||
@@ -656,35 +1101,21 @@ } | ||
| const interceptedExecSync = function(command, options) { | ||
| debugLog(`cp.execSync ENTER command=${command} _checking=${self._checking}`); | ||
| if (self._checking) { | ||
| debugLog(`cp.execSync ENTER command=${command} _checking=${self._checking} _executing=${self._executing}`); | ||
| if (self._checking || self._executing) { | ||
| debugLog(`cp.execSync SKIP (re-entrancy) command=${command}`); | ||
| return original(command, options); | ||
| } | ||
| self._checking = true; | ||
| self.eventReporter.intercept("exec", command); | ||
| const policyResult = self.syncPolicyCheck(command); | ||
| const wrapped = self.wrapCommandStringWithSeatbelt( | ||
| command, | ||
| options, | ||
| policyResult | ||
| ); | ||
| debugLog(`cp.execSync calling original command=${wrapped.command}`); | ||
| self._executing = true; | ||
| try { | ||
| self.eventReporter.intercept("exec", command); | ||
| debugLog(`cp.execSync policy_check START command=${command}`); | ||
| const result = self.syncClient.request( | ||
| "policy_check", | ||
| { operation: "exec", target: command } | ||
| ); | ||
| debugLog(`cp.execSync policy_check DONE allowed=${result.allowed} command=${command}`); | ||
| if (!result.allowed) { | ||
| throw new PolicyDeniedError(result.reason || "Operation denied by policy", { | ||
| operation: "exec", | ||
| target: command | ||
| }); | ||
| } | ||
| } catch (error) { | ||
| debugLog(`cp.execSync policy_check ERROR: ${error.message} command=${command}`); | ||
| if (error instanceof PolicyDeniedError) { | ||
| throw error; | ||
| } | ||
| if (!self.failOpen) { | ||
| throw error; | ||
| } | ||
| return original(wrapped.command, wrapped.options); | ||
| } finally { | ||
| self._checking = false; | ||
| self._executing = false; | ||
| } | ||
| debugLog(`cp.execSync calling original command=${command}`); | ||
| return original(command, options); | ||
| }; | ||
@@ -698,13 +1129,27 @@ return interceptedExecSync; | ||
| const fullCmd = args ? `${command} ${args.join(" ")}` : command; | ||
| debugLog(`cp.spawn ENTER command=${fullCmd} _checking=${self._checking}`); | ||
| if (self._checking) { | ||
| debugLog(`cp.spawn ENTER command=${fullCmd} _checking=${self._checking} _executing=${self._executing}`); | ||
| if (self._checking || self._executing) { | ||
| debugLog(`cp.spawn SKIP (re-entrancy) command=${fullCmd}`); | ||
| return original(command, args, options || {}); | ||
| } | ||
| const fullCommand = args ? `${command} ${args.join(" ")}` : command; | ||
| self.eventReporter.intercept("exec", fullCommand); | ||
| self.checkPolicy("exec", fullCommand).catch((error) => { | ||
| self.eventReporter.error("exec", fullCommand, error.message); | ||
| }); | ||
| return original(command, args, options || {}); | ||
| self.eventReporter.intercept("exec", fullCmd); | ||
| let policyResult = null; | ||
| try { | ||
| policyResult = self.syncPolicyCheck(fullCmd); | ||
| } catch (error) { | ||
| debugLog(`cp.spawn DENIED command=${fullCmd}`); | ||
| const denied = original("false", [], { stdio: "pipe" }); | ||
| process.nextTick(() => { | ||
| denied.emit("error", error); | ||
| }); | ||
| return denied; | ||
| } | ||
| const wrapped = self.wrapWithSeatbelt( | ||
| command, | ||
| Array.from(args || []), | ||
| options, | ||
| policyResult | ||
| ); | ||
| debugLog(`cp.spawn calling original command=${wrapped.command} args=${wrapped.args.join(" ")}`); | ||
| return original(wrapped.command, wrapped.args, wrapped.options || {}); | ||
| }; | ||
@@ -718,45 +1163,37 @@ return interceptedSpawn; | ||
| const fullCommand = args ? `${command} ${args.join(" ")}` : command; | ||
| debugLog(`cp.spawnSync ENTER command=${fullCommand} _checking=${self._checking}`); | ||
| if (self._checking) { | ||
| debugLog(`cp.spawnSync ENTER command=${fullCommand} _checking=${self._checking} _executing=${self._executing}`); | ||
| if (self._checking || self._executing) { | ||
| debugLog(`cp.spawnSync SKIP (re-entrancy) command=${fullCommand}`); | ||
| return original(command, args, options); | ||
| } | ||
| self._checking = true; | ||
| self.eventReporter.intercept("exec", fullCommand); | ||
| let policyResult = null; | ||
| try { | ||
| self.eventReporter.intercept("exec", fullCommand); | ||
| debugLog(`cp.spawnSync policy_check START command=${fullCommand}`); | ||
| const result = self.syncClient.request( | ||
| "policy_check", | ||
| { operation: "exec", target: fullCommand } | ||
| ); | ||
| debugLog(`cp.spawnSync policy_check DONE allowed=${result.allowed} command=${fullCommand}`); | ||
| if (!result.allowed) { | ||
| return { | ||
| pid: -1, | ||
| output: [], | ||
| stdout: Buffer.alloc(0), | ||
| stderr: Buffer.from(result.reason || "Policy denied"), | ||
| status: 1, | ||
| signal: null, | ||
| error: new PolicyDeniedError(result.reason || "Policy denied") | ||
| }; | ||
| } | ||
| policyResult = self.syncPolicyCheck(fullCommand); | ||
| } catch (error) { | ||
| debugLog(`cp.spawnSync policy_check ERROR: ${error.message} command=${fullCommand}`); | ||
| if (!self.failOpen) { | ||
| return { | ||
| pid: -1, | ||
| output: [], | ||
| stdout: Buffer.alloc(0), | ||
| stderr: Buffer.from(error.message), | ||
| status: 1, | ||
| signal: null, | ||
| error | ||
| }; | ||
| } | ||
| } finally { | ||
| self._checking = false; | ||
| debugLog(`cp.spawnSync DENIED command=${fullCommand}`); | ||
| return { | ||
| pid: -1, | ||
| output: [], | ||
| stdout: Buffer.alloc(0), | ||
| stderr: Buffer.from( | ||
| error instanceof PolicyDeniedError ? error.message || "Policy denied" : error.message | ||
| ), | ||
| status: 1, | ||
| signal: null, | ||
| error | ||
| }; | ||
| } | ||
| debugLog(`cp.spawnSync calling original command=${fullCommand}`); | ||
| return original(command, args, options); | ||
| const wrapped = self.wrapWithSeatbelt( | ||
| command, | ||
| Array.from(args || []), | ||
| options, | ||
| policyResult | ||
| ); | ||
| debugLog(`cp.spawnSync calling original command=${wrapped.command}`); | ||
| return original( | ||
| wrapped.command, | ||
| wrapped.args, | ||
| wrapped.options | ||
| ); | ||
| }; | ||
@@ -767,11 +1204,48 @@ } | ||
| const original = this.originalExecFile; | ||
| return function interceptedExecFile(file, ...args) { | ||
| if (self._checking) { | ||
| return original(file, ...args); | ||
| return function interceptedExecFile(file, ...rest) { | ||
| if (self._checking || self._executing) { | ||
| return original(file, ...rest); | ||
| } | ||
| self.eventReporter.intercept("exec", file); | ||
| self.checkPolicy("exec", file).catch((error) => { | ||
| self.eventReporter.error("exec", file, error.message); | ||
| }); | ||
| return original(file, ...args); | ||
| let args = []; | ||
| let options; | ||
| let callback; | ||
| for (const arg of rest) { | ||
| if (typeof arg === "function") { | ||
| callback = arg; | ||
| } else if (Array.isArray(arg)) { | ||
| args = arg; | ||
| } else if (typeof arg === "object" && arg !== null) { | ||
| options = arg; | ||
| } | ||
| } | ||
| const fullCommand = args.length > 0 ? `${file} ${args.join(" ")}` : file; | ||
| debugLog(`cp.execFile ENTER command=${fullCommand}`); | ||
| self.eventReporter.intercept("exec", fullCommand); | ||
| let policyResult = null; | ||
| try { | ||
| policyResult = self.syncPolicyCheck(fullCommand); | ||
| } catch (error) { | ||
| if (callback) { | ||
| process.nextTick(() => callback(error, "", "")); | ||
| } | ||
| return original("false"); | ||
| } | ||
| const wrapped = self.wrapWithSeatbelt(file, args, options, policyResult); | ||
| debugLog(`cp.execFile calling original command=${wrapped.command}`); | ||
| if (callback) { | ||
| return original( | ||
| wrapped.command, | ||
| wrapped.args, | ||
| wrapped.options, | ||
| callback | ||
| ); | ||
| } | ||
| return original( | ||
| wrapped.command, | ||
| wrapped.args, | ||
| wrapped.options || {}, | ||
| // eslint-disable-next-line @typescript-eslint/no-empty-function | ||
| () => { | ||
| } | ||
| ); | ||
| }; | ||
@@ -783,10 +1257,34 @@ } | ||
| const interceptedFork = function(modulePath, args, options) { | ||
| if (self._checking) { | ||
| if (self._checking || self._executing) { | ||
| return original(modulePath, args, options); | ||
| } | ||
| const pathStr = modulePath.toString(); | ||
| self.eventReporter.intercept("exec", `fork:${pathStr}`); | ||
| self.checkPolicy("exec", `fork:${pathStr}`).catch((error) => { | ||
| self.eventReporter.error("exec", pathStr, error.message); | ||
| }); | ||
| const fullCommand = `fork:${pathStr}`; | ||
| debugLog(`cp.fork ENTER command=${fullCommand}`); | ||
| self.eventReporter.intercept("exec", fullCommand); | ||
| let policyResult = null; | ||
| try { | ||
| policyResult = self.syncPolicyCheck(fullCommand); | ||
| } catch (error) { | ||
| debugLog(`cp.fork DENIED command=${fullCommand}`); | ||
| const denied = self.originalSpawn("false", [], { stdio: "pipe" }); | ||
| process.nextTick(() => { | ||
| denied.emit("error", error); | ||
| }); | ||
| return denied; | ||
| } | ||
| if (policyResult?.sandbox) { | ||
| const sandbox = policyResult.sandbox; | ||
| const env = { ...options?.env || process.env }; | ||
| if (sandbox.envInjection) { | ||
| Object.assign(env, sandbox.envInjection); | ||
| } | ||
| if (sandbox.envDeny) { | ||
| for (const key of sandbox.envDeny) { | ||
| delete env[key]; | ||
| } | ||
| } | ||
| options = { ...options, env }; | ||
| } | ||
| debugLog(`cp.fork calling original module=${pathStr}`); | ||
| return original(modulePath, args, options); | ||
@@ -885,4 +1383,4 @@ }; | ||
| const self = this; | ||
| safeOverride(module2, methodName, function intercepted(path, ...args) { | ||
| const pathString = normalizePathArg(path); | ||
| safeOverride(module2, methodName, function intercepted(path2, ...args) { | ||
| const pathString = normalizePathArg(path2); | ||
| const callback = typeof args[args.length - 1] === "function" ? args.pop() : void 0; | ||
@@ -892,3 +1390,3 @@ debugLog(`fs.${methodName} ENTER (async) path=${pathString} _checking=${self._checking}`); | ||
| debugLog(`fs.${methodName} SKIP (re-entrancy, async) path=${pathString}`); | ||
| original.call(module2, path, ...args, callback); | ||
| original.call(module2, path2, ...args, callback); | ||
| return; | ||
@@ -899,3 +1397,3 @@ } | ||
| debugLog(`fs.${methodName} policy OK (async) path=${pathString}`); | ||
| original.call(module2, path, ...args, callback); | ||
| original.call(module2, path2, ...args, callback); | ||
| }).catch((error) => { | ||
@@ -915,8 +1413,8 @@ debugLog(`fs.${methodName} policy ERROR (async): ${error.message} path=${pathString}`); | ||
| const self = this; | ||
| safeOverride(module2, methodName, function interceptedSync(path, ...args) { | ||
| const pathString = normalizePathArg(path); | ||
| safeOverride(module2, methodName, function interceptedSync(path2, ...args) { | ||
| const pathString = normalizePathArg(path2); | ||
| debugLog(`fs.${methodName} ENTER path=${pathString} _checking=${self._checking}`); | ||
| if (self._checking) { | ||
| debugLog(`fs.${methodName} SKIP (re-entrancy) path=${pathString}`); | ||
| return original.call(module2, path, ...args); | ||
| return original.call(module2, path2, ...args); | ||
| } | ||
@@ -950,3 +1448,3 @@ self._checking = true; | ||
| debugLog(`fs.${methodName} calling original path=${pathString}`); | ||
| return original.call(module2, path, ...args); | ||
| return original.call(module2, path2, ...args); | ||
| }); | ||
@@ -960,8 +1458,8 @@ } | ||
| const self = this; | ||
| safeOverride(module2, methodName, async function interceptedPromise(path, ...args) { | ||
| const pathString = normalizePathArg(path); | ||
| safeOverride(module2, methodName, async function interceptedPromise(path2, ...args) { | ||
| const pathString = normalizePathArg(path2); | ||
| debugLog(`fsPromises.${methodName} ENTER path=${pathString} _checking=${self._checking}`); | ||
| if (self._checking) { | ||
| debugLog(`fsPromises.${methodName} SKIP (re-entrancy) path=${pathString}`); | ||
| return original.call(module2, path, ...args); | ||
| return original.call(module2, path2, ...args); | ||
| } | ||
@@ -971,3 +1469,3 @@ self.eventReporter.intercept(operation, pathString); | ||
| debugLog(`fsPromises.${methodName} policy OK path=${pathString}`); | ||
| return original.call(module2, path, ...args); | ||
| return original.call(module2, path2, ...args); | ||
| }); | ||
@@ -1116,7 +1614,7 @@ } | ||
| */ | ||
| async check(operation, target) { | ||
| async check(operation, target, context) { | ||
| try { | ||
| const result = await this.client.request( | ||
| "policy_check", | ||
| { operation, target } | ||
| { operation, target, context } | ||
| ); | ||
@@ -1165,3 +1663,7 @@ return result; | ||
| const prefix = event.type === "allow" ? "\u2713" : event.type === "deny" ? "\u2717" : "\u2022"; | ||
| console[level](`[AgenShield] ${prefix} ${event.operation}: ${event.target}`); | ||
| let detail = `${prefix} ${event.operation}: ${event.target}`; | ||
| if (event.policyId) detail += ` [policy:${event.policyId}]`; | ||
| if (event.error) detail += ` [reason:${event.error}]`; | ||
| if (event.duration) detail += ` [${event.duration}ms]`; | ||
| console[level](`[AgenShield] ${detail}`); | ||
| } | ||
@@ -1286,2 +1788,9 @@ if (this.queue.length >= 100) { | ||
| const config = createConfig(configOverrides); | ||
| if (config.logLevel === "debug") { | ||
| try { | ||
| const safeConfig = { ...config }; | ||
| console.error("[AgenShield:config]", JSON.stringify(safeConfig, null, 2)); | ||
| } catch { | ||
| } | ||
| } | ||
| client = new AsyncClient({ | ||
@@ -1307,3 +1816,4 @@ socketPath: config.socketPath, | ||
| failOpen: config.failOpen, | ||
| brokerHttpPort: config.httpPort | ||
| brokerHttpPort: config.httpPort, | ||
| config | ||
| }); | ||
@@ -1341,3 +1851,4 @@ installed.fetch.install(); | ||
| failOpen: config.failOpen, | ||
| brokerHttpPort: config.httpPort | ||
| brokerHttpPort: config.httpPort, | ||
| config | ||
| }); | ||
@@ -1344,0 +1855,0 @@ installed.childProcess.install(); |
+677
-166
@@ -39,3 +39,8 @@ "use strict"; | ||
| interceptExec: env["AGENSHIELD_INTERCEPT_EXEC"] !== "false", | ||
| timeout: parseInt(env["AGENSHIELD_TIMEOUT"] || "30000", 10), | ||
| timeout: parseInt(env["AGENSHIELD_TIMEOUT"] || "5000", 10), | ||
| contextType: env["AGENSHIELD_CONTEXT_TYPE"] || "agent", | ||
| contextSkillSlug: env["AGENSHIELD_SKILL_SLUG"], | ||
| contextAgentId: env["AGENSHIELD_AGENT_ID"], | ||
| enableSeatbelt: env["AGENSHIELD_SEATBELT"] !== "false" && process.platform === "darwin", | ||
| seatbeltProfileDir: env["AGENSHIELD_SEATBELT_DIR"] || "/tmp/agenshield-profiles", | ||
| ...overrides | ||
@@ -84,9 +89,30 @@ }; | ||
| var _appendFileSync = fs.appendFileSync.bind(fs); | ||
| var _writeSync = fs.writeSync.bind(fs); | ||
| var LOG_PATH = "/var/log/agenshield/interceptor.log"; | ||
| var FALLBACK_LOG_PATH = "/tmp/agenshield-interceptor.log"; | ||
| var resolvedLogPath = null; | ||
| function getLogPath() { | ||
| if (resolvedLogPath !== null) return resolvedLogPath; | ||
| try { | ||
| _appendFileSync(LOG_PATH, ""); | ||
| resolvedLogPath = LOG_PATH; | ||
| } catch { | ||
| resolvedLogPath = FALLBACK_LOG_PATH; | ||
| } | ||
| return resolvedLogPath; | ||
| } | ||
| function debugLog(msg) { | ||
| const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [pid:${process.pid}] ${msg} | ||
| `; | ||
| try { | ||
| _appendFileSync(LOG_PATH, `[${(/* @__PURE__ */ new Date()).toISOString()}] [pid:${process.pid}] ${msg} | ||
| `); | ||
| _appendFileSync(getLogPath(), line); | ||
| } catch { | ||
| } | ||
| if (process.env["AGENSHIELD_LOG_LEVEL"] === "debug") { | ||
| try { | ||
| _writeSync(2, `[AgenShield:debug] ${msg} | ||
| `); | ||
| } catch { | ||
| } | ||
| } | ||
| } | ||
@@ -101,2 +127,3 @@ | ||
| installed = false; | ||
| interceptorConfig; | ||
| brokerHttpPort; | ||
@@ -109,2 +136,3 @@ constructor(options) { | ||
| this.brokerHttpPort = options.brokerHttpPort ?? 5201; | ||
| this.interceptorConfig = options.config; | ||
| } | ||
@@ -135,5 +163,18 @@ /** | ||
| /** | ||
| * Build execution context from config | ||
| */ | ||
| getBasePolicyExecutionContext() { | ||
| const config = this.interceptorConfig; | ||
| if (!config) return void 0; | ||
| return { | ||
| callerType: config.contextType || "agent", | ||
| skillSlug: config.contextSkillSlug, | ||
| agentId: config.contextAgentId, | ||
| depth: 0 | ||
| }; | ||
| } | ||
| /** | ||
| * Check policy and handle the result | ||
| */ | ||
| async checkPolicy(operation, target) { | ||
| async checkPolicy(operation, target, context) { | ||
| const startTime = Date.now(); | ||
@@ -143,3 +184,3 @@ debugLog(`base.checkPolicy START op=${operation} target=${target}`); | ||
| this.eventReporter.intercept(operation, target); | ||
| const result = await this.policyEvaluator.check(operation, target); | ||
| const result = await this.policyEvaluator.check(operation, target, context); | ||
| debugLog(`base.checkPolicy evaluator result op=${operation} target=${target} allowed=${result.allowed} policyId=${result.policyId}`); | ||
@@ -191,2 +232,15 @@ if (!result.allowed) { | ||
| } | ||
| /** | ||
| * Build execution context from config | ||
| */ | ||
| getPolicyExecutionContext() { | ||
| const config = this.interceptorConfig; | ||
| if (!config) return void 0; | ||
| return { | ||
| callerType: config.contextType || "agent", | ||
| skillSlug: config.contextSkillSlug, | ||
| agentId: config.contextAgentId, | ||
| depth: 0 | ||
| }; | ||
| } | ||
| install() { | ||
@@ -222,44 +276,6 @@ if (this.installed) return; | ||
| debugLog(`fetch checkPolicy START url=${url}`); | ||
| await this.checkPolicy("http_request", url); | ||
| debugLog(`fetch checkPolicy DONE url=${url}`); | ||
| try { | ||
| const method = init?.method || "GET"; | ||
| const headers = {}; | ||
| if (init?.headers) { | ||
| if (init.headers instanceof Headers) { | ||
| init.headers.forEach((value, key) => { | ||
| headers[key] = value; | ||
| }); | ||
| } else if (Array.isArray(init.headers)) { | ||
| for (const [key, value] of init.headers) { | ||
| headers[key] = value; | ||
| } | ||
| } else { | ||
| Object.assign(headers, init.headers); | ||
| } | ||
| } | ||
| let body; | ||
| if (init?.body) { | ||
| if (typeof init.body === "string") { | ||
| body = init.body; | ||
| } else if (init.body instanceof ArrayBuffer) { | ||
| body = Buffer.from(init.body).toString("base64"); | ||
| } else { | ||
| body = String(init.body); | ||
| } | ||
| } | ||
| const result = await this.client.request("http_request", { | ||
| url, | ||
| method, | ||
| headers, | ||
| body | ||
| }); | ||
| const responseHeaders = new Headers(result.headers); | ||
| return new Response(result.body, { | ||
| status: result.status, | ||
| statusText: result.statusText, | ||
| headers: responseHeaders | ||
| }); | ||
| await this.checkPolicy("http_request", url, this.getPolicyExecutionContext()); | ||
| debugLog(`fetch checkPolicy DONE url=${url}`); | ||
| } catch (error) { | ||
| debugLog(`fetch ERROR url=${url} error=${error.message}`); | ||
| if (error.name === "PolicyDeniedError") { | ||
@@ -274,2 +290,3 @@ throw error; | ||
| } | ||
| return this.originalFetch(input, init); | ||
| } | ||
@@ -419,2 +436,4 @@ }; | ||
| var _unlinkSync = fs2.unlinkSync.bind(fs2); | ||
| var _readdirSync = fs2.readdirSync.bind(fs2); | ||
| var _statSync = fs2.statSync.bind(fs2); | ||
| var _spawnSync = import_node_child_process.spawnSync; | ||
@@ -427,2 +446,4 @@ var _execSync = import_node_child_process.execSync; | ||
| timeout; | ||
| socketFailCount = 0; | ||
| socketSkipUntil = 0; | ||
| constructor(options) { | ||
@@ -433,4 +454,26 @@ this.socketPath = options.socketPath; | ||
| this.timeout = options.timeout; | ||
| this.cleanupStaleTmpFiles(); | ||
| } | ||
| /** | ||
| * Remove stale /tmp/agenshield-sync-*.json files from previous runs | ||
| */ | ||
| cleanupStaleTmpFiles() { | ||
| try { | ||
| const tmpDir = "/tmp"; | ||
| const files = _readdirSync(tmpDir); | ||
| const cutoff = Date.now() - 5 * 60 * 1e3; | ||
| for (const f of files) { | ||
| if (f.startsWith("agenshield-sync-") && f.endsWith(".json")) { | ||
| const fp = `${tmpDir}/${f}`; | ||
| try { | ||
| const stat = _statSync(fp); | ||
| if (stat.mtimeMs < cutoff) _unlinkSync(fp); | ||
| } catch { | ||
| } | ||
| } | ||
| } | ||
| } catch { | ||
| } | ||
| } | ||
| /** | ||
| * Send a synchronous request to the broker | ||
@@ -440,7 +483,20 @@ */ | ||
| debugLog(`syncClient.request START method=${method}`); | ||
| const now = Date.now(); | ||
| if (now < this.socketSkipUntil) { | ||
| debugLog(`syncClient.request SKIP socket (circuit open for ${this.socketSkipUntil - now}ms), using HTTP`); | ||
| const result = this.httpRequestSync(method, params); | ||
| debugLog(`syncClient.request http OK method=${method}`); | ||
| return result; | ||
| } | ||
| try { | ||
| const result = this.socketRequestSync(method, params); | ||
| this.socketFailCount = 0; | ||
| debugLog(`syncClient.request socket OK method=${method}`); | ||
| return result; | ||
| } catch (socketErr) { | ||
| this.socketFailCount++; | ||
| if (this.socketFailCount >= 2) { | ||
| this.socketSkipUntil = Date.now() + 6e4; | ||
| debugLog(`syncClient.request socket circuit OPEN (${this.socketFailCount} failures)`); | ||
| } | ||
| debugLog(`syncClient.request socket FAILED: ${socketErr.message}, trying HTTP`); | ||
@@ -470,5 +526,14 @@ const result = this.httpRequestSync(method, params); | ||
| let done = false; | ||
| const socket = net.createConnection('${this.socketPath}'); | ||
| let data = ''; | ||
| const timer = setTimeout(() => { | ||
| if (done) return; | ||
| done = true; | ||
| socket.destroy(); | ||
| fs.writeFileSync('${tmpFile}', JSON.stringify({ error: 'timeout' })); | ||
| process.exit(1); | ||
| }, ${this.timeout}); | ||
| socket.on('connect', () => { | ||
@@ -480,5 +545,8 @@ socket.write(${JSON.stringify(request)}); | ||
| data += chunk.toString(); | ||
| if (data.includes('\\n')) { | ||
| if (data.includes('\\n') && !done) { | ||
| done = true; | ||
| clearTimeout(timer); | ||
| socket.end(); | ||
| fs.writeFileSync('${tmpFile}', data.split('\\n')[0]); | ||
| process.exit(0); | ||
| } | ||
@@ -488,9 +556,8 @@ }); | ||
| socket.on('error', (err) => { | ||
| if (done) return; | ||
| done = true; | ||
| clearTimeout(timer); | ||
| fs.writeFileSync('${tmpFile}', JSON.stringify({ error: err.message })); | ||
| process.exit(1); | ||
| }); | ||
| setTimeout(() => { | ||
| socket.destroy(); | ||
| fs.writeFileSync('${tmpFile}', JSON.stringify({ error: 'timeout' })); | ||
| }, ${this.timeout}); | ||
| `; | ||
@@ -572,2 +639,215 @@ try { | ||
| // libs/shield-interceptor/src/seatbelt/profile-manager.ts | ||
| var fs3 = __toESM(require("node:fs"), 1); | ||
| var crypto = __toESM(require("node:crypto"), 1); | ||
| var path = __toESM(require("node:path"), 1); | ||
| var _mkdirSync = fs3.mkdirSync.bind(fs3); | ||
| var _writeFileSync = fs3.writeFileSync.bind(fs3); | ||
| var _existsSync2 = fs3.existsSync.bind(fs3); | ||
| var _readFileSync2 = fs3.readFileSync.bind(fs3); | ||
| var _readdirSync2 = fs3.readdirSync.bind(fs3); | ||
| var _statSync2 = fs3.statSync.bind(fs3); | ||
| var _unlinkSync2 = fs3.unlinkSync.bind(fs3); | ||
| var _chmodSync = fs3.chmodSync.bind(fs3); | ||
| var ProfileManager = class { | ||
| profileDir; | ||
| ensuredDir = false; | ||
| constructor(profileDir) { | ||
| this.profileDir = profileDir; | ||
| } | ||
| /** | ||
| * Get or create a profile file on disk. Returns the absolute path. | ||
| * Uses content-hash naming so identical configs reuse the same file. | ||
| */ | ||
| getOrCreateProfile(content) { | ||
| this.ensureDir(); | ||
| const hash = crypto.createHash("sha256").update(content).digest("hex").slice(0, 16); | ||
| const profilePath = path.join(this.profileDir, `sb-${hash}.sb`); | ||
| if (!_existsSync2(profilePath)) { | ||
| debugLog(`profile-manager: writing new profile ${profilePath} (${content.length} bytes)`); | ||
| _writeFileSync(profilePath, content, { mode: 420 }); | ||
| } | ||
| return profilePath; | ||
| } | ||
| /** | ||
| * Generate an SBPL profile from a SandboxConfig. | ||
| */ | ||
| generateProfile(sandbox) { | ||
| if (sandbox.profileContent) { | ||
| return sandbox.profileContent; | ||
| } | ||
| const lines = [ | ||
| ";; AgenShield dynamic seatbelt profile", | ||
| `;; Generated: ${(/* @__PURE__ */ new Date()).toISOString()}`, | ||
| "(version 1)", | ||
| "(deny default)", | ||
| "" | ||
| ]; | ||
| lines.push( | ||
| ";; Filesystem: reads allowed, writes restricted", | ||
| "(allow file-read*)", | ||
| "" | ||
| ); | ||
| const writePaths = ["/tmp", "/private/tmp", "/var/folders"]; | ||
| if (sandbox.allowedWritePaths.length > 0) { | ||
| writePaths.push(...sandbox.allowedWritePaths); | ||
| } | ||
| lines.push("(allow file-write*"); | ||
| for (const p of writePaths) { | ||
| lines.push(` (subpath "${this.escapeSbpl(p)}")`); | ||
| } | ||
| lines.push(")"); | ||
| lines.push(""); | ||
| lines.push("(allow file-write*"); | ||
| lines.push(' (literal "/dev/null")'); | ||
| lines.push(' (literal "/dev/zero")'); | ||
| lines.push(' (literal "/dev/random")'); | ||
| lines.push(' (literal "/dev/urandom")'); | ||
| lines.push(")"); | ||
| lines.push(""); | ||
| if (sandbox.deniedPaths.length > 0) { | ||
| lines.push(";; Denied paths"); | ||
| for (const p of sandbox.deniedPaths) { | ||
| lines.push(`(deny file-read* file-write* (subpath "${this.escapeSbpl(p)}"))`); | ||
| } | ||
| lines.push(""); | ||
| } | ||
| lines.push(";; Binary execution (system directories allowed as subpaths)"); | ||
| lines.push("(allow process-exec"); | ||
| lines.push(' (subpath "/bin")'); | ||
| lines.push(' (subpath "/sbin")'); | ||
| lines.push(' (subpath "/usr/bin")'); | ||
| lines.push(' (subpath "/usr/sbin")'); | ||
| lines.push(' (subpath "/usr/local/bin")'); | ||
| lines.push(' (subpath "/opt/agenshield/bin")'); | ||
| const coveredSubpaths = ["/bin/", "/sbin/", "/usr/bin/", "/usr/sbin/", "/usr/local/bin/", "/opt/agenshield/bin/"]; | ||
| const home = process.env["HOME"]; | ||
| if (home) { | ||
| lines.push(` (subpath "${this.escapeSbpl(home)}/bin")`); | ||
| lines.push(` (subpath "${this.escapeSbpl(home)}/homebrew")`); | ||
| coveredSubpaths.push(`${home}/bin/`, `${home}/homebrew/`); | ||
| } | ||
| const nvmDir = process.env["NVM_DIR"] || (home ? `${home}/.nvm` : null); | ||
| if (nvmDir) { | ||
| lines.push(` (subpath "${this.escapeSbpl(nvmDir)}")`); | ||
| coveredSubpaths.push(`${nvmDir}/`); | ||
| } | ||
| const brewPrefix = process.env["HOMEBREW_PREFIX"]; | ||
| if (brewPrefix && (!home || !brewPrefix.startsWith(home))) { | ||
| lines.push(` (subpath "${this.escapeSbpl(brewPrefix)}/bin")`); | ||
| lines.push(` (subpath "${this.escapeSbpl(brewPrefix)}/lib")`); | ||
| coveredSubpaths.push(`${brewPrefix}/bin/`, `${brewPrefix}/lib/`); | ||
| } | ||
| const uniqueBinaries = [...new Set(sandbox.allowedBinaries)]; | ||
| for (const bin of uniqueBinaries) { | ||
| if (coveredSubpaths.some((dir) => bin === dir || bin.startsWith(dir))) continue; | ||
| if (bin.endsWith("/")) { | ||
| lines.push(` (subpath "${this.escapeSbpl(bin)}")`); | ||
| } else { | ||
| lines.push(` (literal "${this.escapeSbpl(bin)}")`); | ||
| } | ||
| } | ||
| lines.push(")"); | ||
| lines.push(""); | ||
| const uniqueDenied = [...new Set(sandbox.deniedBinaries)]; | ||
| if (uniqueDenied.length > 0) { | ||
| lines.push(";; Denied binaries"); | ||
| for (const bin of uniqueDenied) { | ||
| lines.push(`(deny process-exec (literal "${this.escapeSbpl(bin)}"))`); | ||
| } | ||
| lines.push(""); | ||
| } | ||
| lines.push(";; Network"); | ||
| if (sandbox.networkAllowed) { | ||
| if (sandbox.allowedHosts.length > 0 || sandbox.allowedPorts.length > 0) { | ||
| lines.push(";; Allow specific network targets"); | ||
| for (const host of sandbox.allowedHosts) { | ||
| lines.push(`(allow network-outbound (remote tcp "${this.escapeSbpl(host)}:*"))`); | ||
| } | ||
| for (const port of sandbox.allowedPorts) { | ||
| lines.push(`(allow network-outbound (remote tcp "*:${port}"))`); | ||
| } | ||
| const isLocalhostOnly = sandbox.allowedHosts.length > 0 && sandbox.allowedHosts.every((h) => h === "localhost" || h === "127.0.0.1"); | ||
| if (!isLocalhostOnly) { | ||
| lines.push('(allow network-outbound (remote udp "*:53") (remote tcp "*:53"))'); | ||
| } | ||
| } else { | ||
| lines.push("(allow network*)"); | ||
| } | ||
| } else { | ||
| lines.push("(deny network*)"); | ||
| } | ||
| lines.push(""); | ||
| lines.push( | ||
| ";; Broker / local unix sockets", | ||
| "(allow network-outbound (remote unix))", | ||
| "(allow network-inbound (local unix))", | ||
| "(allow file-read* file-write*", | ||
| ' (subpath "/var/run/agenshield")', | ||
| ' (subpath "/private/var/run/agenshield"))', | ||
| "" | ||
| ); | ||
| lines.push( | ||
| ";; Process management", | ||
| "(allow process-fork)", | ||
| "(allow signal (target self))", | ||
| "(allow sysctl-read)", | ||
| "" | ||
| ); | ||
| lines.push( | ||
| ";; Mach IPC", | ||
| "(allow mach-lookup)", | ||
| "" | ||
| ); | ||
| return lines.join("\n"); | ||
| } | ||
| /** | ||
| * Remove stale profile files older than maxAgeMs. | ||
| */ | ||
| cleanup(maxAgeMs) { | ||
| if (!_existsSync2(this.profileDir)) return; | ||
| try { | ||
| const now = Date.now(); | ||
| const entries = _readdirSync2(this.profileDir); | ||
| for (const entry of entries) { | ||
| if (!entry.endsWith(".sb")) continue; | ||
| const filePath = path.join(this.profileDir, entry); | ||
| try { | ||
| const stat = _statSync2(filePath); | ||
| if (now - stat.mtimeMs > maxAgeMs) { | ||
| _unlinkSync2(filePath); | ||
| debugLog(`profile-manager: cleaned up stale profile ${filePath}`); | ||
| } | ||
| } catch { | ||
| } | ||
| } | ||
| } catch { | ||
| } | ||
| } | ||
| /** | ||
| * Escape a string for safe inclusion in SBPL | ||
| */ | ||
| escapeSbpl(s) { | ||
| return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); | ||
| } | ||
| /** | ||
| * Ensure the profile directory exists | ||
| */ | ||
| ensureDir() { | ||
| if (this.ensuredDir) return; | ||
| if (!_existsSync2(this.profileDir)) { | ||
| _mkdirSync(this.profileDir, { recursive: true, mode: 1023 }); | ||
| } else { | ||
| try { | ||
| const stat = _statSync2(this.profileDir); | ||
| if ((stat.mode & 511) !== 511) { | ||
| _chmodSync(this.profileDir, 1023); | ||
| } | ||
| } catch { | ||
| } | ||
| } | ||
| this.ensuredDir = true; | ||
| } | ||
| }; | ||
| // libs/shield-interceptor/src/interceptors/child-process.ts | ||
@@ -578,2 +858,5 @@ var childProcessModule = require("node:child_process"); | ||
| _checking = false; | ||
| _executing = false; | ||
| // Guards exec→execFile re-entrancy | ||
| profileManager = null; | ||
| originalExec = null; | ||
@@ -587,9 +870,14 @@ originalExecSync = null; | ||
| super(options); | ||
| const config = this.interceptorConfig; | ||
| this.syncClient = new SyncClient({ | ||
| socketPath: "/var/run/agenshield/agenshield.sock", | ||
| httpHost: "localhost", | ||
| httpPort: 5201, | ||
| // Broker uses 5201 | ||
| timeout: 3e4 | ||
| socketPath: config?.socketPath || "/var/run/agenshield/agenshield.sock", | ||
| httpHost: config?.httpHost || "localhost", | ||
| httpPort: config?.httpPort || 5201, | ||
| timeout: config?.timeout || 3e4 | ||
| }); | ||
| if (config?.enableSeatbelt && process.platform === "darwin") { | ||
| this.profileManager = new ProfileManager( | ||
| config.seatbeltProfileDir || "/tmp/agenshield-profiles" | ||
| ); | ||
| } | ||
| } | ||
@@ -628,2 +916,146 @@ install() { | ||
| } | ||
| /** | ||
| * Build execution context from config for RPC calls | ||
| */ | ||
| getPolicyExecutionContext() { | ||
| const config = this.interceptorConfig; | ||
| return { | ||
| callerType: config?.contextType || "agent", | ||
| skillSlug: config?.contextSkillSlug, | ||
| agentId: config?.contextAgentId, | ||
| depth: 0 | ||
| }; | ||
| } | ||
| /** | ||
| * Synchronous policy check via SyncClient. | ||
| * Returns the full policy result (with sandbox config) or null if broker | ||
| * is unavailable and failOpen is true. | ||
| */ | ||
| syncPolicyCheck(fullCommand) { | ||
| this._checking = true; | ||
| try { | ||
| debugLog(`cp.syncPolicyCheck START command=${fullCommand}`); | ||
| const context = this.getPolicyExecutionContext(); | ||
| const result = this.syncClient.request( | ||
| "policy_check", | ||
| { operation: "exec", target: fullCommand, context } | ||
| ); | ||
| debugLog(`cp.syncPolicyCheck DONE allowed=${result.allowed} command=${fullCommand}`); | ||
| if (!result.allowed) { | ||
| throw new PolicyDeniedError(result.reason || "Operation denied by policy", { | ||
| operation: "exec", | ||
| target: fullCommand, | ||
| policyId: result.policyId | ||
| }); | ||
| } | ||
| return result; | ||
| } catch (error) { | ||
| if (error instanceof PolicyDeniedError) { | ||
| throw error; | ||
| } | ||
| debugLog(`cp.syncPolicyCheck ERROR: ${error.message} command=${fullCommand}`); | ||
| if (!this.failOpen) { | ||
| throw error; | ||
| } | ||
| return null; | ||
| } finally { | ||
| this._checking = false; | ||
| } | ||
| } | ||
| /** | ||
| * Create a restrictive default sandbox config for fail-open scenarios. | ||
| * No network, minimal fs — better than running completely unsandboxed. | ||
| */ | ||
| getFailOpenSandbox() { | ||
| return { | ||
| enabled: true, | ||
| allowedReadPaths: [], | ||
| allowedWritePaths: [], | ||
| deniedPaths: [], | ||
| networkAllowed: false, | ||
| allowedHosts: [], | ||
| allowedPorts: [], | ||
| allowedBinaries: [], | ||
| deniedBinaries: [], | ||
| envInjection: {}, | ||
| envDeny: [] | ||
| }; | ||
| } | ||
| /** | ||
| * Resolve the sandbox config to use: from policy result, fail-open default, or null. | ||
| */ | ||
| resolveSandbox(policyResult) { | ||
| if (policyResult?.sandbox?.enabled) { | ||
| return policyResult.sandbox; | ||
| } | ||
| if (policyResult === null && this.profileManager) { | ||
| return this.getFailOpenSandbox(); | ||
| } | ||
| return null; | ||
| } | ||
| /** | ||
| * Wrap a command with sandbox-exec if seatbelt is enabled and sandbox config is present. | ||
| * Returns modified { command, args, options } for spawn-style calls. | ||
| */ | ||
| wrapWithSeatbelt(command, args, options, policyResult) { | ||
| const sandbox = this.resolveSandbox(policyResult); | ||
| if (!this.profileManager || !sandbox || process.platform !== "darwin") { | ||
| return { command, args, options }; | ||
| } | ||
| if (command === "/opt/agenshield/bin/node-bin" || command.endsWith("/node-bin")) { | ||
| debugLog(`cp.wrapWithSeatbelt: SKIP node-bin (already intercepted) command=${command}`); | ||
| return { command, args, options }; | ||
| } | ||
| if (command === "/usr/bin/sandbox-exec" || command.endsWith("/sandbox-exec")) { | ||
| debugLog(`cp.wrapWithSeatbelt: SKIP already sandbox-exec command=${command}`); | ||
| return { command, args, options }; | ||
| } | ||
| debugLog(`cp.wrapWithSeatbelt: wrapping command=${command}`); | ||
| const profileContent = this.profileManager.generateProfile(sandbox); | ||
| const profilePath = this.profileManager.getOrCreateProfile(profileContent); | ||
| const env = { ...options?.env || process.env }; | ||
| if (sandbox.envInjection) { | ||
| Object.assign(env, sandbox.envInjection); | ||
| } | ||
| if (sandbox.envDeny) { | ||
| for (const key of sandbox.envDeny) { | ||
| delete env[key]; | ||
| } | ||
| } | ||
| return { | ||
| command: "/usr/bin/sandbox-exec", | ||
| args: ["-f", profilePath, command, ...args], | ||
| options: { ...options, env } | ||
| }; | ||
| } | ||
| /** | ||
| * Wrap a shell command string with sandbox-exec. | ||
| * For exec/execSync which take a full command string. | ||
| */ | ||
| wrapCommandStringWithSeatbelt(command, options, policyResult) { | ||
| const sandbox = this.resolveSandbox(policyResult); | ||
| if (!this.profileManager || !sandbox || process.platform !== "darwin") { | ||
| return { command, options }; | ||
| } | ||
| if (command.startsWith("/usr/bin/sandbox-exec ") || command.startsWith("sandbox-exec ")) { | ||
| debugLog(`cp.wrapCommandStringWithSeatbelt: SKIP already sandbox-exec command=${command}`); | ||
| return { command, options }; | ||
| } | ||
| debugLog(`cp.wrapCommandStringWithSeatbelt: wrapping command=${command}`); | ||
| const profileContent = this.profileManager.generateProfile(sandbox); | ||
| const profilePath = this.profileManager.getOrCreateProfile(profileContent); | ||
| const env = { ...options?.env || process.env }; | ||
| if (sandbox.envInjection) { | ||
| Object.assign(env, sandbox.envInjection); | ||
| } | ||
| if (sandbox.envDeny) { | ||
| for (const key of sandbox.envDeny) { | ||
| delete env[key]; | ||
| } | ||
| } | ||
| return { | ||
| command: `/usr/bin/sandbox-exec -f ${profilePath} ${command}`, | ||
| options: { ...options, env } | ||
| }; | ||
| } | ||
| createInterceptedExec() { | ||
@@ -634,4 +1066,5 @@ const self = this; | ||
| const callback = typeof args[args.length - 1] === "function" ? args.pop() : void 0; | ||
| debugLog(`cp.exec ENTER command=${command} _checking=${self._checking}`); | ||
| if (self._checking) { | ||
| const options = args[0]; | ||
| debugLog(`cp.exec ENTER command=${command} _checking=${self._checking} _executing=${self._executing}`); | ||
| if (self._checking || self._executing) { | ||
| debugLog(`cp.exec SKIP (re-entrancy) command=${command}`); | ||
@@ -641,10 +1074,22 @@ return original(command, ...args, callback); | ||
| self.eventReporter.intercept("exec", command); | ||
| self.checkPolicy("exec", command).then(() => { | ||
| original(command, ...args, callback); | ||
| }).catch((error) => { | ||
| let policyResult = null; | ||
| try { | ||
| policyResult = self.syncPolicyCheck(command); | ||
| } catch (error) { | ||
| if (callback) { | ||
| callback(error, "", ""); | ||
| process.nextTick(() => callback(error, "", "")); | ||
| } | ||
| }); | ||
| return original('echo ""'); | ||
| return original('echo ""'); | ||
| } | ||
| const wrapped = self.wrapCommandStringWithSeatbelt(command, options, policyResult); | ||
| debugLog(`cp.exec calling original command=${wrapped.command}`); | ||
| self._executing = true; | ||
| try { | ||
| if (wrapped.options) { | ||
| return original(wrapped.command, wrapped.options, callback); | ||
| } | ||
| return original(wrapped.command, callback); | ||
| } finally { | ||
| self._executing = false; | ||
| } | ||
| }; | ||
@@ -656,35 +1101,21 @@ } | ||
| const interceptedExecSync = function(command, options) { | ||
| debugLog(`cp.execSync ENTER command=${command} _checking=${self._checking}`); | ||
| if (self._checking) { | ||
| debugLog(`cp.execSync ENTER command=${command} _checking=${self._checking} _executing=${self._executing}`); | ||
| if (self._checking || self._executing) { | ||
| debugLog(`cp.execSync SKIP (re-entrancy) command=${command}`); | ||
| return original(command, options); | ||
| } | ||
| self._checking = true; | ||
| self.eventReporter.intercept("exec", command); | ||
| const policyResult = self.syncPolicyCheck(command); | ||
| const wrapped = self.wrapCommandStringWithSeatbelt( | ||
| command, | ||
| options, | ||
| policyResult | ||
| ); | ||
| debugLog(`cp.execSync calling original command=${wrapped.command}`); | ||
| self._executing = true; | ||
| try { | ||
| self.eventReporter.intercept("exec", command); | ||
| debugLog(`cp.execSync policy_check START command=${command}`); | ||
| const result = self.syncClient.request( | ||
| "policy_check", | ||
| { operation: "exec", target: command } | ||
| ); | ||
| debugLog(`cp.execSync policy_check DONE allowed=${result.allowed} command=${command}`); | ||
| if (!result.allowed) { | ||
| throw new PolicyDeniedError(result.reason || "Operation denied by policy", { | ||
| operation: "exec", | ||
| target: command | ||
| }); | ||
| } | ||
| } catch (error) { | ||
| debugLog(`cp.execSync policy_check ERROR: ${error.message} command=${command}`); | ||
| if (error instanceof PolicyDeniedError) { | ||
| throw error; | ||
| } | ||
| if (!self.failOpen) { | ||
| throw error; | ||
| } | ||
| return original(wrapped.command, wrapped.options); | ||
| } finally { | ||
| self._checking = false; | ||
| self._executing = false; | ||
| } | ||
| debugLog(`cp.execSync calling original command=${command}`); | ||
| return original(command, options); | ||
| }; | ||
@@ -698,13 +1129,27 @@ return interceptedExecSync; | ||
| const fullCmd = args ? `${command} ${args.join(" ")}` : command; | ||
| debugLog(`cp.spawn ENTER command=${fullCmd} _checking=${self._checking}`); | ||
| if (self._checking) { | ||
| debugLog(`cp.spawn ENTER command=${fullCmd} _checking=${self._checking} _executing=${self._executing}`); | ||
| if (self._checking || self._executing) { | ||
| debugLog(`cp.spawn SKIP (re-entrancy) command=${fullCmd}`); | ||
| return original(command, args, options || {}); | ||
| } | ||
| const fullCommand = args ? `${command} ${args.join(" ")}` : command; | ||
| self.eventReporter.intercept("exec", fullCommand); | ||
| self.checkPolicy("exec", fullCommand).catch((error) => { | ||
| self.eventReporter.error("exec", fullCommand, error.message); | ||
| }); | ||
| return original(command, args, options || {}); | ||
| self.eventReporter.intercept("exec", fullCmd); | ||
| let policyResult = null; | ||
| try { | ||
| policyResult = self.syncPolicyCheck(fullCmd); | ||
| } catch (error) { | ||
| debugLog(`cp.spawn DENIED command=${fullCmd}`); | ||
| const denied = original("false", [], { stdio: "pipe" }); | ||
| process.nextTick(() => { | ||
| denied.emit("error", error); | ||
| }); | ||
| return denied; | ||
| } | ||
| const wrapped = self.wrapWithSeatbelt( | ||
| command, | ||
| Array.from(args || []), | ||
| options, | ||
| policyResult | ||
| ); | ||
| debugLog(`cp.spawn calling original command=${wrapped.command} args=${wrapped.args.join(" ")}`); | ||
| return original(wrapped.command, wrapped.args, wrapped.options || {}); | ||
| }; | ||
@@ -718,45 +1163,37 @@ return interceptedSpawn; | ||
| const fullCommand = args ? `${command} ${args.join(" ")}` : command; | ||
| debugLog(`cp.spawnSync ENTER command=${fullCommand} _checking=${self._checking}`); | ||
| if (self._checking) { | ||
| debugLog(`cp.spawnSync ENTER command=${fullCommand} _checking=${self._checking} _executing=${self._executing}`); | ||
| if (self._checking || self._executing) { | ||
| debugLog(`cp.spawnSync SKIP (re-entrancy) command=${fullCommand}`); | ||
| return original(command, args, options); | ||
| } | ||
| self._checking = true; | ||
| self.eventReporter.intercept("exec", fullCommand); | ||
| let policyResult = null; | ||
| try { | ||
| self.eventReporter.intercept("exec", fullCommand); | ||
| debugLog(`cp.spawnSync policy_check START command=${fullCommand}`); | ||
| const result = self.syncClient.request( | ||
| "policy_check", | ||
| { operation: "exec", target: fullCommand } | ||
| ); | ||
| debugLog(`cp.spawnSync policy_check DONE allowed=${result.allowed} command=${fullCommand}`); | ||
| if (!result.allowed) { | ||
| return { | ||
| pid: -1, | ||
| output: [], | ||
| stdout: Buffer.alloc(0), | ||
| stderr: Buffer.from(result.reason || "Policy denied"), | ||
| status: 1, | ||
| signal: null, | ||
| error: new PolicyDeniedError(result.reason || "Policy denied") | ||
| }; | ||
| } | ||
| policyResult = self.syncPolicyCheck(fullCommand); | ||
| } catch (error) { | ||
| debugLog(`cp.spawnSync policy_check ERROR: ${error.message} command=${fullCommand}`); | ||
| if (!self.failOpen) { | ||
| return { | ||
| pid: -1, | ||
| output: [], | ||
| stdout: Buffer.alloc(0), | ||
| stderr: Buffer.from(error.message), | ||
| status: 1, | ||
| signal: null, | ||
| error | ||
| }; | ||
| } | ||
| } finally { | ||
| self._checking = false; | ||
| debugLog(`cp.spawnSync DENIED command=${fullCommand}`); | ||
| return { | ||
| pid: -1, | ||
| output: [], | ||
| stdout: Buffer.alloc(0), | ||
| stderr: Buffer.from( | ||
| error instanceof PolicyDeniedError ? error.message || "Policy denied" : error.message | ||
| ), | ||
| status: 1, | ||
| signal: null, | ||
| error | ||
| }; | ||
| } | ||
| debugLog(`cp.spawnSync calling original command=${fullCommand}`); | ||
| return original(command, args, options); | ||
| const wrapped = self.wrapWithSeatbelt( | ||
| command, | ||
| Array.from(args || []), | ||
| options, | ||
| policyResult | ||
| ); | ||
| debugLog(`cp.spawnSync calling original command=${wrapped.command}`); | ||
| return original( | ||
| wrapped.command, | ||
| wrapped.args, | ||
| wrapped.options | ||
| ); | ||
| }; | ||
@@ -767,11 +1204,48 @@ } | ||
| const original = this.originalExecFile; | ||
| return function interceptedExecFile(file, ...args) { | ||
| if (self._checking) { | ||
| return original(file, ...args); | ||
| return function interceptedExecFile(file, ...rest) { | ||
| if (self._checking || self._executing) { | ||
| return original(file, ...rest); | ||
| } | ||
| self.eventReporter.intercept("exec", file); | ||
| self.checkPolicy("exec", file).catch((error) => { | ||
| self.eventReporter.error("exec", file, error.message); | ||
| }); | ||
| return original(file, ...args); | ||
| let args = []; | ||
| let options; | ||
| let callback; | ||
| for (const arg of rest) { | ||
| if (typeof arg === "function") { | ||
| callback = arg; | ||
| } else if (Array.isArray(arg)) { | ||
| args = arg; | ||
| } else if (typeof arg === "object" && arg !== null) { | ||
| options = arg; | ||
| } | ||
| } | ||
| const fullCommand = args.length > 0 ? `${file} ${args.join(" ")}` : file; | ||
| debugLog(`cp.execFile ENTER command=${fullCommand}`); | ||
| self.eventReporter.intercept("exec", fullCommand); | ||
| let policyResult = null; | ||
| try { | ||
| policyResult = self.syncPolicyCheck(fullCommand); | ||
| } catch (error) { | ||
| if (callback) { | ||
| process.nextTick(() => callback(error, "", "")); | ||
| } | ||
| return original("false"); | ||
| } | ||
| const wrapped = self.wrapWithSeatbelt(file, args, options, policyResult); | ||
| debugLog(`cp.execFile calling original command=${wrapped.command}`); | ||
| if (callback) { | ||
| return original( | ||
| wrapped.command, | ||
| wrapped.args, | ||
| wrapped.options, | ||
| callback | ||
| ); | ||
| } | ||
| return original( | ||
| wrapped.command, | ||
| wrapped.args, | ||
| wrapped.options || {}, | ||
| // eslint-disable-next-line @typescript-eslint/no-empty-function | ||
| () => { | ||
| } | ||
| ); | ||
| }; | ||
@@ -783,10 +1257,34 @@ } | ||
| const interceptedFork = function(modulePath, args, options) { | ||
| if (self._checking) { | ||
| if (self._checking || self._executing) { | ||
| return original(modulePath, args, options); | ||
| } | ||
| const pathStr = modulePath.toString(); | ||
| self.eventReporter.intercept("exec", `fork:${pathStr}`); | ||
| self.checkPolicy("exec", `fork:${pathStr}`).catch((error) => { | ||
| self.eventReporter.error("exec", pathStr, error.message); | ||
| }); | ||
| const fullCommand = `fork:${pathStr}`; | ||
| debugLog(`cp.fork ENTER command=${fullCommand}`); | ||
| self.eventReporter.intercept("exec", fullCommand); | ||
| let policyResult = null; | ||
| try { | ||
| policyResult = self.syncPolicyCheck(fullCommand); | ||
| } catch (error) { | ||
| debugLog(`cp.fork DENIED command=${fullCommand}`); | ||
| const denied = self.originalSpawn("false", [], { stdio: "pipe" }); | ||
| process.nextTick(() => { | ||
| denied.emit("error", error); | ||
| }); | ||
| return denied; | ||
| } | ||
| if (policyResult?.sandbox) { | ||
| const sandbox = policyResult.sandbox; | ||
| const env = { ...options?.env || process.env }; | ||
| if (sandbox.envInjection) { | ||
| Object.assign(env, sandbox.envInjection); | ||
| } | ||
| if (sandbox.envDeny) { | ||
| for (const key of sandbox.envDeny) { | ||
| delete env[key]; | ||
| } | ||
| } | ||
| options = { ...options, env }; | ||
| } | ||
| debugLog(`cp.fork calling original module=${pathStr}`); | ||
| return original(modulePath, args, options); | ||
@@ -885,4 +1383,4 @@ }; | ||
| const self = this; | ||
| safeOverride(module2, methodName, function intercepted(path, ...args) { | ||
| const pathString = normalizePathArg(path); | ||
| safeOverride(module2, methodName, function intercepted(path2, ...args) { | ||
| const pathString = normalizePathArg(path2); | ||
| const callback = typeof args[args.length - 1] === "function" ? args.pop() : void 0; | ||
@@ -892,3 +1390,3 @@ debugLog(`fs.${methodName} ENTER (async) path=${pathString} _checking=${self._checking}`); | ||
| debugLog(`fs.${methodName} SKIP (re-entrancy, async) path=${pathString}`); | ||
| original.call(module2, path, ...args, callback); | ||
| original.call(module2, path2, ...args, callback); | ||
| return; | ||
@@ -899,3 +1397,3 @@ } | ||
| debugLog(`fs.${methodName} policy OK (async) path=${pathString}`); | ||
| original.call(module2, path, ...args, callback); | ||
| original.call(module2, path2, ...args, callback); | ||
| }).catch((error) => { | ||
@@ -915,8 +1413,8 @@ debugLog(`fs.${methodName} policy ERROR (async): ${error.message} path=${pathString}`); | ||
| const self = this; | ||
| safeOverride(module2, methodName, function interceptedSync(path, ...args) { | ||
| const pathString = normalizePathArg(path); | ||
| safeOverride(module2, methodName, function interceptedSync(path2, ...args) { | ||
| const pathString = normalizePathArg(path2); | ||
| debugLog(`fs.${methodName} ENTER path=${pathString} _checking=${self._checking}`); | ||
| if (self._checking) { | ||
| debugLog(`fs.${methodName} SKIP (re-entrancy) path=${pathString}`); | ||
| return original.call(module2, path, ...args); | ||
| return original.call(module2, path2, ...args); | ||
| } | ||
@@ -950,3 +1448,3 @@ self._checking = true; | ||
| debugLog(`fs.${methodName} calling original path=${pathString}`); | ||
| return original.call(module2, path, ...args); | ||
| return original.call(module2, path2, ...args); | ||
| }); | ||
@@ -960,8 +1458,8 @@ } | ||
| const self = this; | ||
| safeOverride(module2, methodName, async function interceptedPromise(path, ...args) { | ||
| const pathString = normalizePathArg(path); | ||
| safeOverride(module2, methodName, async function interceptedPromise(path2, ...args) { | ||
| const pathString = normalizePathArg(path2); | ||
| debugLog(`fsPromises.${methodName} ENTER path=${pathString} _checking=${self._checking}`); | ||
| if (self._checking) { | ||
| debugLog(`fsPromises.${methodName} SKIP (re-entrancy) path=${pathString}`); | ||
| return original.call(module2, path, ...args); | ||
| return original.call(module2, path2, ...args); | ||
| } | ||
@@ -971,3 +1469,3 @@ self.eventReporter.intercept(operation, pathString); | ||
| debugLog(`fsPromises.${methodName} policy OK path=${pathString}`); | ||
| return original.call(module2, path, ...args); | ||
| return original.call(module2, path2, ...args); | ||
| }); | ||
@@ -1116,7 +1614,7 @@ } | ||
| */ | ||
| async check(operation, target) { | ||
| async check(operation, target, context) { | ||
| try { | ||
| const result = await this.client.request( | ||
| "policy_check", | ||
| { operation, target } | ||
| { operation, target, context } | ||
| ); | ||
@@ -1165,3 +1663,7 @@ return result; | ||
| const prefix = event.type === "allow" ? "\u2713" : event.type === "deny" ? "\u2717" : "\u2022"; | ||
| console[level](`[AgenShield] ${prefix} ${event.operation}: ${event.target}`); | ||
| let detail = `${prefix} ${event.operation}: ${event.target}`; | ||
| if (event.policyId) detail += ` [policy:${event.policyId}]`; | ||
| if (event.error) detail += ` [reason:${event.error}]`; | ||
| if (event.duration) detail += ` [${event.duration}ms]`; | ||
| console[level](`[AgenShield] ${detail}`); | ||
| } | ||
@@ -1286,2 +1788,9 @@ if (this.queue.length >= 100) { | ||
| const config = createConfig(configOverrides); | ||
| if (config.logLevel === "debug") { | ||
| try { | ||
| const safeConfig = { ...config }; | ||
| console.error("[AgenShield:config]", JSON.stringify(safeConfig, null, 2)); | ||
| } catch { | ||
| } | ||
| } | ||
| client = new AsyncClient({ | ||
@@ -1307,3 +1816,4 @@ socketPath: config.socketPath, | ||
| failOpen: config.failOpen, | ||
| brokerHttpPort: config.httpPort | ||
| brokerHttpPort: config.httpPort, | ||
| config | ||
| }); | ||
@@ -1341,3 +1851,4 @@ installed.fetch.install(); | ||
| failOpen: config.failOpen, | ||
| brokerHttpPort: config.httpPort | ||
| brokerHttpPort: config.httpPort, | ||
| config | ||
| }); | ||
@@ -1344,0 +1855,0 @@ installed.childProcess.install(); |
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 4 instances in 1 package
229671
33.34%44
4.76%6129
36.32%29
262.5%+ Added
- Removed
Updated