@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.
@@ -2292,8 +2292,10 @@ var init_config_schema = __esm({
2292
2292
  agentPolicy: z.enum(["require_approval", "block_on_rules"]).optional(),
2293
2293
  // Where a `review` verdict's prompt is rendered: 'ask' = the agent's own
2294
2294
  // inline approve/deny prompt (Claude Code / GitHub Copilot); 'approver' =
2295
- // node9's own approver (terminal/native/cloud). Unset → smart default
2296
- // (ask for ask-capable agents unless a cloud approver is configured).
2295
+ // node9's own approver (terminal/native/cloud). Unset → default ASK for
2296
+ // ask-capable agents (v2: cloud no longer disables inline — the outcome
2297
+ // ships to the dashboard as audit; admins force routing via managed
2298
+ // reviewChannel, which outranks the local --ask flag).
2297
2299
  reviewChannel: z.enum(["ask", "approver"]).optional(),
2298
2300
  // When true, agents may call WEAKENING node9 MCP tools (shield_disable,
2299
2301
  // approver_set). Default (unset/false): those tools refuse over MCP — a human
@@ -2331,7 +2333,18 @@ var init_config_schema = __esm({
2331
2333
  dlp: z.object({
2332
2334
  enabled: z.boolean().optional(),
2333
2335
  scanIgnoredTools: z.boolean().optional(),
2334
- pii: z.enum(["off", "block"]).optional()
2336
+ pii: z.enum(["off", "block"]).optional(),
2337
+ reviewAction: z.enum(["review", "block"]).optional()
2338
+ }).optional(),
2339
+ // Command-checks governance. Class-B keys (evalDynamic, pipeChainHigh)
2340
+ // deliberately exclude 'off' — tighten-only.
2341
+ commandChecks: z.object({
2342
+ inlineExec: z.enum(["off", "review", "block"]).optional(),
2343
+ rmAdvisory: z.enum(["off", "review", "block"]).optional(),
2344
+ chmod: z.enum(["off", "review", "block"]).optional(),
2345
+ sqlDdl: z.enum(["off", "review", "block"]).optional(),
2346
+ evalDynamic: z.enum(["review", "block"]).optional(),
2347
+ pipeChainHigh: z.enum(["review", "block"]).optional()
2335
2348
  }).optional(),
2336
2349
  egress: z.object({
2337
2350
  enabled: z.boolean().optional(),
@@ -2960,17 +2973,6 @@ var init_costSync = __esm({
2960
2973
  }
2961
2974
  });
2962
2975
 
2963
- // src/daemon/scan-watermark.ts
2964
- var MAX_LINE_BYTES;
2965
- var init_scan_watermark = __esm({
2966
- "src/daemon/scan-watermark.ts"() {
2967
- "use strict";
2968
- init_dlp();
2969
- init_dist();
2970
- MAX_LINE_BYTES = 2 * 1024 * 1024;
2971
- }
2972
- });
2973
-
2974
2976
  // src/audit/decision.ts
2975
2977
  function classifyDecision(a, b) {
2976
2978
  const isRow = !!a && typeof a === "object";
@@ -3005,16 +3007,38 @@ function classifyDecision(a, b) {
3005
3007
  }
3006
3008
  return { outcome: "unknown", label: raw ? `? ${raw}` : "? (none)", raw };
3007
3009
  }
3008
- var HUMAN_SOURCES, TIMEOUT_SOURCES, has;
3010
+ var HUMAN_SOURCES, TIMEOUT_SOURCES, NON_DECISION_SOURCES, has;
3009
3011
  var init_decision = __esm({
3010
3012
  "src/audit/decision.ts"() {
3011
3013
  "use strict";
3012
- HUMAN_SOURCES = /* @__PURE__ */ new Set(["daemon", "cloud", "local-decision", "inline-review-approved"]);
3014
+ HUMAN_SOURCES = /* @__PURE__ */ new Set([
3015
+ "daemon",
3016
+ "cloud",
3017
+ "local-decision",
3018
+ "inline-review-approved",
3019
+ "inline-review"
3020
+ ]);
3013
3021
  TIMEOUT_SOURCES = /* @__PURE__ */ new Set(["timeout"]);
3022
+ NON_DECISION_SOURCES = /* @__PURE__ */ new Set([
3023
+ "post-hook",
3024
+ "inline-review-approved",
3025
+ "response-dlp"
3026
+ ]);
3014
3027
  has = (s, needle) => s.includes(needle);
3015
3028
  }
3016
3029
  });
3017
3030
 
3031
+ // src/daemon/scan-watermark.ts
3032
+ var MAX_LINE_BYTES;
3033
+ var init_scan_watermark = __esm({
3034
+ "src/daemon/scan-watermark.ts"() {
3035
+ "use strict";
3036
+ init_dlp();
3037
+ init_dist();
3038
+ MAX_LINE_BYTES = 2 * 1024 * 1024;
3039
+ }
3040
+ });
3041
+
3018
3042
  // src/cli/aggregate/report-audit.ts
3019
3043
  import fs5 from "fs";
3020
3044
  import os6 from "os";
@@ -3574,8 +3598,7 @@ function aggregateReportFromAudit(period, opts = {}) {
3574
3598
  const priorEnd = new Date(start.getTime() - 1);
3575
3599
  const priorStart = new Date(start.getTime() - periodMs);
3576
3600
  const priorEntries = allEntries.filter((e) => {
3577
- if (e.source === "post-hook") return false;
3578
- if (e.source === "response-dlp") return false;
3601
+ if (typeof e.source === "string" && NON_DECISION_SOURCES.has(e.source)) return false;
3579
3602
  if (typeof e.decision !== "string") return false;
3580
3603
  const ts = new Date(e.ts);
3581
3604
  return ts >= priorStart && ts <= priorEnd;
@@ -3586,8 +3609,7 @@ function aggregateReportFromAudit(period, opts = {}) {
3586
3609
  const testTs = excludeTests ? buildTestTimestamps(allEntries) : /* @__PURE__ */ new Set();
3587
3610
  let excludedTests = 0;
3588
3611
  const entries = allEntries.filter((e) => {
3589
- if (e.source === "post-hook") return false;
3590
- if (e.source === "response-dlp") return false;
3612
+ if (typeof e.source === "string" && NON_DECISION_SOURCES.has(e.source)) return false;
3591
3613
  if (typeof e.decision !== "string") return false;
3592
3614
  const ts = new Date(e.ts);
3593
3615
  if (ts < start || ts > end) return false;
@@ -4915,7 +4937,7 @@ function readAuditEntriesAsync(chunkSize = 1e3, customPath) {
4915
4937
  }
4916
4938
  function aggregateAudit(entries, startMs, endMs = Date.now()) {
4917
4939
  const inWindow = entries.filter((e) => {
4918
- if (e.source === "post-hook" || e.source === "response-dlp") return false;
4940
+ if (typeof e.source === "string" && NON_DECISION_SOURCES.has(e.source)) return false;
4919
4941
  const t = Date.parse(e.ts);
4920
4942
  return t >= startMs && t <= endMs;
4921
4943
  });
@@ -5361,6 +5383,7 @@ var init_data = __esm({
5361
5383
  init_daemon();
5362
5384
  init_costSync();
5363
5385
  init_shields();
5386
+ init_decision();
5364
5387
  init_scan_watermark();
5365
5388
  init_report_audit();
5366
5389
  init_litellm();
package/dist/index.js CHANGED
@@ -91,7 +91,7 @@ function redactSecrets(text) {
91
91
  if (!text) return text;
92
92
  let redacted = text;
93
93
  redacted = redacted.replace(
94
- /(authorization:\s*(?:bearer|basic)\s+)[a-zA-Z0-9._\-\/\\=]+/gi,
94
+ /(authorization:\s*(?:bearer|basic)\s+)[a-zA-Z0-9._\-\/=]+/gi,
95
95
  "$1********"
96
96
  );
97
97
  redacted = redacted.replace(
@@ -152,7 +152,7 @@ function filePathFromArgs(args) {
152
152
  return void 0;
153
153
  }
154
154
  function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
155
- const isDlpRow = checkedBy.toLowerCase().includes("dlp") || Boolean(meta?.dlpPattern);
155
+ const isDlpRow = checkedBy.toLowerCase().includes("dlp") || Boolean(meta?.dlpPattern) || /dlp|taint/i.test(String(meta?.ruleName ?? ""));
156
156
  const preview = auditHashArgsEnabled && !isDlpRow ? buildArgsPreview(args) : void 0;
157
157
  const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args), ...preview ? { argsPreview: preview } : {} } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
158
158
  const testRun = isTestCall(toolName, args) || process.env.NODE9_TESTING === "1" ? { testRun: true } : {};
@@ -314,8 +314,10 @@ var ConfigFileSchema = import_zod.z.object({
314
314
  agentPolicy: import_zod.z.enum(["require_approval", "block_on_rules"]).optional(),
315
315
  // Where a `review` verdict's prompt is rendered: 'ask' = the agent's own
316
316
  // inline approve/deny prompt (Claude Code / GitHub Copilot); 'approver' =
317
- // node9's own approver (terminal/native/cloud). Unset → smart default
318
- // (ask for ask-capable agents unless a cloud approver is configured).
317
+ // node9's own approver (terminal/native/cloud). Unset → default ASK for
318
+ // ask-capable agents (v2: cloud no longer disables inline — the outcome
319
+ // ships to the dashboard as audit; admins force routing via managed
320
+ // reviewChannel, which outranks the local --ask flag).
319
321
  reviewChannel: import_zod.z.enum(["ask", "approver"]).optional(),
320
322
  // When true, agents may call WEAKENING node9 MCP tools (shield_disable,
321
323
  // approver_set). Default (unset/false): those tools refuse over MCP — a human
@@ -353,7 +355,18 @@ var ConfigFileSchema = import_zod.z.object({
353
355
  dlp: import_zod.z.object({
354
356
  enabled: import_zod.z.boolean().optional(),
355
357
  scanIgnoredTools: import_zod.z.boolean().optional(),
356
- pii: import_zod.z.enum(["off", "block"]).optional()
358
+ pii: import_zod.z.enum(["off", "block"]).optional(),
359
+ reviewAction: import_zod.z.enum(["review", "block"]).optional()
360
+ }).optional(),
361
+ // Command-checks governance. Class-B keys (evalDynamic, pipeChainHigh)
362
+ // deliberately exclude 'off' — tighten-only.
363
+ commandChecks: import_zod.z.object({
364
+ inlineExec: import_zod.z.enum(["off", "review", "block"]).optional(),
365
+ rmAdvisory: import_zod.z.enum(["off", "review", "block"]).optional(),
366
+ chmod: import_zod.z.enum(["off", "review", "block"]).optional(),
367
+ sqlDdl: import_zod.z.enum(["off", "review", "block"]).optional(),
368
+ evalDynamic: import_zod.z.enum(["review", "block"]).optional(),
369
+ pipeChainHigh: import_zod.z.enum(["review", "block"]).optional()
357
370
  }).optional(),
358
371
  egress: import_zod.z.object({
359
372
  enabled: import_zod.z.boolean().optional(),
@@ -2365,6 +2378,47 @@ function evaluateSmartConditions(args, rule) {
2365
2378
  });
2366
2379
  return mode === "any" ? results.some((r) => r) : results.every((r) => r);
2367
2380
  }
2381
+ function resolveCheck(v) {
2382
+ return v === "off" || v === "block" ? v : "review";
2383
+ }
2384
+ function resolveCheckTight(v) {
2385
+ return v === "block" ? "block" : "review";
2386
+ }
2387
+ var INLINE_INTERP = /^(python[\d.]*|bash|sh|zsh|perl|ruby|node|php|lua)$/i;
2388
+ var INLINE_SHELL = /^(bash|sh|zsh)$/i;
2389
+ function detectInlineExec(command) {
2390
+ const pipeFed = command.includes("|");
2391
+ const segments = command.split(/\|\||&&|;|\||\n|\$\(|`|\(/);
2392
+ for (const rawSeg of segments) {
2393
+ const tokens = rawSeg.trim().split(/\s+/).filter(Boolean);
2394
+ let i = 0;
2395
+ while (i < tokens.length && /^[A-Za-z_]\w*=/.test(tokens[i])) i++;
2396
+ if (i >= tokens.length) continue;
2397
+ const base = tokens[i].split("/").pop() ?? tokens[i];
2398
+ if (!INLINE_INTERP.test(base)) continue;
2399
+ const args = tokens.slice(i + 1);
2400
+ let hadRedirect = false;
2401
+ const positionals = [];
2402
+ for (let j = 0; j < args.length; j++) {
2403
+ const a = args[j];
2404
+ if (a === "-") return true;
2405
+ if (a.startsWith("<")) {
2406
+ hadRedirect = true;
2407
+ if (a === "<" || a === "<<") j++;
2408
+ continue;
2409
+ }
2410
+ if (a.startsWith("-")) {
2411
+ if (/^-(c|e|eval)$/i.test(a)) return true;
2412
+ continue;
2413
+ }
2414
+ positionals.push(a);
2415
+ }
2416
+ if (positionals.length === 0 && (hadRedirect || pipeFed) && !INLINE_SHELL.test(base)) {
2417
+ return true;
2418
+ }
2419
+ }
2420
+ return false;
2421
+ }
2368
2422
  var VERDICT_RANK = {
2369
2423
  allow: 0,
2370
2424
  review: 1,
@@ -2397,7 +2451,7 @@ function isSqlTool(toolName, toolInspection) {
2397
2451
  return fieldName === "sql" || fieldName === "query";
2398
2452
  }
2399
2453
  var SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
2400
- function pipeChainVerdict(command, isTrustedHost2) {
2454
+ function pipeChainVerdict(command, isTrustedHost2, highAction = "review") {
2401
2455
  const pipeAnalysis = analyzePipeChain(command);
2402
2456
  if (!pipeAnalysis.isPipeline) return null;
2403
2457
  if (pipeAnalysis.risk !== "critical" && pipeAnalysis.risk !== "high") return null;
@@ -2428,7 +2482,7 @@ function pipeChainVerdict(command, isTrustedHost2) {
2428
2482
  };
2429
2483
  }
2430
2484
  return {
2431
- decision: "review",
2485
+ decision: highAction,
2432
2486
  blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
2433
2487
  reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
2434
2488
  tier: 3
@@ -2442,7 +2496,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2442
2496
  const dlpMatch = args !== void 0 ? scanArgs(args) : null;
2443
2497
  if (dlpMatch) {
2444
2498
  return {
2445
- decision: dlpMatch.severity,
2499
+ // reviewAction:'block' (inline-ask v2): the admin upgraded
2500
+ // review-severity matches to a hard block — every evaluatePolicy
2501
+ // caller (orchestrator, explain, gateway) must agree with the gate.
2502
+ decision: dlpMatch.severity === "block" || config.policy.dlp.reviewAction === "block" ? "block" : "review",
2446
2503
  blockedByLabel: `DLP: ${dlpMatch.patternName}`,
2447
2504
  reason: `${dlpMatch.patternName} detected in ${dlpMatch.fieldPath}`
2448
2505
  };
@@ -2451,7 +2508,11 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2451
2508
  if (wouldBeIgnored) return { decision: "allow" };
2452
2509
  const bashCommand = agent !== "Terminal" && isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : null;
2453
2510
  if (bashCommand !== null) {
2454
- const pipeVerdict = pipeChainVerdict(bashCommand, isTrustedHost2);
2511
+ const pipeVerdict = pipeChainVerdict(
2512
+ bashCommand,
2513
+ isTrustedHost2,
2514
+ resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
2515
+ );
2455
2516
  if (pipeVerdict) return pipeVerdict;
2456
2517
  const fsVerdict = analyzeFsOperation(bashCommand);
2457
2518
  if (fsVerdict) {
@@ -2466,10 +2527,12 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2466
2527
  ruleDescription: fsVerdict.reason
2467
2528
  };
2468
2529
  }
2469
- const sqlVerdict = analyzeSqlDestructive(bashCommand);
2530
+ const sqlAction = resolveCheck(config.policy.commandChecks?.sqlDdl);
2531
+ const sqlVerdict = sqlAction === "off" ? null : analyzeSqlDestructive(bashCommand);
2470
2532
  if (sqlVerdict) {
2471
2533
  return {
2472
- decision: sqlVerdict.verdict,
2534
+ // analyzeSqlDestructive is typed review-only, so the knob maps 1:1.
2535
+ decision: sqlAction === "block" ? "block" : "review",
2473
2536
  blockedByLabel: `Node9 (AST): ${sqlVerdict.ruleName}`,
2474
2537
  reason: sqlVerdict.reason,
2475
2538
  tier: 2,
@@ -2477,10 +2540,12 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2477
2540
  ruleDescription: sqlVerdict.description
2478
2541
  };
2479
2542
  }
2480
- const chmodVerdict = analyzeChmod777(bashCommand);
2543
+ const chmodAction = resolveCheck(config.policy.commandChecks?.chmod);
2544
+ const chmodVerdict = chmodAction === "off" ? null : analyzeChmod777(bashCommand);
2481
2545
  if (chmodVerdict) {
2482
2546
  return {
2483
- decision: chmodVerdict.verdict,
2547
+ // analyzeChmod777 is typed review-only, so the knob maps 1:1.
2548
+ decision: chmodAction === "block" ? "block" : "review",
2484
2549
  blockedByLabel: `project-jail (AST): ${chmodVerdict.ruleName}`,
2485
2550
  reason: chmodVerdict.reason,
2486
2551
  tier: 2,
@@ -2523,10 +2588,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2523
2588
  const analyzed = analyzeShellCommand(shellCommand);
2524
2589
  allTokens = analyzed.allTokens;
2525
2590
  pathTokens = analyzed.paths;
2526
- const INLINE_EXEC_PATTERN = /^(python3?|bash|sh|zsh|perl|ruby|node|php|lua)\s+(-c|-e|-eval)\s/i;
2527
- if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
2591
+ const inlineAction = resolveCheck(config.policy.commandChecks?.inlineExec);
2592
+ if (inlineAction !== "off" && detectInlineExec(shellCommand)) {
2528
2593
  return {
2529
- decision: "review",
2594
+ decision: inlineAction === "block" ? "block" : "review",
2530
2595
  blockedByLabel: "Node9 Standard (Inline Execution)",
2531
2596
  ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
2532
2597
  tier: 3
@@ -2544,14 +2609,21 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2544
2609
  }
2545
2610
  if (evalVerdict === "review") {
2546
2611
  return {
2547
- decision: "review",
2612
+ // Class B tighten-only: commandChecks.evalDynamic may upgrade to
2613
+ // block but can never turn this off (eval-remote above is Class A —
2614
+ // no knob at all).
2615
+ decision: resolveCheckTight(config.policy.commandChecks?.evalDynamic),
2548
2616
  blockedByLabel: "Node9: Eval Dynamic Content",
2549
2617
  reason: "eval of dynamic content (variable or subshell expansion) requires approval",
2550
2618
  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.",
2551
2619
  tier: 3
2552
2620
  };
2553
2621
  }
2554
- const ptVerdict = pipeChainVerdict(shellCommand, isTrustedHost2);
2622
+ const ptVerdict = pipeChainVerdict(
2623
+ shellCommand,
2624
+ isTrustedHost2,
2625
+ resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
2626
+ );
2555
2627
  if (ptVerdict) return ptVerdict;
2556
2628
  if (config.policy.egress?.enabled) {
2557
2629
  const dests = extractShellDestinations(shellCommand);
@@ -3638,6 +3710,7 @@ function applyManagedEgress(local, managed, locked) {
3638
3710
  return next;
3639
3711
  }
3640
3712
  var DLP_PII_ORDER = ["off", "block"];
3713
+ var DLP_REVIEW_ACTION_ORDER = ["review", "block"];
3641
3714
  function applyManagedDlp(local, managed, locked) {
3642
3715
  const next = { ...local };
3643
3716
  if (typeof managed.enabled === "boolean") {
@@ -3651,6 +3724,40 @@ function applyManagedDlp(local, managed, locked) {
3651
3724
  locked.includes("dlpPii")
3652
3725
  );
3653
3726
  }
3727
+ if (typeof managed.reviewAction === "string") {
3728
+ next.reviewAction = resolveByOrder(
3729
+ DLP_REVIEW_ACTION_ORDER,
3730
+ local.reviewAction ?? "review",
3731
+ managed.reviewAction,
3732
+ locked.includes("dlpReviewAction")
3733
+ );
3734
+ }
3735
+ return next;
3736
+ }
3737
+ var COMMAND_CHECK_ORDER = ["off", "review", "block"];
3738
+ var COMMAND_CHECK_KEYS = [
3739
+ "inlineExec",
3740
+ "rmAdvisory",
3741
+ "chmod",
3742
+ "sqlDdl",
3743
+ "evalDynamic",
3744
+ "pipeChainHigh"
3745
+ ];
3746
+ function applyManagedCommandChecks(local, managed, locked) {
3747
+ const next = { ...local };
3748
+ for (const key of COMMAND_CHECK_KEYS) {
3749
+ const m = managed[key];
3750
+ if (typeof m !== "string") continue;
3751
+ const lockKey = `commandChecks${key[0].toUpperCase()}${key.slice(1)}`;
3752
+ const resolved = resolveByOrder(
3753
+ COMMAND_CHECK_ORDER,
3754
+ local[key] ?? "review",
3755
+ m,
3756
+ locked.includes(lockKey)
3757
+ );
3758
+ if ((key === "evalDynamic" || key === "pipeChainHigh") && resolved === "off") continue;
3759
+ next[key] = resolved;
3760
+ }
3654
3761
  return next;
3655
3762
  }
3656
3763
  function applyManagedApprovers(local, managed) {
@@ -4221,6 +4328,22 @@ function getConfig(cwd) {
4221
4328
  if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
4222
4329
  if (d.scanIgnoredTools !== void 0) mergedPolicy.dlp.scanIgnoredTools = d.scanIgnoredTools;
4223
4330
  if (d.pii !== void 0) mergedPolicy.dlp.pii = d.pii;
4331
+ if (d.reviewAction !== void 0) mergedPolicy.dlp.reviewAction = d.reviewAction;
4332
+ }
4333
+ if (p.commandChecks && typeof p.commandChecks === "object") {
4334
+ const src = p.commandChecks;
4335
+ const cc2 = {
4336
+ ...mergedPolicy.commandChecks
4337
+ };
4338
+ for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
4339
+ const v = src[k];
4340
+ if (v === "off" || v === "review" || v === "block") cc2[k] = v;
4341
+ }
4342
+ for (const k of ["evalDynamic", "pipeChainHigh"]) {
4343
+ const v = src[k];
4344
+ if (v === "review" || v === "block") cc2[k] = v;
4345
+ }
4346
+ if (Object.keys(cc2).length > 0) mergedPolicy.commandChecks = cc2;
4224
4347
  }
4225
4348
  if (p.egress) {
4226
4349
  const e = p.egress;
@@ -4317,11 +4440,19 @@ function getConfig(cwd) {
4317
4440
  mergedPolicy.dlp,
4318
4441
  {
4319
4442
  enabled: typeof mc.dlp.enabled === "boolean" ? mc.dlp.enabled : void 0,
4320
- pii: typeof mc.dlp.pii === "string" ? mc.dlp.pii : void 0
4443
+ pii: typeof mc.dlp.pii === "string" ? mc.dlp.pii : void 0,
4444
+ reviewAction: mc.dlp.reviewAction === "review" || mc.dlp.reviewAction === "block" ? mc.dlp.reviewAction : void 0
4321
4445
  },
4322
4446
  locked
4323
4447
  );
4324
4448
  }
4449
+ if (mc.commandChecks && typeof mc.commandChecks === "object") {
4450
+ mergedPolicy.commandChecks = applyManagedCommandChecks(
4451
+ mergedPolicy.commandChecks ?? {},
4452
+ mc.commandChecks,
4453
+ locked
4454
+ );
4455
+ }
4325
4456
  if (mc.approvers && typeof mc.approvers === "object") {
4326
4457
  const bool = (v) => typeof v === "boolean" ? v : void 0;
4327
4458
  mergedSettings.approvers = applyManagedApprovers(mergedSettings.approvers, {
@@ -4333,6 +4464,7 @@ function getConfig(cwd) {
4333
4464
  }
4334
4465
  if (mc.reviewChannel === "ask" || mc.reviewChannel === "approver") {
4335
4466
  mergedSettings.reviewChannel = mc.reviewChannel;
4467
+ mergedSettings.reviewChannelManaged = true;
4336
4468
  }
4337
4469
  if (typeof mc.approvalTimeoutMs === "number" && mc.approvalTimeoutMs > 0) {
4338
4470
  mergedSettings.approvalTimeoutMs = mc.approvalTimeoutMs;
@@ -4430,8 +4562,17 @@ function getConfig(cwd) {
4430
4562
  }
4431
4563
  }
4432
4564
  const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
4565
+ const cc = mergedPolicy.commandChecks ?? {};
4566
+ const advisoryKnob = (name) => {
4567
+ if (name === "review-rm") return cc.rmAdvisory;
4568
+ if (name?.endsWith("-sql")) return cc.sqlDdl;
4569
+ return void 0;
4570
+ };
4433
4571
  for (const rule of ADVISORY_SMART_RULES) {
4434
- if (!existingAdvisoryNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
4572
+ if (existingAdvisoryNames.has(rule.name)) continue;
4573
+ const knob = rule.verdict === "review" ? advisoryKnob(rule.name) : void 0;
4574
+ if (knob === "off") continue;
4575
+ mergedPolicy.smartRules.push(knob === "block" ? { ...rule, verdict: "block" } : rule);
4435
4576
  }
4436
4577
  const envMode = process.env.NODE9_MODE;
4437
4578
  if (envMode && !modeCloudControlled && ["observe", "audit", "standard", "strict"].includes(envMode)) {
@@ -5691,7 +5832,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
5691
5832
  const dlpMatch = (filePath ? scanFilePath(filePath) : null) ?? scanArgs(args);
5692
5833
  if (dlpMatch) {
5693
5834
  const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
5694
- if (dlpMatch.severity === "block") {
5835
+ if (dlpMatch.severity === "block" || config.policy.dlp.reviewAction === "block") {
5695
5836
  let dlpEid;
5696
5837
  if (!isManual)
5697
5838
  dlpEid = appendLocalAudit(
@@ -6029,12 +6170,14 @@ ${appPermReview}`
6029
6170
  );
6030
6171
  }
6031
6172
  }
6032
- const cloudEnforcedForDefer = approvers.cloud && !!creds?.apiKey;
6033
- if (options?.deferReview && !hardBlockDowngraded && !taintWarning && !appPermReview && !cloudEnforcedForDefer) {
6173
+ if (options?.deferReview && !hardBlockDowngraded) {
6034
6174
  return {
6035
6175
  approved: false,
6036
6176
  review: true,
6037
- reason: explainableLabel || "Node9 flagged this action for review.",
6177
+ // The prompt must say WHY: the taint sentence beats the bare label so
6178
+ // the dev sees the actual risk context inline. (No appPermReview term:
6179
+ // deferReview and serverKey never co-occur in production — see above.)
6180
+ reason: taintWarning || explainableLabel || "Node9 flagged this action for review.",
6038
6181
  ruleDescription: policyRuleDescription,
6039
6182
  blockedByLabel: explainableLabel
6040
6183
  };