@agenshield/broker 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/main.js CHANGED
@@ -251,6 +251,47 @@ function matchPattern(name, pattern) {
251
251
  // libs/shield-broker/src/handlers/exec.ts
252
252
  import * as path2 from "node:path";
253
253
  import { spawn } from "node:child_process";
254
+
255
+ // libs/shield-broker/src/daemon-forward.ts
256
+ var DAEMON_RPC_TIMEOUT = 2e3;
257
+ async function forwardPolicyToDaemon(operation, target, daemonUrl) {
258
+ try {
259
+ const controller = new AbortController();
260
+ const timeout = setTimeout(() => controller.abort(), DAEMON_RPC_TIMEOUT);
261
+ const response = await fetch(`${daemonUrl}/rpc`, {
262
+ method: "POST",
263
+ headers: { "Content-Type": "application/json" },
264
+ body: JSON.stringify({
265
+ jsonrpc: "2.0",
266
+ id: `broker-fwd-${Date.now()}`,
267
+ method: "policy_check",
268
+ params: { operation, target }
269
+ }),
270
+ signal: controller.signal
271
+ });
272
+ clearTimeout(timeout);
273
+ if (!response.ok) {
274
+ return null;
275
+ }
276
+ const json = await response.json();
277
+ if (json.error || !json.result) {
278
+ return null;
279
+ }
280
+ const result = json.result;
281
+ if (result.policyId) {
282
+ return {
283
+ allowed: !!result.allowed,
284
+ policyId: result.policyId,
285
+ reason: result.reason
286
+ };
287
+ }
288
+ return null;
289
+ } catch {
290
+ return null;
291
+ }
292
+ }
293
+
294
+ // libs/shield-broker/src/handlers/exec.ts
254
295
  var MAX_OUTPUT_SIZE = 10 * 1024 * 1024;
255
296
  var DEFAULT_WORKSPACE = "/Users/clawagent/workspace";
256
297
  var FS_COMMANDS = /* @__PURE__ */ new Set([
@@ -383,12 +424,16 @@ async function handleExec(params, context, deps) {
383
424
  if (url) {
384
425
  const networkCheck = await deps.policyEnforcer.check("http_request", { url }, context);
385
426
  if (!networkCheck.allowed) {
386
- const reason = `URL not allowed: ${url} - ${networkCheck.reason}`;
387
- deps.onExecDenied?.(command, reason);
388
- return {
389
- success: false,
390
- error: { code: 1009, message: reason }
391
- };
427
+ const daemonUrl = deps.daemonUrl || "http://127.0.0.1:5200";
428
+ const override = await forwardPolicyToDaemon("http_request", url, daemonUrl);
429
+ if (!override || !override.allowed) {
430
+ const reason = `URL not allowed: ${url} - ${networkCheck.reason}`;
431
+ deps.onExecDenied?.(command, reason);
432
+ return {
433
+ success: false,
434
+ error: { code: 1009, message: reason }
435
+ };
436
+ }
392
437
  }
393
438
  }
394
439
  }
@@ -807,6 +852,86 @@ async function handleSkillUninstall(params, context, deps) {
807
852
  }
808
853
  }
809
854
 
855
+ // libs/shield-broker/src/handlers/policy-check.ts
856
+ var DEFAULT_DAEMON_URL = "http://127.0.0.1:5200";
857
+ async function handlePolicyCheck(params, context, deps) {
858
+ const { operation, target } = params;
859
+ if (!operation) {
860
+ return {
861
+ success: false,
862
+ error: { code: -32602, message: "Missing required parameter: operation" }
863
+ };
864
+ }
865
+ let checkParams;
866
+ switch (operation) {
867
+ case "http_request":
868
+ case "open_url":
869
+ checkParams = { url: target || "" };
870
+ break;
871
+ case "file_read":
872
+ case "file_write":
873
+ case "file_list":
874
+ checkParams = { path: target || "" };
875
+ break;
876
+ case "exec":
877
+ checkParams = { command: target || "" };
878
+ break;
879
+ case "secret_inject":
880
+ checkParams = { name: target || "" };
881
+ break;
882
+ default:
883
+ checkParams = { target: target || "" };
884
+ break;
885
+ }
886
+ const result = await deps.policyEnforcer.check(operation, checkParams, context);
887
+ if (result.allowed) {
888
+ return {
889
+ success: true,
890
+ data: {
891
+ allowed: true,
892
+ policyId: result.policyId,
893
+ reason: result.reason
894
+ }
895
+ };
896
+ }
897
+ const daemonUrl = deps.daemonUrl || DEFAULT_DAEMON_URL;
898
+ const daemonResult = await forwardPolicyToDaemon(operation, target || "", daemonUrl);
899
+ if (daemonResult && daemonResult.allowed) {
900
+ return { success: true, data: daemonResult };
901
+ }
902
+ return {
903
+ success: true,
904
+ data: {
905
+ allowed: false,
906
+ policyId: result.policyId,
907
+ reason: result.reason
908
+ }
909
+ };
910
+ }
911
+
912
+ // libs/shield-broker/src/handlers/events-batch.ts
913
+ async function handleEventsBatch(params, context, deps) {
914
+ const { events } = params;
915
+ const eventList = events || [];
916
+ for (const event of eventList) {
917
+ const entry = {
918
+ id: event.id || context.requestId,
919
+ timestamp: event.timestamp ? new Date(event.timestamp) : /* @__PURE__ */ new Date(),
920
+ operation: event.operation || "events_batch",
921
+ channel: "socket",
922
+ allowed: event.allowed ?? true,
923
+ target: event.target || "",
924
+ result: event.allowed === false ? "denied" : "success",
925
+ durationMs: 0
926
+ };
927
+ await deps.auditLogger.log(entry);
928
+ }
929
+ return {
930
+ success: true,
931
+ data: { received: eventList.length }
932
+ };
933
+ }
934
+
810
935
  // libs/shield-broker/src/server.ts
811
936
  var UnixSocketServer = class {
812
937
  server = null;
@@ -814,12 +939,14 @@ var UnixSocketServer = class {
814
939
  policyEnforcer;
815
940
  auditLogger;
816
941
  secretVault;
942
+ commandAllowlist;
817
943
  connections = /* @__PURE__ */ new Set();
818
944
  constructor(options) {
819
945
  this.config = options.config;
820
946
  this.policyEnforcer = options.policyEnforcer;
821
947
  this.auditLogger = options.auditLogger;
822
948
  this.secretVault = options.secretVault;
949
+ this.commandAllowlist = options.commandAllowlist;
823
950
  }
824
951
  /**
825
952
  * Start the Unix socket server
@@ -923,20 +1050,29 @@ var UnixSocketServer = class {
923
1050
  request.params,
924
1051
  context
925
1052
  );
926
- if (!policyResult.allowed) {
1053
+ let finalPolicy = policyResult;
1054
+ if (!policyResult.allowed && request.method !== "policy_check") {
1055
+ const target = this.extractTarget(request);
1056
+ const daemonUrl = this.config.daemonUrl || "http://127.0.0.1:5200";
1057
+ const override = await forwardPolicyToDaemon(request.method, target, daemonUrl);
1058
+ if (override) {
1059
+ finalPolicy = override;
1060
+ }
1061
+ }
1062
+ if (!finalPolicy.allowed) {
927
1063
  await this.auditLogger.log({
928
1064
  id: requestId,
929
1065
  timestamp: /* @__PURE__ */ new Date(),
930
1066
  operation: request.method,
931
1067
  channel: "socket",
932
1068
  allowed: false,
933
- policyId: policyResult.policyId,
1069
+ policyId: finalPolicy.policyId,
934
1070
  target: this.extractTarget(request),
935
1071
  result: "denied",
936
- errorMessage: policyResult.reason,
1072
+ errorMessage: finalPolicy.reason,
937
1073
  durationMs: Date.now() - startTime
938
1074
  });
939
- return this.errorResponse(request.id, 1001, policyResult.reason || "Policy denied");
1075
+ return this.errorResponse(request.id, 1001, finalPolicy.reason || "Policy denied");
940
1076
  }
941
1077
  const handler = this.getHandler(request.method);
942
1078
  if (!handler) {
@@ -945,7 +1081,9 @@ var UnixSocketServer = class {
945
1081
  const result = await handler(request.params, context, {
946
1082
  policyEnforcer: this.policyEnforcer,
947
1083
  auditLogger: this.auditLogger,
948
- secretVault: this.secretVault
1084
+ secretVault: this.secretVault,
1085
+ commandAllowlist: this.commandAllowlist,
1086
+ daemonUrl: this.config.daemonUrl
949
1087
  });
950
1088
  await this.auditLogger.log({
951
1089
  id: requestId,
@@ -953,7 +1091,7 @@ var UnixSocketServer = class {
953
1091
  operation: request.method,
954
1092
  channel: "socket",
955
1093
  allowed: true,
956
- policyId: policyResult.policyId,
1094
+ policyId: finalPolicy.policyId,
957
1095
  target: this.extractTarget(request),
958
1096
  result: result.success ? "success" : "error",
959
1097
  errorMessage: result.error?.message,
@@ -992,7 +1130,9 @@ var UnixSocketServer = class {
992
1130
  secret_inject: handleSecretInject,
993
1131
  ping: handlePing,
994
1132
  skill_install: handleSkillInstall,
995
- skill_uninstall: handleSkillUninstall
1133
+ skill_uninstall: handleSkillUninstall,
1134
+ policy_check: handlePolicyCheck,
1135
+ events_batch: handleEventsBatch
996
1136
  };
997
1137
  return handlerMap[method];
998
1138
  }
@@ -1023,7 +1163,9 @@ var HTTP_ALLOWED_OPERATIONS = /* @__PURE__ */ new Set([
1023
1163
  "file_read",
1024
1164
  "file_list",
1025
1165
  "open_url",
1026
- "ping"
1166
+ "ping",
1167
+ "policy_check",
1168
+ "events_batch"
1027
1169
  ]);
1028
1170
  var HTTP_DENIED_OPERATIONS = /* @__PURE__ */ new Set([
1029
1171
  "exec",
@@ -1035,10 +1177,12 @@ var HttpFallbackServer = class {
1035
1177
  config;
1036
1178
  policyEnforcer;
1037
1179
  auditLogger;
1180
+ commandAllowlist;
1038
1181
  constructor(options) {
1039
1182
  this.config = options.config;
1040
1183
  this.policyEnforcer = options.policyEnforcer;
1041
1184
  this.auditLogger = options.auditLogger;
1185
+ this.commandAllowlist = options.commandAllowlist;
1042
1186
  }
1043
1187
  /**
1044
1188
  * Start the HTTP fallback server
@@ -1159,20 +1303,29 @@ var HttpFallbackServer = class {
1159
1303
  request.params,
1160
1304
  context
1161
1305
  );
1162
- if (!policyResult.allowed) {
1306
+ let finalPolicy = policyResult;
1307
+ if (!policyResult.allowed && request.method !== "policy_check") {
1308
+ const target = this.extractTarget(request);
1309
+ const daemonUrl = this.config.daemonUrl || "http://127.0.0.1:5200";
1310
+ const override = await forwardPolicyToDaemon(request.method, target, daemonUrl);
1311
+ if (override) {
1312
+ finalPolicy = override;
1313
+ }
1314
+ }
1315
+ if (!finalPolicy.allowed) {
1163
1316
  await this.auditLogger.log({
1164
1317
  id: requestId,
1165
1318
  timestamp: /* @__PURE__ */ new Date(),
1166
1319
  operation: request.method,
1167
1320
  channel: "http",
1168
1321
  allowed: false,
1169
- policyId: policyResult.policyId,
1322
+ policyId: finalPolicy.policyId,
1170
1323
  target: this.extractTarget(request),
1171
1324
  result: "denied",
1172
- errorMessage: policyResult.reason,
1325
+ errorMessage: finalPolicy.reason,
1173
1326
  durationMs: Date.now() - startTime
1174
1327
  });
1175
- return this.errorResponse(request.id, 1001, policyResult.reason || "Policy denied");
1328
+ return this.errorResponse(request.id, 1001, finalPolicy.reason || "Policy denied");
1176
1329
  }
1177
1330
  const handler = this.getHandler(request.method);
1178
1331
  if (!handler) {
@@ -1181,8 +1334,10 @@ var HttpFallbackServer = class {
1181
1334
  const result = await handler(request.params, context, {
1182
1335
  policyEnforcer: this.policyEnforcer,
1183
1336
  auditLogger: this.auditLogger,
1184
- secretVault: null
1337
+ secretVault: null,
1185
1338
  // Not available over HTTP
1339
+ commandAllowlist: this.commandAllowlist,
1340
+ daemonUrl: this.config.daemonUrl
1186
1341
  });
1187
1342
  await this.auditLogger.log({
1188
1343
  id: requestId,
@@ -1190,7 +1345,7 @@ var HttpFallbackServer = class {
1190
1345
  operation: request.method,
1191
1346
  channel: "http",
1192
1347
  allowed: true,
1193
- policyId: policyResult.policyId,
1348
+ policyId: finalPolicy.policyId,
1194
1349
  target: this.extractTarget(request),
1195
1350
  result: result.success ? "success" : "error",
1196
1351
  errorMessage: result.error?.message,
@@ -1224,7 +1379,9 @@ var HttpFallbackServer = class {
1224
1379
  file_read: handleFileRead,
1225
1380
  file_list: handleFileList,
1226
1381
  open_url: handleOpenUrl,
1227
- ping: handlePing
1382
+ ping: handlePing,
1383
+ policy_check: handlePolicyCheck,
1384
+ events_batch: handleEventsBatch
1228
1385
  };
1229
1386
  return handlerMap[method];
1230
1387
  }
@@ -1263,6 +1420,34 @@ var PolicyEnforcer = class {
1263
1420
  this.policies = options.defaultPolicies;
1264
1421
  this.loadPolicies();
1265
1422
  }
1423
+ /**
1424
+ * Normalize a policy rule — infer operations from target when missing,
1425
+ * default priority to 0.
1426
+ */
1427
+ normalizeRule(rule) {
1428
+ const normalized = { ...rule };
1429
+ if (!normalized.priority && normalized.priority !== 0) {
1430
+ normalized.priority = 0;
1431
+ }
1432
+ if (normalized.operations && normalized.operations.length > 0) {
1433
+ return normalized;
1434
+ }
1435
+ switch (normalized.target) {
1436
+ case "url":
1437
+ normalized.operations = ["http_request", "open_url"];
1438
+ break;
1439
+ case "command":
1440
+ normalized.operations = ["exec"];
1441
+ break;
1442
+ case "skill":
1443
+ normalized.operations = ["skill_install", "skill_uninstall"];
1444
+ break;
1445
+ default:
1446
+ normalized.operations = ["*"];
1447
+ break;
1448
+ }
1449
+ return normalized;
1450
+ }
1266
1451
  /**
1267
1452
  * Load policies from disk
1268
1453
  */
@@ -1275,7 +1460,7 @@ var PolicyEnforcer = class {
1275
1460
  this.policies = {
1276
1461
  ...this.policies,
1277
1462
  ...loaded,
1278
- rules: [...this.policies.rules, ...loaded.rules || []]
1463
+ rules: [...this.policies.rules, ...(loaded.rules || []).map((r) => this.normalizeRule(r))]
1279
1464
  };
1280
1465
  this.lastLoad = Date.now();
1281
1466
  } catch (error) {
@@ -1291,7 +1476,7 @@ var PolicyEnforcer = class {
1291
1476
  const content = fs4.readFileSync(path4.join(customDir, file), "utf-8");
1292
1477
  const custom = JSON.parse(content);
1293
1478
  if (custom.rules) {
1294
- this.policies.rules.push(...custom.rules);
1479
+ this.policies.rules.push(...custom.rules.map((r) => this.normalizeRule(r)));
1295
1480
  }
1296
1481
  }
1297
1482
  }
@@ -1341,6 +1526,12 @@ var PolicyEnforcer = class {
1341
1526
  if (!constraintResult.allowed) {
1342
1527
  return constraintResult;
1343
1528
  }
1529
+ if (["file_read", "file_write", "file_list"].includes(operation) && this.policies.fsConstraints) {
1530
+ return { allowed: true, reason: "Allowed by file system constraints" };
1531
+ }
1532
+ if (operation === "http_request" && this.policies.networkConstraints) {
1533
+ return { allowed: true, reason: "Allowed by network constraints" };
1534
+ }
1344
1535
  return {
1345
1536
  allowed: this.policies.defaultAction === "allow",
1346
1537
  reason: this.policies.defaultAction === "deny" ? "No matching allow policy" : void 0
@@ -1520,6 +1711,28 @@ var BuiltinPolicies = [
1520
1711
  enabled: true,
1521
1712
  priority: 1e3
1522
1713
  },
1714
+ // Allow interceptor policy checks (internal RPC — must not be subject to policy gate)
1715
+ {
1716
+ id: "builtin-allow-policy-check",
1717
+ name: "Allow interceptor policy checks",
1718
+ action: "allow",
1719
+ target: "command",
1720
+ operations: ["policy_check"],
1721
+ patterns: ["*"],
1722
+ enabled: true,
1723
+ priority: 1e3
1724
+ },
1725
+ // Allow interceptor event reporting (internal RPC)
1726
+ {
1727
+ id: "builtin-allow-events-batch",
1728
+ name: "Allow interceptor event reporting",
1729
+ action: "allow",
1730
+ target: "command",
1731
+ operations: ["events_batch"],
1732
+ patterns: ["*"],
1733
+ enabled: true,
1734
+ priority: 1e3
1735
+ },
1523
1736
  // Allow skill installation/uninstallation (daemon management operations)
1524
1737
  {
1525
1738
  id: "builtin-allow-skill-management",
@@ -1540,9 +1753,13 @@ var BuiltinPolicies = [
1540
1753
  operations: ["http_request"],
1541
1754
  patterns: [
1542
1755
  "http://localhost:*",
1756
+ "http://localhost:*/**",
1543
1757
  "http://127.0.0.1:*",
1758
+ "http://127.0.0.1:*/**",
1544
1759
  "https://localhost:*",
1545
- "https://127.0.0.1:*"
1760
+ "https://localhost:*/**",
1761
+ "https://127.0.0.1:*",
1762
+ "https://127.0.0.1:*/**"
1546
1763
  ],
1547
1764
  enabled: true,
1548
1765
  priority: 100
@@ -1642,10 +1859,15 @@ var BuiltinPolicies = [
1642
1859
  target: "url",
1643
1860
  operations: ["http_request"],
1644
1861
  patterns: [
1862
+ "https://api.anthropic.com",
1645
1863
  "https://api.anthropic.com/**",
1864
+ "https://api.openai.com",
1646
1865
  "https://api.openai.com/**",
1866
+ "https://api.cohere.ai",
1647
1867
  "https://api.cohere.ai/**",
1868
+ "https://generativelanguage.googleapis.com",
1648
1869
  "https://generativelanguage.googleapis.com/**",
1870
+ "https://api.mistral.ai",
1649
1871
  "https://api.mistral.ai/**"
1650
1872
  ],
1651
1873
  enabled: true,
@@ -1659,10 +1881,15 @@ var BuiltinPolicies = [
1659
1881
  target: "url",
1660
1882
  operations: ["http_request"],
1661
1883
  patterns: [
1884
+ "https://registry.npmjs.org",
1662
1885
  "https://registry.npmjs.org/**",
1886
+ "https://pypi.org",
1663
1887
  "https://pypi.org/**",
1888
+ "https://files.pythonhosted.org",
1664
1889
  "https://files.pythonhosted.org/**",
1890
+ "https://crates.io",
1665
1891
  "https://crates.io/**",
1892
+ "https://rubygems.org",
1666
1893
  "https://rubygems.org/**"
1667
1894
  ],
1668
1895
  enabled: true,
@@ -1676,23 +1903,28 @@ var BuiltinPolicies = [
1676
1903
  target: "url",
1677
1904
  operations: ["http_request"],
1678
1905
  patterns: [
1906
+ "https://github.com",
1679
1907
  "https://github.com/**",
1908
+ "https://api.github.com",
1680
1909
  "https://api.github.com/**",
1910
+ "https://raw.githubusercontent.com",
1681
1911
  "https://raw.githubusercontent.com/**",
1912
+ "https://gist.github.com",
1682
1913
  "https://gist.github.com/**"
1683
1914
  ],
1684
1915
  enabled: true,
1685
1916
  priority: 50
1686
1917
  }
1687
1918
  ];
1688
- function getDefaultPolicies() {
1919
+ function getDefaultPolicies(options) {
1920
+ const agentHome = options?.agentHome || process.env["AGENSHIELD_AGENT_HOME"] || "/Users/clawagent";
1689
1921
  return {
1690
1922
  version: "1.0.0",
1691
1923
  defaultAction: "deny",
1692
1924
  rules: [...BuiltinPolicies],
1693
1925
  fsConstraints: {
1694
1926
  allowedPaths: [
1695
- "/Users/clawagent/workspace",
1927
+ agentHome,
1696
1928
  "/tmp/agenshield"
1697
1929
  ],
1698
1930
  deniedPatterns: [
@@ -1719,9 +1951,190 @@ function getDefaultPolicies() {
1719
1951
  };
1720
1952
  }
1721
1953
 
1722
- // libs/shield-broker/src/audit/logger.ts
1954
+ // libs/shield-broker/src/policies/command-allowlist.ts
1723
1955
  import * as fs5 from "node:fs";
1724
1956
  import * as path5 from "node:path";
1957
+ var BUILTIN_COMMANDS = {
1958
+ git: ["/usr/bin/git", "/opt/homebrew/bin/git", "/usr/local/bin/git"],
1959
+ ssh: ["/usr/bin/ssh"],
1960
+ scp: ["/usr/bin/scp"],
1961
+ rsync: ["/usr/bin/rsync", "/opt/homebrew/bin/rsync"],
1962
+ brew: ["/opt/homebrew/bin/brew", "/usr/local/bin/brew"],
1963
+ npm: ["/opt/homebrew/bin/npm", "/usr/local/bin/npm"],
1964
+ npx: ["/opt/homebrew/bin/npx", "/usr/local/bin/npx"],
1965
+ pip: ["/usr/bin/pip", "/usr/local/bin/pip", "/opt/homebrew/bin/pip"],
1966
+ pip3: ["/usr/bin/pip3", "/usr/local/bin/pip3", "/opt/homebrew/bin/pip3"],
1967
+ node: ["/opt/homebrew/bin/node", "/usr/local/bin/node"],
1968
+ python: ["/usr/bin/python", "/usr/local/bin/python", "/opt/homebrew/bin/python"],
1969
+ python3: ["/usr/bin/python3", "/usr/local/bin/python3", "/opt/homebrew/bin/python3"],
1970
+ ls: ["/bin/ls"],
1971
+ cat: ["/bin/cat"],
1972
+ grep: ["/usr/bin/grep"],
1973
+ find: ["/usr/bin/find"],
1974
+ mkdir: ["/bin/mkdir"],
1975
+ cp: ["/bin/cp"],
1976
+ mv: ["/bin/mv"],
1977
+ rm: ["/bin/rm"],
1978
+ touch: ["/usr/bin/touch"],
1979
+ chmod: ["/bin/chmod"],
1980
+ head: ["/usr/bin/head"],
1981
+ tail: ["/usr/bin/tail"],
1982
+ wc: ["/usr/bin/wc"],
1983
+ sort: ["/usr/bin/sort"],
1984
+ uniq: ["/usr/bin/uniq"],
1985
+ sed: ["/usr/bin/sed"],
1986
+ awk: ["/usr/bin/awk"],
1987
+ tar: ["/usr/bin/tar"],
1988
+ curl: ["/usr/bin/curl"],
1989
+ wget: ["/usr/local/bin/wget", "/opt/homebrew/bin/wget"]
1990
+ };
1991
+ var CommandAllowlist = class {
1992
+ configPath;
1993
+ dynamicCommands = /* @__PURE__ */ new Map();
1994
+ lastLoad = 0;
1995
+ reloadInterval = 3e4;
1996
+ // 30 seconds
1997
+ constructor(configPath) {
1998
+ this.configPath = configPath;
1999
+ this.load();
2000
+ }
2001
+ /**
2002
+ * Load dynamic commands from disk
2003
+ */
2004
+ load() {
2005
+ if (!fs5.existsSync(this.configPath)) {
2006
+ this.lastLoad = Date.now();
2007
+ return;
2008
+ }
2009
+ try {
2010
+ const content = fs5.readFileSync(this.configPath, "utf-8");
2011
+ const config = JSON.parse(content);
2012
+ this.dynamicCommands.clear();
2013
+ for (const cmd of config.commands || []) {
2014
+ this.dynamicCommands.set(cmd.name, cmd);
2015
+ }
2016
+ this.lastLoad = Date.now();
2017
+ } catch {
2018
+ this.lastLoad = Date.now();
2019
+ }
2020
+ }
2021
+ /**
2022
+ * Reload dynamic commands if stale
2023
+ */
2024
+ maybeReload() {
2025
+ if (Date.now() - this.lastLoad > this.reloadInterval) {
2026
+ this.load();
2027
+ }
2028
+ }
2029
+ /**
2030
+ * Persist dynamic commands to disk
2031
+ */
2032
+ save() {
2033
+ const dir = path5.dirname(this.configPath);
2034
+ if (!fs5.existsSync(dir)) {
2035
+ fs5.mkdirSync(dir, { recursive: true });
2036
+ }
2037
+ const config = {
2038
+ version: "1.0.0",
2039
+ commands: Array.from(this.dynamicCommands.values())
2040
+ };
2041
+ fs5.writeFileSync(this.configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
2042
+ }
2043
+ /**
2044
+ * Add a dynamic command
2045
+ */
2046
+ add(cmd) {
2047
+ this.dynamicCommands.set(cmd.name, cmd);
2048
+ this.save();
2049
+ }
2050
+ /**
2051
+ * Remove a dynamic command
2052
+ */
2053
+ remove(name) {
2054
+ const existed = this.dynamicCommands.delete(name);
2055
+ if (existed) {
2056
+ this.save();
2057
+ }
2058
+ return existed;
2059
+ }
2060
+ /**
2061
+ * Get a dynamic command by name
2062
+ */
2063
+ get(name) {
2064
+ return this.dynamicCommands.get(name);
2065
+ }
2066
+ /**
2067
+ * List all commands (builtin + dynamic)
2068
+ */
2069
+ list() {
2070
+ const result = [];
2071
+ for (const [name, paths] of Object.entries(BUILTIN_COMMANDS)) {
2072
+ result.push({
2073
+ name,
2074
+ paths,
2075
+ addedAt: "",
2076
+ addedBy: "builtin",
2077
+ builtin: true
2078
+ });
2079
+ }
2080
+ for (const cmd of this.dynamicCommands.values()) {
2081
+ result.push({ ...cmd, builtin: false });
2082
+ }
2083
+ return result;
2084
+ }
2085
+ /**
2086
+ * List only dynamic commands
2087
+ */
2088
+ listDynamic() {
2089
+ return Array.from(this.dynamicCommands.values());
2090
+ }
2091
+ /**
2092
+ * Check if a command name conflicts with a builtin
2093
+ */
2094
+ isBuiltin(name) {
2095
+ return name in BUILTIN_COMMANDS;
2096
+ }
2097
+ /**
2098
+ * Resolve a command name to an absolute path.
2099
+ * Checks builtin commands first, then dynamic commands.
2100
+ * Validates that the resolved path exists on disk.
2101
+ * Returns null if the command is not allowed.
2102
+ */
2103
+ resolve(command) {
2104
+ this.maybeReload();
2105
+ if (path5.isAbsolute(command)) {
2106
+ for (const paths of Object.values(BUILTIN_COMMANDS)) {
2107
+ if (paths.includes(command) && fs5.existsSync(command)) {
2108
+ return command;
2109
+ }
2110
+ }
2111
+ for (const cmd of this.dynamicCommands.values()) {
2112
+ if (cmd.paths.includes(command) && fs5.existsSync(command)) {
2113
+ return command;
2114
+ }
2115
+ }
2116
+ return null;
2117
+ }
2118
+ const basename3 = path5.basename(command);
2119
+ const builtinPaths = BUILTIN_COMMANDS[basename3];
2120
+ if (builtinPaths) {
2121
+ for (const p of builtinPaths) {
2122
+ if (fs5.existsSync(p)) return p;
2123
+ }
2124
+ }
2125
+ const dynamicCmd = this.dynamicCommands.get(basename3);
2126
+ if (dynamicCmd && dynamicCmd.paths.length > 0) {
2127
+ for (const p of dynamicCmd.paths) {
2128
+ if (fs5.existsSync(p)) return p;
2129
+ }
2130
+ }
2131
+ return null;
2132
+ }
2133
+ };
2134
+
2135
+ // libs/shield-broker/src/audit/logger.ts
2136
+ import * as fs6 from "node:fs";
2137
+ import * as path6 from "node:path";
1725
2138
  var AuditLogger = class {
1726
2139
  logPath;
1727
2140
  logLevel;
@@ -1746,15 +2159,15 @@ var AuditLogger = class {
1746
2159
  * Initialize the write stream
1747
2160
  */
1748
2161
  initializeStream() {
1749
- const dir = path5.dirname(this.logPath);
1750
- if (!fs5.existsSync(dir)) {
1751
- fs5.mkdirSync(dir, { recursive: true });
2162
+ const dir = path6.dirname(this.logPath);
2163
+ if (!fs6.existsSync(dir)) {
2164
+ fs6.mkdirSync(dir, { recursive: true });
1752
2165
  }
1753
- if (fs5.existsSync(this.logPath)) {
1754
- const stats = fs5.statSync(this.logPath);
2166
+ if (fs6.existsSync(this.logPath)) {
2167
+ const stats = fs6.statSync(this.logPath);
1755
2168
  this.currentSize = stats.size;
1756
2169
  }
1757
- this.writeStream = fs5.createWriteStream(this.logPath, {
2170
+ this.writeStream = fs6.createWriteStream(this.logPath, {
1758
2171
  flags: "a",
1759
2172
  encoding: "utf-8"
1760
2173
  });
@@ -1773,16 +2186,16 @@ var AuditLogger = class {
1773
2186
  for (let i = this.maxFiles - 1; i >= 1; i--) {
1774
2187
  const oldPath = `${this.logPath}.${i}`;
1775
2188
  const newPath = `${this.logPath}.${i + 1}`;
1776
- if (fs5.existsSync(oldPath)) {
2189
+ if (fs6.existsSync(oldPath)) {
1777
2190
  if (i === this.maxFiles - 1) {
1778
- fs5.unlinkSync(oldPath);
2191
+ fs6.unlinkSync(oldPath);
1779
2192
  } else {
1780
- fs5.renameSync(oldPath, newPath);
2193
+ fs6.renameSync(oldPath, newPath);
1781
2194
  }
1782
2195
  }
1783
2196
  }
1784
- if (fs5.existsSync(this.logPath)) {
1785
- fs5.renameSync(this.logPath, `${this.logPath}.1`);
2197
+ if (fs6.existsSync(this.logPath)) {
2198
+ fs6.renameSync(this.logPath, `${this.logPath}.1`);
1786
2199
  }
1787
2200
  this.currentSize = 0;
1788
2201
  this.initializeStream();
@@ -1855,10 +2268,10 @@ var AuditLogger = class {
1855
2268
  async query(options) {
1856
2269
  const results = [];
1857
2270
  const limit = options.limit || 1e3;
1858
- if (!fs5.existsSync(this.logPath)) {
2271
+ if (!fs6.existsSync(this.logPath)) {
1859
2272
  return results;
1860
2273
  }
1861
- const content = fs5.readFileSync(this.logPath, "utf-8");
2274
+ const content = fs6.readFileSync(this.logPath, "utf-8");
1862
2275
  const lines = content.trim().split("\n");
1863
2276
  for (const line of lines.reverse()) {
1864
2277
  if (results.length >= limit) break;
@@ -1896,7 +2309,7 @@ var AuditLogger = class {
1896
2309
  };
1897
2310
 
1898
2311
  // libs/shield-broker/src/secrets/vault.ts
1899
- import * as fs6 from "node:fs/promises";
2312
+ import * as fs7 from "node:fs/promises";
1900
2313
  import * as crypto from "node:crypto";
1901
2314
  var SecretVault = class {
1902
2315
  vaultPath;
@@ -1918,11 +2331,11 @@ var SecretVault = class {
1918
2331
  async loadOrCreateKey() {
1919
2332
  const keyPath = this.vaultPath.replace(".enc", ".key");
1920
2333
  try {
1921
- const keyData = await fs6.readFile(keyPath);
2334
+ const keyData = await fs7.readFile(keyPath);
1922
2335
  return keyData;
1923
2336
  } catch {
1924
2337
  const key = crypto.randomBytes(32);
1925
- await fs6.writeFile(keyPath, key, { mode: 384 });
2338
+ await fs7.writeFile(keyPath, key, { mode: 384 });
1926
2339
  return key;
1927
2340
  }
1928
2341
  }
@@ -1931,7 +2344,7 @@ var SecretVault = class {
1931
2344
  */
1932
2345
  async load() {
1933
2346
  try {
1934
- const content = await fs6.readFile(this.vaultPath, "utf-8");
2347
+ const content = await fs7.readFile(this.vaultPath, "utf-8");
1935
2348
  this.data = JSON.parse(content);
1936
2349
  } catch {
1937
2350
  this.data = {
@@ -1945,7 +2358,7 @@ var SecretVault = class {
1945
2358
  */
1946
2359
  async save() {
1947
2360
  if (!this.data) return;
1948
- await fs6.writeFile(
2361
+ await fs7.writeFile(
1949
2362
  this.vaultPath,
1950
2363
  JSON.stringify(this.data, null, 2),
1951
2364
  { mode: 384 }
@@ -2063,14 +2476,30 @@ var SecretVault = class {
2063
2476
  };
2064
2477
 
2065
2478
  // libs/shield-broker/src/main.ts
2066
- import * as fs7 from "node:fs";
2067
- import * as path6 from "node:path";
2479
+ import * as fs8 from "node:fs";
2480
+ import * as path7 from "node:path";
2481
+ var PROXIED_COMMANDS = [
2482
+ "curl",
2483
+ "wget",
2484
+ "git",
2485
+ "ssh",
2486
+ "scp",
2487
+ "rsync",
2488
+ "brew",
2489
+ "npm",
2490
+ "npx",
2491
+ "pip",
2492
+ "pip3",
2493
+ "open-url",
2494
+ "shieldctl",
2495
+ "agenco"
2496
+ ];
2068
2497
  function loadConfig() {
2069
2498
  const configPath = process.env["AGENSHIELD_CONFIG"] || "/opt/agenshield/config/shield.json";
2070
2499
  let fileConfig = {};
2071
- if (fs7.existsSync(configPath)) {
2500
+ if (fs8.existsSync(configPath)) {
2072
2501
  try {
2073
- const content = fs7.readFileSync(configPath, "utf-8");
2502
+ const content = fs8.readFileSync(configPath, "utf-8");
2074
2503
  fileConfig = JSON.parse(content);
2075
2504
  } catch (error) {
2076
2505
  console.warn(`Warning: Failed to load config from ${configPath}:`, error);
@@ -2091,16 +2520,18 @@ function loadConfig() {
2091
2520
  failOpen: process.env["AGENSHIELD_FAIL_OPEN"] === "true" || (fileConfig.failOpen ?? false),
2092
2521
  socketMode: fileConfig.socketMode || 438,
2093
2522
  socketOwner: fileConfig.socketOwner || "clawbroker",
2094
- socketGroup: fileConfig.socketGroup || "clawshield"
2523
+ socketGroup: fileConfig.socketGroup || "clawshield",
2524
+ agentHome: process.env["AGENSHIELD_AGENT_HOME"] || fileConfig.agentHome,
2525
+ daemonUrl: process.env["AGENSHIELD_DAEMON_URL"] || fileConfig.daemonUrl || "http://127.0.0.1:5200"
2095
2526
  };
2096
2527
  }
2097
2528
  function ensureDirectories(config) {
2098
- const socketDir = path6.dirname(config.socketPath);
2099
- const auditDir = path6.dirname(config.auditLogPath);
2529
+ const socketDir = path7.dirname(config.socketPath);
2530
+ const auditDir = path7.dirname(config.auditLogPath);
2100
2531
  for (const dir of [socketDir, auditDir, config.policiesPath]) {
2101
- if (!fs7.existsSync(dir)) {
2532
+ if (!fs8.existsSync(dir)) {
2102
2533
  try {
2103
- fs7.mkdirSync(dir, { recursive: true, mode: 493 });
2534
+ fs8.mkdirSync(dir, { recursive: true, mode: 493 });
2104
2535
  } catch (error) {
2105
2536
  if (error.code !== "EEXIST") {
2106
2537
  console.warn(`Warning: Could not create directory ${dir}:`, error);
@@ -2109,6 +2540,47 @@ function ensureDirectories(config) {
2109
2540
  }
2110
2541
  }
2111
2542
  }
2543
+ function ensureProxiedCommandWrappers(binDir) {
2544
+ if (!fs8.existsSync(binDir)) {
2545
+ try {
2546
+ fs8.mkdirSync(binDir, { recursive: true, mode: 493 });
2547
+ } catch {
2548
+ console.warn(`[broker] cannot create bin dir ${binDir}`);
2549
+ return;
2550
+ }
2551
+ }
2552
+ const shieldExecPath = "/opt/agenshield/bin/shield-exec";
2553
+ const hasShieldExec = fs8.existsSync(shieldExecPath);
2554
+ let installed = 0;
2555
+ for (const cmd of PROXIED_COMMANDS) {
2556
+ const wrapperPath = path7.join(binDir, cmd);
2557
+ if (fs8.existsSync(wrapperPath)) continue;
2558
+ if (hasShieldExec) {
2559
+ try {
2560
+ fs8.symlinkSync(shieldExecPath, wrapperPath);
2561
+ installed++;
2562
+ continue;
2563
+ } catch {
2564
+ }
2565
+ }
2566
+ try {
2567
+ const script = [
2568
+ "#!/bin/bash",
2569
+ `# ${cmd} - AgenShield proxy (auto-generated)`,
2570
+ "if ! /bin/pwd > /dev/null 2>&1; then cd ~ 2>/dev/null || cd /; fi",
2571
+ `exec /opt/agenshield/bin/shield-client exec ${cmd} "$@"`,
2572
+ ""
2573
+ ].join("\n");
2574
+ fs8.writeFileSync(wrapperPath, script, { mode: 493 });
2575
+ installed++;
2576
+ } catch {
2577
+ console.warn(`[broker] cannot write wrapper for ${cmd}`);
2578
+ }
2579
+ }
2580
+ if (installed > 0) {
2581
+ console.log(`[broker] installed ${installed} command wrappers in ${binDir}`);
2582
+ }
2583
+ }
2112
2584
  async function main() {
2113
2585
  console.log(`AgenShield Broker starting at ${(/* @__PURE__ */ new Date()).toISOString()}`);
2114
2586
  console.log(`PID: ${process.pid}, UID: ${process.getuid?.()}, GID: ${process.getgid?.()}`);
@@ -2126,6 +2598,8 @@ async function main() {
2126
2598
  console.log(`Socket owner: ${config.socketOwner}, group: ${config.socketGroup}`);
2127
2599
  console.log(`HTTP Fallback: ${config.httpEnabled ? `${config.httpHost}:${config.httpPort}` : "disabled"}`);
2128
2600
  console.log(`Policies: ${config.policiesPath}`);
2601
+ console.log(`Agent Home: ${config.agentHome || "(env fallback)"}`);
2602
+ console.log(`Daemon URL: ${config.daemonUrl || "(default)"}`);
2129
2603
  console.log(`Log Level: ${config.logLevel}`);
2130
2604
  try {
2131
2605
  ensureDirectories(config);
@@ -2139,17 +2613,24 @@ async function main() {
2139
2613
  });
2140
2614
  const policyEnforcer = new PolicyEnforcer({
2141
2615
  policiesPath: config.policiesPath,
2142
- defaultPolicies: getDefaultPolicies(),
2616
+ defaultPolicies: getDefaultPolicies({ agentHome: config.agentHome }),
2143
2617
  failOpen: config.failOpen
2144
2618
  });
2145
2619
  const secretVault = new SecretVault({
2146
2620
  vaultPath: "/etc/agenshield/vault.enc"
2147
2621
  });
2622
+ const commandAllowlist = new CommandAllowlist(
2623
+ "/opt/agenshield/config/allowed-commands.json"
2624
+ );
2625
+ if (config.agentHome) {
2626
+ ensureProxiedCommandWrappers(path7.join(config.agentHome, "bin"));
2627
+ }
2148
2628
  const socketServer = new UnixSocketServer({
2149
2629
  config,
2150
2630
  policyEnforcer,
2151
2631
  auditLogger,
2152
- secretVault
2632
+ secretVault,
2633
+ commandAllowlist
2153
2634
  });
2154
2635
  await socketServer.start();
2155
2636
  console.log(`Unix socket server listening on ${config.socketPath}`);
@@ -2158,7 +2639,8 @@ async function main() {
2158
2639
  httpServer = new HttpFallbackServer({
2159
2640
  config,
2160
2641
  policyEnforcer,
2161
- auditLogger
2642
+ auditLogger,
2643
+ commandAllowlist
2162
2644
  });
2163
2645
  await httpServer.start();
2164
2646
  console.log(`HTTP fallback server listening on ${config.httpHost}:${config.httpPort}`);