@node9/proxy 1.63.0 → 1.65.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.
@@ -1959,6 +1959,7 @@ var init_dist = __esm({
1959
1959
  name: "redis",
1960
1960
  description: "Protects Redis instances from destructive AI operations",
1961
1961
  aliases: [],
1962
+ _scopeNote: "Every rule requires Redis to actually be involved: a client in the command (redis-cli / valkey-cli, in either order \u2014 `redis-cli FLUSHALL` and `echo FLUSHALL | redis-cli` are both real), OR the command field holding the bare keyword, which is what the MCP redis_command tool sends ({ command: 'FLUSHALL', args: [] }). Without the client requirement these rules matched the WORD anywhere \u2014 including prose in a heredoc writing a file \u2014 which produced two observed false positives: a test fixture describing the shield, and a commit message naming the rule. Every other builtin shield already scopes this way (aws s3, docker system prune, kubectl delete, gh repo delete). See redis-scope.spec.ts.",
1962
1963
  smartRules: [
1963
1964
  {
1964
1965
  name: "shield:redis:block-flushall",
@@ -1967,7 +1968,7 @@ var init_dist = __esm({
1967
1968
  {
1968
1969
  field: "command",
1969
1970
  op: "matches",
1970
- value: "\\bFLUSHALL\\b",
1971
+ value: "(redis|valkey)-cli.*\\bFLUSHALL\\b|\\bFLUSHALL\\b.*(redis|valkey)-cli|^ ?FLUSHALL\\b|\\.flushall\\s*\\(",
1971
1972
  flags: "i"
1972
1973
  }
1973
1974
  ],
@@ -1981,7 +1982,7 @@ var init_dist = __esm({
1981
1982
  {
1982
1983
  field: "command",
1983
1984
  op: "matches",
1984
- value: "\\bFLUSHDB\\b",
1985
+ value: "(redis|valkey)-cli.*\\bFLUSHDB\\b|\\bFLUSHDB\\b.*(redis|valkey)-cli|^ ?FLUSHDB\\b|\\.flushdb\\s*\\(",
1985
1986
  flags: "i"
1986
1987
  }
1987
1988
  ],
@@ -1995,7 +1996,7 @@ var init_dist = __esm({
1995
1996
  {
1996
1997
  field: "command",
1997
1998
  op: "matches",
1998
- value: "\\bCONFIG\\s+RESETSTAT\\b",
1999
+ value: "(redis|valkey)-cli.*CONFIG\\s+RESETSTAT|^ ?CONFIG\\s+RESETSTAT",
1999
2000
  flags: "i"
2000
2001
  }
2001
2002
  ],
@@ -2009,7 +2010,7 @@ var init_dist = __esm({
2009
2010
  {
2010
2011
  field: "command",
2011
2012
  op: "matches",
2012
- value: "\\bCONFIG\\s+SET\\b",
2013
+ value: "(redis|valkey)-cli.*\\bCONFIG\\s+SET\\b|\\bCONFIG\\s+SET\\b.*(redis|valkey)-cli|^ ?CONFIG\\s+SET\\b",
2013
2014
  flags: "i"
2014
2015
  }
2015
2016
  ],
@@ -2023,7 +2024,7 @@ var init_dist = __esm({
2023
2024
  {
2024
2025
  field: "command",
2025
2026
  op: "matches",
2026
- value: "\\bDEL\\b.*[*?\\[]|redis-cli.*--scan.*\\|.*xargs.*del",
2027
+ value: "(redis|valkey)-cli.*\\bDEL\\b.*[*?\\[]|^ ?DEL\\b.*[*?\\[]|-cli.*--scan.*xargs.*del",
2027
2028
  flags: "i"
2028
2029
  }
2029
2030
  ],
@@ -2302,6 +2303,7 @@ var init_config_schema = __esm({
2302
2303
  // nudge-only (default), and the scan cadence in minutes.
2303
2304
  mcpAutoWrap: z.boolean().optional(),
2304
2305
  mcpReconcileIntervalMinutes: z.number().positive().optional(),
2306
+ mcpStaleAfterDays: z.number().min(0).optional(),
2305
2307
  cloudSyncIntervalHours: z.number().positive().optional(),
2306
2308
  // Seconds-granular override for the cloud policy sync cadence. Wins over
2307
2309
  // cloudSyncIntervalHours when set. Lets you opt into fast apply (e.g. 20)
@@ -2457,7 +2459,7 @@ var init_trusted_hosts = __esm({
2457
2459
  });
2458
2460
 
2459
2461
  // src/config/index.ts
2460
- var DANGEROUS_WORDS, DEFAULT_CONFIG;
2462
+ var DANGEROUS_WORDS, DEFAULT_CONFIG, CACHE_LOG_REARM_MS;
2461
2463
  var init_config = __esm({
2462
2464
  "src/config/index.ts"() {
2463
2465
  "use strict";
@@ -2466,6 +2468,7 @@ var init_config = __esm({
2466
2468
  init_managed();
2467
2469
  init_build();
2468
2470
  init_trusted_hosts();
2471
+ init_dist();
2469
2472
  DANGEROUS_WORDS = [
2470
2473
  "mkfs",
2471
2474
  // formats/wipes a filesystem partition
@@ -2681,6 +2684,7 @@ var init_config = __esm({
2681
2684
  },
2682
2685
  environments: {}
2683
2686
  };
2687
+ CACHE_LOG_REARM_MS = 5 * 60 * 1e3;
2684
2688
  }
2685
2689
  });
2686
2690
 
@@ -2967,6 +2971,50 @@ var init_scan_watermark = __esm({
2967
2971
  }
2968
2972
  });
2969
2973
 
2974
+ // src/audit/decision.ts
2975
+ function classifyDecision(a, b) {
2976
+ const isRow = !!a && typeof a === "object";
2977
+ const decision = isRow ? a.decision : a;
2978
+ const attribution = isRow ? a.checkedBy ?? a.source : b;
2979
+ const raw = typeof decision === "string" ? decision : String(decision ?? "");
2980
+ const src = typeof attribution === "string" ? attribution.toLowerCase() : "";
2981
+ const d = raw.toLowerCase();
2982
+ if (src && has(src, "observe-mode")) {
2983
+ return { outcome: "observe", label: "Would block", raw };
2984
+ }
2985
+ if (d === "dlp") return { outcome: "info", label: "Finding", raw };
2986
+ if (d === "mcp-discovered") return { outcome: "info", label: "Info", raw };
2987
+ if (d === "allow" || d === "allowed") {
2988
+ if (src === "post-hook") return { outcome: "allow", label: "Ran", raw };
2989
+ if (HUMAN_SOURCES.has(src)) {
2990
+ return { outcome: "allow", label: "Approved", raw };
2991
+ }
2992
+ return { outcome: "allow", label: "Auto-allowed", raw };
2993
+ }
2994
+ if (d === "deny" || d === "auto-deny" || d === "block") {
2995
+ if (TIMEOUT_SOURCES.has(src)) {
2996
+ return { outcome: "deny", label: "Timed out", raw };
2997
+ }
2998
+ if (HUMAN_SOURCES.has(src)) {
2999
+ return { outcome: "deny", label: "Denied", raw };
3000
+ }
3001
+ return { outcome: "deny", label: "Blocked", raw };
3002
+ }
3003
+ if (d === "review" || d === "pending") {
3004
+ return { outcome: "info", label: "Pending", raw };
3005
+ }
3006
+ return { outcome: "unknown", label: raw ? `? ${raw}` : "? (none)", raw };
3007
+ }
3008
+ var HUMAN_SOURCES, TIMEOUT_SOURCES, has;
3009
+ var init_decision = __esm({
3010
+ "src/audit/decision.ts"() {
3011
+ "use strict";
3012
+ HUMAN_SOURCES = /* @__PURE__ */ new Set(["daemon", "cloud", "local-decision", "inline-review-approved"]);
3013
+ TIMEOUT_SOURCES = /* @__PURE__ */ new Set(["timeout"]);
3014
+ has = (s, needle) => s.includes(needle);
3015
+ }
3016
+ });
3017
+
2970
3018
  // src/cli/aggregate/report-audit.ts
2971
3019
  import fs5 from "fs";
2972
3020
  import os6 from "os";
@@ -3062,8 +3110,15 @@ function parseAuditLog(logPath) {
3062
3110
  }
3063
3111
  });
3064
3112
  }
3065
- function isAllow(decision) {
3066
- return decision.startsWith("allow");
3113
+ function viewOf(e) {
3114
+ const outcome = classifyDecision(e).outcome;
3115
+ const observed = outcome === "observe";
3116
+ return {
3117
+ observed,
3118
+ ran: outcome === "allow" || observed,
3119
+ blocked: outcome === "deny" || outcome === "unknown",
3120
+ info: outcome === "info"
3121
+ };
3067
3122
  }
3068
3123
  function isDlp(checkedBy) {
3069
3124
  return !!checkedBy?.includes("dlp");
@@ -3520,12 +3575,12 @@ function aggregateReportFromAudit(period, opts = {}) {
3520
3575
  const priorStart = new Date(start.getTime() - periodMs);
3521
3576
  const priorEntries = allEntries.filter((e) => {
3522
3577
  if (e.source === "post-hook") return false;
3578
+ if (e.source === "response-dlp") return false;
3579
+ if (typeof e.decision !== "string") return false;
3523
3580
  const ts = new Date(e.ts);
3524
3581
  return ts >= priorStart && ts <= priorEnd;
3525
3582
  });
3526
- const priorBlocked = priorEntries.filter(
3527
- (e) => typeof e.decision === "string" && !isAllow(e.decision)
3528
- ).length;
3583
+ const priorBlocked = priorEntries.filter((e) => viewOf(e).blocked).length;
3529
3584
  const priorBlockRate = priorEntries.length > 0 ? priorBlocked / priorEntries.length : null;
3530
3585
  const excludeTests = opts.excludeTests === true;
3531
3586
  const testTs = excludeTests ? buildTestTimestamps(allEntries) : /* @__PURE__ */ new Set();
@@ -3568,15 +3623,17 @@ function aggregateReportFromAudit(period, opts = {}) {
3568
3623
  let dimInjectionBlocked = 0;
3569
3624
  for (const e of entries) {
3570
3625
  if (superseded.has(supersedeKey(e))) continue;
3571
- const allow = isAllow(e.decision);
3626
+ const view = viewOf(e);
3572
3627
  const dateKey = e.ts.slice(0, 10);
3573
3628
  const userInteracted = e.source === "daemon";
3574
- if (userInteracted) {
3575
- if (allow) userApproved++;
3629
+ if (view.observed) {
3630
+ if (e.checkedBy === "observe-mode-dlp-would-block") observeDlp++;
3631
+ } else if (view.info) {
3632
+ } else if (userInteracted) {
3633
+ if (view.ran) userApproved++;
3576
3634
  else userDenied++;
3577
- } else if (!allow) {
3635
+ } else if (view.blocked) {
3578
3636
  if (e.checkedBy === "timeout") timedOut++;
3579
- else if (e.checkedBy === "observe-mode-dlp-would-block") observeDlp++;
3580
3637
  else if (isDlp(e.checkedBy)) dlpBlocked++;
3581
3638
  else if (e.checkedBy === "local-decision") userDenied++;
3582
3639
  else if (e.checkedBy !== "loop-detected") hardBlocked++;
@@ -3588,7 +3645,7 @@ function aggregateReportFromAudit(period, opts = {}) {
3588
3645
  if (cb.includes("would-block") && (cb.includes("pii") || cb.includes("dlp"))) {
3589
3646
  dimDataObserved++;
3590
3647
  }
3591
- if (!allow && !userInteracted) {
3648
+ if (view.blocked && !userInteracted) {
3592
3649
  switch (dimensionOfBlock(cb, e.ruleName ?? "")) {
3593
3650
  case "network":
3594
3651
  dimNetworkBlocked++;
@@ -3606,15 +3663,15 @@ function aggregateReportFromAudit(period, opts = {}) {
3606
3663
  }
3607
3664
  const t = toolMap.get(e.tool) ?? { calls: 0, blocked: 0 };
3608
3665
  t.calls++;
3609
- if (!allow) t.blocked++;
3666
+ if (view.blocked) t.blocked++;
3610
3667
  toolMap.set(e.tool, t);
3611
- if (!allow) {
3668
+ if (view.blocked) {
3612
3669
  const key = e.checkedBy ?? (e.source === "daemon" ? "local-decision" : null);
3613
3670
  if (key) {
3614
3671
  blockMap.set(key, (blockMap.get(key) ?? 0) + 1);
3615
3672
  }
3616
3673
  }
3617
- if (!allow && e.ruleName) {
3674
+ if ((view.blocked || view.observed) && e.ruleName) {
3618
3675
  ruleMap.set(e.ruleName, (ruleMap.get(e.ruleName) ?? 0) + 1);
3619
3676
  }
3620
3677
  if (e.agent) agentMap.set(e.agent, (agentMap.get(e.agent) ?? 0) + 1);
@@ -3623,7 +3680,7 @@ function aggregateReportFromAudit(period, opts = {}) {
3623
3680
  hourMap.set(hour, (hourMap.get(hour) ?? 0) + 1);
3624
3681
  const d = dailyMap.get(dateKey) ?? { calls: 0, blocked: 0 };
3625
3682
  d.calls++;
3626
- if (!allow) d.blocked++;
3683
+ if (view.blocked) d.blocked++;
3627
3684
  dailyMap.set(dateKey, d);
3628
3685
  }
3629
3686
  for (const e of allEntries) {
@@ -3699,6 +3756,7 @@ var init_report_audit = __esm({
3699
3756
  init_costSync();
3700
3757
  init_litellm();
3701
3758
  init_cost_codex();
3759
+ init_decision();
3702
3760
  TEST_COMMAND_RE = /(?:^|\s)(npm\s+(?:run\s+)?test|npx\s+(?:vitest|jest|mocha)|yarn\s+(?:run\s+)?test|pnpm\s+(?:run\s+)?test|vitest|jest|mocha|pytest|py\.test|cargo\s+test|go\s+test|bundle\s+exec\s+rspec|rspec|phpunit|dotnet\s+test)\b/i;
3703
3761
  SUPERSEDE_WINDOW_MS = 6e4;
3704
3762
  GEMINI_FALLBACK_MODELS2 = ["gemini-2.5-flash", "gemini-2.0-flash"];
@@ -4874,20 +4932,20 @@ function aggregateAudit(entries, startMs, endMs = Date.now()) {
4874
4932
  for (const e of inWindow) {
4875
4933
  if (e.sessionId) sessionSet.add(e.sessionId);
4876
4934
  if (e.mcpServer) mcpSet.add(e.mcpServer);
4877
- const isAllow2 = e.decision === "allow" || e.decision === "observe-allow";
4935
+ const isAllow = e.decision === "allow" || e.decision === "observe-allow";
4878
4936
  const isReview = e.decision === "review";
4879
4937
  const isLoop = e.checkedBy === "loop-detected";
4880
4938
  const isDlp2 = !!(e.checkedBy && e.checkedBy.toLowerCase().includes("dlp"));
4881
- if (isAllow2) allow++;
4939
+ if (isAllow) allow++;
4882
4940
  else if (isReview) review++;
4883
4941
  else block++;
4884
4942
  if (isLoop) loops++;
4885
4943
  if (isDlp2) dlpHits++;
4886
4944
  const t = toolMap.get(e.tool) ?? { calls: 0, blocked: 0 };
4887
4945
  t.calls++;
4888
- if (!isAllow2 && !isReview) t.blocked++;
4946
+ if (!isAllow && !isReview) t.blocked++;
4889
4947
  toolMap.set(e.tool, t);
4890
- if (!isAllow2 && e.checkedBy) {
4948
+ if (!isAllow && e.checkedBy) {
4891
4949
  blockMap.set(e.checkedBy, (blockMap.get(e.checkedBy) ?? 0) + 1);
4892
4950
  }
4893
4951
  if (TEST_TOOLS.has(e.tool) && typeof e.args?.command === "string") {
@@ -4895,7 +4953,7 @@ function aggregateAudit(entries, startMs, endMs = Date.now()) {
4895
4953
  if (head && /^[a-zA-Z0-9._-]+$/.test(head)) {
4896
4954
  const s = shellMap.get(head) ?? { count: 0, blocked: 0 };
4897
4955
  s.count++;
4898
- if (!isAllow2 && !isReview) s.blocked++;
4956
+ if (!isAllow && !isReview) s.blocked++;
4899
4957
  shellMap.set(head, s);
4900
4958
  }
4901
4959
  }
package/dist/index.js CHANGED
@@ -325,6 +325,7 @@ var ConfigFileSchema = import_zod.z.object({
325
325
  // nudge-only (default), and the scan cadence in minutes.
326
326
  mcpAutoWrap: import_zod.z.boolean().optional(),
327
327
  mcpReconcileIntervalMinutes: import_zod.z.number().positive().optional(),
328
+ mcpStaleAfterDays: import_zod.z.number().min(0).optional(),
328
329
  cloudSyncIntervalHours: import_zod.z.number().positive().optional(),
329
330
  // Seconds-granular override for the cloud policy sync cadence. Wins over
330
331
  // cloudSyncIntervalHours when set. Lets you opt into fast apply (e.g. 20)
@@ -3335,6 +3336,7 @@ var redis_default = {
3335
3336
  name: "redis",
3336
3337
  description: "Protects Redis instances from destructive AI operations",
3337
3338
  aliases: [],
3339
+ _scopeNote: "Every rule requires Redis to actually be involved: a client in the command (redis-cli / valkey-cli, in either order \u2014 `redis-cli FLUSHALL` and `echo FLUSHALL | redis-cli` are both real), OR the command field holding the bare keyword, which is what the MCP redis_command tool sends ({ command: 'FLUSHALL', args: [] }). Without the client requirement these rules matched the WORD anywhere \u2014 including prose in a heredoc writing a file \u2014 which produced two observed false positives: a test fixture describing the shield, and a commit message naming the rule. Every other builtin shield already scopes this way (aws s3, docker system prune, kubectl delete, gh repo delete). See redis-scope.spec.ts.",
3338
3340
  smartRules: [
3339
3341
  {
3340
3342
  name: "shield:redis:block-flushall",
@@ -3343,7 +3345,7 @@ var redis_default = {
3343
3345
  {
3344
3346
  field: "command",
3345
3347
  op: "matches",
3346
- value: "\\bFLUSHALL\\b",
3348
+ value: "(redis|valkey)-cli.*\\bFLUSHALL\\b|\\bFLUSHALL\\b.*(redis|valkey)-cli|^ ?FLUSHALL\\b|\\.flushall\\s*\\(",
3347
3349
  flags: "i"
3348
3350
  }
3349
3351
  ],
@@ -3357,7 +3359,7 @@ var redis_default = {
3357
3359
  {
3358
3360
  field: "command",
3359
3361
  op: "matches",
3360
- value: "\\bFLUSHDB\\b",
3362
+ value: "(redis|valkey)-cli.*\\bFLUSHDB\\b|\\bFLUSHDB\\b.*(redis|valkey)-cli|^ ?FLUSHDB\\b|\\.flushdb\\s*\\(",
3361
3363
  flags: "i"
3362
3364
  }
3363
3365
  ],
@@ -3371,7 +3373,7 @@ var redis_default = {
3371
3373
  {
3372
3374
  field: "command",
3373
3375
  op: "matches",
3374
- value: "\\bCONFIG\\s+RESETSTAT\\b",
3376
+ value: "(redis|valkey)-cli.*CONFIG\\s+RESETSTAT|^ ?CONFIG\\s+RESETSTAT",
3375
3377
  flags: "i"
3376
3378
  }
3377
3379
  ],
@@ -3385,7 +3387,7 @@ var redis_default = {
3385
3387
  {
3386
3388
  field: "command",
3387
3389
  op: "matches",
3388
- value: "\\bCONFIG\\s+SET\\b",
3390
+ value: "(redis|valkey)-cli.*\\bCONFIG\\s+SET\\b|\\bCONFIG\\s+SET\\b.*(redis|valkey)-cli|^ ?CONFIG\\s+SET\\b",
3389
3391
  flags: "i"
3390
3392
  }
3391
3393
  ],
@@ -3399,7 +3401,7 @@ var redis_default = {
3399
3401
  {
3400
3402
  field: "command",
3401
3403
  op: "matches",
3402
- value: "\\bDEL\\b.*[*?\\[]|redis-cli.*--scan.*\\|.*xargs.*del",
3404
+ value: "(redis|valkey)-cli.*\\bDEL\\b.*[*?\\[]|^ ?DEL\\b.*[*?\\[]|-cli.*--scan.*xargs.*del",
3403
3405
  flags: "i"
3404
3406
  }
3405
3407
  ],
@@ -4068,6 +4070,61 @@ function getActiveEnvironment(config) {
4068
4070
  const env = config.settings.environment || process.env.NODE_ENV || "development";
4069
4071
  return config.environments[env] ?? null;
4070
4072
  }
4073
+ function readRulesCacheResilient(cacheFile) {
4074
+ let existed = false;
4075
+ let sawReadError = false;
4076
+ for (let attempt = 0; attempt < 3; attempt++) {
4077
+ let content;
4078
+ try {
4079
+ content = import_fs4.default.readFileSync(cacheFile, "utf-8");
4080
+ existed = true;
4081
+ } catch (err) {
4082
+ if (err.code === "ENOENT") return {};
4083
+ sawReadError = true;
4084
+ continue;
4085
+ }
4086
+ try {
4087
+ const parsed = JSON.parse(content);
4088
+ lastParsedRulesCache = parsed;
4089
+ return parsed;
4090
+ } catch {
4091
+ }
4092
+ }
4093
+ if (existed || sawReadError) {
4094
+ const backup = import_path4.default.join(import_path4.default.dirname(cacheFile), "rules-cache.last-good.json");
4095
+ if (backup !== cacheFile) {
4096
+ try {
4097
+ const raw = JSON.parse(import_fs4.default.readFileSync(backup, "utf-8"));
4098
+ logCacheReadIssue(cacheFile, "RULES_CACHE_CORRUPT_USED_BACKUP");
4099
+ lastParsedRulesCache = raw;
4100
+ return raw;
4101
+ } catch {
4102
+ }
4103
+ }
4104
+ if (lastParsedRulesCache) {
4105
+ logCacheReadIssue(cacheFile, "RULES_CACHE_USED_MEMORY");
4106
+ return lastParsedRulesCache;
4107
+ }
4108
+ logCacheReadIssue(cacheFile, "RULES_CACHE_UNREADABLE");
4109
+ }
4110
+ return {};
4111
+ }
4112
+ var lastParsedRulesCache = null;
4113
+ var CACHE_LOG_REARM_MS = 5 * 60 * 1e3;
4114
+ var cacheReadLastLoggedAt = 0;
4115
+ function logCacheReadIssue(cacheFile, kind) {
4116
+ const now = Date.now();
4117
+ if (now - cacheReadLastLoggedAt < CACHE_LOG_REARM_MS) return;
4118
+ cacheReadLastLoggedAt = now;
4119
+ try {
4120
+ import_fs4.default.appendFileSync(
4121
+ import_path4.default.join(import_os4.default.homedir(), ".node9", "hook-debug.log"),
4122
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] ${kind} ${cacheFile}
4123
+ `
4124
+ );
4125
+ } catch {
4126
+ }
4127
+ }
4071
4128
  function getConfig(cwd) {
4072
4129
  if (!cwd && cachedConfig) return cachedConfig;
4073
4130
  const globalPath = import_path4.default.join(import_os4.default.homedir(), ".node9", "config.json");
@@ -4135,6 +4192,7 @@ function getConfig(cwd) {
4135
4192
  if (s.mcpAutoWrap !== void 0) mergedSettings.mcpAutoWrap = s.mcpAutoWrap === true;
4136
4193
  if (s.mcpReconcileIntervalMinutes !== void 0)
4137
4194
  mergedSettings.mcpReconcileIntervalMinutes = s.mcpReconcileIntervalMinutes;
4195
+ if (s.mcpStaleAfterDays !== void 0) mergedSettings.mcpStaleAfterDays = s.mcpStaleAfterDays;
4138
4196
  if (s.hud !== void 0) mergedSettings.hud = { ...mergedSettings.hud, ...s.hud };
4139
4197
  if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
4140
4198
  if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
@@ -4144,12 +4202,13 @@ function getConfig(cwd) {
4144
4202
  if (p.smartRules) {
4145
4203
  const defaultBlocks = mergedPolicy.smartRules.filter((r) => r.verdict === "block");
4146
4204
  const defaultNonBlocks = mergedPolicy.smartRules.filter((r) => r.verdict !== "block");
4147
- const userRuleNames = new Set(p.smartRules.filter((r) => r.name).map((r) => r.name));
4205
+ const localRules = p.smartRules.map(({ pinned: _pinned, ...r }) => r);
4206
+ const userRuleNames = new Set(localRules.filter((r) => r.name).map((r) => r.name));
4148
4207
  const filteredBlocks = defaultBlocks.filter((r) => !r.name || !userRuleNames.has(r.name));
4149
4208
  const filteredNonBlocks = defaultNonBlocks.filter(
4150
4209
  (r) => !r.name || !userRuleNames.has(r.name)
4151
4210
  );
4152
- mergedPolicy.smartRules = [...filteredBlocks, ...p.smartRules, ...filteredNonBlocks];
4211
+ mergedPolicy.smartRules = [...filteredBlocks, ...localRules, ...filteredNonBlocks];
4153
4212
  }
4154
4213
  if (p.snapshot) {
4155
4214
  const s2 = p.snapshot;
@@ -4214,10 +4273,12 @@ function getConfig(cwd) {
4214
4273
  applyLayer(globalConfig);
4215
4274
  applyLayer(projectConfig);
4216
4275
  let cloudManagedShields = [];
4276
+ let modeCloudControlled = false;
4277
+ let modeCloudStaged = false;
4217
4278
  {
4218
4279
  const cacheFile = import_path4.default.join(import_os4.default.homedir(), ".node9", "rules-cache.json");
4219
4280
  try {
4220
- const raw = JSON.parse(import_fs4.default.readFileSync(cacheFile, "utf-8"));
4281
+ const raw = readRulesCacheResilient(cacheFile);
4221
4282
  if (Array.isArray(raw.rules) && raw.rules.length > 0) {
4222
4283
  applyLayer({ policy: { smartRules: raw.rules } });
4223
4284
  }
@@ -4234,6 +4295,9 @@ function getConfig(cwd) {
4234
4295
  locked.includes("mode")
4235
4296
  );
4236
4297
  }
4298
+ if (typeof mc.mode === "string" || locked.includes("mode")) {
4299
+ modeCloudControlled = true;
4300
+ }
4237
4301
  if (mc.egress && typeof mc.egress === "object") {
4238
4302
  const hosts = (v) => Array.isArray(v) ? v.filter((h) => typeof h === "string") : void 0;
4239
4303
  mergedPolicy.egress = applyManagedEgress(
@@ -4332,19 +4396,28 @@ function getConfig(cwd) {
4332
4396
  }
4333
4397
  if (raw.shadowMode === true) {
4334
4398
  mergedSettings.mode = "observe";
4399
+ modeCloudStaged = true;
4335
4400
  }
4336
4401
  } catch {
4337
4402
  }
4338
4403
  }
4339
4404
  const shieldOverrides = readShieldOverrides();
4340
4405
  const activeShieldNames = [.../* @__PURE__ */ new Set([...readActiveShields(), ...cloudManagedShields])];
4406
+ const cloudManagedSet = new Set(cloudManagedShields);
4341
4407
  for (const shieldName of activeShieldNames) {
4342
- const shield = getShield(shieldName);
4408
+ const isCloudMandated = cloudManagedSet.has(shieldName);
4409
+ const shield = isCloudMandated ? BUILTIN_SHIELDS[shieldName] : getShield(shieldName);
4343
4410
  if (!shield) continue;
4344
4411
  const existingRuleNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
4345
- const ruleOverrides = shieldOverrides[shieldName] ?? {};
4412
+ const ruleOverrides = isCloudMandated ? {} : shieldOverrides[shieldName] ?? {};
4346
4413
  for (const rule of shield.smartRules) {
4347
- if (!existingRuleNames.has(rule.name)) {
4414
+ const collides = rule.name ? existingRuleNames.has(rule.name) : false;
4415
+ if (isCloudMandated) {
4416
+ if (collides) {
4417
+ mergedPolicy.smartRules = mergedPolicy.smartRules.filter((r) => r.name !== rule.name);
4418
+ }
4419
+ mergedPolicy.smartRules.push({ ...rule, pinned: true });
4420
+ } else if (!collides) {
4348
4421
  const overrideVerdict = rule.name ? ruleOverrides[rule.name] : void 0;
4349
4422
  mergedPolicy.smartRules.push(
4350
4423
  overrideVerdict !== void 0 ? { ...rule, verdict: overrideVerdict } : rule
@@ -4360,7 +4433,27 @@ function getConfig(cwd) {
4360
4433
  for (const rule of ADVISORY_SMART_RULES) {
4361
4434
  if (!existingAdvisoryNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
4362
4435
  }
4363
- if (process.env.NODE9_MODE) mergedSettings.mode = process.env.NODE9_MODE;
4436
+ const envMode = process.env.NODE9_MODE;
4437
+ if (envMode && !modeCloudControlled && ["observe", "audit", "standard", "strict"].includes(envMode)) {
4438
+ mergedSettings.mode = envMode;
4439
+ }
4440
+ if (cloudManagedShields.length > 0 && !modeCloudControlled && !modeCloudStaged && (mergedSettings.mode === "observe" || mergedSettings.mode === "audit")) {
4441
+ mergedSettings.mode = "standard";
4442
+ }
4443
+ const managedFloorActive = cloudManagedShields.length > 0 || modeCloudControlled && mergedSettings.mode === "strict";
4444
+ if (modeCloudControlled && mergedSettings.mode === "strict") {
4445
+ for (const name of Object.keys(mergedEnvironments)) {
4446
+ if (mergedEnvironments[name]?.requireApproval === false) {
4447
+ const cleaned = { ...mergedEnvironments[name] };
4448
+ delete cleaned.requireApproval;
4449
+ mergedEnvironments[name] = cleaned;
4450
+ }
4451
+ }
4452
+ }
4453
+ if (managedFloorActive) {
4454
+ mergedPolicy.ignoredTools = [...DEFAULT_CONFIG.policy.ignoredTools];
4455
+ mergedPolicy.sandboxPaths = [...DEFAULT_CONFIG.policy.sandboxPaths];
4456
+ }
4364
4457
  mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
4365
4458
  mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
4366
4459
  mergedPolicy.ignoredTools = [...new Set(mergedPolicy.ignoredTools)];
@@ -4748,6 +4841,18 @@ function isDaemonRunning() {
4748
4841
  }
4749
4842
  return false;
4750
4843
  }
4844
+ async function daemonHasInteractiveApprover(timeoutMs = 400) {
4845
+ try {
4846
+ const res = await fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/approver`, {
4847
+ signal: AbortSignal.timeout(timeoutMs)
4848
+ });
4849
+ if (!res.ok) return false;
4850
+ const body = await res.json();
4851
+ return body.interactive === true;
4852
+ } catch {
4853
+ return false;
4854
+ }
4855
+ }
4751
4856
  async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched, socketActivitySent) {
4752
4857
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
4753
4858
  const ctrl = new AbortController();
@@ -5444,6 +5549,12 @@ function isNetworkTool(toolName, args) {
5444
5549
  function notifyActivity(data) {
5445
5550
  return notifyActivitySocket(data);
5446
5551
  }
5552
+ async function hasReachableHumanApprover(opts) {
5553
+ const hasDisplay = !!(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
5554
+ const nativeReachable = !opts.calledFromDaemon && opts.approvers.native !== false && hasDisplay;
5555
+ if (nativeReachable) return true;
5556
+ return opts.approvers.terminal !== false && await daemonHasInteractiveApprover();
5557
+ }
5447
5558
  async function authorizeHeadless(toolName, args, meta, options) {
5448
5559
  if (!options?.calledFromDaemon) {
5449
5560
  const actId = (0, import_crypto4.randomUUID)();
@@ -5517,6 +5628,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
5517
5628
  let riskMetadata;
5518
5629
  let statefulRecoveryCommand;
5519
5630
  let localSmartRuleMatched = false;
5631
+ let hardBlockDowngraded = false;
5520
5632
  let taintWarning = null;
5521
5633
  if (isNetworkTool(toolName, args)) {
5522
5634
  const filePaths = extractFilePaths(toolName, args);
@@ -5758,6 +5870,36 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
5758
5870
  return { approved: true, checkedBy: "local-policy" };
5759
5871
  }
5760
5872
  if (policyResult.decision === "block") {
5873
+ hardBlockDowngraded = true;
5874
+ const daemonUp = isDaemonRunning();
5875
+ let humanApproverReachable = false;
5876
+ if (!policyResult.dependsOnStatePredicates?.length && daemonUp && !isTestEnv2) {
5877
+ humanApproverReachable = await hasReachableHumanApprover({
5878
+ approvers,
5879
+ calledFromDaemon: options?.calledFromDaemon
5880
+ });
5881
+ }
5882
+ const mayDowngrade = daemonUp && !isTestEnv2 && humanApproverReachable;
5883
+ const hardBlock = () => {
5884
+ if (!isManual)
5885
+ appendLocalAudit(
5886
+ toolName,
5887
+ args,
5888
+ "deny",
5889
+ "smart-rule-block",
5890
+ { ...meta, ruleName: policyResult.ruleName },
5891
+ hashAuditArgs
5892
+ );
5893
+ return {
5894
+ approved: false,
5895
+ reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
5896
+ blockedBy: "local-config",
5897
+ blockedByLabel: policyResult.blockedByLabel,
5898
+ ruleHit: policyResult.ruleName,
5899
+ ...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand },
5900
+ ...policyResult.ruleDescription && { ruleDescription: policyResult.ruleDescription }
5901
+ };
5902
+ };
5761
5903
  if (policyResult.dependsOnStatePredicates?.length) {
5762
5904
  const stateResults = await checkStatePredicates(policyResult.dependsOnStatePredicates);
5763
5905
  const predicatesMet = stateResults !== null && policyResult.dependsOnStatePredicates.every((p) => stateResults[p] === true);
@@ -5774,7 +5916,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
5774
5916
  if (predicatesMet && policyResult.recoveryCommand) {
5775
5917
  statefulRecoveryCommand = policyResult.recoveryCommand;
5776
5918
  }
5777
- } else if (isDaemonRunning() && !isTestEnv2) {
5919
+ } else if (mayDowngrade) {
5778
5920
  if (!isManual)
5779
5921
  appendLocalAudit(
5780
5922
  toolName,
@@ -5796,36 +5938,14 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
5796
5938
  };
5797
5939
  }
5798
5940
  } else {
5799
- if (!isManual)
5800
- appendLocalAudit(
5801
- toolName,
5802
- args,
5803
- "deny",
5804
- "smart-rule-block",
5805
- // Include policyResult.ruleName so the [2] Report SHIELDS
5806
- // panel can attribute this block to its specific shield
5807
- // (e.g. `shield:project-jail:block-read-ssh`) via the
5808
- // rule→shield map. checkedBy stays as the generic
5809
- // `smart-rule-block` for backward compat with existing
5810
- // log readers.
5811
- { ...meta, ruleName: policyResult.ruleName },
5812
- hashAuditArgs
5813
- );
5814
- return {
5815
- approved: false,
5816
- reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
5817
- blockedBy: "local-config",
5818
- blockedByLabel: policyResult.blockedByLabel,
5819
- ruleHit: policyResult.ruleName,
5820
- ...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand },
5821
- ...policyResult.ruleDescription && { ruleDescription: policyResult.ruleDescription }
5822
- };
5941
+ return hardBlock();
5823
5942
  }
5824
5943
  }
5825
5944
  explainableLabel = policyResult.blockedByLabel || "Local Config";
5826
5945
  policyMatchedField = policyResult.matchedField;
5827
5946
  policyMatchedWord = policyResult.matchedWord;
5828
- if (policyResult.ruleName) localSmartRuleMatched = true;
5947
+ if (policyResult.ruleName || policyResult.tier === 7 || hardBlockDowngraded)
5948
+ localSmartRuleMatched = true;
5829
5949
  if (policyResult.ruleDescription) policyRuleDescription = policyResult.ruleDescription;
5830
5950
  else if (policyResult.reason) policyRuleDescription = policyResult.reason;
5831
5951
  riskMetadata = computeRiskMetadata(
@@ -5837,7 +5957,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
5837
5957
  policyResult.ruleName
5838
5958
  );
5839
5959
  if (policyRuleDescription) riskMetadata.ruleDescription = policyRuleDescription.slice(0, 200);
5840
- const persistent = policyResult.ruleName ? null : getPersistentDecision(toolName);
5960
+ const persistent = policyResult.ruleName || policyResult.tier === 7 || hardBlockDowngraded ? null : getPersistentDecision(toolName);
5841
5961
  if (persistent === "allow" && !appPermReview) {
5842
5962
  if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta, hashAuditArgs);
5843
5963
  return { approved: true, checkedBy: "persistent" };
@@ -5870,7 +5990,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
5870
5990
  return { approved: true };
5871
5991
  }
5872
5992
  }
5873
- if (!taintWarning && !appPermReview && getActiveTrustSession(toolName, args)) {
5993
+ if (!taintWarning && !appPermReview && !hardBlockDowngraded && getActiveTrustSession(toolName, args)) {
5874
5994
  if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta, hashAuditArgs);
5875
5995
  return { approved: true, checkedBy: "trust" };
5876
5996
  }
@@ -5934,7 +6054,7 @@ ${appPermReview}`
5934
6054
  forceReview
5935
6055
  );
5936
6056
  if (!initResult.pending) {
5937
- if (initResult.shadowMode && !appPermReview) {
6057
+ if (initResult.shadowMode && !localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview) {
5938
6058
  return { approved: true, checkedBy: "cloud" };
5939
6059
  }
5940
6060
  if (!localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview) {