@node9/proxy 1.65.1 → 1.67.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -102,7 +102,7 @@ function redactSecrets(text) {
102
102
  if (!text) return text;
103
103
  let redacted = text;
104
104
  redacted = redacted.replace(
105
- /(authorization:\s*(?:bearer|basic)\s+)[a-zA-Z0-9._\-\/\\=]+/gi,
105
+ /(authorization:\s*(?:bearer|basic)\s+)[a-zA-Z0-9._\-\/=]+/gi,
106
106
  "$1********"
107
107
  );
108
108
  redacted = redacted.replace(
@@ -163,7 +163,7 @@ function filePathFromArgs(args) {
163
163
  return void 0;
164
164
  }
165
165
  function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
166
- const isDlpRow = checkedBy.toLowerCase().includes("dlp") || Boolean(meta?.dlpPattern);
166
+ const isDlpRow = checkedBy.toLowerCase().includes("dlp") || Boolean(meta?.dlpPattern) || /dlp|taint/i.test(String(meta?.ruleName ?? ""));
167
167
  const preview2 = auditHashArgsEnabled && !isDlpRow ? buildArgsPreview(args) : void 0;
168
168
  const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args), ...preview2 ? { argsPreview: preview2 } : {} } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
169
169
  const testRun = isTestCall(toolName, args) || process.env.NODE9_TESTING === "1" ? { testRun: true } : {};
@@ -336,8 +336,10 @@ var init_config_schema = __esm({
336
336
  agentPolicy: z.enum(["require_approval", "block_on_rules"]).optional(),
337
337
  // Where a `review` verdict's prompt is rendered: 'ask' = the agent's own
338
338
  // inline approve/deny prompt (Claude Code / GitHub Copilot); 'approver' =
339
- // node9's own approver (terminal/native/cloud). Unset → smart default
340
- // (ask for ask-capable agents unless a cloud approver is configured).
339
+ // node9's own approver (terminal/native/cloud). Unset → default ASK for
340
+ // ask-capable agents (v2: cloud no longer disables inline — the outcome
341
+ // ships to the dashboard as audit; admins force routing via managed
342
+ // reviewChannel, which outranks the local --ask flag).
341
343
  reviewChannel: z.enum(["ask", "approver"]).optional(),
342
344
  // When true, agents may call WEAKENING node9 MCP tools (shield_disable,
343
345
  // approver_set). Default (unset/false): those tools refuse over MCP — a human
@@ -375,7 +377,18 @@ var init_config_schema = __esm({
375
377
  dlp: z.object({
376
378
  enabled: z.boolean().optional(),
377
379
  scanIgnoredTools: z.boolean().optional(),
378
- pii: z.enum(["off", "block"]).optional()
380
+ pii: z.enum(["off", "block"]).optional(),
381
+ reviewAction: z.enum(["review", "block"]).optional()
382
+ }).optional(),
383
+ // Command-checks governance. Class-B keys (evalDynamic, pipeChainHigh)
384
+ // deliberately exclude 'off' — tighten-only.
385
+ commandChecks: z.object({
386
+ inlineExec: z.enum(["off", "review", "block"]).optional(),
387
+ rmAdvisory: z.enum(["off", "review", "block"]).optional(),
388
+ chmod: z.enum(["off", "review", "block"]).optional(),
389
+ sqlDdl: z.enum(["off", "review", "block"]).optional(),
390
+ evalDynamic: z.enum(["review", "block"]).optional(),
391
+ pipeChainHigh: z.enum(["review", "block"]).optional()
379
392
  }).optional(),
380
393
  egress: z.object({
381
394
  enabled: z.boolean().optional(),
@@ -1520,6 +1533,45 @@ function evaluateSmartConditions(args, rule) {
1520
1533
  });
1521
1534
  return mode === "any" ? results.some((r) => r) : results.every((r) => r);
1522
1535
  }
1536
+ function resolveCheck(v) {
1537
+ return v === "off" || v === "block" ? v : "review";
1538
+ }
1539
+ function resolveCheckTight(v) {
1540
+ return v === "block" ? "block" : "review";
1541
+ }
1542
+ function detectInlineExec(command) {
1543
+ const pipeFed = command.includes("|");
1544
+ const segments = command.split(/\|\||&&|;|\||\n|\$\(|`|\(/);
1545
+ for (const rawSeg of segments) {
1546
+ const tokens = rawSeg.trim().split(/\s+/).filter(Boolean);
1547
+ let i = 0;
1548
+ while (i < tokens.length && /^[A-Za-z_]\w*=/.test(tokens[i])) i++;
1549
+ if (i >= tokens.length) continue;
1550
+ const base = tokens[i].split("/").pop() ?? tokens[i];
1551
+ if (!INLINE_INTERP.test(base)) continue;
1552
+ const args = tokens.slice(i + 1);
1553
+ let hadRedirect = false;
1554
+ const positionals = [];
1555
+ for (let j = 0; j < args.length; j++) {
1556
+ const a = args[j];
1557
+ if (a === "-") return true;
1558
+ if (a.startsWith("<")) {
1559
+ hadRedirect = true;
1560
+ if (a === "<" || a === "<<") j++;
1561
+ continue;
1562
+ }
1563
+ if (a.startsWith("-")) {
1564
+ if (/^-(c|e|eval)$/i.test(a)) return true;
1565
+ continue;
1566
+ }
1567
+ positionals.push(a);
1568
+ }
1569
+ if (positionals.length === 0 && (hadRedirect || pipeFed) && !INLINE_SHELL.test(base)) {
1570
+ return true;
1571
+ }
1572
+ }
1573
+ return false;
1574
+ }
1523
1575
  function resolvePinned(matches) {
1524
1576
  if (matches.length === 0) return void 0;
1525
1577
  const pinned = matches.filter((r) => r.pinned);
@@ -1546,7 +1598,7 @@ function isSqlTool(toolName, toolInspection) {
1546
1598
  const fieldName = toolInspection[matchingPattern];
1547
1599
  return fieldName === "sql" || fieldName === "query";
1548
1600
  }
1549
- function pipeChainVerdict(command, isTrustedHost2) {
1601
+ function pipeChainVerdict(command, isTrustedHost2, highAction = "review") {
1550
1602
  const pipeAnalysis = analyzePipeChain(command);
1551
1603
  if (!pipeAnalysis.isPipeline) return null;
1552
1604
  if (pipeAnalysis.risk !== "critical" && pipeAnalysis.risk !== "high") return null;
@@ -1577,7 +1629,7 @@ function pipeChainVerdict(command, isTrustedHost2) {
1577
1629
  };
1578
1630
  }
1579
1631
  return {
1580
- decision: "review",
1632
+ decision: highAction,
1581
1633
  blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
1582
1634
  reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
1583
1635
  tier: 3
@@ -1591,7 +1643,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1591
1643
  const dlpMatch = args !== void 0 ? scanArgs(args) : null;
1592
1644
  if (dlpMatch) {
1593
1645
  return {
1594
- decision: dlpMatch.severity,
1646
+ // reviewAction:'block' (inline-ask v2): the admin upgraded
1647
+ // review-severity matches to a hard block — every evaluatePolicy
1648
+ // caller (orchestrator, explain, gateway) must agree with the gate.
1649
+ decision: dlpMatch.severity === "block" || config.policy.dlp.reviewAction === "block" ? "block" : "review",
1595
1650
  blockedByLabel: `DLP: ${dlpMatch.patternName}`,
1596
1651
  reason: `${dlpMatch.patternName} detected in ${dlpMatch.fieldPath}`
1597
1652
  };
@@ -1600,7 +1655,11 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1600
1655
  if (wouldBeIgnored) return { decision: "allow" };
1601
1656
  const bashCommand = agent !== "Terminal" && isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : null;
1602
1657
  if (bashCommand !== null) {
1603
- const pipeVerdict = pipeChainVerdict(bashCommand, isTrustedHost2);
1658
+ const pipeVerdict = pipeChainVerdict(
1659
+ bashCommand,
1660
+ isTrustedHost2,
1661
+ resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
1662
+ );
1604
1663
  if (pipeVerdict) return pipeVerdict;
1605
1664
  const fsVerdict = analyzeFsOperation(bashCommand);
1606
1665
  if (fsVerdict) {
@@ -1615,10 +1674,12 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1615
1674
  ruleDescription: fsVerdict.reason
1616
1675
  };
1617
1676
  }
1618
- const sqlVerdict = analyzeSqlDestructive(bashCommand);
1677
+ const sqlAction = resolveCheck(config.policy.commandChecks?.sqlDdl);
1678
+ const sqlVerdict = sqlAction === "off" ? null : analyzeSqlDestructive(bashCommand);
1619
1679
  if (sqlVerdict) {
1620
1680
  return {
1621
- decision: sqlVerdict.verdict,
1681
+ // analyzeSqlDestructive is typed review-only, so the knob maps 1:1.
1682
+ decision: sqlAction === "block" ? "block" : "review",
1622
1683
  blockedByLabel: `Node9 (AST): ${sqlVerdict.ruleName}`,
1623
1684
  reason: sqlVerdict.reason,
1624
1685
  tier: 2,
@@ -1626,10 +1687,12 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1626
1687
  ruleDescription: sqlVerdict.description
1627
1688
  };
1628
1689
  }
1629
- const chmodVerdict = analyzeChmod777(bashCommand);
1690
+ const chmodAction = resolveCheck(config.policy.commandChecks?.chmod);
1691
+ const chmodVerdict = chmodAction === "off" ? null : analyzeChmod777(bashCommand);
1630
1692
  if (chmodVerdict) {
1631
1693
  return {
1632
- decision: chmodVerdict.verdict,
1694
+ // analyzeChmod777 is typed review-only, so the knob maps 1:1.
1695
+ decision: chmodAction === "block" ? "block" : "review",
1633
1696
  blockedByLabel: `project-jail (AST): ${chmodVerdict.ruleName}`,
1634
1697
  reason: chmodVerdict.reason,
1635
1698
  tier: 2,
@@ -1672,10 +1735,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1672
1735
  const analyzed = analyzeShellCommand(shellCommand);
1673
1736
  allTokens = analyzed.allTokens;
1674
1737
  pathTokens = analyzed.paths;
1675
- const INLINE_EXEC_PATTERN = /^(python3?|bash|sh|zsh|perl|ruby|node|php|lua)\s+(-c|-e|-eval)\s/i;
1676
- if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
1738
+ const inlineAction = resolveCheck(config.policy.commandChecks?.inlineExec);
1739
+ if (inlineAction !== "off" && detectInlineExec(shellCommand)) {
1677
1740
  return {
1678
- decision: "review",
1741
+ decision: inlineAction === "block" ? "block" : "review",
1679
1742
  blockedByLabel: "Node9 Standard (Inline Execution)",
1680
1743
  ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
1681
1744
  tier: 3
@@ -1693,14 +1756,21 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1693
1756
  }
1694
1757
  if (evalVerdict === "review") {
1695
1758
  return {
1696
- decision: "review",
1759
+ // Class B tighten-only: commandChecks.evalDynamic may upgrade to
1760
+ // block but can never turn this off (eval-remote above is Class A —
1761
+ // no knob at all).
1762
+ decision: resolveCheckTight(config.policy.commandChecks?.evalDynamic),
1697
1763
  blockedByLabel: "Node9: Eval Dynamic Content",
1698
1764
  reason: "eval of dynamic content (variable or subshell expansion) requires approval",
1699
1765
  ruleDescription: "The AI is running a command that includes a variable or subshell expansion. The actual command executed at runtime may differ from what is shown here.",
1700
1766
  tier: 3
1701
1767
  };
1702
1768
  }
1703
- const ptVerdict = pipeChainVerdict(shellCommand, isTrustedHost2);
1769
+ const ptVerdict = pipeChainVerdict(
1770
+ shellCommand,
1771
+ isTrustedHost2,
1772
+ resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
1773
+ );
1704
1774
  if (ptVerdict) return ptVerdict;
1705
1775
  if (config.policy.egress?.enabled) {
1706
1776
  const dests = extractShellDestinations(shellCommand);
@@ -2382,7 +2452,7 @@ function* stringValues(obj, depth = 0) {
2382
2452
  }
2383
2453
  for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
2384
2454
  }
2385
- var MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, NET_BINARIES, VALUE_FLAGS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_HEREDOC_OPS, DEFAULT_EGRESS_ALLOWLIST, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, VERDICT_RANK, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, COST_PER_LOOP_ITER_USD, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE, ENGINE_VERSION;
2455
+ var MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, NET_BINARIES, VALUE_FLAGS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_HEREDOC_OPS, DEFAULT_EGRESS_ALLOWLIST, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, INLINE_INTERP, INLINE_SHELL, VERDICT_RANK, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, COST_PER_LOOP_ITER_USD, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE, ENGINE_VERSION;
2386
2456
  var init_dist = __esm({
2387
2457
  "packages/policy-engine/dist/index.mjs"() {
2388
2458
  "use strict";
@@ -3313,6 +3383,8 @@ var init_dist = __esm({
3313
3383
  REGEX_CACHE_MAX = 500;
3314
3384
  regexCache = /* @__PURE__ */ new Map();
3315
3385
  FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
3386
+ INLINE_INTERP = /^(python[\d.]*|bash|sh|zsh|perl|ruby|node|php|lua)$/i;
3387
+ INLINE_SHELL = /^(bash|sh|zsh)$/i;
3316
3388
  VERDICT_RANK = {
3317
3389
  allow: 0,
3318
3390
  review: 1,
@@ -4370,6 +4442,31 @@ function applyManagedDlp(local, managed, locked) {
4370
4442
  locked.includes("dlpPii")
4371
4443
  );
4372
4444
  }
4445
+ if (typeof managed.reviewAction === "string") {
4446
+ next.reviewAction = resolveByOrder(
4447
+ DLP_REVIEW_ACTION_ORDER,
4448
+ local.reviewAction ?? "review",
4449
+ managed.reviewAction,
4450
+ locked.includes("dlpReviewAction")
4451
+ );
4452
+ }
4453
+ return next;
4454
+ }
4455
+ function applyManagedCommandChecks(local, managed, locked) {
4456
+ const next = { ...local };
4457
+ for (const key of COMMAND_CHECK_KEYS) {
4458
+ const m = managed[key];
4459
+ if (typeof m !== "string") continue;
4460
+ const lockKey = `commandChecks${key[0].toUpperCase()}${key.slice(1)}`;
4461
+ const resolved = resolveByOrder(
4462
+ COMMAND_CHECK_ORDER,
4463
+ local[key] ?? "review",
4464
+ m,
4465
+ locked.includes(lockKey)
4466
+ );
4467
+ if ((key === "evalDynamic" || key === "pipeChainHigh") && resolved === "off") continue;
4468
+ next[key] = resolved;
4469
+ }
4373
4470
  return next;
4374
4471
  }
4375
4472
  function applyManagedApprovers(local, managed) {
@@ -4381,13 +4478,23 @@ function applyManagedApprovers(local, managed) {
4381
4478
  terminal: typeof managed.terminal === "boolean" ? managed.terminal : local.terminal
4382
4479
  };
4383
4480
  }
4384
- var MODE_ORDER, EGRESS_MODE_ORDER, DLP_PII_ORDER;
4481
+ var MODE_ORDER, EGRESS_MODE_ORDER, DLP_PII_ORDER, DLP_REVIEW_ACTION_ORDER, COMMAND_CHECK_ORDER, COMMAND_CHECK_KEYS;
4385
4482
  var init_managed = __esm({
4386
4483
  "src/config/managed.ts"() {
4387
4484
  "use strict";
4388
4485
  MODE_ORDER = ["observe", "audit", "standard", "strict"];
4389
4486
  EGRESS_MODE_ORDER = ["off", "review", "block"];
4390
4487
  DLP_PII_ORDER = ["off", "block"];
4488
+ DLP_REVIEW_ACTION_ORDER = ["review", "block"];
4489
+ COMMAND_CHECK_ORDER = ["off", "review", "block"];
4490
+ COMMAND_CHECK_KEYS = [
4491
+ "inlineExec",
4492
+ "rmAdvisory",
4493
+ "chmod",
4494
+ "sqlDdl",
4495
+ "evalDynamic",
4496
+ "pipeChainHigh"
4497
+ ];
4391
4498
  }
4392
4499
  });
4393
4500
 
@@ -4765,6 +4872,22 @@ function getConfig(cwd) {
4765
4872
  if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
4766
4873
  if (d.scanIgnoredTools !== void 0) mergedPolicy.dlp.scanIgnoredTools = d.scanIgnoredTools;
4767
4874
  if (d.pii !== void 0) mergedPolicy.dlp.pii = d.pii;
4875
+ if (d.reviewAction !== void 0) mergedPolicy.dlp.reviewAction = d.reviewAction;
4876
+ }
4877
+ if (p.commandChecks && typeof p.commandChecks === "object") {
4878
+ const src = p.commandChecks;
4879
+ const cc2 = {
4880
+ ...mergedPolicy.commandChecks
4881
+ };
4882
+ for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
4883
+ const v = src[k];
4884
+ if (v === "off" || v === "review" || v === "block") cc2[k] = v;
4885
+ }
4886
+ for (const k of ["evalDynamic", "pipeChainHigh"]) {
4887
+ const v = src[k];
4888
+ if (v === "review" || v === "block") cc2[k] = v;
4889
+ }
4890
+ if (Object.keys(cc2).length > 0) mergedPolicy.commandChecks = cc2;
4768
4891
  }
4769
4892
  if (p.egress) {
4770
4893
  const e = p.egress;
@@ -4861,11 +4984,19 @@ function getConfig(cwd) {
4861
4984
  mergedPolicy.dlp,
4862
4985
  {
4863
4986
  enabled: typeof mc.dlp.enabled === "boolean" ? mc.dlp.enabled : void 0,
4864
- pii: typeof mc.dlp.pii === "string" ? mc.dlp.pii : void 0
4987
+ pii: typeof mc.dlp.pii === "string" ? mc.dlp.pii : void 0,
4988
+ reviewAction: mc.dlp.reviewAction === "review" || mc.dlp.reviewAction === "block" ? mc.dlp.reviewAction : void 0
4865
4989
  },
4866
4990
  locked
4867
4991
  );
4868
4992
  }
4993
+ if (mc.commandChecks && typeof mc.commandChecks === "object") {
4994
+ mergedPolicy.commandChecks = applyManagedCommandChecks(
4995
+ mergedPolicy.commandChecks ?? {},
4996
+ mc.commandChecks,
4997
+ locked
4998
+ );
4999
+ }
4869
5000
  if (mc.approvers && typeof mc.approvers === "object") {
4870
5001
  const bool = (v) => typeof v === "boolean" ? v : void 0;
4871
5002
  mergedSettings.approvers = applyManagedApprovers(mergedSettings.approvers, {
@@ -4877,6 +5008,7 @@ function getConfig(cwd) {
4877
5008
  }
4878
5009
  if (mc.reviewChannel === "ask" || mc.reviewChannel === "approver") {
4879
5010
  mergedSettings.reviewChannel = mc.reviewChannel;
5011
+ mergedSettings.reviewChannelManaged = true;
4880
5012
  }
4881
5013
  if (typeof mc.approvalTimeoutMs === "number" && mc.approvalTimeoutMs > 0) {
4882
5014
  mergedSettings.approvalTimeoutMs = mc.approvalTimeoutMs;
@@ -4974,8 +5106,17 @@ function getConfig(cwd) {
4974
5106
  }
4975
5107
  }
4976
5108
  const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
5109
+ const cc = mergedPolicy.commandChecks ?? {};
5110
+ const advisoryKnob = (name) => {
5111
+ if (name === "review-rm") return cc.rmAdvisory;
5112
+ if (name?.endsWith("-sql")) return cc.sqlDdl;
5113
+ return void 0;
5114
+ };
4977
5115
  for (const rule of ADVISORY_SMART_RULES) {
4978
- if (!existingAdvisoryNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
5116
+ if (existingAdvisoryNames.has(rule.name)) continue;
5117
+ const knob = rule.verdict === "review" ? advisoryKnob(rule.name) : void 0;
5118
+ if (knob === "off") continue;
5119
+ mergedPolicy.smartRules.push(knob === "block" ? { ...rule, verdict: "block" } : rule);
4979
5120
  }
4980
5121
  const envMode = process.env.NODE9_MODE;
4981
5122
  if (envMode && !modeCloudControlled && ["observe", "audit", "standard", "strict"].includes(envMode)) {
@@ -5621,13 +5762,14 @@ async function deriveExplainTrace(toolName, args) {
5621
5762
  const filePathE = String(argsObjE.file_path ?? argsObjE.path ?? argsObjE.filename ?? "");
5622
5763
  const dlpMatch = (filePathE ? scanFilePath(filePathE) : null) ?? (args !== void 0 ? scanArgs(args) : null);
5623
5764
  if (dlpMatch) {
5765
+ const dlpBlocks = dlpMatch.severity === "block" || config.policy.dlp.reviewAction === "block";
5624
5766
  steps.push({
5625
5767
  name: "DLP Content Scanner",
5626
- outcome: dlpMatch.severity === "block" ? "block" : "review",
5768
+ outcome: dlpBlocks ? "block" : "review",
5627
5769
  detail: `\u{1F6A8} ${dlpMatch.patternName} detected in ${dlpMatch.fieldPath} \u2014 sample: ${dlpMatch.redactedSample}`,
5628
- isFinal: dlpMatch.severity === "block"
5770
+ isFinal: dlpBlocks
5629
5771
  });
5630
- if (dlpMatch.severity === "block") {
5772
+ if (dlpBlocks) {
5631
5773
  return { tool: toolName, args, waterfall, steps, decision: "block" };
5632
5774
  }
5633
5775
  } else {
@@ -7122,7 +7264,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
7122
7264
  const dlpMatch = (filePath ? scanFilePath(filePath) : null) ?? scanArgs(args);
7123
7265
  if (dlpMatch) {
7124
7266
  const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
7125
- if (dlpMatch.severity === "block") {
7267
+ if (dlpMatch.severity === "block" || config.policy.dlp.reviewAction === "block") {
7126
7268
  let dlpEid;
7127
7269
  if (!isManual)
7128
7270
  dlpEid = appendLocalAudit(
@@ -7460,12 +7602,14 @@ ${appPermReview}`
7460
7602
  );
7461
7603
  }
7462
7604
  }
7463
- const cloudEnforcedForDefer = approvers.cloud && !!creds?.apiKey;
7464
- if (options?.deferReview && !hardBlockDowngraded && !taintWarning && !appPermReview && !cloudEnforcedForDefer) {
7605
+ if (options?.deferReview && !hardBlockDowngraded) {
7465
7606
  return {
7466
7607
  approved: false,
7467
7608
  review: true,
7468
- reason: explainableLabel || "Node9 flagged this action for review.",
7609
+ // The prompt must say WHY: the taint sentence beats the bare label so
7610
+ // the dev sees the actual risk context inline. (No appPermReview term:
7611
+ // deferReview and serverKey never co-occur in production — see above.)
7612
+ reason: taintWarning || explainableLabel || "Node9 flagged this action for review.",
7469
7613
  ruleDescription: policyRuleDescription,
7470
7614
  blockedByLabel: explainableLabel
7471
7615
  };
@@ -8412,7 +8556,7 @@ function printInlineAskNotice() {
8412
8556
  );
8413
8557
  console.log(
8414
8558
  chalk.gray(
8415
- ' Prefer node9\u2019s own approver? Set "reviewChannel": "approver" in config, or add --no-ask to the hook.\n (Inline prompts are auto-disabled when a cloud approver is configured.)'
8559
+ ' Prefer node9\u2019s own approver? Set "reviewChannel": "approver" in config, or add --no-ask to the hook.\n (Inline is the default for every review; with a cloud workspace the outcome ships to the dashboard as audit.)'
8416
8560
  )
8417
8561
  );
8418
8562
  }
@@ -18351,7 +18495,21 @@ function extractManagedConfig(body) {
18351
18495
  const d = {};
18352
18496
  if (typeof mc.dlp.enabled === "boolean") d.enabled = mc.dlp.enabled;
18353
18497
  if (typeof mc.dlp.pii === "string") d.pii = mc.dlp.pii;
18354
- if (d.enabled !== void 0 || d.pii !== void 0) out.dlp = d;
18498
+ if (mc.dlp.reviewAction === "review" || mc.dlp.reviewAction === "block")
18499
+ d.reviewAction = mc.dlp.reviewAction;
18500
+ if (d.enabled !== void 0 || d.pii !== void 0 || d.reviewAction !== void 0) out.dlp = d;
18501
+ }
18502
+ if (mc.commandChecks && typeof mc.commandChecks === "object") {
18503
+ const cc = {};
18504
+ for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
18505
+ const v = mc.commandChecks[k];
18506
+ if (v === "off" || v === "review" || v === "block") cc[k] = v;
18507
+ }
18508
+ for (const k of ["evalDynamic", "pipeChainHigh"]) {
18509
+ const v = mc.commandChecks[k];
18510
+ if (v === "review" || v === "block") cc[k] = v;
18511
+ }
18512
+ if (Object.keys(cc).length > 0) out.commandChecks = cc;
18355
18513
  }
18356
18514
  if (mc.approvers && typeof mc.approvers === "object") {
18357
18515
  const a = {};
@@ -18415,7 +18573,7 @@ function extractManagedConfig(body) {
18415
18573
  }
18416
18574
  if (Object.keys(ap).length) out.appPermissions = ap;
18417
18575
  }
18418
- return out.mode !== void 0 || out.egress !== void 0 || out.dlp !== void 0 || out.approvers !== void 0 || out.reviewChannel !== void 0 || out.approvalTimeoutMs !== void 0 || out.injectionScan !== void 0 || out.loopDetection !== void 0 || out.skillPinning !== void 0 || out.jailPaths !== void 0 || out.trustedHosts !== void 0 || out.appPermissions !== void 0 ? out : void 0;
18576
+ return out.mode !== void 0 || out.egress !== void 0 || out.dlp !== void 0 || out.commandChecks !== void 0 || out.approvers !== void 0 || out.reviewChannel !== void 0 || out.approvalTimeoutMs !== void 0 || out.injectionScan !== void 0 || out.loopDetection !== void 0 || out.skillPinning !== void 0 || out.jailPaths !== void 0 || out.trustedHosts !== void 0 || out.appPermissions !== void 0 ? out : void 0;
18419
18577
  }
18420
18578
  function sweepStaleTmp(target) {
18421
18579
  try {
@@ -19043,12 +19201,23 @@ function classifyDecision(a, b) {
19043
19201
  function decisionTag(view) {
19044
19202
  return `[${view.label}]`.padEnd(14);
19045
19203
  }
19046
- var HUMAN_SOURCES, TIMEOUT_SOURCES, has;
19204
+ var HUMAN_SOURCES, TIMEOUT_SOURCES, NON_DECISION_SOURCES, has;
19047
19205
  var init_decision = __esm({
19048
19206
  "src/audit/decision.ts"() {
19049
19207
  "use strict";
19050
- HUMAN_SOURCES = /* @__PURE__ */ new Set(["daemon", "cloud", "local-decision", "inline-review-approved"]);
19208
+ HUMAN_SOURCES = /* @__PURE__ */ new Set([
19209
+ "daemon",
19210
+ "cloud",
19211
+ "local-decision",
19212
+ "inline-review-approved",
19213
+ "inline-review"
19214
+ ]);
19051
19215
  TIMEOUT_SOURCES = /* @__PURE__ */ new Set(["timeout"]);
19216
+ NON_DECISION_SOURCES = /* @__PURE__ */ new Set([
19217
+ "post-hook",
19218
+ "inline-review-approved",
19219
+ "response-dlp"
19220
+ ]);
19052
19221
  has = (s, needle) => s.includes(needle);
19053
19222
  }
19054
19223
  });
@@ -19631,7 +19800,7 @@ function buildDaemonReport(allEntries, period, now) {
19631
19800
  else if (period === "30d") start.setDate(start.getDate() - 29);
19632
19801
  else if (period === "month") start = new Date(now.getFullYear(), now.getMonth(), 1);
19633
19802
  const entries = allEntries.filter((e) => {
19634
- if (e.source === "post-hook" || e.source === "response-dlp") return false;
19803
+ if (typeof e.source === "string" && NON_DECISION_SOURCES.has(e.source)) return false;
19635
19804
  return new Date(e.ts) >= start;
19636
19805
  });
19637
19806
  const isBlocked = (e) => classifyDecision(e).outcome === "deny";
@@ -47372,8 +47541,8 @@ INSTRUCTIONS:
47372
47541
  - Do NOT retry this exact command or attempt to bypass the rule.${recovery}
47373
47542
  - Inform the user which security rule was triggered and ask how to proceed.`;
47374
47543
  }
47375
- function buildReviewMessage(blockedByLabel, ruleDescription) {
47376
- const why = ruleDescription || blockedByLabel || "this action needs your review";
47544
+ function buildReviewMessage(blockedByLabel, ruleDescription, reason) {
47545
+ const why = ruleDescription || (reason && reason !== blockedByLabel ? reason : void 0) || blockedByLabel || "this action needs your review";
47377
47546
  return `Node9 flagged this for your review: ${why}. Approve to proceed, or deny to cancel.`;
47378
47547
  }
47379
47548
 
@@ -48114,6 +48283,7 @@ import path46 from "path";
48114
48283
  function storePath() {
48115
48284
  return process.env.NODE9_PENDING_STORE || path46.join(os43.homedir(), ".node9", "pending-reviews.json");
48116
48285
  }
48286
+ var TTL_TUID_MS = 72 * 60 * 60 * 1e3;
48117
48287
  var TTL_MS2 = 6 * 60 * 60 * 1e3;
48118
48288
  var MAX_ENTRIES = 500;
48119
48289
  function reviewCorrelationKey(payload) {
@@ -48147,7 +48317,9 @@ function write(store) {
48147
48317
  }
48148
48318
  }
48149
48319
  function prune(entries, now) {
48150
- const fresh = entries.filter((e) => now - e.ts < TTL_MS2);
48320
+ const fresh = entries.filter(
48321
+ (e) => now - e.ts < (e.key.startsWith("tuid:") ? TTL_TUID_MS : TTL_MS2)
48322
+ );
48151
48323
  return fresh.length > MAX_ENTRIES ? fresh.slice(fresh.length - MAX_ENTRIES) : fresh;
48152
48324
  }
48153
48325
  function recordPendingReview(entry) {
@@ -48162,19 +48334,30 @@ function recordPendingReview(entry) {
48162
48334
  function resolvePendingReview(key, now = Date.now()) {
48163
48335
  try {
48164
48336
  const store = read();
48165
- const idx = store.entries.findIndex((e) => e.key === key);
48337
+ const live = prune(store.entries, now);
48338
+ const idx = live.findIndex((e) => e.key === key);
48166
48339
  if (idx === -1) {
48167
- const pruned = prune(store.entries, now);
48168
- if (pruned.length !== store.entries.length) write({ entries: pruned });
48340
+ if (live.length !== store.entries.length) write({ entries: live });
48169
48341
  return null;
48170
48342
  }
48171
- const [match] = store.entries.splice(idx, 1);
48172
- write({ entries: prune(store.entries, now) });
48343
+ const [match] = live.splice(idx, 1);
48344
+ write({ entries: live });
48173
48345
  return match;
48174
48346
  } catch {
48175
48347
  return null;
48176
48348
  }
48177
48349
  }
48350
+ function discardPendingReview(key, now = Date.now()) {
48351
+ try {
48352
+ const store = read();
48353
+ const kept = prune(
48354
+ store.entries.filter((e) => e.key !== key),
48355
+ now
48356
+ );
48357
+ if (kept.length !== store.entries.length) write({ entries: kept });
48358
+ } catch {
48359
+ }
48360
+ }
48178
48361
 
48179
48362
  // src/cli/commands/check.ts
48180
48363
  init_hook_payload();
@@ -48230,7 +48413,10 @@ function agentSupportsAsk(agent) {
48230
48413
  }
48231
48414
  function resolveAskMode(agent, opts, config) {
48232
48415
  if (!agentSupportsAsk(agent)) return false;
48233
- if (config.settings.approvers.cloud === true) return false;
48416
+ if (config.settings.reviewChannelManaged === true) {
48417
+ if (config.settings.reviewChannel === "ask") return true;
48418
+ if (config.settings.reviewChannel === "approver") return false;
48419
+ }
48234
48420
  if (opts.ask === true) return true;
48235
48421
  if (opts.ask === false) return false;
48236
48422
  if (config.settings.reviewChannel === "ask") return true;
@@ -48469,7 +48655,11 @@ RAW: ${raw}
48469
48655
  process.exit(2);
48470
48656
  };
48471
48657
  const sendAsk = (result2) => {
48472
- const msg = buildReviewMessage(result2.blockedByLabel, result2.ruleDescription);
48658
+ const msg = buildReviewMessage(
48659
+ result2.blockedByLabel,
48660
+ result2.ruleDescription,
48661
+ result2.reason
48662
+ );
48473
48663
  try {
48474
48664
  const key = reviewCorrelationKey(payload);
48475
48665
  if (key) {
@@ -48652,6 +48842,13 @@ RAW: ${raw}
48652
48842
  cwd: safeCwdForAuth,
48653
48843
  deferReview: askMode
48654
48844
  });
48845
+ if (!result.review) {
48846
+ try {
48847
+ const key = reviewCorrelationKey(payload);
48848
+ if (key) discardPendingReview(key);
48849
+ } catch {
48850
+ }
48851
+ }
48655
48852
  if (result.approved) {
48656
48853
  if (result.checkedBy && process.env.NODE9_DEBUG === "1")
48657
48854
  process.stderr.write(`\u2713 node9 [${result.checkedBy}]: "${toolName}" allowed
@@ -48739,6 +48936,7 @@ import path48 from "path";
48739
48936
  import os45 from "os";
48740
48937
  init_daemon();
48741
48938
  init_dlp();
48939
+ init_hasher();
48742
48940
 
48743
48941
  // src/utils/cp-mv-parser.ts
48744
48942
  function parseCpMvOp(command) {
@@ -48800,6 +48998,20 @@ function atLeastConfidence(c, min) {
48800
48998
  function sanitize3(value) {
48801
48999
  return value.replace(/[\x00-\x1F\x7F]/g, "");
48802
49000
  }
49001
+ function buildArgsField(rawInput) {
49002
+ let hashed = {};
49003
+ try {
49004
+ hashed = { argsHash: hashArgs(rawInput) };
49005
+ } catch {
49006
+ }
49007
+ try {
49008
+ const hit = scanArgs(rawInput);
49009
+ if (hit) return { ...hashed, dlpPattern: hit.patternName, dlpSample: hit.redactedSample };
49010
+ return { args: JSON.parse(redactSecrets(JSON.stringify(rawInput))) };
49011
+ } catch {
49012
+ return hashed;
49013
+ }
49014
+ }
48803
49015
  function registerLogCommand(program2) {
48804
49016
  program2.command("log", { hidden: true }).description("PostToolUse hook \u2014 records executed tool calls").argument("[data]", "JSON string of the tool call").option(
48805
49017
  "--agent <name>",
@@ -48828,15 +49040,17 @@ function registerLogCommand(program2) {
48828
49040
  })();
48829
49041
  const agent = agentOverride !== void 0 ? agentOverride : metaTag !== void 0 ? metaTag : payload.turn_id !== void 0 ? "Codex" : payload.toolCall !== void 0 || payload.conversationId !== void 0 ? "Antigravity" : payload.hook_event_name === "pre_tool_call" || payload.hook_event_name === "post_tool_call" ? "Hermes" : payload.hook_event_name === "PreToolUse" || payload.hook_event_name === "PostToolUse" || payload.tool_use_id !== void 0 || payload.permission_mode !== void 0 ? "Claude Code" : payload.hook_event_name === "BeforeTool" || payload.hook_event_name === "AfterTool" || payload.timestamp !== void 0 ? "Gemini CLI" : process.env.HERMES_SESSION_ID || process.env.HERMES_HOME || process.env.HERMES_INTERACTIVE ? "Hermes" : process.env.ANTIGRAVITY_CONVERSATION_ID ? "Antigravity" : void 0;
48830
49042
  let reviewApproved = false;
49043
+ let resolvedReview = null;
48831
49044
  try {
48832
49045
  const key = reviewCorrelationKey(payload);
48833
- if (key && resolvePendingReview(key)) reviewApproved = true;
49046
+ if (key) resolvedReview = resolvePendingReview(key);
49047
+ if (resolvedReview) reviewApproved = true;
48834
49048
  } catch {
48835
49049
  }
48836
49050
  const entry = {
48837
49051
  ts: (/* @__PURE__ */ new Date()).toISOString(),
48838
49052
  tool,
48839
- args: JSON.parse(redactSecrets(JSON.stringify(rawInput))),
49053
+ ...buildArgsField(rawInput),
48840
49054
  decision: "allowed",
48841
49055
  source: reviewApproved ? "inline-review-approved" : "post-hook"
48842
49056
  };
@@ -48883,6 +49097,33 @@ function registerLogCommand(program2) {
48883
49097
  const payloadCwd = typeof payload.cwd === "string" ? payload.cwd : Array.isArray(payload.workspacePaths) && typeof payload.workspacePaths[0] === "string" ? payload.workspacePaths[0] : void 0;
48884
49098
  const safeCwd = typeof payloadCwd === "string" && path48.isAbsolute(payloadCwd) ? payloadCwd : void 0;
48885
49099
  const config = getConfig(safeCwd);
49100
+ if (resolvedReview) {
49101
+ try {
49102
+ const reviewLabel = resolvedReview.label || "inline-review";
49103
+ const sensitiveReview = /dlp|taint/i.test(reviewLabel);
49104
+ appendLocalAudit(
49105
+ tool,
49106
+ rawInput,
49107
+ "allow",
49108
+ "inline-review",
49109
+ {
49110
+ agent,
49111
+ ruleName: reviewLabel,
49112
+ ...rawToolName !== tool ? { agentToolName: rawToolName } : {},
49113
+ ...typeof payloadSessionId === "string" ? { sessionId: payloadSessionId } : {},
49114
+ ...safeCwd ? { workingDir: safeCwd } : {}
49115
+ },
49116
+ sensitiveReview || config.settings.auditHashArgs === true
49117
+ );
49118
+ } catch (err2) {
49119
+ appendToLog(HOOK_DEBUG_LOG, {
49120
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
49121
+ event: "inline-review-ship-row-fail",
49122
+ tool,
49123
+ error: err2 instanceof Error ? err2.message : String(err2)
49124
+ });
49125
+ }
49126
+ }
48886
49127
  {
48887
49128
  const toolOutput = payload.tool_response?.output;
48888
49129
  const inj = config.policy.injectionScan;
@@ -50351,8 +50592,7 @@ function aggregateReportFromAudit(period, opts = {}) {
50351
50592
  const priorEnd = new Date(start.getTime() - 1);
50352
50593
  const priorStart = new Date(start.getTime() - periodMs);
50353
50594
  const priorEntries = allEntries.filter((e) => {
50354
- if (e.source === "post-hook") return false;
50355
- if (e.source === "response-dlp") return false;
50595
+ if (typeof e.source === "string" && NON_DECISION_SOURCES.has(e.source)) return false;
50356
50596
  if (typeof e.decision !== "string") return false;
50357
50597
  const ts = new Date(e.ts);
50358
50598
  return ts >= priorStart && ts <= priorEnd;
@@ -50363,8 +50603,7 @@ function aggregateReportFromAudit(period, opts = {}) {
50363
50603
  const testTs = excludeTests ? buildTestTimestamps(allEntries) : /* @__PURE__ */ new Set();
50364
50604
  let excludedTests = 0;
50365
50605
  const entries = allEntries.filter((e) => {
50366
- if (e.source === "post-hook") return false;
50367
- if (e.source === "response-dlp") return false;
50606
+ if (typeof e.source === "string" && NON_DECISION_SOURCES.has(e.source)) return false;
50368
50607
  if (typeof e.decision !== "string") return false;
50369
50608
  const ts = new Date(e.ts);
50370
50609
  if (ts < start || ts > end) return false;