@node9/proxy 1.65.0 → 1.66.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,8 @@ 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()
375
378
  }).optional(),
376
379
  egress: import_zod.z.object({
377
380
  enabled: import_zod.z.boolean().optional(),
@@ -1581,7 +1584,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1581
1584
  const dlpMatch = args !== void 0 ? scanArgs(args) : null;
1582
1585
  if (dlpMatch) {
1583
1586
  return {
1584
- decision: dlpMatch.severity,
1587
+ // reviewAction:'block' (inline-ask v2): the admin upgraded
1588
+ // review-severity matches to a hard block — every evaluatePolicy
1589
+ // caller (orchestrator, explain, gateway) must agree with the gate.
1590
+ decision: dlpMatch.severity === "block" || config.policy.dlp.reviewAction === "block" ? "block" : "review",
1585
1591
  blockedByLabel: `DLP: ${dlpMatch.patternName}`,
1586
1592
  reason: `${dlpMatch.patternName} detected in ${dlpMatch.fieldPath}`
1587
1593
  };
@@ -4366,6 +4372,14 @@ function applyManagedDlp(local, managed, locked) {
4366
4372
  locked.includes("dlpPii")
4367
4373
  );
4368
4374
  }
4375
+ if (typeof managed.reviewAction === "string") {
4376
+ next.reviewAction = resolveByOrder(
4377
+ DLP_REVIEW_ACTION_ORDER,
4378
+ local.reviewAction ?? "review",
4379
+ managed.reviewAction,
4380
+ locked.includes("dlpReviewAction")
4381
+ );
4382
+ }
4369
4383
  return next;
4370
4384
  }
4371
4385
  function applyManagedApprovers(local, managed) {
@@ -4377,13 +4391,14 @@ function applyManagedApprovers(local, managed) {
4377
4391
  terminal: typeof managed.terminal === "boolean" ? managed.terminal : local.terminal
4378
4392
  };
4379
4393
  }
4380
- var MODE_ORDER, EGRESS_MODE_ORDER, DLP_PII_ORDER;
4394
+ var MODE_ORDER, EGRESS_MODE_ORDER, DLP_PII_ORDER, DLP_REVIEW_ACTION_ORDER;
4381
4395
  var init_managed = __esm({
4382
4396
  "src/config/managed.ts"() {
4383
4397
  "use strict";
4384
4398
  MODE_ORDER = ["observe", "audit", "standard", "strict"];
4385
4399
  EGRESS_MODE_ORDER = ["off", "review", "block"];
4386
4400
  DLP_PII_ORDER = ["off", "block"];
4401
+ DLP_REVIEW_ACTION_ORDER = ["review", "block"];
4387
4402
  }
4388
4403
  });
4389
4404
 
@@ -4758,6 +4773,7 @@ function getConfig(cwd) {
4758
4773
  if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
4759
4774
  if (d.scanIgnoredTools !== void 0) mergedPolicy.dlp.scanIgnoredTools = d.scanIgnoredTools;
4760
4775
  if (d.pii !== void 0) mergedPolicy.dlp.pii = d.pii;
4776
+ if (d.reviewAction !== void 0) mergedPolicy.dlp.reviewAction = d.reviewAction;
4761
4777
  }
4762
4778
  if (p.egress) {
4763
4779
  const e = p.egress;
@@ -4854,7 +4870,8 @@ function getConfig(cwd) {
4854
4870
  mergedPolicy.dlp,
4855
4871
  {
4856
4872
  enabled: typeof mc.dlp.enabled === "boolean" ? mc.dlp.enabled : void 0,
4857
- pii: typeof mc.dlp.pii === "string" ? mc.dlp.pii : void 0
4873
+ pii: typeof mc.dlp.pii === "string" ? mc.dlp.pii : void 0,
4874
+ reviewAction: mc.dlp.reviewAction === "review" || mc.dlp.reviewAction === "block" ? mc.dlp.reviewAction : void 0
4858
4875
  },
4859
4876
  locked
4860
4877
  );
@@ -4870,6 +4887,7 @@ function getConfig(cwd) {
4870
4887
  }
4871
4888
  if (mc.reviewChannel === "ask" || mc.reviewChannel === "approver") {
4872
4889
  mergedSettings.reviewChannel = mc.reviewChannel;
4890
+ mergedSettings.reviewChannelManaged = true;
4873
4891
  }
4874
4892
  if (typeof mc.approvalTimeoutMs === "number" && mc.approvalTimeoutMs > 0) {
4875
4893
  mergedSettings.approvalTimeoutMs = mc.approvalTimeoutMs;
@@ -5614,13 +5632,14 @@ async function deriveExplainTrace(toolName, args) {
5614
5632
  const filePathE = String(argsObjE.file_path ?? argsObjE.path ?? argsObjE.filename ?? "");
5615
5633
  const dlpMatch = (filePathE ? scanFilePath(filePathE) : null) ?? (args !== void 0 ? scanArgs(args) : null);
5616
5634
  if (dlpMatch) {
5635
+ const dlpBlocks = dlpMatch.severity === "block" || config.policy.dlp.reviewAction === "block";
5617
5636
  steps.push({
5618
5637
  name: "DLP Content Scanner",
5619
- outcome: dlpMatch.severity === "block" ? "block" : "review",
5638
+ outcome: dlpBlocks ? "block" : "review",
5620
5639
  detail: `\u{1F6A8} ${dlpMatch.patternName} detected in ${dlpMatch.fieldPath} \u2014 sample: ${dlpMatch.redactedSample}`,
5621
- isFinal: dlpMatch.severity === "block"
5640
+ isFinal: dlpBlocks
5622
5641
  });
5623
- if (dlpMatch.severity === "block") {
5642
+ if (dlpBlocks) {
5624
5643
  return { tool: toolName, args, waterfall, steps, decision: "block" };
5625
5644
  }
5626
5645
  } else {
@@ -7119,7 +7138,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
7119
7138
  const dlpMatch = (filePath ? scanFilePath(filePath) : null) ?? scanArgs(args);
7120
7139
  if (dlpMatch) {
7121
7140
  const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
7122
- if (dlpMatch.severity === "block") {
7141
+ if (dlpMatch.severity === "block" || config.policy.dlp.reviewAction === "block") {
7123
7142
  let dlpEid;
7124
7143
  if (!isManual)
7125
7144
  dlpEid = appendLocalAudit(
@@ -7457,12 +7476,14 @@ ${appPermReview}`
7457
7476
  );
7458
7477
  }
7459
7478
  }
7460
- const cloudEnforcedForDefer = approvers.cloud && !!creds?.apiKey;
7461
- if (options?.deferReview && !taintWarning && !appPermReview && !cloudEnforcedForDefer) {
7479
+ if (options?.deferReview && !hardBlockDowngraded) {
7462
7480
  return {
7463
7481
  approved: false,
7464
7482
  review: true,
7465
- reason: explainableLabel || "Node9 flagged this action for review.",
7483
+ // The prompt must say WHY: the taint sentence beats the bare label so
7484
+ // the dev sees the actual risk context inline. (No appPermReview term:
7485
+ // deferReview and serverKey never co-occur in production — see above.)
7486
+ reason: taintWarning || explainableLabel || "Node9 flagged this action for review.",
7466
7487
  ruleDescription: policyRuleDescription,
7467
7488
  blockedByLabel: explainableLabel
7468
7489
  };
@@ -8404,7 +8425,7 @@ function printInlineAskNotice() {
8404
8425
  );
8405
8426
  console.log(
8406
8427
  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.)'
8428
+ ' 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
8429
  )
8409
8430
  );
8410
8431
  }
@@ -18353,7 +18374,9 @@ function extractManagedConfig(body) {
18353
18374
  const d = {};
18354
18375
  if (typeof mc.dlp.enabled === "boolean") d.enabled = mc.dlp.enabled;
18355
18376
  if (typeof mc.dlp.pii === "string") d.pii = mc.dlp.pii;
18356
- if (d.enabled !== void 0 || d.pii !== void 0) out.dlp = d;
18377
+ if (mc.dlp.reviewAction === "review" || mc.dlp.reviewAction === "block")
18378
+ d.reviewAction = mc.dlp.reviewAction;
18379
+ if (d.enabled !== void 0 || d.pii !== void 0 || d.reviewAction !== void 0) out.dlp = d;
18357
18380
  }
18358
18381
  if (mc.approvers && typeof mc.approvers === "object") {
18359
18382
  const a = {};
@@ -19049,12 +19072,23 @@ function classifyDecision(a, b) {
19049
19072
  function decisionTag(view) {
19050
19073
  return `[${view.label}]`.padEnd(14);
19051
19074
  }
19052
- var HUMAN_SOURCES, TIMEOUT_SOURCES, has;
19075
+ var HUMAN_SOURCES, TIMEOUT_SOURCES, NON_DECISION_SOURCES, has;
19053
19076
  var init_decision = __esm({
19054
19077
  "src/audit/decision.ts"() {
19055
19078
  "use strict";
19056
- HUMAN_SOURCES = /* @__PURE__ */ new Set(["daemon", "cloud", "local-decision", "inline-review-approved"]);
19079
+ HUMAN_SOURCES = /* @__PURE__ */ new Set([
19080
+ "daemon",
19081
+ "cloud",
19082
+ "local-decision",
19083
+ "inline-review-approved",
19084
+ "inline-review"
19085
+ ]);
19057
19086
  TIMEOUT_SOURCES = /* @__PURE__ */ new Set(["timeout"]);
19087
+ NON_DECISION_SOURCES = /* @__PURE__ */ new Set([
19088
+ "post-hook",
19089
+ "inline-review-approved",
19090
+ "response-dlp"
19091
+ ]);
19058
19092
  has = (s, needle) => s.includes(needle);
19059
19093
  }
19060
19094
  });
@@ -19630,7 +19664,7 @@ function buildDaemonReport(allEntries, period, now) {
19630
19664
  else if (period === "30d") start.setDate(start.getDate() - 29);
19631
19665
  else if (period === "month") start = new Date(now.getFullYear(), now.getMonth(), 1);
19632
19666
  const entries = allEntries.filter((e) => {
19633
- if (e.source === "post-hook" || e.source === "response-dlp") return false;
19667
+ if (typeof e.source === "string" && NON_DECISION_SOURCES.has(e.source)) return false;
19634
19668
  return new Date(e.ts) >= start;
19635
19669
  });
19636
19670
  const isBlocked = (e) => classifyDecision(e).outcome === "deny";
@@ -47379,8 +47413,8 @@ INSTRUCTIONS:
47379
47413
  - Do NOT retry this exact command or attempt to bypass the rule.${recovery}
47380
47414
  - Inform the user which security rule was triggered and ask how to proceed.`;
47381
47415
  }
47382
- function buildReviewMessage(blockedByLabel, ruleDescription) {
47383
- const why = ruleDescription || blockedByLabel || "this action needs your review";
47416
+ function buildReviewMessage(blockedByLabel, ruleDescription, reason) {
47417
+ const why = ruleDescription || (reason && reason !== blockedByLabel ? reason : void 0) || blockedByLabel || "this action needs your review";
47384
47418
  return `Node9 flagged this for your review: ${why}. Approve to proceed, or deny to cancel.`;
47385
47419
  }
47386
47420
 
@@ -48121,6 +48155,7 @@ init_hasher();
48121
48155
  function storePath() {
48122
48156
  return process.env.NODE9_PENDING_STORE || import_path45.default.join(import_os42.default.homedir(), ".node9", "pending-reviews.json");
48123
48157
  }
48158
+ var TTL_TUID_MS = 72 * 60 * 60 * 1e3;
48124
48159
  var TTL_MS2 = 6 * 60 * 60 * 1e3;
48125
48160
  var MAX_ENTRIES = 500;
48126
48161
  function reviewCorrelationKey(payload) {
@@ -48154,7 +48189,9 @@ function write(store) {
48154
48189
  }
48155
48190
  }
48156
48191
  function prune(entries, now) {
48157
- const fresh = entries.filter((e) => now - e.ts < TTL_MS2);
48192
+ const fresh = entries.filter(
48193
+ (e) => now - e.ts < (e.key.startsWith("tuid:") ? TTL_TUID_MS : TTL_MS2)
48194
+ );
48158
48195
  return fresh.length > MAX_ENTRIES ? fresh.slice(fresh.length - MAX_ENTRIES) : fresh;
48159
48196
  }
48160
48197
  function recordPendingReview(entry) {
@@ -48169,19 +48206,30 @@ function recordPendingReview(entry) {
48169
48206
  function resolvePendingReview(key, now = Date.now()) {
48170
48207
  try {
48171
48208
  const store = read();
48172
- const idx = store.entries.findIndex((e) => e.key === key);
48209
+ const live = prune(store.entries, now);
48210
+ const idx = live.findIndex((e) => e.key === key);
48173
48211
  if (idx === -1) {
48174
- const pruned = prune(store.entries, now);
48175
- if (pruned.length !== store.entries.length) write({ entries: pruned });
48212
+ if (live.length !== store.entries.length) write({ entries: live });
48176
48213
  return null;
48177
48214
  }
48178
- const [match] = store.entries.splice(idx, 1);
48179
- write({ entries: prune(store.entries, now) });
48215
+ const [match] = live.splice(idx, 1);
48216
+ write({ entries: live });
48180
48217
  return match;
48181
48218
  } catch {
48182
48219
  return null;
48183
48220
  }
48184
48221
  }
48222
+ function discardPendingReview(key, now = Date.now()) {
48223
+ try {
48224
+ const store = read();
48225
+ const kept = prune(
48226
+ store.entries.filter((e) => e.key !== key),
48227
+ now
48228
+ );
48229
+ if (kept.length !== store.entries.length) write({ entries: kept });
48230
+ } catch {
48231
+ }
48232
+ }
48185
48233
 
48186
48234
  // src/cli/commands/check.ts
48187
48235
  init_hook_payload();
@@ -48237,7 +48285,10 @@ function agentSupportsAsk(agent) {
48237
48285
  }
48238
48286
  function resolveAskMode(agent, opts, config) {
48239
48287
  if (!agentSupportsAsk(agent)) return false;
48240
- if (config.settings.approvers.cloud === true) return false;
48288
+ if (config.settings.reviewChannelManaged === true) {
48289
+ if (config.settings.reviewChannel === "ask") return true;
48290
+ if (config.settings.reviewChannel === "approver") return false;
48291
+ }
48241
48292
  if (opts.ask === true) return true;
48242
48293
  if (opts.ask === false) return false;
48243
48294
  if (config.settings.reviewChannel === "ask") return true;
@@ -48476,7 +48527,11 @@ RAW: ${raw}
48476
48527
  process.exit(2);
48477
48528
  };
48478
48529
  const sendAsk = (result2) => {
48479
- const msg = buildReviewMessage(result2.blockedByLabel, result2.ruleDescription);
48530
+ const msg = buildReviewMessage(
48531
+ result2.blockedByLabel,
48532
+ result2.ruleDescription,
48533
+ result2.reason
48534
+ );
48480
48535
  try {
48481
48536
  const key = reviewCorrelationKey(payload);
48482
48537
  if (key) {
@@ -48659,6 +48714,13 @@ RAW: ${raw}
48659
48714
  cwd: safeCwdForAuth,
48660
48715
  deferReview: askMode
48661
48716
  });
48717
+ if (!result.review) {
48718
+ try {
48719
+ const key = reviewCorrelationKey(payload);
48720
+ if (key) discardPendingReview(key);
48721
+ } catch {
48722
+ }
48723
+ }
48662
48724
  if (result.approved) {
48663
48725
  if (result.checkedBy && process.env.NODE9_DEBUG === "1")
48664
48726
  process.stderr.write(`\u2713 node9 [${result.checkedBy}]: "${toolName}" allowed
@@ -48835,9 +48897,11 @@ function registerLogCommand(program2) {
48835
48897
  })();
48836
48898
  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
48899
  let reviewApproved = false;
48900
+ let resolvedReview = null;
48838
48901
  try {
48839
48902
  const key = reviewCorrelationKey(payload);
48840
- if (key && resolvePendingReview(key)) reviewApproved = true;
48903
+ if (key) resolvedReview = resolvePendingReview(key);
48904
+ if (resolvedReview) reviewApproved = true;
48841
48905
  } catch {
48842
48906
  }
48843
48907
  const entry = {
@@ -48890,6 +48954,33 @@ function registerLogCommand(program2) {
48890
48954
  const payloadCwd = typeof payload.cwd === "string" ? payload.cwd : Array.isArray(payload.workspacePaths) && typeof payload.workspacePaths[0] === "string" ? payload.workspacePaths[0] : void 0;
48891
48955
  const safeCwd = typeof payloadCwd === "string" && import_path47.default.isAbsolute(payloadCwd) ? payloadCwd : void 0;
48892
48956
  const config = getConfig(safeCwd);
48957
+ if (resolvedReview) {
48958
+ try {
48959
+ const reviewLabel = resolvedReview.label || "inline-review";
48960
+ const sensitiveReview = /dlp|taint/i.test(reviewLabel);
48961
+ appendLocalAudit(
48962
+ tool,
48963
+ rawInput,
48964
+ "allow",
48965
+ "inline-review",
48966
+ {
48967
+ agent,
48968
+ ruleName: reviewLabel,
48969
+ ...rawToolName !== tool ? { agentToolName: rawToolName } : {},
48970
+ ...typeof payloadSessionId === "string" ? { sessionId: payloadSessionId } : {},
48971
+ ...safeCwd ? { workingDir: safeCwd } : {}
48972
+ },
48973
+ sensitiveReview || config.settings.auditHashArgs === true
48974
+ );
48975
+ } catch (err2) {
48976
+ appendToLog(HOOK_DEBUG_LOG, {
48977
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
48978
+ event: "inline-review-ship-row-fail",
48979
+ tool,
48980
+ error: err2 instanceof Error ? err2.message : String(err2)
48981
+ });
48982
+ }
48983
+ }
48893
48984
  {
48894
48985
  const toolOutput = payload.tool_response?.output;
48895
48986
  const inj = config.policy.injectionScan;
@@ -50358,8 +50449,7 @@ function aggregateReportFromAudit(period, opts = {}) {
50358
50449
  const priorEnd = new Date(start.getTime() - 1);
50359
50450
  const priorStart = new Date(start.getTime() - periodMs);
50360
50451
  const priorEntries = allEntries.filter((e) => {
50361
- if (e.source === "post-hook") return false;
50362
- if (e.source === "response-dlp") return false;
50452
+ if (typeof e.source === "string" && NON_DECISION_SOURCES.has(e.source)) return false;
50363
50453
  if (typeof e.decision !== "string") return false;
50364
50454
  const ts = new Date(e.ts);
50365
50455
  return ts >= priorStart && ts <= priorEnd;
@@ -50370,8 +50460,7 @@ function aggregateReportFromAudit(period, opts = {}) {
50370
50460
  const testTs = excludeTests ? buildTestTimestamps(allEntries) : /* @__PURE__ */ new Set();
50371
50461
  let excludedTests = 0;
50372
50462
  const entries = allEntries.filter((e) => {
50373
- if (e.source === "post-hook") return false;
50374
- if (e.source === "response-dlp") return false;
50463
+ if (typeof e.source === "string" && NON_DECISION_SOURCES.has(e.source)) return false;
50375
50464
  if (typeof e.decision !== "string") return false;
50376
50465
  const ts = new Date(e.ts);
50377
50466
  if (ts < start || ts > end) return false;
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,8 @@ 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()
379
382
  }).optional(),
380
383
  egress: z.object({
381
384
  enabled: z.boolean().optional(),
@@ -1591,7 +1594,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1591
1594
  const dlpMatch = args !== void 0 ? scanArgs(args) : null;
1592
1595
  if (dlpMatch) {
1593
1596
  return {
1594
- decision: dlpMatch.severity,
1597
+ // reviewAction:'block' (inline-ask v2): the admin upgraded
1598
+ // review-severity matches to a hard block — every evaluatePolicy
1599
+ // caller (orchestrator, explain, gateway) must agree with the gate.
1600
+ decision: dlpMatch.severity === "block" || config.policy.dlp.reviewAction === "block" ? "block" : "review",
1595
1601
  blockedByLabel: `DLP: ${dlpMatch.patternName}`,
1596
1602
  reason: `${dlpMatch.patternName} detected in ${dlpMatch.fieldPath}`
1597
1603
  };
@@ -4370,6 +4376,14 @@ function applyManagedDlp(local, managed, locked) {
4370
4376
  locked.includes("dlpPii")
4371
4377
  );
4372
4378
  }
4379
+ if (typeof managed.reviewAction === "string") {
4380
+ next.reviewAction = resolveByOrder(
4381
+ DLP_REVIEW_ACTION_ORDER,
4382
+ local.reviewAction ?? "review",
4383
+ managed.reviewAction,
4384
+ locked.includes("dlpReviewAction")
4385
+ );
4386
+ }
4373
4387
  return next;
4374
4388
  }
4375
4389
  function applyManagedApprovers(local, managed) {
@@ -4381,13 +4395,14 @@ function applyManagedApprovers(local, managed) {
4381
4395
  terminal: typeof managed.terminal === "boolean" ? managed.terminal : local.terminal
4382
4396
  };
4383
4397
  }
4384
- var MODE_ORDER, EGRESS_MODE_ORDER, DLP_PII_ORDER;
4398
+ var MODE_ORDER, EGRESS_MODE_ORDER, DLP_PII_ORDER, DLP_REVIEW_ACTION_ORDER;
4385
4399
  var init_managed = __esm({
4386
4400
  "src/config/managed.ts"() {
4387
4401
  "use strict";
4388
4402
  MODE_ORDER = ["observe", "audit", "standard", "strict"];
4389
4403
  EGRESS_MODE_ORDER = ["off", "review", "block"];
4390
4404
  DLP_PII_ORDER = ["off", "block"];
4405
+ DLP_REVIEW_ACTION_ORDER = ["review", "block"];
4391
4406
  }
4392
4407
  });
4393
4408
 
@@ -4765,6 +4780,7 @@ function getConfig(cwd) {
4765
4780
  if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
4766
4781
  if (d.scanIgnoredTools !== void 0) mergedPolicy.dlp.scanIgnoredTools = d.scanIgnoredTools;
4767
4782
  if (d.pii !== void 0) mergedPolicy.dlp.pii = d.pii;
4783
+ if (d.reviewAction !== void 0) mergedPolicy.dlp.reviewAction = d.reviewAction;
4768
4784
  }
4769
4785
  if (p.egress) {
4770
4786
  const e = p.egress;
@@ -4861,7 +4877,8 @@ function getConfig(cwd) {
4861
4877
  mergedPolicy.dlp,
4862
4878
  {
4863
4879
  enabled: typeof mc.dlp.enabled === "boolean" ? mc.dlp.enabled : void 0,
4864
- pii: typeof mc.dlp.pii === "string" ? mc.dlp.pii : void 0
4880
+ pii: typeof mc.dlp.pii === "string" ? mc.dlp.pii : void 0,
4881
+ reviewAction: mc.dlp.reviewAction === "review" || mc.dlp.reviewAction === "block" ? mc.dlp.reviewAction : void 0
4865
4882
  },
4866
4883
  locked
4867
4884
  );
@@ -4877,6 +4894,7 @@ function getConfig(cwd) {
4877
4894
  }
4878
4895
  if (mc.reviewChannel === "ask" || mc.reviewChannel === "approver") {
4879
4896
  mergedSettings.reviewChannel = mc.reviewChannel;
4897
+ mergedSettings.reviewChannelManaged = true;
4880
4898
  }
4881
4899
  if (typeof mc.approvalTimeoutMs === "number" && mc.approvalTimeoutMs > 0) {
4882
4900
  mergedSettings.approvalTimeoutMs = mc.approvalTimeoutMs;
@@ -5621,13 +5639,14 @@ async function deriveExplainTrace(toolName, args) {
5621
5639
  const filePathE = String(argsObjE.file_path ?? argsObjE.path ?? argsObjE.filename ?? "");
5622
5640
  const dlpMatch = (filePathE ? scanFilePath(filePathE) : null) ?? (args !== void 0 ? scanArgs(args) : null);
5623
5641
  if (dlpMatch) {
5642
+ const dlpBlocks = dlpMatch.severity === "block" || config.policy.dlp.reviewAction === "block";
5624
5643
  steps.push({
5625
5644
  name: "DLP Content Scanner",
5626
- outcome: dlpMatch.severity === "block" ? "block" : "review",
5645
+ outcome: dlpBlocks ? "block" : "review",
5627
5646
  detail: `\u{1F6A8} ${dlpMatch.patternName} detected in ${dlpMatch.fieldPath} \u2014 sample: ${dlpMatch.redactedSample}`,
5628
- isFinal: dlpMatch.severity === "block"
5647
+ isFinal: dlpBlocks
5629
5648
  });
5630
- if (dlpMatch.severity === "block") {
5649
+ if (dlpBlocks) {
5631
5650
  return { tool: toolName, args, waterfall, steps, decision: "block" };
5632
5651
  }
5633
5652
  } else {
@@ -7122,7 +7141,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
7122
7141
  const dlpMatch = (filePath ? scanFilePath(filePath) : null) ?? scanArgs(args);
7123
7142
  if (dlpMatch) {
7124
7143
  const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
7125
- if (dlpMatch.severity === "block") {
7144
+ if (dlpMatch.severity === "block" || config.policy.dlp.reviewAction === "block") {
7126
7145
  let dlpEid;
7127
7146
  if (!isManual)
7128
7147
  dlpEid = appendLocalAudit(
@@ -7460,12 +7479,14 @@ ${appPermReview}`
7460
7479
  );
7461
7480
  }
7462
7481
  }
7463
- const cloudEnforcedForDefer = approvers.cloud && !!creds?.apiKey;
7464
- if (options?.deferReview && !taintWarning && !appPermReview && !cloudEnforcedForDefer) {
7482
+ if (options?.deferReview && !hardBlockDowngraded) {
7465
7483
  return {
7466
7484
  approved: false,
7467
7485
  review: true,
7468
- reason: explainableLabel || "Node9 flagged this action for review.",
7486
+ // The prompt must say WHY: the taint sentence beats the bare label so
7487
+ // the dev sees the actual risk context inline. (No appPermReview term:
7488
+ // deferReview and serverKey never co-occur in production — see above.)
7489
+ reason: taintWarning || explainableLabel || "Node9 flagged this action for review.",
7469
7490
  ruleDescription: policyRuleDescription,
7470
7491
  blockedByLabel: explainableLabel
7471
7492
  };
@@ -8412,7 +8433,7 @@ function printInlineAskNotice() {
8412
8433
  );
8413
8434
  console.log(
8414
8435
  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.)'
8436
+ ' 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
8437
  )
8417
8438
  );
8418
8439
  }
@@ -18351,7 +18372,9 @@ function extractManagedConfig(body) {
18351
18372
  const d = {};
18352
18373
  if (typeof mc.dlp.enabled === "boolean") d.enabled = mc.dlp.enabled;
18353
18374
  if (typeof mc.dlp.pii === "string") d.pii = mc.dlp.pii;
18354
- if (d.enabled !== void 0 || d.pii !== void 0) out.dlp = d;
18375
+ if (mc.dlp.reviewAction === "review" || mc.dlp.reviewAction === "block")
18376
+ d.reviewAction = mc.dlp.reviewAction;
18377
+ if (d.enabled !== void 0 || d.pii !== void 0 || d.reviewAction !== void 0) out.dlp = d;
18355
18378
  }
18356
18379
  if (mc.approvers && typeof mc.approvers === "object") {
18357
18380
  const a = {};
@@ -19043,12 +19066,23 @@ function classifyDecision(a, b) {
19043
19066
  function decisionTag(view) {
19044
19067
  return `[${view.label}]`.padEnd(14);
19045
19068
  }
19046
- var HUMAN_SOURCES, TIMEOUT_SOURCES, has;
19069
+ var HUMAN_SOURCES, TIMEOUT_SOURCES, NON_DECISION_SOURCES, has;
19047
19070
  var init_decision = __esm({
19048
19071
  "src/audit/decision.ts"() {
19049
19072
  "use strict";
19050
- HUMAN_SOURCES = /* @__PURE__ */ new Set(["daemon", "cloud", "local-decision", "inline-review-approved"]);
19073
+ HUMAN_SOURCES = /* @__PURE__ */ new Set([
19074
+ "daemon",
19075
+ "cloud",
19076
+ "local-decision",
19077
+ "inline-review-approved",
19078
+ "inline-review"
19079
+ ]);
19051
19080
  TIMEOUT_SOURCES = /* @__PURE__ */ new Set(["timeout"]);
19081
+ NON_DECISION_SOURCES = /* @__PURE__ */ new Set([
19082
+ "post-hook",
19083
+ "inline-review-approved",
19084
+ "response-dlp"
19085
+ ]);
19052
19086
  has = (s, needle) => s.includes(needle);
19053
19087
  }
19054
19088
  });
@@ -19631,7 +19665,7 @@ function buildDaemonReport(allEntries, period, now) {
19631
19665
  else if (period === "30d") start.setDate(start.getDate() - 29);
19632
19666
  else if (period === "month") start = new Date(now.getFullYear(), now.getMonth(), 1);
19633
19667
  const entries = allEntries.filter((e) => {
19634
- if (e.source === "post-hook" || e.source === "response-dlp") return false;
19668
+ if (typeof e.source === "string" && NON_DECISION_SOURCES.has(e.source)) return false;
19635
19669
  return new Date(e.ts) >= start;
19636
19670
  });
19637
19671
  const isBlocked = (e) => classifyDecision(e).outcome === "deny";
@@ -47372,8 +47406,8 @@ INSTRUCTIONS:
47372
47406
  - Do NOT retry this exact command or attempt to bypass the rule.${recovery}
47373
47407
  - Inform the user which security rule was triggered and ask how to proceed.`;
47374
47408
  }
47375
- function buildReviewMessage(blockedByLabel, ruleDescription) {
47376
- const why = ruleDescription || blockedByLabel || "this action needs your review";
47409
+ function buildReviewMessage(blockedByLabel, ruleDescription, reason) {
47410
+ const why = ruleDescription || (reason && reason !== blockedByLabel ? reason : void 0) || blockedByLabel || "this action needs your review";
47377
47411
  return `Node9 flagged this for your review: ${why}. Approve to proceed, or deny to cancel.`;
47378
47412
  }
47379
47413
 
@@ -48114,6 +48148,7 @@ import path46 from "path";
48114
48148
  function storePath() {
48115
48149
  return process.env.NODE9_PENDING_STORE || path46.join(os43.homedir(), ".node9", "pending-reviews.json");
48116
48150
  }
48151
+ var TTL_TUID_MS = 72 * 60 * 60 * 1e3;
48117
48152
  var TTL_MS2 = 6 * 60 * 60 * 1e3;
48118
48153
  var MAX_ENTRIES = 500;
48119
48154
  function reviewCorrelationKey(payload) {
@@ -48147,7 +48182,9 @@ function write(store) {
48147
48182
  }
48148
48183
  }
48149
48184
  function prune(entries, now) {
48150
- const fresh = entries.filter((e) => now - e.ts < TTL_MS2);
48185
+ const fresh = entries.filter(
48186
+ (e) => now - e.ts < (e.key.startsWith("tuid:") ? TTL_TUID_MS : TTL_MS2)
48187
+ );
48151
48188
  return fresh.length > MAX_ENTRIES ? fresh.slice(fresh.length - MAX_ENTRIES) : fresh;
48152
48189
  }
48153
48190
  function recordPendingReview(entry) {
@@ -48162,19 +48199,30 @@ function recordPendingReview(entry) {
48162
48199
  function resolvePendingReview(key, now = Date.now()) {
48163
48200
  try {
48164
48201
  const store = read();
48165
- const idx = store.entries.findIndex((e) => e.key === key);
48202
+ const live = prune(store.entries, now);
48203
+ const idx = live.findIndex((e) => e.key === key);
48166
48204
  if (idx === -1) {
48167
- const pruned = prune(store.entries, now);
48168
- if (pruned.length !== store.entries.length) write({ entries: pruned });
48205
+ if (live.length !== store.entries.length) write({ entries: live });
48169
48206
  return null;
48170
48207
  }
48171
- const [match] = store.entries.splice(idx, 1);
48172
- write({ entries: prune(store.entries, now) });
48208
+ const [match] = live.splice(idx, 1);
48209
+ write({ entries: live });
48173
48210
  return match;
48174
48211
  } catch {
48175
48212
  return null;
48176
48213
  }
48177
48214
  }
48215
+ function discardPendingReview(key, now = Date.now()) {
48216
+ try {
48217
+ const store = read();
48218
+ const kept = prune(
48219
+ store.entries.filter((e) => e.key !== key),
48220
+ now
48221
+ );
48222
+ if (kept.length !== store.entries.length) write({ entries: kept });
48223
+ } catch {
48224
+ }
48225
+ }
48178
48226
 
48179
48227
  // src/cli/commands/check.ts
48180
48228
  init_hook_payload();
@@ -48230,7 +48278,10 @@ function agentSupportsAsk(agent) {
48230
48278
  }
48231
48279
  function resolveAskMode(agent, opts, config) {
48232
48280
  if (!agentSupportsAsk(agent)) return false;
48233
- if (config.settings.approvers.cloud === true) return false;
48281
+ if (config.settings.reviewChannelManaged === true) {
48282
+ if (config.settings.reviewChannel === "ask") return true;
48283
+ if (config.settings.reviewChannel === "approver") return false;
48284
+ }
48234
48285
  if (opts.ask === true) return true;
48235
48286
  if (opts.ask === false) return false;
48236
48287
  if (config.settings.reviewChannel === "ask") return true;
@@ -48469,7 +48520,11 @@ RAW: ${raw}
48469
48520
  process.exit(2);
48470
48521
  };
48471
48522
  const sendAsk = (result2) => {
48472
- const msg = buildReviewMessage(result2.blockedByLabel, result2.ruleDescription);
48523
+ const msg = buildReviewMessage(
48524
+ result2.blockedByLabel,
48525
+ result2.ruleDescription,
48526
+ result2.reason
48527
+ );
48473
48528
  try {
48474
48529
  const key = reviewCorrelationKey(payload);
48475
48530
  if (key) {
@@ -48652,6 +48707,13 @@ RAW: ${raw}
48652
48707
  cwd: safeCwdForAuth,
48653
48708
  deferReview: askMode
48654
48709
  });
48710
+ if (!result.review) {
48711
+ try {
48712
+ const key = reviewCorrelationKey(payload);
48713
+ if (key) discardPendingReview(key);
48714
+ } catch {
48715
+ }
48716
+ }
48655
48717
  if (result.approved) {
48656
48718
  if (result.checkedBy && process.env.NODE9_DEBUG === "1")
48657
48719
  process.stderr.write(`\u2713 node9 [${result.checkedBy}]: "${toolName}" allowed
@@ -48828,9 +48890,11 @@ function registerLogCommand(program2) {
48828
48890
  })();
48829
48891
  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
48892
  let reviewApproved = false;
48893
+ let resolvedReview = null;
48831
48894
  try {
48832
48895
  const key = reviewCorrelationKey(payload);
48833
- if (key && resolvePendingReview(key)) reviewApproved = true;
48896
+ if (key) resolvedReview = resolvePendingReview(key);
48897
+ if (resolvedReview) reviewApproved = true;
48834
48898
  } catch {
48835
48899
  }
48836
48900
  const entry = {
@@ -48883,6 +48947,33 @@ function registerLogCommand(program2) {
48883
48947
  const payloadCwd = typeof payload.cwd === "string" ? payload.cwd : Array.isArray(payload.workspacePaths) && typeof payload.workspacePaths[0] === "string" ? payload.workspacePaths[0] : void 0;
48884
48948
  const safeCwd = typeof payloadCwd === "string" && path48.isAbsolute(payloadCwd) ? payloadCwd : void 0;
48885
48949
  const config = getConfig(safeCwd);
48950
+ if (resolvedReview) {
48951
+ try {
48952
+ const reviewLabel = resolvedReview.label || "inline-review";
48953
+ const sensitiveReview = /dlp|taint/i.test(reviewLabel);
48954
+ appendLocalAudit(
48955
+ tool,
48956
+ rawInput,
48957
+ "allow",
48958
+ "inline-review",
48959
+ {
48960
+ agent,
48961
+ ruleName: reviewLabel,
48962
+ ...rawToolName !== tool ? { agentToolName: rawToolName } : {},
48963
+ ...typeof payloadSessionId === "string" ? { sessionId: payloadSessionId } : {},
48964
+ ...safeCwd ? { workingDir: safeCwd } : {}
48965
+ },
48966
+ sensitiveReview || config.settings.auditHashArgs === true
48967
+ );
48968
+ } catch (err2) {
48969
+ appendToLog(HOOK_DEBUG_LOG, {
48970
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
48971
+ event: "inline-review-ship-row-fail",
48972
+ tool,
48973
+ error: err2 instanceof Error ? err2.message : String(err2)
48974
+ });
48975
+ }
48976
+ }
48886
48977
  {
48887
48978
  const toolOutput = payload.tool_response?.output;
48888
48979
  const inj = config.policy.injectionScan;
@@ -50351,8 +50442,7 @@ function aggregateReportFromAudit(period, opts = {}) {
50351
50442
  const priorEnd = new Date(start.getTime() - 1);
50352
50443
  const priorStart = new Date(start.getTime() - periodMs);
50353
50444
  const priorEntries = allEntries.filter((e) => {
50354
- if (e.source === "post-hook") return false;
50355
- if (e.source === "response-dlp") return false;
50445
+ if (typeof e.source === "string" && NON_DECISION_SOURCES.has(e.source)) return false;
50356
50446
  if (typeof e.decision !== "string") return false;
50357
50447
  const ts = new Date(e.ts);
50358
50448
  return ts >= priorStart && ts <= priorEnd;
@@ -50363,8 +50453,7 @@ function aggregateReportFromAudit(period, opts = {}) {
50363
50453
  const testTs = excludeTests ? buildTestTimestamps(allEntries) : /* @__PURE__ */ new Set();
50364
50454
  let excludedTests = 0;
50365
50455
  const entries = allEntries.filter((e) => {
50366
- if (e.source === "post-hook") return false;
50367
- if (e.source === "response-dlp") return false;
50456
+ if (typeof e.source === "string" && NON_DECISION_SOURCES.has(e.source)) return false;
50368
50457
  if (typeof e.decision !== "string") return false;
50369
50458
  const ts = new Date(e.ts);
50370
50459
  if (ts < start || ts > end) return false;
@@ -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,8 @@ 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()
2335
2338
  }).optional(),
2336
2339
  egress: z.object({
2337
2340
  enabled: z.boolean().optional(),
@@ -2960,17 +2963,6 @@ var init_costSync = __esm({
2960
2963
  }
2961
2964
  });
2962
2965
 
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
2966
  // src/audit/decision.ts
2975
2967
  function classifyDecision(a, b) {
2976
2968
  const isRow = !!a && typeof a === "object";
@@ -3005,16 +2997,38 @@ function classifyDecision(a, b) {
3005
2997
  }
3006
2998
  return { outcome: "unknown", label: raw ? `? ${raw}` : "? (none)", raw };
3007
2999
  }
3008
- var HUMAN_SOURCES, TIMEOUT_SOURCES, has;
3000
+ var HUMAN_SOURCES, TIMEOUT_SOURCES, NON_DECISION_SOURCES, has;
3009
3001
  var init_decision = __esm({
3010
3002
  "src/audit/decision.ts"() {
3011
3003
  "use strict";
3012
- HUMAN_SOURCES = /* @__PURE__ */ new Set(["daemon", "cloud", "local-decision", "inline-review-approved"]);
3004
+ HUMAN_SOURCES = /* @__PURE__ */ new Set([
3005
+ "daemon",
3006
+ "cloud",
3007
+ "local-decision",
3008
+ "inline-review-approved",
3009
+ "inline-review"
3010
+ ]);
3013
3011
  TIMEOUT_SOURCES = /* @__PURE__ */ new Set(["timeout"]);
3012
+ NON_DECISION_SOURCES = /* @__PURE__ */ new Set([
3013
+ "post-hook",
3014
+ "inline-review-approved",
3015
+ "response-dlp"
3016
+ ]);
3014
3017
  has = (s, needle) => s.includes(needle);
3015
3018
  }
3016
3019
  });
3017
3020
 
3021
+ // src/daemon/scan-watermark.ts
3022
+ var MAX_LINE_BYTES;
3023
+ var init_scan_watermark = __esm({
3024
+ "src/daemon/scan-watermark.ts"() {
3025
+ "use strict";
3026
+ init_dlp();
3027
+ init_dist();
3028
+ MAX_LINE_BYTES = 2 * 1024 * 1024;
3029
+ }
3030
+ });
3031
+
3018
3032
  // src/cli/aggregate/report-audit.ts
3019
3033
  import fs5 from "fs";
3020
3034
  import os6 from "os";
@@ -3574,8 +3588,7 @@ function aggregateReportFromAudit(period, opts = {}) {
3574
3588
  const priorEnd = new Date(start.getTime() - 1);
3575
3589
  const priorStart = new Date(start.getTime() - periodMs);
3576
3590
  const priorEntries = allEntries.filter((e) => {
3577
- if (e.source === "post-hook") return false;
3578
- if (e.source === "response-dlp") return false;
3591
+ if (typeof e.source === "string" && NON_DECISION_SOURCES.has(e.source)) return false;
3579
3592
  if (typeof e.decision !== "string") return false;
3580
3593
  const ts = new Date(e.ts);
3581
3594
  return ts >= priorStart && ts <= priorEnd;
@@ -3586,8 +3599,7 @@ function aggregateReportFromAudit(period, opts = {}) {
3586
3599
  const testTs = excludeTests ? buildTestTimestamps(allEntries) : /* @__PURE__ */ new Set();
3587
3600
  let excludedTests = 0;
3588
3601
  const entries = allEntries.filter((e) => {
3589
- if (e.source === "post-hook") return false;
3590
- if (e.source === "response-dlp") return false;
3602
+ if (typeof e.source === "string" && NON_DECISION_SOURCES.has(e.source)) return false;
3591
3603
  if (typeof e.decision !== "string") return false;
3592
3604
  const ts = new Date(e.ts);
3593
3605
  if (ts < start || ts > end) return false;
@@ -4915,7 +4927,7 @@ function readAuditEntriesAsync(chunkSize = 1e3, customPath) {
4915
4927
  }
4916
4928
  function aggregateAudit(entries, startMs, endMs = Date.now()) {
4917
4929
  const inWindow = entries.filter((e) => {
4918
- if (e.source === "post-hook" || e.source === "response-dlp") return false;
4930
+ if (typeof e.source === "string" && NON_DECISION_SOURCES.has(e.source)) return false;
4919
4931
  const t = Date.parse(e.ts);
4920
4932
  return t >= startMs && t <= endMs;
4921
4933
  });
@@ -5361,6 +5373,7 @@ var init_data = __esm({
5361
5373
  init_daemon();
5362
5374
  init_costSync();
5363
5375
  init_shields();
5376
+ init_decision();
5364
5377
  init_scan_watermark();
5365
5378
  init_report_audit();
5366
5379
  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,8 @@ 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()
357
360
  }).optional(),
358
361
  egress: import_zod.z.object({
359
362
  enabled: import_zod.z.boolean().optional(),
@@ -2442,7 +2445,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2442
2445
  const dlpMatch = args !== void 0 ? scanArgs(args) : null;
2443
2446
  if (dlpMatch) {
2444
2447
  return {
2445
- decision: dlpMatch.severity,
2448
+ // reviewAction:'block' (inline-ask v2): the admin upgraded
2449
+ // review-severity matches to a hard block — every evaluatePolicy
2450
+ // caller (orchestrator, explain, gateway) must agree with the gate.
2451
+ decision: dlpMatch.severity === "block" || config.policy.dlp.reviewAction === "block" ? "block" : "review",
2446
2452
  blockedByLabel: `DLP: ${dlpMatch.patternName}`,
2447
2453
  reason: `${dlpMatch.patternName} detected in ${dlpMatch.fieldPath}`
2448
2454
  };
@@ -3638,6 +3644,7 @@ function applyManagedEgress(local, managed, locked) {
3638
3644
  return next;
3639
3645
  }
3640
3646
  var DLP_PII_ORDER = ["off", "block"];
3647
+ var DLP_REVIEW_ACTION_ORDER = ["review", "block"];
3641
3648
  function applyManagedDlp(local, managed, locked) {
3642
3649
  const next = { ...local };
3643
3650
  if (typeof managed.enabled === "boolean") {
@@ -3651,6 +3658,14 @@ function applyManagedDlp(local, managed, locked) {
3651
3658
  locked.includes("dlpPii")
3652
3659
  );
3653
3660
  }
3661
+ if (typeof managed.reviewAction === "string") {
3662
+ next.reviewAction = resolveByOrder(
3663
+ DLP_REVIEW_ACTION_ORDER,
3664
+ local.reviewAction ?? "review",
3665
+ managed.reviewAction,
3666
+ locked.includes("dlpReviewAction")
3667
+ );
3668
+ }
3654
3669
  return next;
3655
3670
  }
3656
3671
  function applyManagedApprovers(local, managed) {
@@ -4221,6 +4236,7 @@ function getConfig(cwd) {
4221
4236
  if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
4222
4237
  if (d.scanIgnoredTools !== void 0) mergedPolicy.dlp.scanIgnoredTools = d.scanIgnoredTools;
4223
4238
  if (d.pii !== void 0) mergedPolicy.dlp.pii = d.pii;
4239
+ if (d.reviewAction !== void 0) mergedPolicy.dlp.reviewAction = d.reviewAction;
4224
4240
  }
4225
4241
  if (p.egress) {
4226
4242
  const e = p.egress;
@@ -4317,7 +4333,8 @@ function getConfig(cwd) {
4317
4333
  mergedPolicy.dlp,
4318
4334
  {
4319
4335
  enabled: typeof mc.dlp.enabled === "boolean" ? mc.dlp.enabled : void 0,
4320
- pii: typeof mc.dlp.pii === "string" ? mc.dlp.pii : void 0
4336
+ pii: typeof mc.dlp.pii === "string" ? mc.dlp.pii : void 0,
4337
+ reviewAction: mc.dlp.reviewAction === "review" || mc.dlp.reviewAction === "block" ? mc.dlp.reviewAction : void 0
4321
4338
  },
4322
4339
  locked
4323
4340
  );
@@ -4333,6 +4350,7 @@ function getConfig(cwd) {
4333
4350
  }
4334
4351
  if (mc.reviewChannel === "ask" || mc.reviewChannel === "approver") {
4335
4352
  mergedSettings.reviewChannel = mc.reviewChannel;
4353
+ mergedSettings.reviewChannelManaged = true;
4336
4354
  }
4337
4355
  if (typeof mc.approvalTimeoutMs === "number" && mc.approvalTimeoutMs > 0) {
4338
4356
  mergedSettings.approvalTimeoutMs = mc.approvalTimeoutMs;
@@ -5691,7 +5709,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
5691
5709
  const dlpMatch = (filePath ? scanFilePath(filePath) : null) ?? scanArgs(args);
5692
5710
  if (dlpMatch) {
5693
5711
  const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
5694
- if (dlpMatch.severity === "block") {
5712
+ if (dlpMatch.severity === "block" || config.policy.dlp.reviewAction === "block") {
5695
5713
  let dlpEid;
5696
5714
  if (!isManual)
5697
5715
  dlpEid = appendLocalAudit(
@@ -6029,12 +6047,14 @@ ${appPermReview}`
6029
6047
  );
6030
6048
  }
6031
6049
  }
6032
- const cloudEnforcedForDefer = approvers.cloud && !!creds?.apiKey;
6033
- if (options?.deferReview && !taintWarning && !appPermReview && !cloudEnforcedForDefer) {
6050
+ if (options?.deferReview && !hardBlockDowngraded) {
6034
6051
  return {
6035
6052
  approved: false,
6036
6053
  review: true,
6037
- reason: explainableLabel || "Node9 flagged this action for review.",
6054
+ // The prompt must say WHY: the taint sentence beats the bare label so
6055
+ // the dev sees the actual risk context inline. (No appPermReview term:
6056
+ // deferReview and serverKey never co-occur in production — see above.)
6057
+ reason: taintWarning || explainableLabel || "Node9 flagged this action for review.",
6038
6058
  ruleDescription: policyRuleDescription,
6039
6059
  blockedByLabel: explainableLabel
6040
6060
  };
package/dist/index.mjs CHANGED
@@ -72,7 +72,7 @@ function redactSecrets(text) {
72
72
  if (!text) return text;
73
73
  let redacted = text;
74
74
  redacted = redacted.replace(
75
- /(authorization:\s*(?:bearer|basic)\s+)[a-zA-Z0-9._\-\/\\=]+/gi,
75
+ /(authorization:\s*(?:bearer|basic)\s+)[a-zA-Z0-9._\-\/=]+/gi,
76
76
  "$1********"
77
77
  );
78
78
  redacted = redacted.replace(
@@ -133,7 +133,7 @@ function filePathFromArgs(args) {
133
133
  return void 0;
134
134
  }
135
135
  function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
136
- const isDlpRow = checkedBy.toLowerCase().includes("dlp") || Boolean(meta?.dlpPattern);
136
+ const isDlpRow = checkedBy.toLowerCase().includes("dlp") || Boolean(meta?.dlpPattern) || /dlp|taint/i.test(String(meta?.ruleName ?? ""));
137
137
  const preview = auditHashArgsEnabled && !isDlpRow ? buildArgsPreview(args) : void 0;
138
138
  const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args), ...preview ? { argsPreview: preview } : {} } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
139
139
  const testRun = isTestCall(toolName, args) || process.env.NODE9_TESTING === "1" ? { testRun: true } : {};
@@ -284,8 +284,10 @@ var ConfigFileSchema = z.object({
284
284
  agentPolicy: z.enum(["require_approval", "block_on_rules"]).optional(),
285
285
  // Where a `review` verdict's prompt is rendered: 'ask' = the agent's own
286
286
  // inline approve/deny prompt (Claude Code / GitHub Copilot); 'approver' =
287
- // node9's own approver (terminal/native/cloud). Unset → smart default
288
- // (ask for ask-capable agents unless a cloud approver is configured).
287
+ // node9's own approver (terminal/native/cloud). Unset → default ASK for
288
+ // ask-capable agents (v2: cloud no longer disables inline — the outcome
289
+ // ships to the dashboard as audit; admins force routing via managed
290
+ // reviewChannel, which outranks the local --ask flag).
289
291
  reviewChannel: z.enum(["ask", "approver"]).optional(),
290
292
  // When true, agents may call WEAKENING node9 MCP tools (shield_disable,
291
293
  // approver_set). Default (unset/false): those tools refuse over MCP — a human
@@ -323,7 +325,8 @@ var ConfigFileSchema = z.object({
323
325
  dlp: z.object({
324
326
  enabled: z.boolean().optional(),
325
327
  scanIgnoredTools: z.boolean().optional(),
326
- pii: z.enum(["off", "block"]).optional()
328
+ pii: z.enum(["off", "block"]).optional(),
329
+ reviewAction: z.enum(["review", "block"]).optional()
327
330
  }).optional(),
328
331
  egress: z.object({
329
332
  enabled: z.boolean().optional(),
@@ -2412,7 +2415,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2412
2415
  const dlpMatch = args !== void 0 ? scanArgs(args) : null;
2413
2416
  if (dlpMatch) {
2414
2417
  return {
2415
- decision: dlpMatch.severity,
2418
+ // reviewAction:'block' (inline-ask v2): the admin upgraded
2419
+ // review-severity matches to a hard block — every evaluatePolicy
2420
+ // caller (orchestrator, explain, gateway) must agree with the gate.
2421
+ decision: dlpMatch.severity === "block" || config.policy.dlp.reviewAction === "block" ? "block" : "review",
2416
2422
  blockedByLabel: `DLP: ${dlpMatch.patternName}`,
2417
2423
  reason: `${dlpMatch.patternName} detected in ${dlpMatch.fieldPath}`
2418
2424
  };
@@ -3608,6 +3614,7 @@ function applyManagedEgress(local, managed, locked) {
3608
3614
  return next;
3609
3615
  }
3610
3616
  var DLP_PII_ORDER = ["off", "block"];
3617
+ var DLP_REVIEW_ACTION_ORDER = ["review", "block"];
3611
3618
  function applyManagedDlp(local, managed, locked) {
3612
3619
  const next = { ...local };
3613
3620
  if (typeof managed.enabled === "boolean") {
@@ -3621,6 +3628,14 @@ function applyManagedDlp(local, managed, locked) {
3621
3628
  locked.includes("dlpPii")
3622
3629
  );
3623
3630
  }
3631
+ if (typeof managed.reviewAction === "string") {
3632
+ next.reviewAction = resolveByOrder(
3633
+ DLP_REVIEW_ACTION_ORDER,
3634
+ local.reviewAction ?? "review",
3635
+ managed.reviewAction,
3636
+ locked.includes("dlpReviewAction")
3637
+ );
3638
+ }
3624
3639
  return next;
3625
3640
  }
3626
3641
  function applyManagedApprovers(local, managed) {
@@ -4191,6 +4206,7 @@ function getConfig(cwd) {
4191
4206
  if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
4192
4207
  if (d.scanIgnoredTools !== void 0) mergedPolicy.dlp.scanIgnoredTools = d.scanIgnoredTools;
4193
4208
  if (d.pii !== void 0) mergedPolicy.dlp.pii = d.pii;
4209
+ if (d.reviewAction !== void 0) mergedPolicy.dlp.reviewAction = d.reviewAction;
4194
4210
  }
4195
4211
  if (p.egress) {
4196
4212
  const e = p.egress;
@@ -4287,7 +4303,8 @@ function getConfig(cwd) {
4287
4303
  mergedPolicy.dlp,
4288
4304
  {
4289
4305
  enabled: typeof mc.dlp.enabled === "boolean" ? mc.dlp.enabled : void 0,
4290
- pii: typeof mc.dlp.pii === "string" ? mc.dlp.pii : void 0
4306
+ pii: typeof mc.dlp.pii === "string" ? mc.dlp.pii : void 0,
4307
+ reviewAction: mc.dlp.reviewAction === "review" || mc.dlp.reviewAction === "block" ? mc.dlp.reviewAction : void 0
4291
4308
  },
4292
4309
  locked
4293
4310
  );
@@ -4303,6 +4320,7 @@ function getConfig(cwd) {
4303
4320
  }
4304
4321
  if (mc.reviewChannel === "ask" || mc.reviewChannel === "approver") {
4305
4322
  mergedSettings.reviewChannel = mc.reviewChannel;
4323
+ mergedSettings.reviewChannelManaged = true;
4306
4324
  }
4307
4325
  if (typeof mc.approvalTimeoutMs === "number" && mc.approvalTimeoutMs > 0) {
4308
4326
  mergedSettings.approvalTimeoutMs = mc.approvalTimeoutMs;
@@ -5661,7 +5679,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
5661
5679
  const dlpMatch = (filePath ? scanFilePath(filePath) : null) ?? scanArgs(args);
5662
5680
  if (dlpMatch) {
5663
5681
  const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
5664
- if (dlpMatch.severity === "block") {
5682
+ if (dlpMatch.severity === "block" || config.policy.dlp.reviewAction === "block") {
5665
5683
  let dlpEid;
5666
5684
  if (!isManual)
5667
5685
  dlpEid = appendLocalAudit(
@@ -5999,12 +6017,14 @@ ${appPermReview}`
5999
6017
  );
6000
6018
  }
6001
6019
  }
6002
- const cloudEnforcedForDefer = approvers.cloud && !!creds?.apiKey;
6003
- if (options?.deferReview && !taintWarning && !appPermReview && !cloudEnforcedForDefer) {
6020
+ if (options?.deferReview && !hardBlockDowngraded) {
6004
6021
  return {
6005
6022
  approved: false,
6006
6023
  review: true,
6007
- reason: explainableLabel || "Node9 flagged this action for review.",
6024
+ // The prompt must say WHY: the taint sentence beats the bare label so
6025
+ // the dev sees the actual risk context inline. (No appPermReview term:
6026
+ // deferReview and serverKey never co-occur in production — see above.)
6027
+ reason: taintWarning || explainableLabel || "Node9 flagged this action for review.",
6008
6028
  ruleDescription: policyRuleDescription,
6009
6029
  blockedByLabel: explainableLabel
6010
6030
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "1.65.0",
3
+ "version": "1.66.0",
4
4
  "description": "The Sudo Command for AI Agents. Execution Security for Claude Code, Codex, Gemini, Cursor, Opencode, Pi, and any MCP server.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",