@node9/proxy 1.63.0 → 1.64.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)
@@ -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
@@ -2967,6 +2970,50 @@ var init_scan_watermark = __esm({
2967
2970
  }
2968
2971
  });
2969
2972
 
2973
+ // src/audit/decision.ts
2974
+ function classifyDecision(a, b) {
2975
+ const isRow = !!a && typeof a === "object";
2976
+ const decision = isRow ? a.decision : a;
2977
+ const attribution = isRow ? a.checkedBy ?? a.source : b;
2978
+ const raw = typeof decision === "string" ? decision : String(decision ?? "");
2979
+ const src = typeof attribution === "string" ? attribution.toLowerCase() : "";
2980
+ const d = raw.toLowerCase();
2981
+ if (src && has(src, "observe-mode")) {
2982
+ return { outcome: "observe", label: "Would block", raw };
2983
+ }
2984
+ if (d === "dlp") return { outcome: "info", label: "Finding", raw };
2985
+ if (d === "mcp-discovered") return { outcome: "info", label: "Info", raw };
2986
+ if (d === "allow" || d === "allowed") {
2987
+ if (src === "post-hook") return { outcome: "allow", label: "Ran", raw };
2988
+ if (HUMAN_SOURCES.has(src)) {
2989
+ return { outcome: "allow", label: "Approved", raw };
2990
+ }
2991
+ return { outcome: "allow", label: "Auto-allowed", raw };
2992
+ }
2993
+ if (d === "deny" || d === "auto-deny" || d === "block") {
2994
+ if (TIMEOUT_SOURCES.has(src)) {
2995
+ return { outcome: "deny", label: "Timed out", raw };
2996
+ }
2997
+ if (HUMAN_SOURCES.has(src)) {
2998
+ return { outcome: "deny", label: "Denied", raw };
2999
+ }
3000
+ return { outcome: "deny", label: "Blocked", raw };
3001
+ }
3002
+ if (d === "review" || d === "pending") {
3003
+ return { outcome: "info", label: "Pending", raw };
3004
+ }
3005
+ return { outcome: "unknown", label: raw ? `? ${raw}` : "? (none)", raw };
3006
+ }
3007
+ var HUMAN_SOURCES, TIMEOUT_SOURCES, has;
3008
+ var init_decision = __esm({
3009
+ "src/audit/decision.ts"() {
3010
+ "use strict";
3011
+ HUMAN_SOURCES = /* @__PURE__ */ new Set(["daemon", "cloud", "local-decision", "inline-review-approved"]);
3012
+ TIMEOUT_SOURCES = /* @__PURE__ */ new Set(["timeout"]);
3013
+ has = (s, needle) => s.includes(needle);
3014
+ }
3015
+ });
3016
+
2970
3017
  // src/cli/aggregate/report-audit.ts
2971
3018
  import fs5 from "fs";
2972
3019
  import os6 from "os";
@@ -3062,8 +3109,15 @@ function parseAuditLog(logPath) {
3062
3109
  }
3063
3110
  });
3064
3111
  }
3065
- function isAllow(decision) {
3066
- return decision.startsWith("allow");
3112
+ function viewOf(e) {
3113
+ const outcome = classifyDecision(e).outcome;
3114
+ const observed = outcome === "observe";
3115
+ return {
3116
+ observed,
3117
+ ran: outcome === "allow" || observed,
3118
+ blocked: outcome === "deny" || outcome === "unknown",
3119
+ info: outcome === "info"
3120
+ };
3067
3121
  }
3068
3122
  function isDlp(checkedBy) {
3069
3123
  return !!checkedBy?.includes("dlp");
@@ -3520,12 +3574,12 @@ function aggregateReportFromAudit(period, opts = {}) {
3520
3574
  const priorStart = new Date(start.getTime() - periodMs);
3521
3575
  const priorEntries = allEntries.filter((e) => {
3522
3576
  if (e.source === "post-hook") return false;
3577
+ if (e.source === "response-dlp") return false;
3578
+ if (typeof e.decision !== "string") return false;
3523
3579
  const ts = new Date(e.ts);
3524
3580
  return ts >= priorStart && ts <= priorEnd;
3525
3581
  });
3526
- const priorBlocked = priorEntries.filter(
3527
- (e) => typeof e.decision === "string" && !isAllow(e.decision)
3528
- ).length;
3582
+ const priorBlocked = priorEntries.filter((e) => viewOf(e).blocked).length;
3529
3583
  const priorBlockRate = priorEntries.length > 0 ? priorBlocked / priorEntries.length : null;
3530
3584
  const excludeTests = opts.excludeTests === true;
3531
3585
  const testTs = excludeTests ? buildTestTimestamps(allEntries) : /* @__PURE__ */ new Set();
@@ -3568,15 +3622,17 @@ function aggregateReportFromAudit(period, opts = {}) {
3568
3622
  let dimInjectionBlocked = 0;
3569
3623
  for (const e of entries) {
3570
3624
  if (superseded.has(supersedeKey(e))) continue;
3571
- const allow = isAllow(e.decision);
3625
+ const view = viewOf(e);
3572
3626
  const dateKey = e.ts.slice(0, 10);
3573
3627
  const userInteracted = e.source === "daemon";
3574
- if (userInteracted) {
3575
- if (allow) userApproved++;
3628
+ if (view.observed) {
3629
+ if (e.checkedBy === "observe-mode-dlp-would-block") observeDlp++;
3630
+ } else if (view.info) {
3631
+ } else if (userInteracted) {
3632
+ if (view.ran) userApproved++;
3576
3633
  else userDenied++;
3577
- } else if (!allow) {
3634
+ } else if (view.blocked) {
3578
3635
  if (e.checkedBy === "timeout") timedOut++;
3579
- else if (e.checkedBy === "observe-mode-dlp-would-block") observeDlp++;
3580
3636
  else if (isDlp(e.checkedBy)) dlpBlocked++;
3581
3637
  else if (e.checkedBy === "local-decision") userDenied++;
3582
3638
  else if (e.checkedBy !== "loop-detected") hardBlocked++;
@@ -3588,7 +3644,7 @@ function aggregateReportFromAudit(period, opts = {}) {
3588
3644
  if (cb.includes("would-block") && (cb.includes("pii") || cb.includes("dlp"))) {
3589
3645
  dimDataObserved++;
3590
3646
  }
3591
- if (!allow && !userInteracted) {
3647
+ if (view.blocked && !userInteracted) {
3592
3648
  switch (dimensionOfBlock(cb, e.ruleName ?? "")) {
3593
3649
  case "network":
3594
3650
  dimNetworkBlocked++;
@@ -3606,15 +3662,15 @@ function aggregateReportFromAudit(period, opts = {}) {
3606
3662
  }
3607
3663
  const t = toolMap.get(e.tool) ?? { calls: 0, blocked: 0 };
3608
3664
  t.calls++;
3609
- if (!allow) t.blocked++;
3665
+ if (view.blocked) t.blocked++;
3610
3666
  toolMap.set(e.tool, t);
3611
- if (!allow) {
3667
+ if (view.blocked) {
3612
3668
  const key = e.checkedBy ?? (e.source === "daemon" ? "local-decision" : null);
3613
3669
  if (key) {
3614
3670
  blockMap.set(key, (blockMap.get(key) ?? 0) + 1);
3615
3671
  }
3616
3672
  }
3617
- if (!allow && e.ruleName) {
3673
+ if ((view.blocked || view.observed) && e.ruleName) {
3618
3674
  ruleMap.set(e.ruleName, (ruleMap.get(e.ruleName) ?? 0) + 1);
3619
3675
  }
3620
3676
  if (e.agent) agentMap.set(e.agent, (agentMap.get(e.agent) ?? 0) + 1);
@@ -3623,7 +3679,7 @@ function aggregateReportFromAudit(period, opts = {}) {
3623
3679
  hourMap.set(hour, (hourMap.get(hour) ?? 0) + 1);
3624
3680
  const d = dailyMap.get(dateKey) ?? { calls: 0, blocked: 0 };
3625
3681
  d.calls++;
3626
- if (!allow) d.blocked++;
3682
+ if (view.blocked) d.blocked++;
3627
3683
  dailyMap.set(dateKey, d);
3628
3684
  }
3629
3685
  for (const e of allEntries) {
@@ -3699,6 +3755,7 @@ var init_report_audit = __esm({
3699
3755
  init_costSync();
3700
3756
  init_litellm();
3701
3757
  init_cost_codex();
3758
+ init_decision();
3702
3759
  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
3760
  SUPERSEDE_WINDOW_MS = 6e4;
3704
3761
  GEMINI_FALLBACK_MODELS2 = ["gemini-2.5-flash", "gemini-2.0-flash"];
@@ -4874,20 +4931,20 @@ function aggregateAudit(entries, startMs, endMs = Date.now()) {
4874
4931
  for (const e of inWindow) {
4875
4932
  if (e.sessionId) sessionSet.add(e.sessionId);
4876
4933
  if (e.mcpServer) mcpSet.add(e.mcpServer);
4877
- const isAllow2 = e.decision === "allow" || e.decision === "observe-allow";
4934
+ const isAllow = e.decision === "allow" || e.decision === "observe-allow";
4878
4935
  const isReview = e.decision === "review";
4879
4936
  const isLoop = e.checkedBy === "loop-detected";
4880
4937
  const isDlp2 = !!(e.checkedBy && e.checkedBy.toLowerCase().includes("dlp"));
4881
- if (isAllow2) allow++;
4938
+ if (isAllow) allow++;
4882
4939
  else if (isReview) review++;
4883
4940
  else block++;
4884
4941
  if (isLoop) loops++;
4885
4942
  if (isDlp2) dlpHits++;
4886
4943
  const t = toolMap.get(e.tool) ?? { calls: 0, blocked: 0 };
4887
4944
  t.calls++;
4888
- if (!isAllow2 && !isReview) t.blocked++;
4945
+ if (!isAllow && !isReview) t.blocked++;
4889
4946
  toolMap.set(e.tool, t);
4890
- if (!isAllow2 && e.checkedBy) {
4947
+ if (!isAllow && e.checkedBy) {
4891
4948
  blockMap.set(e.checkedBy, (blockMap.get(e.checkedBy) ?? 0) + 1);
4892
4949
  }
4893
4950
  if (TEST_TOOLS.has(e.tool) && typeof e.args?.command === "string") {
@@ -4895,7 +4952,7 @@ function aggregateAudit(entries, startMs, endMs = Date.now()) {
4895
4952
  if (head && /^[a-zA-Z0-9._-]+$/.test(head)) {
4896
4953
  const s = shellMap.get(head) ?? { count: 0, blocked: 0 };
4897
4954
  s.count++;
4898
- if (!isAllow2 && !isReview) s.blocked++;
4955
+ if (!isAllow && !isReview) s.blocked++;
4899
4956
  shellMap.set(head, s);
4900
4957
  }
4901
4958
  }
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,49 @@ 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
+ var cacheReadFailureLogged = false;
4074
+ function readRulesCacheResilient(cacheFile) {
4075
+ let existed = 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
+ continue;
4084
+ }
4085
+ try {
4086
+ return JSON.parse(content);
4087
+ } catch {
4088
+ }
4089
+ }
4090
+ if (existed) {
4091
+ const backup = import_path4.default.join(import_path4.default.dirname(cacheFile), "rules-cache.last-good.json");
4092
+ if (backup !== cacheFile) {
4093
+ try {
4094
+ const raw = JSON.parse(import_fs4.default.readFileSync(backup, "utf-8"));
4095
+ logCacheReadIssue(cacheFile, "RULES_CACHE_CORRUPT_USED_BACKUP");
4096
+ return raw;
4097
+ } catch {
4098
+ }
4099
+ }
4100
+ logCacheReadIssue(cacheFile, "RULES_CACHE_UNREADABLE");
4101
+ }
4102
+ return {};
4103
+ }
4104
+ function logCacheReadIssue(cacheFile, kind) {
4105
+ if (cacheReadFailureLogged) return;
4106
+ cacheReadFailureLogged = true;
4107
+ try {
4108
+ import_fs4.default.appendFileSync(
4109
+ import_path4.default.join(import_os4.default.homedir(), ".node9", "hook-debug.log"),
4110
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] ${kind} ${cacheFile}
4111
+ `
4112
+ );
4113
+ } catch {
4114
+ }
4115
+ }
4071
4116
  function getConfig(cwd) {
4072
4117
  if (!cwd && cachedConfig) return cachedConfig;
4073
4118
  const globalPath = import_path4.default.join(import_os4.default.homedir(), ".node9", "config.json");
@@ -4135,6 +4180,7 @@ function getConfig(cwd) {
4135
4180
  if (s.mcpAutoWrap !== void 0) mergedSettings.mcpAutoWrap = s.mcpAutoWrap === true;
4136
4181
  if (s.mcpReconcileIntervalMinutes !== void 0)
4137
4182
  mergedSettings.mcpReconcileIntervalMinutes = s.mcpReconcileIntervalMinutes;
4183
+ if (s.mcpStaleAfterDays !== void 0) mergedSettings.mcpStaleAfterDays = s.mcpStaleAfterDays;
4138
4184
  if (s.hud !== void 0) mergedSettings.hud = { ...mergedSettings.hud, ...s.hud };
4139
4185
  if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
4140
4186
  if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
@@ -4144,12 +4190,13 @@ function getConfig(cwd) {
4144
4190
  if (p.smartRules) {
4145
4191
  const defaultBlocks = mergedPolicy.smartRules.filter((r) => r.verdict === "block");
4146
4192
  const defaultNonBlocks = mergedPolicy.smartRules.filter((r) => r.verdict !== "block");
4147
- const userRuleNames = new Set(p.smartRules.filter((r) => r.name).map((r) => r.name));
4193
+ const localRules = p.smartRules.map(({ pinned: _pinned, ...r }) => r);
4194
+ const userRuleNames = new Set(localRules.filter((r) => r.name).map((r) => r.name));
4148
4195
  const filteredBlocks = defaultBlocks.filter((r) => !r.name || !userRuleNames.has(r.name));
4149
4196
  const filteredNonBlocks = defaultNonBlocks.filter(
4150
4197
  (r) => !r.name || !userRuleNames.has(r.name)
4151
4198
  );
4152
- mergedPolicy.smartRules = [...filteredBlocks, ...p.smartRules, ...filteredNonBlocks];
4199
+ mergedPolicy.smartRules = [...filteredBlocks, ...localRules, ...filteredNonBlocks];
4153
4200
  }
4154
4201
  if (p.snapshot) {
4155
4202
  const s2 = p.snapshot;
@@ -4214,10 +4261,12 @@ function getConfig(cwd) {
4214
4261
  applyLayer(globalConfig);
4215
4262
  applyLayer(projectConfig);
4216
4263
  let cloudManagedShields = [];
4264
+ let modeCloudControlled = false;
4265
+ let modeCloudStaged = false;
4217
4266
  {
4218
4267
  const cacheFile = import_path4.default.join(import_os4.default.homedir(), ".node9", "rules-cache.json");
4219
4268
  try {
4220
- const raw = JSON.parse(import_fs4.default.readFileSync(cacheFile, "utf-8"));
4269
+ const raw = readRulesCacheResilient(cacheFile);
4221
4270
  if (Array.isArray(raw.rules) && raw.rules.length > 0) {
4222
4271
  applyLayer({ policy: { smartRules: raw.rules } });
4223
4272
  }
@@ -4234,6 +4283,9 @@ function getConfig(cwd) {
4234
4283
  locked.includes("mode")
4235
4284
  );
4236
4285
  }
4286
+ if (typeof mc.mode === "string" || locked.includes("mode")) {
4287
+ modeCloudControlled = true;
4288
+ }
4237
4289
  if (mc.egress && typeof mc.egress === "object") {
4238
4290
  const hosts = (v) => Array.isArray(v) ? v.filter((h) => typeof h === "string") : void 0;
4239
4291
  mergedPolicy.egress = applyManagedEgress(
@@ -4332,19 +4384,28 @@ function getConfig(cwd) {
4332
4384
  }
4333
4385
  if (raw.shadowMode === true) {
4334
4386
  mergedSettings.mode = "observe";
4387
+ modeCloudStaged = true;
4335
4388
  }
4336
4389
  } catch {
4337
4390
  }
4338
4391
  }
4339
4392
  const shieldOverrides = readShieldOverrides();
4340
4393
  const activeShieldNames = [.../* @__PURE__ */ new Set([...readActiveShields(), ...cloudManagedShields])];
4394
+ const cloudManagedSet = new Set(cloudManagedShields);
4341
4395
  for (const shieldName of activeShieldNames) {
4342
- const shield = getShield(shieldName);
4396
+ const isCloudMandated = cloudManagedSet.has(shieldName);
4397
+ const shield = isCloudMandated ? BUILTIN_SHIELDS[shieldName] : getShield(shieldName);
4343
4398
  if (!shield) continue;
4344
4399
  const existingRuleNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
4345
- const ruleOverrides = shieldOverrides[shieldName] ?? {};
4400
+ const ruleOverrides = isCloudMandated ? {} : shieldOverrides[shieldName] ?? {};
4346
4401
  for (const rule of shield.smartRules) {
4347
- if (!existingRuleNames.has(rule.name)) {
4402
+ const collides = rule.name ? existingRuleNames.has(rule.name) : false;
4403
+ if (isCloudMandated) {
4404
+ if (collides) {
4405
+ mergedPolicy.smartRules = mergedPolicy.smartRules.filter((r) => r.name !== rule.name);
4406
+ }
4407
+ mergedPolicy.smartRules.push({ ...rule, pinned: true });
4408
+ } else if (!collides) {
4348
4409
  const overrideVerdict = rule.name ? ruleOverrides[rule.name] : void 0;
4349
4410
  mergedPolicy.smartRules.push(
4350
4411
  overrideVerdict !== void 0 ? { ...rule, verdict: overrideVerdict } : rule
@@ -4360,7 +4421,27 @@ function getConfig(cwd) {
4360
4421
  for (const rule of ADVISORY_SMART_RULES) {
4361
4422
  if (!existingAdvisoryNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
4362
4423
  }
4363
- if (process.env.NODE9_MODE) mergedSettings.mode = process.env.NODE9_MODE;
4424
+ const envMode = process.env.NODE9_MODE;
4425
+ if (envMode && !modeCloudControlled && ["observe", "audit", "standard", "strict"].includes(envMode)) {
4426
+ mergedSettings.mode = envMode;
4427
+ }
4428
+ if (cloudManagedShields.length > 0 && !modeCloudControlled && !modeCloudStaged && (mergedSettings.mode === "observe" || mergedSettings.mode === "audit")) {
4429
+ mergedSettings.mode = "standard";
4430
+ }
4431
+ const managedFloorActive = cloudManagedShields.length > 0 || modeCloudControlled && mergedSettings.mode === "strict";
4432
+ if (modeCloudControlled && mergedSettings.mode === "strict") {
4433
+ for (const name of Object.keys(mergedEnvironments)) {
4434
+ if (mergedEnvironments[name]?.requireApproval === false) {
4435
+ const cleaned = { ...mergedEnvironments[name] };
4436
+ delete cleaned.requireApproval;
4437
+ mergedEnvironments[name] = cleaned;
4438
+ }
4439
+ }
4440
+ }
4441
+ if (managedFloorActive) {
4442
+ mergedPolicy.ignoredTools = [...DEFAULT_CONFIG.policy.ignoredTools];
4443
+ mergedPolicy.sandboxPaths = [...DEFAULT_CONFIG.policy.sandboxPaths];
4444
+ }
4364
4445
  mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
4365
4446
  mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
4366
4447
  mergedPolicy.ignoredTools = [...new Set(mergedPolicy.ignoredTools)];
@@ -4748,6 +4829,18 @@ function isDaemonRunning() {
4748
4829
  }
4749
4830
  return false;
4750
4831
  }
4832
+ async function daemonHasInteractiveApprover(timeoutMs = 400) {
4833
+ try {
4834
+ const res = await fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/approver`, {
4835
+ signal: AbortSignal.timeout(timeoutMs)
4836
+ });
4837
+ if (!res.ok) return false;
4838
+ const body = await res.json();
4839
+ return body.interactive === true;
4840
+ } catch {
4841
+ return false;
4842
+ }
4843
+ }
4751
4844
  async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched, socketActivitySent) {
4752
4845
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
4753
4846
  const ctrl = new AbortController();
@@ -5444,6 +5537,12 @@ function isNetworkTool(toolName, args) {
5444
5537
  function notifyActivity(data) {
5445
5538
  return notifyActivitySocket(data);
5446
5539
  }
5540
+ async function hasReachableHumanApprover(opts) {
5541
+ const hasDisplay = !!(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
5542
+ const nativeReachable = !opts.calledFromDaemon && opts.approvers.native !== false && hasDisplay;
5543
+ if (nativeReachable) return true;
5544
+ return opts.approvers.terminal !== false && await daemonHasInteractiveApprover();
5545
+ }
5447
5546
  async function authorizeHeadless(toolName, args, meta, options) {
5448
5547
  if (!options?.calledFromDaemon) {
5449
5548
  const actId = (0, import_crypto4.randomUUID)();
@@ -5758,6 +5857,35 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
5758
5857
  return { approved: true, checkedBy: "local-policy" };
5759
5858
  }
5760
5859
  if (policyResult.decision === "block") {
5860
+ const daemonUp = isDaemonRunning();
5861
+ let humanApproverReachable = false;
5862
+ if (!policyResult.dependsOnStatePredicates?.length && daemonUp && !isTestEnv2) {
5863
+ humanApproverReachable = await hasReachableHumanApprover({
5864
+ approvers,
5865
+ calledFromDaemon: options?.calledFromDaemon
5866
+ });
5867
+ }
5868
+ const mayDowngrade = daemonUp && !isTestEnv2 && humanApproverReachable;
5869
+ const hardBlock = () => {
5870
+ if (!isManual)
5871
+ appendLocalAudit(
5872
+ toolName,
5873
+ args,
5874
+ "deny",
5875
+ "smart-rule-block",
5876
+ { ...meta, ruleName: policyResult.ruleName },
5877
+ hashAuditArgs
5878
+ );
5879
+ return {
5880
+ approved: false,
5881
+ reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
5882
+ blockedBy: "local-config",
5883
+ blockedByLabel: policyResult.blockedByLabel,
5884
+ ruleHit: policyResult.ruleName,
5885
+ ...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand },
5886
+ ...policyResult.ruleDescription && { ruleDescription: policyResult.ruleDescription }
5887
+ };
5888
+ };
5761
5889
  if (policyResult.dependsOnStatePredicates?.length) {
5762
5890
  const stateResults = await checkStatePredicates(policyResult.dependsOnStatePredicates);
5763
5891
  const predicatesMet = stateResults !== null && policyResult.dependsOnStatePredicates.every((p) => stateResults[p] === true);
@@ -5774,7 +5902,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
5774
5902
  if (predicatesMet && policyResult.recoveryCommand) {
5775
5903
  statefulRecoveryCommand = policyResult.recoveryCommand;
5776
5904
  }
5777
- } else if (isDaemonRunning() && !isTestEnv2) {
5905
+ } else if (mayDowngrade) {
5778
5906
  if (!isManual)
5779
5907
  appendLocalAudit(
5780
5908
  toolName,
@@ -5796,36 +5924,13 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
5796
5924
  };
5797
5925
  }
5798
5926
  } 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
- };
5927
+ return hardBlock();
5823
5928
  }
5824
5929
  }
5825
5930
  explainableLabel = policyResult.blockedByLabel || "Local Config";
5826
5931
  policyMatchedField = policyResult.matchedField;
5827
5932
  policyMatchedWord = policyResult.matchedWord;
5828
- if (policyResult.ruleName) localSmartRuleMatched = true;
5933
+ if (policyResult.ruleName || policyResult.tier === 7) localSmartRuleMatched = true;
5829
5934
  if (policyResult.ruleDescription) policyRuleDescription = policyResult.ruleDescription;
5830
5935
  else if (policyResult.reason) policyRuleDescription = policyResult.reason;
5831
5936
  riskMetadata = computeRiskMetadata(
@@ -5837,7 +5942,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
5837
5942
  policyResult.ruleName
5838
5943
  );
5839
5944
  if (policyRuleDescription) riskMetadata.ruleDescription = policyRuleDescription.slice(0, 200);
5840
- const persistent = policyResult.ruleName ? null : getPersistentDecision(toolName);
5945
+ const persistent = policyResult.ruleName || policyResult.tier === 7 ? null : getPersistentDecision(toolName);
5841
5946
  if (persistent === "allow" && !appPermReview) {
5842
5947
  if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta, hashAuditArgs);
5843
5948
  return { approved: true, checkedBy: "persistent" };