@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.js CHANGED
@@ -94,7 +94,7 @@ function redactSecrets(text) {
94
94
  if (!text) return text;
95
95
  let redacted = text;
96
96
  redacted = redacted.replace(
97
- /(authorization:\s*(?:bearer|basic)\s+)[a-zA-Z0-9._\-\/\\=]+/gi,
97
+ /(authorization:\s*(?:bearer|basic)\s+)[a-zA-Z0-9._\-\/=]+/gi,
98
98
  "$1********"
99
99
  );
100
100
  redacted = redacted.replace(
@@ -155,7 +155,7 @@ function filePathFromArgs(args) {
155
155
  return void 0;
156
156
  }
157
157
  function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
158
- const isDlpRow = checkedBy.toLowerCase().includes("dlp") || Boolean(meta?.dlpPattern);
158
+ const isDlpRow = checkedBy.toLowerCase().includes("dlp") || Boolean(meta?.dlpPattern) || /dlp|taint/i.test(String(meta?.ruleName ?? ""));
159
159
  const preview2 = auditHashArgsEnabled && !isDlpRow ? buildArgsPreview(args) : void 0;
160
160
  const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args), ...preview2 ? { argsPreview: preview2 } : {} } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
161
161
  const testRun = isTestCall(toolName, args) || process.env.NODE9_TESTING === "1" ? { testRun: true } : {};
@@ -332,8 +332,10 @@ var init_config_schema = __esm({
332
332
  agentPolicy: import_zod.z.enum(["require_approval", "block_on_rules"]).optional(),
333
333
  // Where a `review` verdict's prompt is rendered: 'ask' = the agent's own
334
334
  // inline approve/deny prompt (Claude Code / GitHub Copilot); 'approver' =
335
- // node9's own approver (terminal/native/cloud). Unset → smart default
336
- // (ask for ask-capable agents unless a cloud approver is configured).
335
+ // node9's own approver (terminal/native/cloud). Unset → default ASK for
336
+ // ask-capable agents (v2: cloud no longer disables inline — the outcome
337
+ // ships to the dashboard as audit; admins force routing via managed
338
+ // reviewChannel, which outranks the local --ask flag).
337
339
  reviewChannel: import_zod.z.enum(["ask", "approver"]).optional(),
338
340
  // When true, agents may call WEAKENING node9 MCP tools (shield_disable,
339
341
  // approver_set). Default (unset/false): those tools refuse over MCP — a human
@@ -371,7 +373,18 @@ var init_config_schema = __esm({
371
373
  dlp: import_zod.z.object({
372
374
  enabled: import_zod.z.boolean().optional(),
373
375
  scanIgnoredTools: import_zod.z.boolean().optional(),
374
- pii: import_zod.z.enum(["off", "block"]).optional()
376
+ pii: import_zod.z.enum(["off", "block"]).optional(),
377
+ reviewAction: import_zod.z.enum(["review", "block"]).optional()
378
+ }).optional(),
379
+ // Command-checks governance. Class-B keys (evalDynamic, pipeChainHigh)
380
+ // deliberately exclude 'off' — tighten-only.
381
+ commandChecks: import_zod.z.object({
382
+ inlineExec: import_zod.z.enum(["off", "review", "block"]).optional(),
383
+ rmAdvisory: import_zod.z.enum(["off", "review", "block"]).optional(),
384
+ chmod: import_zod.z.enum(["off", "review", "block"]).optional(),
385
+ sqlDdl: import_zod.z.enum(["off", "review", "block"]).optional(),
386
+ evalDynamic: import_zod.z.enum(["review", "block"]).optional(),
387
+ pipeChainHigh: import_zod.z.enum(["review", "block"]).optional()
375
388
  }).optional(),
376
389
  egress: import_zod.z.object({
377
390
  enabled: import_zod.z.boolean().optional(),
@@ -1510,6 +1523,45 @@ function evaluateSmartConditions(args, rule) {
1510
1523
  });
1511
1524
  return mode === "any" ? results.some((r) => r) : results.every((r) => r);
1512
1525
  }
1526
+ function resolveCheck(v) {
1527
+ return v === "off" || v === "block" ? v : "review";
1528
+ }
1529
+ function resolveCheckTight(v) {
1530
+ return v === "block" ? "block" : "review";
1531
+ }
1532
+ function detectInlineExec(command) {
1533
+ const pipeFed = command.includes("|");
1534
+ const segments = command.split(/\|\||&&|;|\||\n|\$\(|`|\(/);
1535
+ for (const rawSeg of segments) {
1536
+ const tokens = rawSeg.trim().split(/\s+/).filter(Boolean);
1537
+ let i = 0;
1538
+ while (i < tokens.length && /^[A-Za-z_]\w*=/.test(tokens[i])) i++;
1539
+ if (i >= tokens.length) continue;
1540
+ const base = tokens[i].split("/").pop() ?? tokens[i];
1541
+ if (!INLINE_INTERP.test(base)) continue;
1542
+ const args = tokens.slice(i + 1);
1543
+ let hadRedirect = false;
1544
+ const positionals = [];
1545
+ for (let j = 0; j < args.length; j++) {
1546
+ const a = args[j];
1547
+ if (a === "-") return true;
1548
+ if (a.startsWith("<")) {
1549
+ hadRedirect = true;
1550
+ if (a === "<" || a === "<<") j++;
1551
+ continue;
1552
+ }
1553
+ if (a.startsWith("-")) {
1554
+ if (/^-(c|e|eval)$/i.test(a)) return true;
1555
+ continue;
1556
+ }
1557
+ positionals.push(a);
1558
+ }
1559
+ if (positionals.length === 0 && (hadRedirect || pipeFed) && !INLINE_SHELL.test(base)) {
1560
+ return true;
1561
+ }
1562
+ }
1563
+ return false;
1564
+ }
1513
1565
  function resolvePinned(matches) {
1514
1566
  if (matches.length === 0) return void 0;
1515
1567
  const pinned = matches.filter((r) => r.pinned);
@@ -1536,7 +1588,7 @@ function isSqlTool(toolName, toolInspection) {
1536
1588
  const fieldName = toolInspection[matchingPattern];
1537
1589
  return fieldName === "sql" || fieldName === "query";
1538
1590
  }
1539
- function pipeChainVerdict(command, isTrustedHost2) {
1591
+ function pipeChainVerdict(command, isTrustedHost2, highAction = "review") {
1540
1592
  const pipeAnalysis = analyzePipeChain(command);
1541
1593
  if (!pipeAnalysis.isPipeline) return null;
1542
1594
  if (pipeAnalysis.risk !== "critical" && pipeAnalysis.risk !== "high") return null;
@@ -1567,7 +1619,7 @@ function pipeChainVerdict(command, isTrustedHost2) {
1567
1619
  };
1568
1620
  }
1569
1621
  return {
1570
- decision: "review",
1622
+ decision: highAction,
1571
1623
  blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
1572
1624
  reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
1573
1625
  tier: 3
@@ -1581,7 +1633,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1581
1633
  const dlpMatch = args !== void 0 ? scanArgs(args) : null;
1582
1634
  if (dlpMatch) {
1583
1635
  return {
1584
- decision: dlpMatch.severity,
1636
+ // reviewAction:'block' (inline-ask v2): the admin upgraded
1637
+ // review-severity matches to a hard block — every evaluatePolicy
1638
+ // caller (orchestrator, explain, gateway) must agree with the gate.
1639
+ decision: dlpMatch.severity === "block" || config.policy.dlp.reviewAction === "block" ? "block" : "review",
1585
1640
  blockedByLabel: `DLP: ${dlpMatch.patternName}`,
1586
1641
  reason: `${dlpMatch.patternName} detected in ${dlpMatch.fieldPath}`
1587
1642
  };
@@ -1590,7 +1645,11 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1590
1645
  if (wouldBeIgnored) return { decision: "allow" };
1591
1646
  const bashCommand = agent !== "Terminal" && isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : null;
1592
1647
  if (bashCommand !== null) {
1593
- const pipeVerdict = pipeChainVerdict(bashCommand, isTrustedHost2);
1648
+ const pipeVerdict = pipeChainVerdict(
1649
+ bashCommand,
1650
+ isTrustedHost2,
1651
+ resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
1652
+ );
1594
1653
  if (pipeVerdict) return pipeVerdict;
1595
1654
  const fsVerdict = analyzeFsOperation(bashCommand);
1596
1655
  if (fsVerdict) {
@@ -1605,10 +1664,12 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1605
1664
  ruleDescription: fsVerdict.reason
1606
1665
  };
1607
1666
  }
1608
- const sqlVerdict = analyzeSqlDestructive(bashCommand);
1667
+ const sqlAction = resolveCheck(config.policy.commandChecks?.sqlDdl);
1668
+ const sqlVerdict = sqlAction === "off" ? null : analyzeSqlDestructive(bashCommand);
1609
1669
  if (sqlVerdict) {
1610
1670
  return {
1611
- decision: sqlVerdict.verdict,
1671
+ // analyzeSqlDestructive is typed review-only, so the knob maps 1:1.
1672
+ decision: sqlAction === "block" ? "block" : "review",
1612
1673
  blockedByLabel: `Node9 (AST): ${sqlVerdict.ruleName}`,
1613
1674
  reason: sqlVerdict.reason,
1614
1675
  tier: 2,
@@ -1616,10 +1677,12 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1616
1677
  ruleDescription: sqlVerdict.description
1617
1678
  };
1618
1679
  }
1619
- const chmodVerdict = analyzeChmod777(bashCommand);
1680
+ const chmodAction = resolveCheck(config.policy.commandChecks?.chmod);
1681
+ const chmodVerdict = chmodAction === "off" ? null : analyzeChmod777(bashCommand);
1620
1682
  if (chmodVerdict) {
1621
1683
  return {
1622
- decision: chmodVerdict.verdict,
1684
+ // analyzeChmod777 is typed review-only, so the knob maps 1:1.
1685
+ decision: chmodAction === "block" ? "block" : "review",
1623
1686
  blockedByLabel: `project-jail (AST): ${chmodVerdict.ruleName}`,
1624
1687
  reason: chmodVerdict.reason,
1625
1688
  tier: 2,
@@ -1662,10 +1725,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1662
1725
  const analyzed = analyzeShellCommand(shellCommand);
1663
1726
  allTokens = analyzed.allTokens;
1664
1727
  pathTokens = analyzed.paths;
1665
- const INLINE_EXEC_PATTERN = /^(python3?|bash|sh|zsh|perl|ruby|node|php|lua)\s+(-c|-e|-eval)\s/i;
1666
- if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
1728
+ const inlineAction = resolveCheck(config.policy.commandChecks?.inlineExec);
1729
+ if (inlineAction !== "off" && detectInlineExec(shellCommand)) {
1667
1730
  return {
1668
- decision: "review",
1731
+ decision: inlineAction === "block" ? "block" : "review",
1669
1732
  blockedByLabel: "Node9 Standard (Inline Execution)",
1670
1733
  ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
1671
1734
  tier: 3
@@ -1683,14 +1746,21 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1683
1746
  }
1684
1747
  if (evalVerdict === "review") {
1685
1748
  return {
1686
- decision: "review",
1749
+ // Class B tighten-only: commandChecks.evalDynamic may upgrade to
1750
+ // block but can never turn this off (eval-remote above is Class A —
1751
+ // no knob at all).
1752
+ decision: resolveCheckTight(config.policy.commandChecks?.evalDynamic),
1687
1753
  blockedByLabel: "Node9: Eval Dynamic Content",
1688
1754
  reason: "eval of dynamic content (variable or subshell expansion) requires approval",
1689
1755
  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.",
1690
1756
  tier: 3
1691
1757
  };
1692
1758
  }
1693
- const ptVerdict = pipeChainVerdict(shellCommand, isTrustedHost2);
1759
+ const ptVerdict = pipeChainVerdict(
1760
+ shellCommand,
1761
+ isTrustedHost2,
1762
+ resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
1763
+ );
1694
1764
  if (ptVerdict) return ptVerdict;
1695
1765
  if (config.policy.egress?.enabled) {
1696
1766
  const dests = extractShellDestinations(shellCommand);
@@ -2372,7 +2442,7 @@ function* stringValues(obj, depth = 0) {
2372
2442
  }
2373
2443
  for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
2374
2444
  }
2375
- var import_safe_regex2, import_mvdan_sh, import_picomatch, import_safe_regex22, import_safe_regex23, import_crypto3, 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;
2445
+ var import_safe_regex2, import_mvdan_sh, import_picomatch, import_safe_regex22, import_safe_regex23, import_crypto3, 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;
2376
2446
  var init_dist = __esm({
2377
2447
  "packages/policy-engine/dist/index.mjs"() {
2378
2448
  "use strict";
@@ -3309,6 +3379,8 @@ var init_dist = __esm({
3309
3379
  REGEX_CACHE_MAX = 500;
3310
3380
  regexCache = /* @__PURE__ */ new Map();
3311
3381
  FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
3382
+ INLINE_INTERP = /^(python[\d.]*|bash|sh|zsh|perl|ruby|node|php|lua)$/i;
3383
+ INLINE_SHELL = /^(bash|sh|zsh)$/i;
3312
3384
  VERDICT_RANK = {
3313
3385
  allow: 0,
3314
3386
  review: 1,
@@ -4366,6 +4438,31 @@ function applyManagedDlp(local, managed, locked) {
4366
4438
  locked.includes("dlpPii")
4367
4439
  );
4368
4440
  }
4441
+ if (typeof managed.reviewAction === "string") {
4442
+ next.reviewAction = resolveByOrder(
4443
+ DLP_REVIEW_ACTION_ORDER,
4444
+ local.reviewAction ?? "review",
4445
+ managed.reviewAction,
4446
+ locked.includes("dlpReviewAction")
4447
+ );
4448
+ }
4449
+ return next;
4450
+ }
4451
+ function applyManagedCommandChecks(local, managed, locked) {
4452
+ const next = { ...local };
4453
+ for (const key of COMMAND_CHECK_KEYS) {
4454
+ const m = managed[key];
4455
+ if (typeof m !== "string") continue;
4456
+ const lockKey = `commandChecks${key[0].toUpperCase()}${key.slice(1)}`;
4457
+ const resolved = resolveByOrder(
4458
+ COMMAND_CHECK_ORDER,
4459
+ local[key] ?? "review",
4460
+ m,
4461
+ locked.includes(lockKey)
4462
+ );
4463
+ if ((key === "evalDynamic" || key === "pipeChainHigh") && resolved === "off") continue;
4464
+ next[key] = resolved;
4465
+ }
4369
4466
  return next;
4370
4467
  }
4371
4468
  function applyManagedApprovers(local, managed) {
@@ -4377,13 +4474,23 @@ function applyManagedApprovers(local, managed) {
4377
4474
  terminal: typeof managed.terminal === "boolean" ? managed.terminal : local.terminal
4378
4475
  };
4379
4476
  }
4380
- var MODE_ORDER, EGRESS_MODE_ORDER, DLP_PII_ORDER;
4477
+ var MODE_ORDER, EGRESS_MODE_ORDER, DLP_PII_ORDER, DLP_REVIEW_ACTION_ORDER, COMMAND_CHECK_ORDER, COMMAND_CHECK_KEYS;
4381
4478
  var init_managed = __esm({
4382
4479
  "src/config/managed.ts"() {
4383
4480
  "use strict";
4384
4481
  MODE_ORDER = ["observe", "audit", "standard", "strict"];
4385
4482
  EGRESS_MODE_ORDER = ["off", "review", "block"];
4386
4483
  DLP_PII_ORDER = ["off", "block"];
4484
+ DLP_REVIEW_ACTION_ORDER = ["review", "block"];
4485
+ COMMAND_CHECK_ORDER = ["off", "review", "block"];
4486
+ COMMAND_CHECK_KEYS = [
4487
+ "inlineExec",
4488
+ "rmAdvisory",
4489
+ "chmod",
4490
+ "sqlDdl",
4491
+ "evalDynamic",
4492
+ "pipeChainHigh"
4493
+ ];
4387
4494
  }
4388
4495
  });
4389
4496
 
@@ -4758,6 +4865,22 @@ function getConfig(cwd) {
4758
4865
  if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
4759
4866
  if (d.scanIgnoredTools !== void 0) mergedPolicy.dlp.scanIgnoredTools = d.scanIgnoredTools;
4760
4867
  if (d.pii !== void 0) mergedPolicy.dlp.pii = d.pii;
4868
+ if (d.reviewAction !== void 0) mergedPolicy.dlp.reviewAction = d.reviewAction;
4869
+ }
4870
+ if (p.commandChecks && typeof p.commandChecks === "object") {
4871
+ const src = p.commandChecks;
4872
+ const cc2 = {
4873
+ ...mergedPolicy.commandChecks
4874
+ };
4875
+ for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
4876
+ const v = src[k];
4877
+ if (v === "off" || v === "review" || v === "block") cc2[k] = v;
4878
+ }
4879
+ for (const k of ["evalDynamic", "pipeChainHigh"]) {
4880
+ const v = src[k];
4881
+ if (v === "review" || v === "block") cc2[k] = v;
4882
+ }
4883
+ if (Object.keys(cc2).length > 0) mergedPolicy.commandChecks = cc2;
4761
4884
  }
4762
4885
  if (p.egress) {
4763
4886
  const e = p.egress;
@@ -4854,11 +4977,19 @@ function getConfig(cwd) {
4854
4977
  mergedPolicy.dlp,
4855
4978
  {
4856
4979
  enabled: typeof mc.dlp.enabled === "boolean" ? mc.dlp.enabled : void 0,
4857
- pii: typeof mc.dlp.pii === "string" ? mc.dlp.pii : void 0
4980
+ pii: typeof mc.dlp.pii === "string" ? mc.dlp.pii : void 0,
4981
+ reviewAction: mc.dlp.reviewAction === "review" || mc.dlp.reviewAction === "block" ? mc.dlp.reviewAction : void 0
4858
4982
  },
4859
4983
  locked
4860
4984
  );
4861
4985
  }
4986
+ if (mc.commandChecks && typeof mc.commandChecks === "object") {
4987
+ mergedPolicy.commandChecks = applyManagedCommandChecks(
4988
+ mergedPolicy.commandChecks ?? {},
4989
+ mc.commandChecks,
4990
+ locked
4991
+ );
4992
+ }
4862
4993
  if (mc.approvers && typeof mc.approvers === "object") {
4863
4994
  const bool = (v) => typeof v === "boolean" ? v : void 0;
4864
4995
  mergedSettings.approvers = applyManagedApprovers(mergedSettings.approvers, {
@@ -4870,6 +5001,7 @@ function getConfig(cwd) {
4870
5001
  }
4871
5002
  if (mc.reviewChannel === "ask" || mc.reviewChannel === "approver") {
4872
5003
  mergedSettings.reviewChannel = mc.reviewChannel;
5004
+ mergedSettings.reviewChannelManaged = true;
4873
5005
  }
4874
5006
  if (typeof mc.approvalTimeoutMs === "number" && mc.approvalTimeoutMs > 0) {
4875
5007
  mergedSettings.approvalTimeoutMs = mc.approvalTimeoutMs;
@@ -4967,8 +5099,17 @@ function getConfig(cwd) {
4967
5099
  }
4968
5100
  }
4969
5101
  const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
5102
+ const cc = mergedPolicy.commandChecks ?? {};
5103
+ const advisoryKnob = (name) => {
5104
+ if (name === "review-rm") return cc.rmAdvisory;
5105
+ if (name?.endsWith("-sql")) return cc.sqlDdl;
5106
+ return void 0;
5107
+ };
4970
5108
  for (const rule of ADVISORY_SMART_RULES) {
4971
- if (!existingAdvisoryNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
5109
+ if (existingAdvisoryNames.has(rule.name)) continue;
5110
+ const knob = rule.verdict === "review" ? advisoryKnob(rule.name) : void 0;
5111
+ if (knob === "off") continue;
5112
+ mergedPolicy.smartRules.push(knob === "block" ? { ...rule, verdict: "block" } : rule);
4972
5113
  }
4973
5114
  const envMode = process.env.NODE9_MODE;
4974
5115
  if (envMode && !modeCloudControlled && ["observe", "audit", "standard", "strict"].includes(envMode)) {
@@ -5614,13 +5755,14 @@ async function deriveExplainTrace(toolName, args) {
5614
5755
  const filePathE = String(argsObjE.file_path ?? argsObjE.path ?? argsObjE.filename ?? "");
5615
5756
  const dlpMatch = (filePathE ? scanFilePath(filePathE) : null) ?? (args !== void 0 ? scanArgs(args) : null);
5616
5757
  if (dlpMatch) {
5758
+ const dlpBlocks = dlpMatch.severity === "block" || config.policy.dlp.reviewAction === "block";
5617
5759
  steps.push({
5618
5760
  name: "DLP Content Scanner",
5619
- outcome: dlpMatch.severity === "block" ? "block" : "review",
5761
+ outcome: dlpBlocks ? "block" : "review",
5620
5762
  detail: `\u{1F6A8} ${dlpMatch.patternName} detected in ${dlpMatch.fieldPath} \u2014 sample: ${dlpMatch.redactedSample}`,
5621
- isFinal: dlpMatch.severity === "block"
5763
+ isFinal: dlpBlocks
5622
5764
  });
5623
- if (dlpMatch.severity === "block") {
5765
+ if (dlpBlocks) {
5624
5766
  return { tool: toolName, args, waterfall, steps, decision: "block" };
5625
5767
  }
5626
5768
  } else {
@@ -7119,7 +7261,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
7119
7261
  const dlpMatch = (filePath ? scanFilePath(filePath) : null) ?? scanArgs(args);
7120
7262
  if (dlpMatch) {
7121
7263
  const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
7122
- if (dlpMatch.severity === "block") {
7264
+ if (dlpMatch.severity === "block" || config.policy.dlp.reviewAction === "block") {
7123
7265
  let dlpEid;
7124
7266
  if (!isManual)
7125
7267
  dlpEid = appendLocalAudit(
@@ -7457,12 +7599,14 @@ ${appPermReview}`
7457
7599
  );
7458
7600
  }
7459
7601
  }
7460
- const cloudEnforcedForDefer = approvers.cloud && !!creds?.apiKey;
7461
- if (options?.deferReview && !hardBlockDowngraded && !taintWarning && !appPermReview && !cloudEnforcedForDefer) {
7602
+ if (options?.deferReview && !hardBlockDowngraded) {
7462
7603
  return {
7463
7604
  approved: false,
7464
7605
  review: true,
7465
- reason: explainableLabel || "Node9 flagged this action for review.",
7606
+ // The prompt must say WHY: the taint sentence beats the bare label so
7607
+ // the dev sees the actual risk context inline. (No appPermReview term:
7608
+ // deferReview and serverKey never co-occur in production — see above.)
7609
+ reason: taintWarning || explainableLabel || "Node9 flagged this action for review.",
7466
7610
  ruleDescription: policyRuleDescription,
7467
7611
  blockedByLabel: explainableLabel
7468
7612
  };
@@ -8404,7 +8548,7 @@ function printInlineAskNotice() {
8404
8548
  );
8405
8549
  console.log(
8406
8550
  import_chalk.default.gray(
8407
- ' 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.)'
8551
+ ' 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.)'
8408
8552
  )
8409
8553
  );
8410
8554
  }
@@ -18353,7 +18497,21 @@ function extractManagedConfig(body) {
18353
18497
  const d = {};
18354
18498
  if (typeof mc.dlp.enabled === "boolean") d.enabled = mc.dlp.enabled;
18355
18499
  if (typeof mc.dlp.pii === "string") d.pii = mc.dlp.pii;
18356
- if (d.enabled !== void 0 || d.pii !== void 0) out.dlp = d;
18500
+ if (mc.dlp.reviewAction === "review" || mc.dlp.reviewAction === "block")
18501
+ d.reviewAction = mc.dlp.reviewAction;
18502
+ if (d.enabled !== void 0 || d.pii !== void 0 || d.reviewAction !== void 0) out.dlp = d;
18503
+ }
18504
+ if (mc.commandChecks && typeof mc.commandChecks === "object") {
18505
+ const cc = {};
18506
+ for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
18507
+ const v = mc.commandChecks[k];
18508
+ if (v === "off" || v === "review" || v === "block") cc[k] = v;
18509
+ }
18510
+ for (const k of ["evalDynamic", "pipeChainHigh"]) {
18511
+ const v = mc.commandChecks[k];
18512
+ if (v === "review" || v === "block") cc[k] = v;
18513
+ }
18514
+ if (Object.keys(cc).length > 0) out.commandChecks = cc;
18357
18515
  }
18358
18516
  if (mc.approvers && typeof mc.approvers === "object") {
18359
18517
  const a = {};
@@ -18417,7 +18575,7 @@ function extractManagedConfig(body) {
18417
18575
  }
18418
18576
  if (Object.keys(ap).length) out.appPermissions = ap;
18419
18577
  }
18420
- 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;
18578
+ 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;
18421
18579
  }
18422
18580
  function sweepStaleTmp(target) {
18423
18581
  try {
@@ -19049,12 +19207,23 @@ function classifyDecision(a, b) {
19049
19207
  function decisionTag(view) {
19050
19208
  return `[${view.label}]`.padEnd(14);
19051
19209
  }
19052
- var HUMAN_SOURCES, TIMEOUT_SOURCES, has;
19210
+ var HUMAN_SOURCES, TIMEOUT_SOURCES, NON_DECISION_SOURCES, has;
19053
19211
  var init_decision = __esm({
19054
19212
  "src/audit/decision.ts"() {
19055
19213
  "use strict";
19056
- HUMAN_SOURCES = /* @__PURE__ */ new Set(["daemon", "cloud", "local-decision", "inline-review-approved"]);
19214
+ HUMAN_SOURCES = /* @__PURE__ */ new Set([
19215
+ "daemon",
19216
+ "cloud",
19217
+ "local-decision",
19218
+ "inline-review-approved",
19219
+ "inline-review"
19220
+ ]);
19057
19221
  TIMEOUT_SOURCES = /* @__PURE__ */ new Set(["timeout"]);
19222
+ NON_DECISION_SOURCES = /* @__PURE__ */ new Set([
19223
+ "post-hook",
19224
+ "inline-review-approved",
19225
+ "response-dlp"
19226
+ ]);
19058
19227
  has = (s, needle) => s.includes(needle);
19059
19228
  }
19060
19229
  });
@@ -19630,7 +19799,7 @@ function buildDaemonReport(allEntries, period, now) {
19630
19799
  else if (period === "30d") start.setDate(start.getDate() - 29);
19631
19800
  else if (period === "month") start = new Date(now.getFullYear(), now.getMonth(), 1);
19632
19801
  const entries = allEntries.filter((e) => {
19633
- if (e.source === "post-hook" || e.source === "response-dlp") return false;
19802
+ if (typeof e.source === "string" && NON_DECISION_SOURCES.has(e.source)) return false;
19634
19803
  return new Date(e.ts) >= start;
19635
19804
  });
19636
19805
  const isBlocked = (e) => classifyDecision(e).outcome === "deny";
@@ -47379,8 +47548,8 @@ INSTRUCTIONS:
47379
47548
  - Do NOT retry this exact command or attempt to bypass the rule.${recovery}
47380
47549
  - Inform the user which security rule was triggered and ask how to proceed.`;
47381
47550
  }
47382
- function buildReviewMessage(blockedByLabel, ruleDescription) {
47383
- const why = ruleDescription || blockedByLabel || "this action needs your review";
47551
+ function buildReviewMessage(blockedByLabel, ruleDescription, reason) {
47552
+ const why = ruleDescription || (reason && reason !== blockedByLabel ? reason : void 0) || blockedByLabel || "this action needs your review";
47384
47553
  return `Node9 flagged this for your review: ${why}. Approve to proceed, or deny to cancel.`;
47385
47554
  }
47386
47555
 
@@ -48121,6 +48290,7 @@ init_hasher();
48121
48290
  function storePath() {
48122
48291
  return process.env.NODE9_PENDING_STORE || import_path45.default.join(import_os42.default.homedir(), ".node9", "pending-reviews.json");
48123
48292
  }
48293
+ var TTL_TUID_MS = 72 * 60 * 60 * 1e3;
48124
48294
  var TTL_MS2 = 6 * 60 * 60 * 1e3;
48125
48295
  var MAX_ENTRIES = 500;
48126
48296
  function reviewCorrelationKey(payload) {
@@ -48154,7 +48324,9 @@ function write(store) {
48154
48324
  }
48155
48325
  }
48156
48326
  function prune(entries, now) {
48157
- const fresh = entries.filter((e) => now - e.ts < TTL_MS2);
48327
+ const fresh = entries.filter(
48328
+ (e) => now - e.ts < (e.key.startsWith("tuid:") ? TTL_TUID_MS : TTL_MS2)
48329
+ );
48158
48330
  return fresh.length > MAX_ENTRIES ? fresh.slice(fresh.length - MAX_ENTRIES) : fresh;
48159
48331
  }
48160
48332
  function recordPendingReview(entry) {
@@ -48169,19 +48341,30 @@ function recordPendingReview(entry) {
48169
48341
  function resolvePendingReview(key, now = Date.now()) {
48170
48342
  try {
48171
48343
  const store = read();
48172
- const idx = store.entries.findIndex((e) => e.key === key);
48344
+ const live = prune(store.entries, now);
48345
+ const idx = live.findIndex((e) => e.key === key);
48173
48346
  if (idx === -1) {
48174
- const pruned = prune(store.entries, now);
48175
- if (pruned.length !== store.entries.length) write({ entries: pruned });
48347
+ if (live.length !== store.entries.length) write({ entries: live });
48176
48348
  return null;
48177
48349
  }
48178
- const [match] = store.entries.splice(idx, 1);
48179
- write({ entries: prune(store.entries, now) });
48350
+ const [match] = live.splice(idx, 1);
48351
+ write({ entries: live });
48180
48352
  return match;
48181
48353
  } catch {
48182
48354
  return null;
48183
48355
  }
48184
48356
  }
48357
+ function discardPendingReview(key, now = Date.now()) {
48358
+ try {
48359
+ const store = read();
48360
+ const kept = prune(
48361
+ store.entries.filter((e) => e.key !== key),
48362
+ now
48363
+ );
48364
+ if (kept.length !== store.entries.length) write({ entries: kept });
48365
+ } catch {
48366
+ }
48367
+ }
48185
48368
 
48186
48369
  // src/cli/commands/check.ts
48187
48370
  init_hook_payload();
@@ -48237,7 +48420,10 @@ function agentSupportsAsk(agent) {
48237
48420
  }
48238
48421
  function resolveAskMode(agent, opts, config) {
48239
48422
  if (!agentSupportsAsk(agent)) return false;
48240
- if (config.settings.approvers.cloud === true) return false;
48423
+ if (config.settings.reviewChannelManaged === true) {
48424
+ if (config.settings.reviewChannel === "ask") return true;
48425
+ if (config.settings.reviewChannel === "approver") return false;
48426
+ }
48241
48427
  if (opts.ask === true) return true;
48242
48428
  if (opts.ask === false) return false;
48243
48429
  if (config.settings.reviewChannel === "ask") return true;
@@ -48476,7 +48662,11 @@ RAW: ${raw}
48476
48662
  process.exit(2);
48477
48663
  };
48478
48664
  const sendAsk = (result2) => {
48479
- const msg = buildReviewMessage(result2.blockedByLabel, result2.ruleDescription);
48665
+ const msg = buildReviewMessage(
48666
+ result2.blockedByLabel,
48667
+ result2.ruleDescription,
48668
+ result2.reason
48669
+ );
48480
48670
  try {
48481
48671
  const key = reviewCorrelationKey(payload);
48482
48672
  if (key) {
@@ -48659,6 +48849,13 @@ RAW: ${raw}
48659
48849
  cwd: safeCwdForAuth,
48660
48850
  deferReview: askMode
48661
48851
  });
48852
+ if (!result.review) {
48853
+ try {
48854
+ const key = reviewCorrelationKey(payload);
48855
+ if (key) discardPendingReview(key);
48856
+ } catch {
48857
+ }
48858
+ }
48662
48859
  if (result.approved) {
48663
48860
  if (result.checkedBy && process.env.NODE9_DEBUG === "1")
48664
48861
  process.stderr.write(`\u2713 node9 [${result.checkedBy}]: "${toolName}" allowed
@@ -48746,6 +48943,7 @@ init_audit();
48746
48943
  init_config();
48747
48944
  init_daemon();
48748
48945
  init_dlp();
48946
+ init_hasher();
48749
48947
 
48750
48948
  // src/utils/cp-mv-parser.ts
48751
48949
  function parseCpMvOp(command) {
@@ -48807,6 +49005,20 @@ function atLeastConfidence(c, min) {
48807
49005
  function sanitize3(value) {
48808
49006
  return value.replace(/[\x00-\x1F\x7F]/g, "");
48809
49007
  }
49008
+ function buildArgsField(rawInput) {
49009
+ let hashed = {};
49010
+ try {
49011
+ hashed = { argsHash: hashArgs(rawInput) };
49012
+ } catch {
49013
+ }
49014
+ try {
49015
+ const hit = scanArgs(rawInput);
49016
+ if (hit) return { ...hashed, dlpPattern: hit.patternName, dlpSample: hit.redactedSample };
49017
+ return { args: JSON.parse(redactSecrets(JSON.stringify(rawInput))) };
49018
+ } catch {
49019
+ return hashed;
49020
+ }
49021
+ }
48810
49022
  function registerLogCommand(program2) {
48811
49023
  program2.command("log", { hidden: true }).description("PostToolUse hook \u2014 records executed tool calls").argument("[data]", "JSON string of the tool call").option(
48812
49024
  "--agent <name>",
@@ -48835,15 +49047,17 @@ function registerLogCommand(program2) {
48835
49047
  })();
48836
49048
  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;
48837
49049
  let reviewApproved = false;
49050
+ let resolvedReview = null;
48838
49051
  try {
48839
49052
  const key = reviewCorrelationKey(payload);
48840
- if (key && resolvePendingReview(key)) reviewApproved = true;
49053
+ if (key) resolvedReview = resolvePendingReview(key);
49054
+ if (resolvedReview) reviewApproved = true;
48841
49055
  } catch {
48842
49056
  }
48843
49057
  const entry = {
48844
49058
  ts: (/* @__PURE__ */ new Date()).toISOString(),
48845
49059
  tool,
48846
- args: JSON.parse(redactSecrets(JSON.stringify(rawInput))),
49060
+ ...buildArgsField(rawInput),
48847
49061
  decision: "allowed",
48848
49062
  source: reviewApproved ? "inline-review-approved" : "post-hook"
48849
49063
  };
@@ -48890,6 +49104,33 @@ function registerLogCommand(program2) {
48890
49104
  const payloadCwd = typeof payload.cwd === "string" ? payload.cwd : Array.isArray(payload.workspacePaths) && typeof payload.workspacePaths[0] === "string" ? payload.workspacePaths[0] : void 0;
48891
49105
  const safeCwd = typeof payloadCwd === "string" && import_path47.default.isAbsolute(payloadCwd) ? payloadCwd : void 0;
48892
49106
  const config = getConfig(safeCwd);
49107
+ if (resolvedReview) {
49108
+ try {
49109
+ const reviewLabel = resolvedReview.label || "inline-review";
49110
+ const sensitiveReview = /dlp|taint/i.test(reviewLabel);
49111
+ appendLocalAudit(
49112
+ tool,
49113
+ rawInput,
49114
+ "allow",
49115
+ "inline-review",
49116
+ {
49117
+ agent,
49118
+ ruleName: reviewLabel,
49119
+ ...rawToolName !== tool ? { agentToolName: rawToolName } : {},
49120
+ ...typeof payloadSessionId === "string" ? { sessionId: payloadSessionId } : {},
49121
+ ...safeCwd ? { workingDir: safeCwd } : {}
49122
+ },
49123
+ sensitiveReview || config.settings.auditHashArgs === true
49124
+ );
49125
+ } catch (err2) {
49126
+ appendToLog(HOOK_DEBUG_LOG, {
49127
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
49128
+ event: "inline-review-ship-row-fail",
49129
+ tool,
49130
+ error: err2 instanceof Error ? err2.message : String(err2)
49131
+ });
49132
+ }
49133
+ }
48893
49134
  {
48894
49135
  const toolOutput = payload.tool_response?.output;
48895
49136
  const inj = config.policy.injectionScan;
@@ -50358,8 +50599,7 @@ function aggregateReportFromAudit(period, opts = {}) {
50358
50599
  const priorEnd = new Date(start.getTime() - 1);
50359
50600
  const priorStart = new Date(start.getTime() - periodMs);
50360
50601
  const priorEntries = allEntries.filter((e) => {
50361
- if (e.source === "post-hook") return false;
50362
- if (e.source === "response-dlp") return false;
50602
+ if (typeof e.source === "string" && NON_DECISION_SOURCES.has(e.source)) return false;
50363
50603
  if (typeof e.decision !== "string") return false;
50364
50604
  const ts = new Date(e.ts);
50365
50605
  return ts >= priorStart && ts <= priorEnd;
@@ -50370,8 +50610,7 @@ function aggregateReportFromAudit(period, opts = {}) {
50370
50610
  const testTs = excludeTests ? buildTestTimestamps(allEntries) : /* @__PURE__ */ new Set();
50371
50611
  let excludedTests = 0;
50372
50612
  const entries = allEntries.filter((e) => {
50373
- if (e.source === "post-hook") return false;
50374
- if (e.source === "response-dlp") return false;
50613
+ if (typeof e.source === "string" && NON_DECISION_SOURCES.has(e.source)) return false;
50375
50614
  if (typeof e.decision !== "string") return false;
50376
50615
  const ts = new Date(e.ts);
50377
50616
  if (ts < start || ts > end) return false;