@tuent/sentinel 0.1.3 → 0.1.4

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.
@@ -3,9 +3,8 @@ import {
3
3
  } from "./chunk-B5QKJHSV.js";
4
4
  import {
5
5
  discoverPolicy
6
- } from "./chunk-FMZWHT4M.js";
6
+ } from "./chunk-B6S2PBS4.js";
7
7
  import {
8
- DEFAULT_FORBIDDEN_PATTERNS,
9
8
  FORBIDDEN_BASENAMES,
10
9
  classifyDeny,
11
10
  isPositionallySafeMention,
@@ -14,16 +13,18 @@ import {
14
13
  scanBashCommand,
15
14
  scanContentForForbiddenBasenames,
16
15
  scanGlobPattern,
17
- tokenizePaths
18
- } from "./chunk-JTR2E7RD.js";
16
+ tokenizePaths,
17
+ unionWithDefaultForbiddenPatterns
18
+ } from "./chunk-FIEIGBYL.js";
19
19
  import {
20
+ DEFAULT_FORBIDDEN_PATTERNS,
20
21
  loadPolicy,
21
22
  policyToConfig,
22
23
  policyToRole
23
- } from "./chunk-WLIDSTS4.js";
24
+ } from "./chunk-KWZ7JKKO.js";
24
25
 
25
26
  // src/gateway/workspaceRouter.ts
26
- import { resolve, dirname } from "path";
27
+ import { resolve, dirname, sep } from "path";
27
28
 
28
29
  // src/mergeRoles.ts
29
30
  function isWithinActiveHours(hour, range) {
@@ -251,15 +252,35 @@ async function resolveWorkspace(cwd, home) {
251
252
  const policyPath = discoverPolicy(start, home);
252
253
  let root = start;
253
254
  let workspaceRole = null;
255
+ const warnings = [];
254
256
  if (policyPath) {
255
257
  const policy = await loadPolicy(policyPath);
258
+ const policyDir = dirname(resolve(policyPath));
259
+ root = policyDir;
256
260
  const repoRoot = policyToConfig(policy).repo?.root;
257
- root = repoRoot ? resolve(repoRoot) : dirname(resolve(policyPath));
261
+ if (repoRoot) {
262
+ const declared = resolve(policyDir, repoRoot);
263
+ const homeAbs = resolve(home);
264
+ const declaredPrefix = declared.endsWith(sep) ? declared : declared + sep;
265
+ const containsPolicy = policyDir === declared || policyDir.startsWith(declaredPrefix);
266
+ const aboveHome = declared !== homeAbs && homeAbs.startsWith(declaredPrefix);
267
+ if (!containsPolicy) {
268
+ warnings.push(
269
+ `repo.root "${repoRoot}" (resolves to "${declared}") does not contain the policy file "${policyPath}" \u2014 anchoring to the policy's own directory "${policyDir}" instead`
270
+ );
271
+ } else if (aboveHome) {
272
+ warnings.push(
273
+ `repo.root "${repoRoot}" (resolves to "${declared}") reaches above the home directory \u2014 anchoring to the policy's own directory "${policyDir}" instead`
274
+ );
275
+ } else {
276
+ root = declared;
277
+ }
278
+ }
258
279
  workspaceRole = policyToRole(policy);
259
280
  }
260
281
  return {
261
282
  ok: true,
262
- resolution: { root, agentId: deriveAgentId(root), policyPath, workspaceRole }
283
+ resolution: { root, agentId: deriveAgentId(root), policyPath, workspaceRole, warnings }
263
284
  };
264
285
  }
265
286
  function effectiveRole(ceiling, workspaceRole) {
@@ -347,11 +368,11 @@ var TranslatorRegistry = class {
347
368
 
348
369
  // src/gateway/runtimeConstructionResolvers.ts
349
370
  var MAX_RECURSION_DEPTH = 3;
350
- var INTERPRETER_RE = /\b(?:python[23]?|node|ruby|perl|php)\s+(?:-[cer])\s+(.+)/s;
371
+ var INTERPRETER_RE = /\b(?:python|node|ruby|perl|php)[0-9.]*\s+(?:\S+\s+)*?-[cer]/;
351
372
  var NESTED_ENCODING_RE = /chr\(\d+\)|String\.fromCharCode|\\x[0-9a-fA-F]{2}|\\[0-7]{1,3}|printf\s/;
352
373
  var PRINTF_HEX_RE = /printf\s+['"]?[^'"]*\\x[0-9a-fA-F]{2}/;
353
374
  var PRINTF_OCT_RE = /printf\s+['"]?[^'"]*\\[0-7]{1,3}/;
354
- var B1_CONTEXT_RE = /(?:\bbase64\s+(?:-d|--decode)\b|\bopenssl\s+(?:enc\s+)?(?:-?base64\s+(?:-\w+\s+)*-d|-d\s+(?:-\w+\s+)*-?base64)\b)/;
375
+ var B1_CONTEXT_RE = /(?:\bbase64\s+(?:--decode\b|-[a-zA-Z]*[dD][a-zA-Z]*\b)|\bopenssl\s+(?:enc\s+)?(?:-?base64\s+(?:-\w+\s+)*-d|-d\s+(?:-\w+\s+)*-?base64)\b)/;
355
376
  var ECHO_E_HEX_RE = /\becho\s+(?:-\w+\s+)*-\w*e\w*\b[^|;&\n]*\\x[0-9a-fA-F]{2}/;
356
377
  var ANSI_C_QUOTE_HEX_RE = /\$'[^']*\\x[0-9a-fA-F]{2}/;
357
378
  var ANSI_C_QUOTE_OCT_RE = /\$'[^']*\\[0-7]{1,3}/;
@@ -1032,17 +1053,25 @@ var ClaudeCodeTranslator = class {
1032
1053
  };
1033
1054
 
1034
1055
  // src/gateway/grepRewriter.ts
1056
+ function toRelativeExclusionGlob(pattern) {
1057
+ return pattern.startsWith("/") ? "**" + pattern : pattern;
1058
+ }
1035
1059
  function buildGrepExclusions(forbiddenPatterns) {
1036
1060
  const flags = [];
1037
1061
  for (const pattern of forbiddenPatterns) {
1038
- flags.push("--glob", `!${pattern}`);
1062
+ flags.push("--glob", `!${toRelativeExclusionGlob(pattern)}`);
1039
1063
  }
1040
1064
  return flags;
1041
1065
  }
1042
1066
  function isPathItselfForbidden(searchPath, forbiddenPatterns) {
1043
1067
  const matched = [];
1044
1068
  for (const pattern of forbiddenPatterns) {
1045
- if (matchGlobInsensitive(pattern, searchPath)) {
1069
+ if (matchGlobInsensitive(pattern, searchPath) || // A directory-glob (`…/**`, e.g. `**/.ssh/**` or `/etc/**`) does NOT match
1070
+ // the bare directory itself — the trailing `/.*` requires a char after the
1071
+ // slash, so `matchGlob('**/.ssh/**', '/home/u/.ssh')` is false. When the
1072
+ // SEARCH PATH *is* the forbidden directory, treat it as forbidden so it
1073
+ // routes to DENY (no rewrite), not MODIFY.
1074
+ pattern.endsWith("/**") && matchGlobInsensitive(pattern.slice(0, -3), searchPath)) {
1046
1075
  matched.push(pattern);
1047
1076
  }
1048
1077
  }
@@ -1424,6 +1453,9 @@ var SentinelGateway = class {
1424
1453
  return { ok: false, finding };
1425
1454
  }
1426
1455
  const { root, agentId, workspaceRole } = resolved.resolution;
1456
+ for (const w of resolved.resolution.warnings) {
1457
+ console.warn(`[SENTINEL GATEWAY] workspace-resolution warning (${agentId}): ${w}`);
1458
+ }
1427
1459
  const ceiling = this.operatorCeiling;
1428
1460
  if (!ceiling) {
1429
1461
  const check = {
@@ -1450,7 +1482,24 @@ var SentinelGateway = class {
1450
1482
  name: agentId
1451
1483
  });
1452
1484
  this.sentinel.touchAgent(agentId);
1453
- return { ok: true, agentId };
1485
+ return { ok: true, agentId, effectiveRole: merged.role };
1486
+ }
1487
+ /**
1488
+ * Forbidden-pattern list for ONE request: the gateway's construction-time
1489
+ * list unioned with the request's merged-role patterns (operator-ceiling ∩
1490
+ * workspace yaml). Without this, a workspace's custom forbid targets were
1491
+ * enforced by the role validator inside wrap() but never reached the
1492
+ * gateway's own bash-L1 / Grep layers, which checked only the static list.
1493
+ * Normalization is idempotent (the chokepoint globstar-prepend).
1494
+ */
1495
+ patternsForRequest(effectiveRole2) {
1496
+ if (!effectiveRole2?.forbiddenTargetPatterns?.length) return this.forbiddenPatterns;
1497
+ return [
1498
+ .../* @__PURE__ */ new Set([
1499
+ ...this.forbiddenPatterns,
1500
+ ...effectiveRole2.forbiddenTargetPatterns.map(normalizeForbiddenPattern)
1501
+ ])
1502
+ ];
1454
1503
  }
1455
1504
  async handlePreToolUse(body, res, translator) {
1456
1505
  let payload;
@@ -1466,6 +1515,7 @@ var SentinelGateway = class {
1466
1515
  return;
1467
1516
  }
1468
1517
  let routingId = this.agentId;
1518
+ let requestForbiddenPatterns = this.forbiddenPatterns;
1469
1519
  if (this.workspaceIsolation) {
1470
1520
  const routed = await this.resolveBPathRouting(
1471
1521
  event.metadata?.cwd,
@@ -1483,6 +1533,7 @@ var SentinelGateway = class {
1483
1533
  }
1484
1534
  routingId = routed.agentId;
1485
1535
  event.agentId = routingId;
1536
+ requestForbiddenPatterns = this.patternsForRequest(routed.effectiveRole);
1486
1537
  }
1487
1538
  if (event.metadata?._unknownTool === "true") {
1488
1539
  const unknownName = event.metadata.ccToolName ?? event.primaryTarget;
@@ -1555,7 +1606,7 @@ var SentinelGateway = class {
1555
1606
  let matchedPath = null;
1556
1607
  let matchedPattern = null;
1557
1608
  for (const tokenPath of allTokenPaths) {
1558
- for (const pattern of this.forbiddenPatterns) {
1609
+ for (const pattern of requestForbiddenPatterns) {
1559
1610
  if (matchGlobInsensitive(pattern, tokenPath)) {
1560
1611
  matchedPath = tokenPath;
1561
1612
  matchedPattern = pattern;
@@ -1737,7 +1788,7 @@ var SentinelGateway = class {
1737
1788
  if (ccToolName === "Grep") {
1738
1789
  const toolInput = parsedPayload.tool_input ?? {};
1739
1790
  const searchPath = typeof toolInput.path === "string" && toolInput.path.length > 0 ? toolInput.path : parsedPayload.cwd ?? ".";
1740
- const pathCheck = isPathItselfForbidden(searchPath, this.forbiddenPatterns);
1791
+ const pathCheck = isPathItselfForbidden(searchPath, requestForbiddenPatterns);
1741
1792
  if (pathCheck.forbidden) {
1742
1793
  const finding2 = {
1743
1794
  severity: "HIGH",
@@ -1762,7 +1813,7 @@ var SentinelGateway = class {
1762
1813
  this.sendJson(res, 200, response);
1763
1814
  return;
1764
1815
  }
1765
- const exclusions = buildGrepExclusions(this.forbiddenPatterns);
1816
+ const exclusions = buildGrepExclusions(requestForbiddenPatterns);
1766
1817
  const updatedInput = buildModifiedGrepInput(toolInput, exclusions);
1767
1818
  const finding = {
1768
1819
  severity: "MEDIUM",
@@ -1785,7 +1836,7 @@ var SentinelGateway = class {
1785
1836
  await this.sentinel.logFinding(routingId, finding);
1786
1837
  this.telemetry.recordToolCall(event.action, "pre", "allowed", 0);
1787
1838
  const ccTranslator = translator;
1788
- const excludedPatterns = this.forbiddenPatterns.join(", ");
1839
+ const excludedPatterns = requestForbiddenPatterns.join(", ");
1789
1840
  const additionalContext = `Sentinel: this Grep search was modified to exclude forbidden paths. Excluded patterns: ${excludedPatterns}. To search specific forbidden files, use Read with explicit approval.`;
1790
1841
  if (typeof ccTranslator.formatPreToolUseModifyResponse === "function") {
1791
1842
  const response = ccTranslator.formatPreToolUseModifyResponse({
@@ -2052,11 +2103,11 @@ async function runGatewayDaemon({
2052
2103
  port = DEFAULT_PORT,
2053
2104
  buildId
2054
2105
  }) {
2055
- const { Sentinel: SentinelClass } = await import("./Sentinel-5CQ6HKXS.js");
2106
+ const { Sentinel: SentinelClass } = await import("./Sentinel-4QKPFHTI.js");
2056
2107
  const { writePidFile, writeReleaseToken } = await import("./pidManager-DOGVN6ZT.js");
2057
2108
  const { homedir } = await import("os");
2058
2109
  const { randomBytes } = await import("crypto");
2059
- const { loadPolicy: loadPolicy2, policyToRole: policyToRole2, policyToConfig: policyToConfig2 } = await import("./policyLoader-KZL2U4M2.js");
2110
+ const { loadPolicy: loadPolicy2, policyToRole: policyToRole2, policyToConfig: policyToConfig2 } = await import("./policyLoader-XX6BQXNB.js");
2060
2111
  const sentinel = await SentinelClass.fromPolicy(policyPath);
2061
2112
  const baseline = await sentinel.computeBaseline("claude-code");
2062
2113
  sentinel.setBaseline("claude-code", baseline);
@@ -2075,7 +2126,11 @@ async function runGatewayDaemon({
2075
2126
  releaseToken,
2076
2127
  unknownTools: operatorConfig.enforcement?.unknownTools,
2077
2128
  allowUnknownTools: operatorConfig.enforcement?.allowUnknownTools,
2078
- buildId
2129
+ buildId,
2130
+ // The operator policy's custom forbid targets must reach the gateway's own
2131
+ // bash-L1/Grep layers (defaults-as-floor union, same chokepoint as role
2132
+ // construction) — previously the gateway saw only the built-in defaults.
2133
+ forbiddenPatterns: unionWithDefaultForbiddenPatterns(operatorCeiling.forbiddenTargetPatterns)
2079
2134
  });
2080
2135
  await gateway.start();
2081
2136
  const home = homedir();
@@ -2088,4 +2143,4 @@ export {
2088
2143
  SentinelGateway,
2089
2144
  runGatewayDaemon
2090
2145
  };
2091
- //# sourceMappingURL=chunk-G74MMDKA.js.map
2146
+ //# sourceMappingURL=chunk-HRI2Y326.js.map