@node9/proxy 1.16.0 → 1.18.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/index.js CHANGED
@@ -110,6 +110,7 @@ function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
110
110
  ...argsField,
111
111
  agent: meta?.agent,
112
112
  mcpServer: meta?.mcpServer,
113
+ sessionId: meta?.sessionId,
113
114
  hostname: import_os.default.hostname(),
114
115
  cwd: process.cwd()
115
116
  });
@@ -2088,13 +2089,6 @@ var k8s_default = {
2088
2089
  ],
2089
2090
  dangerousWords: []
2090
2091
  };
2091
- var mcp_tool_gating_default = {
2092
- name: "mcp-tool-gating",
2093
- description: "Intercept MCP tool lists and require user approval before the agent can use any tools from a new server",
2094
- aliases: ["mcp-gating", "mcp-tools"],
2095
- smartRules: [],
2096
- dangerousWords: []
2097
- };
2098
2092
  var mongodb_default = {
2099
2093
  name: "mongodb",
2100
2094
  description: "Protects MongoDB databases from destructive AI operations",
@@ -2400,7 +2394,6 @@ var BUILTIN_SHIELDS = {
2400
2394
  [filesystem_default.name]: filesystem_default,
2401
2395
  [github_default.name]: github_default,
2402
2396
  [k8s_default.name]: k8s_default,
2403
- [mcp_tool_gating_default.name]: mcp_tool_gating_default,
2404
2397
  [mongodb_default.name]: mongodb_default,
2405
2398
  [postgres_default.name]: postgres_default,
2406
2399
  [project_jail_default.name]: project_jail_default,
@@ -2929,6 +2922,12 @@ function getConfig(cwd) {
2929
2922
  if (Array.isArray(raw.rules) && raw.rules.length > 0) {
2930
2923
  applyLayer({ policy: { smartRules: raw.rules } });
2931
2924
  }
2925
+ if (raw.panicMode === true) {
2926
+ mergedSettings.panicMode = true;
2927
+ }
2928
+ if (raw.shadowMode === true) {
2929
+ mergedSettings.mode = "observe";
2930
+ }
2932
2931
  } catch {
2933
2932
  }
2934
2933
  }
@@ -3866,6 +3865,12 @@ var KNOWN_CHECKED_BY = /* @__PURE__ */ new Set([
3866
3865
  "audit-mode",
3867
3866
  "local-policy",
3868
3867
  "smart-rule-block",
3868
+ // Smart-rule block was downgraded to review because the daemon was
3869
+ // running and we're not in CI. The block attempt is still recorded;
3870
+ // the user got a popup. Distinct from 'smart-rule-block' so the
3871
+ // dashboard can show "block rule overridden" separately from a hard
3872
+ // block that fired with no human in the loop.
3873
+ "smart-rule-block-override",
3869
3874
  "persistent",
3870
3875
  "trust",
3871
3876
  "observe-mode",
@@ -3886,7 +3891,7 @@ function validateApiUrl(raw) {
3886
3891
  }
3887
3892
  return null;
3888
3893
  }
3889
- function auditLocalAllow(toolName, args, checkedBy, creds, meta, dlpInfo, containsSensitiveArgs = false) {
3894
+ function auditLocalAllow(toolName, args, checkedBy, creds, meta, dlpInfo, containsSensitiveArgs = false, riskMetadata) {
3890
3895
  const validated = validateApiUrl(creds.apiUrl);
3891
3896
  if (!validated) {
3892
3897
  try {
@@ -3903,6 +3908,10 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta, dlpInfo, contai
3903
3908
  const dlpSample = dlpInfo && typeof dlpInfo.redactedSample === "string" ? dlpInfo.redactedSample.slice(0, DLP_SAMPLE_MAX_LEN) : void 0;
3904
3909
  const dlpPattern = dlpInfo && typeof dlpInfo.pattern === "string" ? dlpInfo.pattern.slice(0, DLP_PATTERN_MAX_LEN) : void 0;
3905
3910
  const safeCheckedBy = KNOWN_CHECKED_BY.has(checkedBy) ? checkedBy : "unknown";
3911
+ const cleanedRiskMetadata = riskMetadata ? Object.fromEntries(
3912
+ Object.entries(riskMetadata).filter(([, v]) => typeof v === "string" && v.length > 0)
3913
+ ) : void 0;
3914
+ const hasRiskMetadata = cleanedRiskMetadata && Object.keys(cleanedRiskMetadata).length > 0;
3906
3915
  return fetch(`${validated.toString().replace(/\/$/, "")}/audit`, {
3907
3916
  method: "POST",
3908
3917
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${creds.apiKey}` },
@@ -3911,6 +3920,13 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta, dlpInfo, contai
3911
3920
  args: safeArgs,
3912
3921
  checkedBy: safeCheckedBy,
3913
3922
  ...dlpInfo && { dlpPattern, dlpSample },
3923
+ ...hasRiskMetadata && { riskMetadata: cleanedRiskMetadata },
3924
+ // session_id (Claude Code + Gemini CLI) groups all audit rows from one
3925
+ // agent run; transcript_path is the authoritative pointer to the
3926
+ // session log (survives Gemini resume drift). Both optional —
3927
+ // unsupported agents (MCP-mediated) leave them undefined.
3928
+ ...meta?.sessionId && { runId: meta.sessionId },
3929
+ ...meta?.transcriptPath && { transcriptPath: meta.transcriptPath },
3914
3930
  context: {
3915
3931
  agent: meta?.agent,
3916
3932
  mcpServer: meta?.mcpServer,
@@ -3961,6 +3977,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
3961
3977
  body: JSON.stringify({
3962
3978
  toolName,
3963
3979
  args,
3980
+ // See auditLocalAllow above for the rationale on these two fields.
3981
+ ...meta?.sessionId && { runId: meta.sessionId },
3982
+ ...meta?.transcriptPath && { transcriptPath: meta.transcriptPath },
3964
3983
  context: {
3965
3984
  agent: meta?.agent,
3966
3985
  mcpServer: meta?.mcpServer,
@@ -4133,15 +4152,18 @@ async function authorizeHeadless(toolName, args, meta, options) {
4133
4152
  if (!options?.calledFromDaemon) {
4134
4153
  const actId = (0, import_crypto3.randomUUID)();
4135
4154
  const actTs = Date.now();
4155
+ const stripAnsi = (s) => s.replace(/\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g, "");
4156
+ const sanitizedAgent = meta?.agent ? stripAnsi(meta.agent).slice(0, 80) : void 0;
4157
+ const sanitizedMcpServer = meta?.mcpServer ? stripAnsi(meta.mcpServer).slice(0, 40) : void 0;
4136
4158
  const socketOk = await notifyActivity({
4137
4159
  id: actId,
4138
4160
  ts: actTs,
4139
4161
  tool: toolName,
4140
4162
  args,
4141
4163
  status: "pending",
4142
- // Strip ANSI escape sequences — agent name comes from caller-supplied metadata
4143
- // and may be displayed in a terminal (node9 tail/watch), enabling injection.
4144
- agent: meta?.agent ? meta.agent.replace(/\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g, "").slice(0, 80) : void 0
4164
+ agent: sanitizedAgent,
4165
+ mcpServer: sanitizedMcpServer,
4166
+ sessionId: meta?.sessionId
4145
4167
  });
4146
4168
  const result = await _authorizeHeadlessCore(toolName, args, meta, {
4147
4169
  ...options,
@@ -4159,7 +4181,10 @@ async function authorizeHeadless(toolName, args, meta, options) {
4159
4181
  status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
4160
4182
  label: result.blockedByLabel,
4161
4183
  ruleHit: result.ruleHit,
4162
- observeWouldBlock: result.observeWouldBlock
4184
+ observeWouldBlock: result.observeWouldBlock,
4185
+ agent: sanitizedAgent,
4186
+ mcpServer: sanitizedMcpServer,
4187
+ sessionId: meta?.sessionId
4163
4188
  });
4164
4189
  }
4165
4190
  return result;
@@ -4179,9 +4204,9 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
4179
4204
  };
4180
4205
  if (isTestEnv2) {
4181
4206
  approvers.native = false;
4182
- approvers.browser = false;
4183
4207
  approvers.terminal = false;
4184
4208
  }
4209
+ approvers.browser = false;
4185
4210
  if (config.settings.enableHookLogDebug && !isTestEnv2) {
4186
4211
  appendHookDebug(toolName, args, meta, hashAuditArgs);
4187
4212
  }
@@ -4311,10 +4336,24 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
4311
4336
  };
4312
4337
  }
4313
4338
  }
4314
- const policyResult = await evaluatePolicy2(toolName, args, meta?.agent);
4339
+ let policyResult = await evaluatePolicy2(toolName, args, meta?.agent);
4340
+ if (config.settings.panicMode && policyResult.decision === "review") {
4341
+ policyResult = {
4342
+ ...policyResult,
4343
+ decision: "block",
4344
+ blockedByLabel: "\u{1F6A8} Panic mode (org policy)",
4345
+ reason: "Workspace is in panic mode \u2014 all review-verdict actions are blocked. Contact your admin to disable panic mode in the Node9 dashboard."
4346
+ };
4347
+ }
4315
4348
  if (policyResult.decision === "allow") {
4316
4349
  if (approvers.cloud && creds?.apiKey)
4317
- await auditLocalAllow(toolName, args, "local-policy", creds, meta);
4350
+ await auditLocalAllow(toolName, args, "local-policy", creds, meta, void 0, false, {
4351
+ ruleName: policyResult.ruleName,
4352
+ ruleDescription: policyResult.ruleDescription,
4353
+ blockedByLabel: policyResult.blockedByLabel,
4354
+ matchedField: policyResult.matchedField,
4355
+ matchedWord: policyResult.matchedWord
4356
+ });
4318
4357
  if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta, hashAuditArgs);
4319
4358
  return { approved: true, checkedBy: "local-policy" };
4320
4359
  }
@@ -4337,14 +4376,50 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
4337
4376
  }
4338
4377
  } else if (isDaemonRunning() && !isTestEnv2) {
4339
4378
  if (!isManual)
4340
- appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
4379
+ appendLocalAudit(
4380
+ toolName,
4381
+ args,
4382
+ "deny",
4383
+ "smart-rule-block-override",
4384
+ meta,
4385
+ hashAuditArgs
4386
+ );
4341
4387
  if (approvers.cloud && creds?.apiKey)
4342
- auditLocalAllow(toolName, args, "smart-rule-block", creds, meta);
4388
+ auditLocalAllow(
4389
+ toolName,
4390
+ args,
4391
+ "smart-rule-block-override",
4392
+ creds,
4393
+ meta,
4394
+ void 0,
4395
+ false,
4396
+ {
4397
+ ruleName: policyResult.ruleName,
4398
+ ruleDescription: policyResult.ruleDescription,
4399
+ blockedByLabel: policyResult.blockedByLabel,
4400
+ matchedField: policyResult.matchedField,
4401
+ matchedWord: policyResult.matchedWord
4402
+ }
4403
+ );
4404
+ const baseLabel = policyResult.blockedByLabel || "Smart Rule";
4405
+ const OVERRIDE_PREFIX = "\u26A0\uFE0F Override block rule: ";
4406
+ if (!baseLabel.startsWith(OVERRIDE_PREFIX)) {
4407
+ policyResult = {
4408
+ ...policyResult,
4409
+ blockedByLabel: `${OVERRIDE_PREFIX}${baseLabel}`
4410
+ };
4411
+ }
4343
4412
  } else {
4344
4413
  if (!isManual)
4345
4414
  appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
4346
4415
  if (approvers.cloud && creds?.apiKey)
4347
- auditLocalAllow(toolName, args, "smart-rule-block", creds, meta);
4416
+ auditLocalAllow(toolName, args, "smart-rule-block", creds, meta, void 0, false, {
4417
+ ruleName: policyResult.ruleName,
4418
+ ruleDescription: policyResult.ruleDescription,
4419
+ blockedByLabel: policyResult.blockedByLabel,
4420
+ matchedField: policyResult.matchedField,
4421
+ matchedWord: policyResult.matchedWord
4422
+ });
4348
4423
  return {
4349
4424
  approved: false,
4350
4425
  reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
@@ -4482,7 +4557,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
4482
4557
  const internalToken = getInternalToken();
4483
4558
  let daemonEntryId = null;
4484
4559
  let daemonAllowCount = 1;
4485
- if ((approvers.browser || approvers.terminal) && isDaemonRunning() && !options?.calledFromDaemon) {
4560
+ if (approvers.terminal && isDaemonRunning() && !options?.calledFromDaemon) {
4486
4561
  if (cloudEnforced && cloudRequestId) {
4487
4562
  const viewer = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(() => null);
4488
4563
  viewerId = viewer?.id ?? null;
@@ -4560,7 +4635,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
4560
4635
  })()
4561
4636
  );
4562
4637
  }
4563
- if (daemonEntryId && (approvers.browser || approvers.terminal)) {
4638
+ if (daemonEntryId && approvers.terminal) {
4564
4639
  racePromises.push(
4565
4640
  (async () => {
4566
4641
  const {
@@ -4571,19 +4646,14 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
4571
4646
  if (daemonDecision === "abandoned") throw new Error("Abandoned");
4572
4647
  const isApproved = daemonDecision === "allow";
4573
4648
  const isRedirect = decisionSource === "terminal-redirect";
4574
- const src = decisionSource === "terminal" || decisionSource === "terminal-redirect" || decisionSource === "browser" ? decisionSource === "browser" ? "browser" : "terminal" : approvers.browser ? "browser" : "terminal";
4575
- const via = src === "terminal" ? "Terminal (node9 tail)" : "Browser Dashboard";
4649
+ const via = "Terminal (node9 tail)";
4576
4650
  return {
4577
4651
  approved: isApproved,
4578
- reason: isApproved ? void 0 : (
4579
- // Use the redirect reason from the tail when choice [2] was selected;
4580
- // otherwise fall back to the generic rejection message.
4581
- isRedirect && daemonReason || `The human user rejected this action via the Node9 ${via}.`
4582
- ),
4652
+ reason: isApproved ? void 0 : isRedirect && daemonReason || `The human user rejected this action via the Node9 ${via}.`,
4583
4653
  checkedBy: isApproved ? "daemon" : void 0,
4584
4654
  blockedBy: isApproved ? void 0 : "local-decision",
4585
4655
  blockedByLabel: isRedirect ? "Steered Redirect (Terminal)" : `User Decision (${via})`,
4586
- decisionSource: src
4656
+ decisionSource: "terminal"
4587
4657
  };
4588
4658
  })()
4589
4659
  );
package/dist/index.mjs CHANGED
@@ -90,6 +90,7 @@ function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
90
90
  ...argsField,
91
91
  agent: meta?.agent,
92
92
  mcpServer: meta?.mcpServer,
93
+ sessionId: meta?.sessionId,
93
94
  hostname: os.hostname(),
94
95
  cwd: process.cwd()
95
96
  });
@@ -2058,13 +2059,6 @@ var k8s_default = {
2058
2059
  ],
2059
2060
  dangerousWords: []
2060
2061
  };
2061
- var mcp_tool_gating_default = {
2062
- name: "mcp-tool-gating",
2063
- description: "Intercept MCP tool lists and require user approval before the agent can use any tools from a new server",
2064
- aliases: ["mcp-gating", "mcp-tools"],
2065
- smartRules: [],
2066
- dangerousWords: []
2067
- };
2068
2062
  var mongodb_default = {
2069
2063
  name: "mongodb",
2070
2064
  description: "Protects MongoDB databases from destructive AI operations",
@@ -2370,7 +2364,6 @@ var BUILTIN_SHIELDS = {
2370
2364
  [filesystem_default.name]: filesystem_default,
2371
2365
  [github_default.name]: github_default,
2372
2366
  [k8s_default.name]: k8s_default,
2373
- [mcp_tool_gating_default.name]: mcp_tool_gating_default,
2374
2367
  [mongodb_default.name]: mongodb_default,
2375
2368
  [postgres_default.name]: postgres_default,
2376
2369
  [project_jail_default.name]: project_jail_default,
@@ -2899,6 +2892,12 @@ function getConfig(cwd) {
2899
2892
  if (Array.isArray(raw.rules) && raw.rules.length > 0) {
2900
2893
  applyLayer({ policy: { smartRules: raw.rules } });
2901
2894
  }
2895
+ if (raw.panicMode === true) {
2896
+ mergedSettings.panicMode = true;
2897
+ }
2898
+ if (raw.shadowMode === true) {
2899
+ mergedSettings.mode = "observe";
2900
+ }
2902
2901
  } catch {
2903
2902
  }
2904
2903
  }
@@ -3836,6 +3835,12 @@ var KNOWN_CHECKED_BY = /* @__PURE__ */ new Set([
3836
3835
  "audit-mode",
3837
3836
  "local-policy",
3838
3837
  "smart-rule-block",
3838
+ // Smart-rule block was downgraded to review because the daemon was
3839
+ // running and we're not in CI. The block attempt is still recorded;
3840
+ // the user got a popup. Distinct from 'smart-rule-block' so the
3841
+ // dashboard can show "block rule overridden" separately from a hard
3842
+ // block that fired with no human in the loop.
3843
+ "smart-rule-block-override",
3839
3844
  "persistent",
3840
3845
  "trust",
3841
3846
  "observe-mode",
@@ -3856,7 +3861,7 @@ function validateApiUrl(raw) {
3856
3861
  }
3857
3862
  return null;
3858
3863
  }
3859
- function auditLocalAllow(toolName, args, checkedBy, creds, meta, dlpInfo, containsSensitiveArgs = false) {
3864
+ function auditLocalAllow(toolName, args, checkedBy, creds, meta, dlpInfo, containsSensitiveArgs = false, riskMetadata) {
3860
3865
  const validated = validateApiUrl(creds.apiUrl);
3861
3866
  if (!validated) {
3862
3867
  try {
@@ -3873,6 +3878,10 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta, dlpInfo, contai
3873
3878
  const dlpSample = dlpInfo && typeof dlpInfo.redactedSample === "string" ? dlpInfo.redactedSample.slice(0, DLP_SAMPLE_MAX_LEN) : void 0;
3874
3879
  const dlpPattern = dlpInfo && typeof dlpInfo.pattern === "string" ? dlpInfo.pattern.slice(0, DLP_PATTERN_MAX_LEN) : void 0;
3875
3880
  const safeCheckedBy = KNOWN_CHECKED_BY.has(checkedBy) ? checkedBy : "unknown";
3881
+ const cleanedRiskMetadata = riskMetadata ? Object.fromEntries(
3882
+ Object.entries(riskMetadata).filter(([, v]) => typeof v === "string" && v.length > 0)
3883
+ ) : void 0;
3884
+ const hasRiskMetadata = cleanedRiskMetadata && Object.keys(cleanedRiskMetadata).length > 0;
3876
3885
  return fetch(`${validated.toString().replace(/\/$/, "")}/audit`, {
3877
3886
  method: "POST",
3878
3887
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${creds.apiKey}` },
@@ -3881,6 +3890,13 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta, dlpInfo, contai
3881
3890
  args: safeArgs,
3882
3891
  checkedBy: safeCheckedBy,
3883
3892
  ...dlpInfo && { dlpPattern, dlpSample },
3893
+ ...hasRiskMetadata && { riskMetadata: cleanedRiskMetadata },
3894
+ // session_id (Claude Code + Gemini CLI) groups all audit rows from one
3895
+ // agent run; transcript_path is the authoritative pointer to the
3896
+ // session log (survives Gemini resume drift). Both optional —
3897
+ // unsupported agents (MCP-mediated) leave them undefined.
3898
+ ...meta?.sessionId && { runId: meta.sessionId },
3899
+ ...meta?.transcriptPath && { transcriptPath: meta.transcriptPath },
3884
3900
  context: {
3885
3901
  agent: meta?.agent,
3886
3902
  mcpServer: meta?.mcpServer,
@@ -3931,6 +3947,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
3931
3947
  body: JSON.stringify({
3932
3948
  toolName,
3933
3949
  args,
3950
+ // See auditLocalAllow above for the rationale on these two fields.
3951
+ ...meta?.sessionId && { runId: meta.sessionId },
3952
+ ...meta?.transcriptPath && { transcriptPath: meta.transcriptPath },
3934
3953
  context: {
3935
3954
  agent: meta?.agent,
3936
3955
  mcpServer: meta?.mcpServer,
@@ -4103,15 +4122,18 @@ async function authorizeHeadless(toolName, args, meta, options) {
4103
4122
  if (!options?.calledFromDaemon) {
4104
4123
  const actId = randomUUID();
4105
4124
  const actTs = Date.now();
4125
+ const stripAnsi = (s) => s.replace(/\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g, "");
4126
+ const sanitizedAgent = meta?.agent ? stripAnsi(meta.agent).slice(0, 80) : void 0;
4127
+ const sanitizedMcpServer = meta?.mcpServer ? stripAnsi(meta.mcpServer).slice(0, 40) : void 0;
4106
4128
  const socketOk = await notifyActivity({
4107
4129
  id: actId,
4108
4130
  ts: actTs,
4109
4131
  tool: toolName,
4110
4132
  args,
4111
4133
  status: "pending",
4112
- // Strip ANSI escape sequences — agent name comes from caller-supplied metadata
4113
- // and may be displayed in a terminal (node9 tail/watch), enabling injection.
4114
- agent: meta?.agent ? meta.agent.replace(/\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g, "").slice(0, 80) : void 0
4134
+ agent: sanitizedAgent,
4135
+ mcpServer: sanitizedMcpServer,
4136
+ sessionId: meta?.sessionId
4115
4137
  });
4116
4138
  const result = await _authorizeHeadlessCore(toolName, args, meta, {
4117
4139
  ...options,
@@ -4129,7 +4151,10 @@ async function authorizeHeadless(toolName, args, meta, options) {
4129
4151
  status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
4130
4152
  label: result.blockedByLabel,
4131
4153
  ruleHit: result.ruleHit,
4132
- observeWouldBlock: result.observeWouldBlock
4154
+ observeWouldBlock: result.observeWouldBlock,
4155
+ agent: sanitizedAgent,
4156
+ mcpServer: sanitizedMcpServer,
4157
+ sessionId: meta?.sessionId
4133
4158
  });
4134
4159
  }
4135
4160
  return result;
@@ -4149,9 +4174,9 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
4149
4174
  };
4150
4175
  if (isTestEnv2) {
4151
4176
  approvers.native = false;
4152
- approvers.browser = false;
4153
4177
  approvers.terminal = false;
4154
4178
  }
4179
+ approvers.browser = false;
4155
4180
  if (config.settings.enableHookLogDebug && !isTestEnv2) {
4156
4181
  appendHookDebug(toolName, args, meta, hashAuditArgs);
4157
4182
  }
@@ -4281,10 +4306,24 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
4281
4306
  };
4282
4307
  }
4283
4308
  }
4284
- const policyResult = await evaluatePolicy2(toolName, args, meta?.agent);
4309
+ let policyResult = await evaluatePolicy2(toolName, args, meta?.agent);
4310
+ if (config.settings.panicMode && policyResult.decision === "review") {
4311
+ policyResult = {
4312
+ ...policyResult,
4313
+ decision: "block",
4314
+ blockedByLabel: "\u{1F6A8} Panic mode (org policy)",
4315
+ reason: "Workspace is in panic mode \u2014 all review-verdict actions are blocked. Contact your admin to disable panic mode in the Node9 dashboard."
4316
+ };
4317
+ }
4285
4318
  if (policyResult.decision === "allow") {
4286
4319
  if (approvers.cloud && creds?.apiKey)
4287
- await auditLocalAllow(toolName, args, "local-policy", creds, meta);
4320
+ await auditLocalAllow(toolName, args, "local-policy", creds, meta, void 0, false, {
4321
+ ruleName: policyResult.ruleName,
4322
+ ruleDescription: policyResult.ruleDescription,
4323
+ blockedByLabel: policyResult.blockedByLabel,
4324
+ matchedField: policyResult.matchedField,
4325
+ matchedWord: policyResult.matchedWord
4326
+ });
4288
4327
  if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta, hashAuditArgs);
4289
4328
  return { approved: true, checkedBy: "local-policy" };
4290
4329
  }
@@ -4307,14 +4346,50 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
4307
4346
  }
4308
4347
  } else if (isDaemonRunning() && !isTestEnv2) {
4309
4348
  if (!isManual)
4310
- appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
4349
+ appendLocalAudit(
4350
+ toolName,
4351
+ args,
4352
+ "deny",
4353
+ "smart-rule-block-override",
4354
+ meta,
4355
+ hashAuditArgs
4356
+ );
4311
4357
  if (approvers.cloud && creds?.apiKey)
4312
- auditLocalAllow(toolName, args, "smart-rule-block", creds, meta);
4358
+ auditLocalAllow(
4359
+ toolName,
4360
+ args,
4361
+ "smart-rule-block-override",
4362
+ creds,
4363
+ meta,
4364
+ void 0,
4365
+ false,
4366
+ {
4367
+ ruleName: policyResult.ruleName,
4368
+ ruleDescription: policyResult.ruleDescription,
4369
+ blockedByLabel: policyResult.blockedByLabel,
4370
+ matchedField: policyResult.matchedField,
4371
+ matchedWord: policyResult.matchedWord
4372
+ }
4373
+ );
4374
+ const baseLabel = policyResult.blockedByLabel || "Smart Rule";
4375
+ const OVERRIDE_PREFIX = "\u26A0\uFE0F Override block rule: ";
4376
+ if (!baseLabel.startsWith(OVERRIDE_PREFIX)) {
4377
+ policyResult = {
4378
+ ...policyResult,
4379
+ blockedByLabel: `${OVERRIDE_PREFIX}${baseLabel}`
4380
+ };
4381
+ }
4313
4382
  } else {
4314
4383
  if (!isManual)
4315
4384
  appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
4316
4385
  if (approvers.cloud && creds?.apiKey)
4317
- auditLocalAllow(toolName, args, "smart-rule-block", creds, meta);
4386
+ auditLocalAllow(toolName, args, "smart-rule-block", creds, meta, void 0, false, {
4387
+ ruleName: policyResult.ruleName,
4388
+ ruleDescription: policyResult.ruleDescription,
4389
+ blockedByLabel: policyResult.blockedByLabel,
4390
+ matchedField: policyResult.matchedField,
4391
+ matchedWord: policyResult.matchedWord
4392
+ });
4318
4393
  return {
4319
4394
  approved: false,
4320
4395
  reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
@@ -4452,7 +4527,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
4452
4527
  const internalToken = getInternalToken();
4453
4528
  let daemonEntryId = null;
4454
4529
  let daemonAllowCount = 1;
4455
- if ((approvers.browser || approvers.terminal) && isDaemonRunning() && !options?.calledFromDaemon) {
4530
+ if (approvers.terminal && isDaemonRunning() && !options?.calledFromDaemon) {
4456
4531
  if (cloudEnforced && cloudRequestId) {
4457
4532
  const viewer = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(() => null);
4458
4533
  viewerId = viewer?.id ?? null;
@@ -4530,7 +4605,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
4530
4605
  })()
4531
4606
  );
4532
4607
  }
4533
- if (daemonEntryId && (approvers.browser || approvers.terminal)) {
4608
+ if (daemonEntryId && approvers.terminal) {
4534
4609
  racePromises.push(
4535
4610
  (async () => {
4536
4611
  const {
@@ -4541,19 +4616,14 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
4541
4616
  if (daemonDecision === "abandoned") throw new Error("Abandoned");
4542
4617
  const isApproved = daemonDecision === "allow";
4543
4618
  const isRedirect = decisionSource === "terminal-redirect";
4544
- const src = decisionSource === "terminal" || decisionSource === "terminal-redirect" || decisionSource === "browser" ? decisionSource === "browser" ? "browser" : "terminal" : approvers.browser ? "browser" : "terminal";
4545
- const via = src === "terminal" ? "Terminal (node9 tail)" : "Browser Dashboard";
4619
+ const via = "Terminal (node9 tail)";
4546
4620
  return {
4547
4621
  approved: isApproved,
4548
- reason: isApproved ? void 0 : (
4549
- // Use the redirect reason from the tail when choice [2] was selected;
4550
- // otherwise fall back to the generic rejection message.
4551
- isRedirect && daemonReason || `The human user rejected this action via the Node9 ${via}.`
4552
- ),
4622
+ reason: isApproved ? void 0 : isRedirect && daemonReason || `The human user rejected this action via the Node9 ${via}.`,
4553
4623
  checkedBy: isApproved ? "daemon" : void 0,
4554
4624
  blockedBy: isApproved ? void 0 : "local-decision",
4555
4625
  blockedByLabel: isRedirect ? "Steered Redirect (Terminal)" : `User Decision (${via})`,
4556
- decisionSource: src
4626
+ decisionSource: "terminal"
4557
4627
  };
4558
4628
  })()
4559
4629
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "1.16.0",
3
+ "version": "1.18.0",
4
4
  "description": "The Sudo Command for AI Agents. Execution Security for Claude Code & MCP.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",