@node9/proxy 1.65.1 → 1.67.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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,18 @@ 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()
330
+ }).optional(),
331
+ // Command-checks governance. Class-B keys (evalDynamic, pipeChainHigh)
332
+ // deliberately exclude 'off' — tighten-only.
333
+ commandChecks: z.object({
334
+ inlineExec: z.enum(["off", "review", "block"]).optional(),
335
+ rmAdvisory: z.enum(["off", "review", "block"]).optional(),
336
+ chmod: z.enum(["off", "review", "block"]).optional(),
337
+ sqlDdl: z.enum(["off", "review", "block"]).optional(),
338
+ evalDynamic: z.enum(["review", "block"]).optional(),
339
+ pipeChainHigh: z.enum(["review", "block"]).optional()
327
340
  }).optional(),
328
341
  egress: z.object({
329
342
  enabled: z.boolean().optional(),
@@ -2335,6 +2348,47 @@ function evaluateSmartConditions(args, rule) {
2335
2348
  });
2336
2349
  return mode === "any" ? results.some((r) => r) : results.every((r) => r);
2337
2350
  }
2351
+ function resolveCheck(v) {
2352
+ return v === "off" || v === "block" ? v : "review";
2353
+ }
2354
+ function resolveCheckTight(v) {
2355
+ return v === "block" ? "block" : "review";
2356
+ }
2357
+ var INLINE_INTERP = /^(python[\d.]*|bash|sh|zsh|perl|ruby|node|php|lua)$/i;
2358
+ var INLINE_SHELL = /^(bash|sh|zsh)$/i;
2359
+ function detectInlineExec(command) {
2360
+ const pipeFed = command.includes("|");
2361
+ const segments = command.split(/\|\||&&|;|\||\n|\$\(|`|\(/);
2362
+ for (const rawSeg of segments) {
2363
+ const tokens = rawSeg.trim().split(/\s+/).filter(Boolean);
2364
+ let i = 0;
2365
+ while (i < tokens.length && /^[A-Za-z_]\w*=/.test(tokens[i])) i++;
2366
+ if (i >= tokens.length) continue;
2367
+ const base = tokens[i].split("/").pop() ?? tokens[i];
2368
+ if (!INLINE_INTERP.test(base)) continue;
2369
+ const args = tokens.slice(i + 1);
2370
+ let hadRedirect = false;
2371
+ const positionals = [];
2372
+ for (let j = 0; j < args.length; j++) {
2373
+ const a = args[j];
2374
+ if (a === "-") return true;
2375
+ if (a.startsWith("<")) {
2376
+ hadRedirect = true;
2377
+ if (a === "<" || a === "<<") j++;
2378
+ continue;
2379
+ }
2380
+ if (a.startsWith("-")) {
2381
+ if (/^-(c|e|eval)$/i.test(a)) return true;
2382
+ continue;
2383
+ }
2384
+ positionals.push(a);
2385
+ }
2386
+ if (positionals.length === 0 && (hadRedirect || pipeFed) && !INLINE_SHELL.test(base)) {
2387
+ return true;
2388
+ }
2389
+ }
2390
+ return false;
2391
+ }
2338
2392
  var VERDICT_RANK = {
2339
2393
  allow: 0,
2340
2394
  review: 1,
@@ -2367,7 +2421,7 @@ function isSqlTool(toolName, toolInspection) {
2367
2421
  return fieldName === "sql" || fieldName === "query";
2368
2422
  }
2369
2423
  var SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
2370
- function pipeChainVerdict(command, isTrustedHost2) {
2424
+ function pipeChainVerdict(command, isTrustedHost2, highAction = "review") {
2371
2425
  const pipeAnalysis = analyzePipeChain(command);
2372
2426
  if (!pipeAnalysis.isPipeline) return null;
2373
2427
  if (pipeAnalysis.risk !== "critical" && pipeAnalysis.risk !== "high") return null;
@@ -2398,7 +2452,7 @@ function pipeChainVerdict(command, isTrustedHost2) {
2398
2452
  };
2399
2453
  }
2400
2454
  return {
2401
- decision: "review",
2455
+ decision: highAction,
2402
2456
  blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
2403
2457
  reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
2404
2458
  tier: 3
@@ -2412,7 +2466,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2412
2466
  const dlpMatch = args !== void 0 ? scanArgs(args) : null;
2413
2467
  if (dlpMatch) {
2414
2468
  return {
2415
- decision: dlpMatch.severity,
2469
+ // reviewAction:'block' (inline-ask v2): the admin upgraded
2470
+ // review-severity matches to a hard block — every evaluatePolicy
2471
+ // caller (orchestrator, explain, gateway) must agree with the gate.
2472
+ decision: dlpMatch.severity === "block" || config.policy.dlp.reviewAction === "block" ? "block" : "review",
2416
2473
  blockedByLabel: `DLP: ${dlpMatch.patternName}`,
2417
2474
  reason: `${dlpMatch.patternName} detected in ${dlpMatch.fieldPath}`
2418
2475
  };
@@ -2421,7 +2478,11 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2421
2478
  if (wouldBeIgnored) return { decision: "allow" };
2422
2479
  const bashCommand = agent !== "Terminal" && isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : null;
2423
2480
  if (bashCommand !== null) {
2424
- const pipeVerdict = pipeChainVerdict(bashCommand, isTrustedHost2);
2481
+ const pipeVerdict = pipeChainVerdict(
2482
+ bashCommand,
2483
+ isTrustedHost2,
2484
+ resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
2485
+ );
2425
2486
  if (pipeVerdict) return pipeVerdict;
2426
2487
  const fsVerdict = analyzeFsOperation(bashCommand);
2427
2488
  if (fsVerdict) {
@@ -2436,10 +2497,12 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2436
2497
  ruleDescription: fsVerdict.reason
2437
2498
  };
2438
2499
  }
2439
- const sqlVerdict = analyzeSqlDestructive(bashCommand);
2500
+ const sqlAction = resolveCheck(config.policy.commandChecks?.sqlDdl);
2501
+ const sqlVerdict = sqlAction === "off" ? null : analyzeSqlDestructive(bashCommand);
2440
2502
  if (sqlVerdict) {
2441
2503
  return {
2442
- decision: sqlVerdict.verdict,
2504
+ // analyzeSqlDestructive is typed review-only, so the knob maps 1:1.
2505
+ decision: sqlAction === "block" ? "block" : "review",
2443
2506
  blockedByLabel: `Node9 (AST): ${sqlVerdict.ruleName}`,
2444
2507
  reason: sqlVerdict.reason,
2445
2508
  tier: 2,
@@ -2447,10 +2510,12 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2447
2510
  ruleDescription: sqlVerdict.description
2448
2511
  };
2449
2512
  }
2450
- const chmodVerdict = analyzeChmod777(bashCommand);
2513
+ const chmodAction = resolveCheck(config.policy.commandChecks?.chmod);
2514
+ const chmodVerdict = chmodAction === "off" ? null : analyzeChmod777(bashCommand);
2451
2515
  if (chmodVerdict) {
2452
2516
  return {
2453
- decision: chmodVerdict.verdict,
2517
+ // analyzeChmod777 is typed review-only, so the knob maps 1:1.
2518
+ decision: chmodAction === "block" ? "block" : "review",
2454
2519
  blockedByLabel: `project-jail (AST): ${chmodVerdict.ruleName}`,
2455
2520
  reason: chmodVerdict.reason,
2456
2521
  tier: 2,
@@ -2493,10 +2558,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2493
2558
  const analyzed = analyzeShellCommand(shellCommand);
2494
2559
  allTokens = analyzed.allTokens;
2495
2560
  pathTokens = analyzed.paths;
2496
- const INLINE_EXEC_PATTERN = /^(python3?|bash|sh|zsh|perl|ruby|node|php|lua)\s+(-c|-e|-eval)\s/i;
2497
- if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
2561
+ const inlineAction = resolveCheck(config.policy.commandChecks?.inlineExec);
2562
+ if (inlineAction !== "off" && detectInlineExec(shellCommand)) {
2498
2563
  return {
2499
- decision: "review",
2564
+ decision: inlineAction === "block" ? "block" : "review",
2500
2565
  blockedByLabel: "Node9 Standard (Inline Execution)",
2501
2566
  ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
2502
2567
  tier: 3
@@ -2514,14 +2579,21 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2514
2579
  }
2515
2580
  if (evalVerdict === "review") {
2516
2581
  return {
2517
- decision: "review",
2582
+ // Class B tighten-only: commandChecks.evalDynamic may upgrade to
2583
+ // block but can never turn this off (eval-remote above is Class A —
2584
+ // no knob at all).
2585
+ decision: resolveCheckTight(config.policy.commandChecks?.evalDynamic),
2518
2586
  blockedByLabel: "Node9: Eval Dynamic Content",
2519
2587
  reason: "eval of dynamic content (variable or subshell expansion) requires approval",
2520
2588
  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.",
2521
2589
  tier: 3
2522
2590
  };
2523
2591
  }
2524
- const ptVerdict = pipeChainVerdict(shellCommand, isTrustedHost2);
2592
+ const ptVerdict = pipeChainVerdict(
2593
+ shellCommand,
2594
+ isTrustedHost2,
2595
+ resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
2596
+ );
2525
2597
  if (ptVerdict) return ptVerdict;
2526
2598
  if (config.policy.egress?.enabled) {
2527
2599
  const dests = extractShellDestinations(shellCommand);
@@ -3608,6 +3680,7 @@ function applyManagedEgress(local, managed, locked) {
3608
3680
  return next;
3609
3681
  }
3610
3682
  var DLP_PII_ORDER = ["off", "block"];
3683
+ var DLP_REVIEW_ACTION_ORDER = ["review", "block"];
3611
3684
  function applyManagedDlp(local, managed, locked) {
3612
3685
  const next = { ...local };
3613
3686
  if (typeof managed.enabled === "boolean") {
@@ -3621,6 +3694,40 @@ function applyManagedDlp(local, managed, locked) {
3621
3694
  locked.includes("dlpPii")
3622
3695
  );
3623
3696
  }
3697
+ if (typeof managed.reviewAction === "string") {
3698
+ next.reviewAction = resolveByOrder(
3699
+ DLP_REVIEW_ACTION_ORDER,
3700
+ local.reviewAction ?? "review",
3701
+ managed.reviewAction,
3702
+ locked.includes("dlpReviewAction")
3703
+ );
3704
+ }
3705
+ return next;
3706
+ }
3707
+ var COMMAND_CHECK_ORDER = ["off", "review", "block"];
3708
+ var COMMAND_CHECK_KEYS = [
3709
+ "inlineExec",
3710
+ "rmAdvisory",
3711
+ "chmod",
3712
+ "sqlDdl",
3713
+ "evalDynamic",
3714
+ "pipeChainHigh"
3715
+ ];
3716
+ function applyManagedCommandChecks(local, managed, locked) {
3717
+ const next = { ...local };
3718
+ for (const key of COMMAND_CHECK_KEYS) {
3719
+ const m = managed[key];
3720
+ if (typeof m !== "string") continue;
3721
+ const lockKey = `commandChecks${key[0].toUpperCase()}${key.slice(1)}`;
3722
+ const resolved = resolveByOrder(
3723
+ COMMAND_CHECK_ORDER,
3724
+ local[key] ?? "review",
3725
+ m,
3726
+ locked.includes(lockKey)
3727
+ );
3728
+ if ((key === "evalDynamic" || key === "pipeChainHigh") && resolved === "off") continue;
3729
+ next[key] = resolved;
3730
+ }
3624
3731
  return next;
3625
3732
  }
3626
3733
  function applyManagedApprovers(local, managed) {
@@ -4191,6 +4298,22 @@ function getConfig(cwd) {
4191
4298
  if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
4192
4299
  if (d.scanIgnoredTools !== void 0) mergedPolicy.dlp.scanIgnoredTools = d.scanIgnoredTools;
4193
4300
  if (d.pii !== void 0) mergedPolicy.dlp.pii = d.pii;
4301
+ if (d.reviewAction !== void 0) mergedPolicy.dlp.reviewAction = d.reviewAction;
4302
+ }
4303
+ if (p.commandChecks && typeof p.commandChecks === "object") {
4304
+ const src = p.commandChecks;
4305
+ const cc2 = {
4306
+ ...mergedPolicy.commandChecks
4307
+ };
4308
+ for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
4309
+ const v = src[k];
4310
+ if (v === "off" || v === "review" || v === "block") cc2[k] = v;
4311
+ }
4312
+ for (const k of ["evalDynamic", "pipeChainHigh"]) {
4313
+ const v = src[k];
4314
+ if (v === "review" || v === "block") cc2[k] = v;
4315
+ }
4316
+ if (Object.keys(cc2).length > 0) mergedPolicy.commandChecks = cc2;
4194
4317
  }
4195
4318
  if (p.egress) {
4196
4319
  const e = p.egress;
@@ -4287,11 +4410,19 @@ function getConfig(cwd) {
4287
4410
  mergedPolicy.dlp,
4288
4411
  {
4289
4412
  enabled: typeof mc.dlp.enabled === "boolean" ? mc.dlp.enabled : void 0,
4290
- pii: typeof mc.dlp.pii === "string" ? mc.dlp.pii : void 0
4413
+ pii: typeof mc.dlp.pii === "string" ? mc.dlp.pii : void 0,
4414
+ reviewAction: mc.dlp.reviewAction === "review" || mc.dlp.reviewAction === "block" ? mc.dlp.reviewAction : void 0
4291
4415
  },
4292
4416
  locked
4293
4417
  );
4294
4418
  }
4419
+ if (mc.commandChecks && typeof mc.commandChecks === "object") {
4420
+ mergedPolicy.commandChecks = applyManagedCommandChecks(
4421
+ mergedPolicy.commandChecks ?? {},
4422
+ mc.commandChecks,
4423
+ locked
4424
+ );
4425
+ }
4295
4426
  if (mc.approvers && typeof mc.approvers === "object") {
4296
4427
  const bool = (v) => typeof v === "boolean" ? v : void 0;
4297
4428
  mergedSettings.approvers = applyManagedApprovers(mergedSettings.approvers, {
@@ -4303,6 +4434,7 @@ function getConfig(cwd) {
4303
4434
  }
4304
4435
  if (mc.reviewChannel === "ask" || mc.reviewChannel === "approver") {
4305
4436
  mergedSettings.reviewChannel = mc.reviewChannel;
4437
+ mergedSettings.reviewChannelManaged = true;
4306
4438
  }
4307
4439
  if (typeof mc.approvalTimeoutMs === "number" && mc.approvalTimeoutMs > 0) {
4308
4440
  mergedSettings.approvalTimeoutMs = mc.approvalTimeoutMs;
@@ -4400,8 +4532,17 @@ function getConfig(cwd) {
4400
4532
  }
4401
4533
  }
4402
4534
  const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
4535
+ const cc = mergedPolicy.commandChecks ?? {};
4536
+ const advisoryKnob = (name) => {
4537
+ if (name === "review-rm") return cc.rmAdvisory;
4538
+ if (name?.endsWith("-sql")) return cc.sqlDdl;
4539
+ return void 0;
4540
+ };
4403
4541
  for (const rule of ADVISORY_SMART_RULES) {
4404
- if (!existingAdvisoryNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
4542
+ if (existingAdvisoryNames.has(rule.name)) continue;
4543
+ const knob = rule.verdict === "review" ? advisoryKnob(rule.name) : void 0;
4544
+ if (knob === "off") continue;
4545
+ mergedPolicy.smartRules.push(knob === "block" ? { ...rule, verdict: "block" } : rule);
4405
4546
  }
4406
4547
  const envMode = process.env.NODE9_MODE;
4407
4548
  if (envMode && !modeCloudControlled && ["observe", "audit", "standard", "strict"].includes(envMode)) {
@@ -5661,7 +5802,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
5661
5802
  const dlpMatch = (filePath ? scanFilePath(filePath) : null) ?? scanArgs(args);
5662
5803
  if (dlpMatch) {
5663
5804
  const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
5664
- if (dlpMatch.severity === "block") {
5805
+ if (dlpMatch.severity === "block" || config.policy.dlp.reviewAction === "block") {
5665
5806
  let dlpEid;
5666
5807
  if (!isManual)
5667
5808
  dlpEid = appendLocalAudit(
@@ -5999,12 +6140,14 @@ ${appPermReview}`
5999
6140
  );
6000
6141
  }
6001
6142
  }
6002
- const cloudEnforcedForDefer = approvers.cloud && !!creds?.apiKey;
6003
- if (options?.deferReview && !hardBlockDowngraded && !taintWarning && !appPermReview && !cloudEnforcedForDefer) {
6143
+ if (options?.deferReview && !hardBlockDowngraded) {
6004
6144
  return {
6005
6145
  approved: false,
6006
6146
  review: true,
6007
- reason: explainableLabel || "Node9 flagged this action for review.",
6147
+ // The prompt must say WHY: the taint sentence beats the bare label so
6148
+ // the dev sees the actual risk context inline. (No appPermReview term:
6149
+ // deferReview and serverKey never co-occur in production — see above.)
6150
+ reason: taintWarning || explainableLabel || "Node9 flagged this action for review.",
6008
6151
  ruleDescription: policyRuleDescription,
6009
6152
  blockedByLabel: explainableLabel
6010
6153
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "1.65.1",
3
+ "version": "1.67.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",