@node9/proxy 1.67.5 → 2.1.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.
@@ -2596,19 +2596,6 @@ var init_config = __esm({
2596
2596
  settings: {
2597
2597
  mode: "standard",
2598
2598
  autoStartDaemon: true,
2599
- // The undo feature was REMOVED. This default is one of two places that say
2600
- // so; the other is the pin further down, which overwrites whatever a config
2601
- // file, the dashboard or the cloud supplies. Setting `enableUndo` anywhere
2602
- // has no effect — do not restore the advice that used to live here, because
2603
- // a comment recommending a knob that is silently discarded is the same lie
2604
- // on disk the pin exists to prevent.
2605
- //
2606
- // Why it went: the store was a per-project bare git repo with no size
2607
- // ceiling, and eviction dropped the index row without deleting the objects.
2608
- // On one machine it reached 378G (352G of it orphaned tmp_pack_* from an
2609
- // interrupted `git gc`) and filled the disk. A security tool must not be
2610
- // what fills a customer's disk. Leftovers: `node9 undo --purge`.
2611
- enableUndo: false,
2612
2599
  enableHookLogDebug: true,
2613
2600
  approvalTimeoutMs: 12e4,
2614
2601
  // 120-second auto-deny timeout
@@ -2649,25 +2636,6 @@ var init_config = __esm({
2649
2636
  "terminal.execute": "command",
2650
2637
  "postgres:query": "sql"
2651
2638
  },
2652
- snapshot: {
2653
- tools: [
2654
- "str_replace_based_edit_tool",
2655
- "write_file",
2656
- "edit_file",
2657
- "create_file",
2658
- "edit",
2659
- "replace",
2660
- // Claude / canonicalised Hermes — shouldSnapshot lowercases the
2661
- // incoming name before set-membership, so we list the lowercase
2662
- // forms of `Bash`/`Write`/`Edit`/`MultiEdit`. Without these,
2663
- // post-canonicalisation Hermes `patch` / `write_file` (which now
2664
- // arrive as `Edit` / `Write`) silently skipped snapshotting.
2665
- "write",
2666
- "multiedit"
2667
- ],
2668
- onlyPaths: [],
2669
- ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
2670
- },
2671
2639
  smartRules: [
2672
2640
  // ── rm safety (critical — always evaluated first) ──────────────────────
2673
2641
  {
@@ -3919,7 +3887,6 @@ var init_provenance = __esm({
3919
3887
  });
3920
3888
 
3921
3889
  // src/policy/index.ts
3922
- import pm2 from "picomatch";
3923
3890
  var init_policy = __esm({
3924
3891
  "src/policy/index.ts"() {
3925
3892
  "use strict";
@@ -5442,17 +5409,6 @@ function subscribeToSse(onEvent, onResolve, onForensic, onError) {
5442
5409
  function toActivityEvent(eventName, data) {
5443
5410
  const payload = data.activity ?? data;
5444
5411
  const ts = normalizeTs(payload.ts);
5445
- if (eventName === "snapshot") {
5446
- const rawSummary = payload.argsSummary ?? payload.tool ?? "snapshot";
5447
- return {
5448
- kind: "snapshot",
5449
- id: payload.id ?? `${ts}-snapshot`,
5450
- ts,
5451
- hash: payload.hash ?? "",
5452
- summary: compactPath(rawSummary),
5453
- fileCount: typeof payload.fileCount === "number" ? payload.fileCount : 0
5454
- };
5455
- }
5456
5412
  if (eventName !== "activity" && eventName !== "add") return null;
5457
5413
  const toolName = payload.tool ?? payload.toolName;
5458
5414
  if (!toolName) return null;
@@ -5843,9 +5799,6 @@ function applyFilter(events, filter) {
5843
5799
  if (!filter) return events;
5844
5800
  const needle = filter.toLowerCase();
5845
5801
  return events.filter((e) => {
5846
- if (e.kind === "snapshot") {
5847
- return e.hash.toLowerCase().includes(needle) || e.summary.toLowerCase().includes(needle);
5848
- }
5849
5802
  if (e.tool.toLowerCase().includes(needle)) return true;
5850
5803
  if (e.agent && e.agent.toLowerCase().includes(needle)) return true;
5851
5804
  if (e.preview.toLowerCase().includes(needle)) return true;
@@ -5856,19 +5809,6 @@ function applyFilter(events, filter) {
5856
5809
  }
5857
5810
  function ActivityRow({ event }) {
5858
5811
  const t = localTimeOf(event.ts);
5859
- if (event.kind === "snapshot") {
5860
- const filesSuffix = event.fileCount > 0 ? ` \xB7 ${event.fileCount} file${event.fileCount === 1 ? "" : "s"}` : "";
5861
- return /* @__PURE__ */ jsxs(Text, { wrap: "truncate-end", children: [
5862
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
5863
- t,
5864
- " "
5865
- ] }),
5866
- /* @__PURE__ */ jsx(Text, { color: COL.agentClaude, children: "\u{1F4F8} snapshot" }),
5867
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` ${event.hash} ` }),
5868
- /* @__PURE__ */ jsx(Text, { children: event.summary }),
5869
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: filesSuffix })
5870
- ] });
5871
- }
5872
5812
  const agentLabel = event.agent ? `[${truncate(capitalize(event.agent), 8)}]`.padEnd(10) : " ".repeat(10);
5873
5813
  const isLoop = event.checkedBy === "loop-detected";
5874
5814
  const verdictIcon = isLoop ? "\u{1F501}" : event.verdict === "block" ? "\u{1F6D1}" : event.verdict === "review" ? "\u{1F7E1}" : event.verdict === "allow" ? "\u2713 " : "\u2026 ";
package/dist/index.js CHANGED
@@ -4180,19 +4180,6 @@ var DEFAULT_CONFIG = {
4180
4180
  settings: {
4181
4181
  mode: "standard",
4182
4182
  autoStartDaemon: true,
4183
- // The undo feature was REMOVED. This default is one of two places that say
4184
- // so; the other is the pin further down, which overwrites whatever a config
4185
- // file, the dashboard or the cloud supplies. Setting `enableUndo` anywhere
4186
- // has no effect — do not restore the advice that used to live here, because
4187
- // a comment recommending a knob that is silently discarded is the same lie
4188
- // on disk the pin exists to prevent.
4189
- //
4190
- // Why it went: the store was a per-project bare git repo with no size
4191
- // ceiling, and eviction dropped the index row without deleting the objects.
4192
- // On one machine it reached 378G (352G of it orphaned tmp_pack_* from an
4193
- // interrupted `git gc`) and filled the disk. A security tool must not be
4194
- // what fills a customer's disk. Leftovers: `node9 undo --purge`.
4195
- enableUndo: false,
4196
4183
  enableHookLogDebug: true,
4197
4184
  approvalTimeoutMs: 12e4,
4198
4185
  // 120-second auto-deny timeout
@@ -4233,25 +4220,6 @@ var DEFAULT_CONFIG = {
4233
4220
  "terminal.execute": "command",
4234
4221
  "postgres:query": "sql"
4235
4222
  },
4236
- snapshot: {
4237
- tools: [
4238
- "str_replace_based_edit_tool",
4239
- "write_file",
4240
- "edit_file",
4241
- "create_file",
4242
- "edit",
4243
- "replace",
4244
- // Claude / canonicalised Hermes — shouldSnapshot lowercases the
4245
- // incoming name before set-membership, so we list the lowercase
4246
- // forms of `Bash`/`Write`/`Edit`/`MultiEdit`. Without these,
4247
- // post-canonicalisation Hermes `patch` / `write_file` (which now
4248
- // arrive as `Edit` / `Write`) silently skipped snapshotting.
4249
- "write",
4250
- "multiedit"
4251
- ],
4252
- onlyPaths: [],
4253
- ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
4254
- },
4255
4223
  smartRules: [
4256
4224
  // ── rm safety (critical — always evaluated first) ──────────────────────
4257
4225
  {
@@ -4562,11 +4530,6 @@ function getConfig(cwd) {
4562
4530
  ignoredTools: [...DEFAULT_CONFIG.policy.ignoredTools],
4563
4531
  toolInspection: { ...DEFAULT_CONFIG.policy.toolInspection },
4564
4532
  smartRules: [...DEFAULT_CONFIG.policy.smartRules],
4565
- snapshot: {
4566
- tools: [...DEFAULT_CONFIG.policy.snapshot.tools],
4567
- onlyPaths: [...DEFAULT_CONFIG.policy.snapshot.onlyPaths],
4568
- ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
4569
- },
4570
4533
  dlp: { ...DEFAULT_CONFIG.policy.dlp },
4571
4534
  egress: {
4572
4535
  ...DEFAULT_CONFIG.policy.egress,
@@ -4634,12 +4597,6 @@ function getConfig(cwd) {
4634
4597
  );
4635
4598
  mergedPolicy.smartRules = [...filteredBlocks, ...localRules, ...filteredNonBlocks];
4636
4599
  }
4637
- if (p.snapshot) {
4638
- const s2 = p.snapshot;
4639
- if (s2.tools) mergedPolicy.snapshot.tools.push(...s2.tools);
4640
- if (s2.onlyPaths) mergedPolicy.snapshot.onlyPaths.push(...s2.onlyPaths);
4641
- if (s2.ignorePaths) mergedPolicy.snapshot.ignorePaths.push(...s2.ignorePaths);
4642
- }
4643
4600
  if (p.dlp) {
4644
4601
  const d = p.dlp;
4645
4602
  if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
@@ -4890,11 +4847,13 @@ function getConfig(cwd) {
4890
4847
  }
4891
4848
  const shieldOverrides = readShieldOverrides();
4892
4849
  const activeShieldNames = [.../* @__PURE__ */ new Set([...readActiveShields(), ...cloudManagedShields])];
4850
+ const appliedShields = [];
4893
4851
  const cloudManagedSet = new Set(cloudManagedShields);
4894
4852
  for (const shieldName of activeShieldNames) {
4895
4853
  const isCloudMandated = cloudManagedSet.has(shieldName);
4896
4854
  const shield = isCloudMandated ? BUILTIN_SHIELDS[shieldName] : getShield(shieldName);
4897
4855
  if (!shield) continue;
4856
+ appliedShields.push(shieldName);
4898
4857
  const existingRuleNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
4899
4858
  const ruleOverrides = isCloudMandated ? {} : shieldOverrides[shieldName] ?? {};
4900
4859
  for (const rule of shield.smartRules) {
@@ -4916,6 +4875,7 @@ function getConfig(cwd) {
4916
4875
  if (!existingWords.has(word)) mergedPolicy.dangerousWords.push(word);
4917
4876
  }
4918
4877
  }
4878
+ mergedPolicy.appliedShields = appliedShields.sort();
4919
4879
  const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
4920
4880
  const cc = mergedPolicy.commandChecks ?? {};
4921
4881
  const advisoryKnobKey = (name) => {
@@ -4974,10 +4934,6 @@ function getConfig(cwd) {
4974
4934
  mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
4975
4935
  mergedPolicy.ignoredTools = [...new Set(mergedPolicy.ignoredTools)];
4976
4936
  mergedPolicy.skillPinning.roots = [...new Set(mergedPolicy.skillPinning.roots)];
4977
- mergedPolicy.snapshot.tools = [...new Set(mergedPolicy.snapshot.tools)];
4978
- mergedPolicy.snapshot.onlyPaths = [...new Set(mergedPolicy.snapshot.onlyPaths)];
4979
- mergedPolicy.snapshot.ignorePaths = [...new Set(mergedPolicy.snapshot.ignorePaths)];
4980
- mergedSettings.enableUndo = false;
4981
4937
  const result = {
4982
4938
  settings: mergedSettings,
4983
4939
  policy: mergedPolicy,
@@ -5040,9 +4996,6 @@ ${error.replace("Invalid config:\n", "")}
5040
4996
  return sanitized;
5041
4997
  }
5042
4998
 
5043
- // src/policy/index.ts
5044
- var import_picomatch2 = __toESM(require("picomatch"));
5045
-
5046
4999
  // src/dlp.ts
5047
5000
  var import_fs5 = __toESM(require("fs"));
5048
5001
  var import_path5 = __toESM(require("path"));
package/dist/index.mjs CHANGED
@@ -4150,19 +4150,6 @@ var DEFAULT_CONFIG = {
4150
4150
  settings: {
4151
4151
  mode: "standard",
4152
4152
  autoStartDaemon: true,
4153
- // The undo feature was REMOVED. This default is one of two places that say
4154
- // so; the other is the pin further down, which overwrites whatever a config
4155
- // file, the dashboard or the cloud supplies. Setting `enableUndo` anywhere
4156
- // has no effect — do not restore the advice that used to live here, because
4157
- // a comment recommending a knob that is silently discarded is the same lie
4158
- // on disk the pin exists to prevent.
4159
- //
4160
- // Why it went: the store was a per-project bare git repo with no size
4161
- // ceiling, and eviction dropped the index row without deleting the objects.
4162
- // On one machine it reached 378G (352G of it orphaned tmp_pack_* from an
4163
- // interrupted `git gc`) and filled the disk. A security tool must not be
4164
- // what fills a customer's disk. Leftovers: `node9 undo --purge`.
4165
- enableUndo: false,
4166
4153
  enableHookLogDebug: true,
4167
4154
  approvalTimeoutMs: 12e4,
4168
4155
  // 120-second auto-deny timeout
@@ -4203,25 +4190,6 @@ var DEFAULT_CONFIG = {
4203
4190
  "terminal.execute": "command",
4204
4191
  "postgres:query": "sql"
4205
4192
  },
4206
- snapshot: {
4207
- tools: [
4208
- "str_replace_based_edit_tool",
4209
- "write_file",
4210
- "edit_file",
4211
- "create_file",
4212
- "edit",
4213
- "replace",
4214
- // Claude / canonicalised Hermes — shouldSnapshot lowercases the
4215
- // incoming name before set-membership, so we list the lowercase
4216
- // forms of `Bash`/`Write`/`Edit`/`MultiEdit`. Without these,
4217
- // post-canonicalisation Hermes `patch` / `write_file` (which now
4218
- // arrive as `Edit` / `Write`) silently skipped snapshotting.
4219
- "write",
4220
- "multiedit"
4221
- ],
4222
- onlyPaths: [],
4223
- ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
4224
- },
4225
4193
  smartRules: [
4226
4194
  // ── rm safety (critical — always evaluated first) ──────────────────────
4227
4195
  {
@@ -4532,11 +4500,6 @@ function getConfig(cwd) {
4532
4500
  ignoredTools: [...DEFAULT_CONFIG.policy.ignoredTools],
4533
4501
  toolInspection: { ...DEFAULT_CONFIG.policy.toolInspection },
4534
4502
  smartRules: [...DEFAULT_CONFIG.policy.smartRules],
4535
- snapshot: {
4536
- tools: [...DEFAULT_CONFIG.policy.snapshot.tools],
4537
- onlyPaths: [...DEFAULT_CONFIG.policy.snapshot.onlyPaths],
4538
- ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
4539
- },
4540
4503
  dlp: { ...DEFAULT_CONFIG.policy.dlp },
4541
4504
  egress: {
4542
4505
  ...DEFAULT_CONFIG.policy.egress,
@@ -4604,12 +4567,6 @@ function getConfig(cwd) {
4604
4567
  );
4605
4568
  mergedPolicy.smartRules = [...filteredBlocks, ...localRules, ...filteredNonBlocks];
4606
4569
  }
4607
- if (p.snapshot) {
4608
- const s2 = p.snapshot;
4609
- if (s2.tools) mergedPolicy.snapshot.tools.push(...s2.tools);
4610
- if (s2.onlyPaths) mergedPolicy.snapshot.onlyPaths.push(...s2.onlyPaths);
4611
- if (s2.ignorePaths) mergedPolicy.snapshot.ignorePaths.push(...s2.ignorePaths);
4612
- }
4613
4570
  if (p.dlp) {
4614
4571
  const d = p.dlp;
4615
4572
  if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
@@ -4860,11 +4817,13 @@ function getConfig(cwd) {
4860
4817
  }
4861
4818
  const shieldOverrides = readShieldOverrides();
4862
4819
  const activeShieldNames = [.../* @__PURE__ */ new Set([...readActiveShields(), ...cloudManagedShields])];
4820
+ const appliedShields = [];
4863
4821
  const cloudManagedSet = new Set(cloudManagedShields);
4864
4822
  for (const shieldName of activeShieldNames) {
4865
4823
  const isCloudMandated = cloudManagedSet.has(shieldName);
4866
4824
  const shield = isCloudMandated ? BUILTIN_SHIELDS[shieldName] : getShield(shieldName);
4867
4825
  if (!shield) continue;
4826
+ appliedShields.push(shieldName);
4868
4827
  const existingRuleNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
4869
4828
  const ruleOverrides = isCloudMandated ? {} : shieldOverrides[shieldName] ?? {};
4870
4829
  for (const rule of shield.smartRules) {
@@ -4886,6 +4845,7 @@ function getConfig(cwd) {
4886
4845
  if (!existingWords.has(word)) mergedPolicy.dangerousWords.push(word);
4887
4846
  }
4888
4847
  }
4848
+ mergedPolicy.appliedShields = appliedShields.sort();
4889
4849
  const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
4890
4850
  const cc = mergedPolicy.commandChecks ?? {};
4891
4851
  const advisoryKnobKey = (name) => {
@@ -4944,10 +4904,6 @@ function getConfig(cwd) {
4944
4904
  mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
4945
4905
  mergedPolicy.ignoredTools = [...new Set(mergedPolicy.ignoredTools)];
4946
4906
  mergedPolicy.skillPinning.roots = [...new Set(mergedPolicy.skillPinning.roots)];
4947
- mergedPolicy.snapshot.tools = [...new Set(mergedPolicy.snapshot.tools)];
4948
- mergedPolicy.snapshot.onlyPaths = [...new Set(mergedPolicy.snapshot.onlyPaths)];
4949
- mergedPolicy.snapshot.ignorePaths = [...new Set(mergedPolicy.snapshot.ignorePaths)];
4950
- mergedSettings.enableUndo = false;
4951
4907
  const result = {
4952
4908
  settings: mergedSettings,
4953
4909
  policy: mergedPolicy,
@@ -5010,9 +4966,6 @@ ${error.replace("Invalid config:\n", "")}
5010
4966
  return sanitized;
5011
4967
  }
5012
4968
 
5013
- // src/policy/index.ts
5014
- import pm2 from "picomatch";
5015
-
5016
4969
  // src/dlp.ts
5017
4970
  import fs5 from "fs";
5018
4971
  import path5 from "path";
package/dist/scan-ink.mjs CHANGED
@@ -152,6 +152,18 @@ function rollupByShield(sections, topRulesPerShield = 3) {
152
152
  }
153
153
  return out.sort((a, b) => b.totalCatches - a.totalCatches);
154
154
  }
155
+ function originForRule(ruleName, sections, enabled) {
156
+ for (const section of sections) {
157
+ if (section.rules.some((r) => r.name === ruleName)) {
158
+ if (section.sourceType === "default") return "default";
159
+ if (section.sourceType === "shield") {
160
+ const key = section.shieldKey ?? section.id;
161
+ return enabled.has(key) ? `\u2713 shield:${key}` : `needs shield:${key}`;
162
+ }
163
+ }
164
+ }
165
+ return "";
166
+ }
155
167
  function relativeDate(timestamp, now = /* @__PURE__ */ new Date()) {
156
168
  const t = new Date(timestamp).getTime();
157
169
  if (Number.isNaN(t)) return "?";
@@ -161,6 +173,11 @@ function relativeDate(timestamp, now = /* @__PURE__ */ new Date()) {
161
173
  return `${days}d`;
162
174
  }
163
175
 
176
+ // src/cli/render/ink/panels/title-count.ts
177
+ function topOf(shown, total) {
178
+ return total > shown ? ` \xB7 top ${shown} of ${total}` : "";
179
+ }
180
+
164
181
  // src/cli/render/ink/panels/LeaksPanel.tsx
165
182
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
166
183
  var ROW_LIMIT = 4;
@@ -168,7 +185,10 @@ function LeaksPanel({ summary, width, now = /* @__PURE__ */ new Date() }) {
168
185
  const leaks = summary.leaks;
169
186
  if (leaks.length === 0) return null;
170
187
  return /* @__PURE__ */ jsxs5(Box5, { borderStyle: "round", borderColor: "red", paddingX: 1, flexDirection: "column", width, children: [
171
- /* @__PURE__ */ jsx5(Text5, { bold: true, color: "red", children: "CREDENTIAL LEAKS" }),
188
+ /* @__PURE__ */ jsxs5(Text5, { bold: true, color: "red", children: [
189
+ "CREDENTIAL LEAKS",
190
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: topOf(Math.min(leaks.length, ROW_LIMIT), leaks.length) })
191
+ ] }),
172
192
  leaks.slice(0, ROW_LIMIT).map((leak, i) => /* @__PURE__ */ jsxs5(Box5, { children: [
173
193
  /* @__PURE__ */ jsx5(Box5, { width: 5, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: relativeDate(leak.timestamp, now).padStart(4) }) }),
174
194
  /* @__PURE__ */ jsx5(Box5, { width: 16, children: /* @__PURE__ */ jsx5(Text5, { color: "red", bold: true, wrap: "truncate-end", children: leak.patternName }) }),
@@ -188,30 +208,31 @@ function LeaksPanel({ summary, width, now = /* @__PURE__ */ new Date() }) {
188
208
  // src/cli/render/ink/panels/BlockedPanel.tsx
189
209
  import { Box as Box6, Text as Text6 } from "ink";
190
210
  import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
191
- function originForRule(ruleName, sections) {
192
- for (const section of sections) {
193
- if (section.rules.some((r) => r.name === ruleName)) {
194
- if (section.sourceType === "default") return "default";
195
- if (section.sourceType === "shield") {
196
- return `needs shield:${section.shieldKey ?? section.id}`;
197
- }
198
- }
199
- }
200
- return "";
201
- }
202
211
  var ROW_LIMIT2 = 12;
203
- function BlockedPanel({ summary, width }) {
204
- const rules = topRulesByVerdict(summary.sections, "block", ROW_LIMIT2);
212
+ function BlockedPanel({
213
+ summary,
214
+ width,
215
+ enabledShields = []
216
+ }) {
217
+ const enabled = new Set(enabledShields);
218
+ const allRules = topRulesByVerdict(summary.sections, "block", Number.MAX_SAFE_INTEGER);
219
+ const rules = allRules.slice(0, ROW_LIMIT2);
205
220
  if (rules.length === 0) return null;
221
+ const origins = rules.map((r) => originForRule(r.name, summary.sections, enabled));
222
+ const anyNeeds = origins.some((o) => o.startsWith("needs shield:"));
223
+ const footer = enabled.size === 0 ? "\u2192 install node9 + enable shields above" : anyNeeds ? "\u2192 \u2713 marks rules from enabled shields \xB7 enable the rest above" : "\u2192 \u2713 all these rules come from enabled shields";
206
224
  return /* @__PURE__ */ jsxs6(Box6, { borderStyle: "round", borderColor: "red", paddingX: 1, flexDirection: "column", width, children: [
207
- /* @__PURE__ */ jsx6(Text6, { bold: true, color: "red", children: "WOULD HAVE BLOCKED" }),
225
+ /* @__PURE__ */ jsxs6(Text6, { bold: true, color: "red", children: [
226
+ "WOULD HAVE BLOCKED",
227
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: topOf(rules.length, allRules.length) })
228
+ ] }),
208
229
  rules.map((rule, i) => /* @__PURE__ */ jsxs6(Box6, { children: [
209
230
  /* @__PURE__ */ jsx6(Box6, { width: 3, children: /* @__PURE__ */ jsx6(Text6, { color: "red", children: "\u2717" }) }),
210
231
  /* @__PURE__ */ jsx6(Box6, { width: 24, children: /* @__PURE__ */ jsx6(Text6, { bold: true, wrap: "truncate-end", children: rule.name }) }),
211
232
  /* @__PURE__ */ jsx6(Box6, { width: 6, children: /* @__PURE__ */ jsx6(Text6, { bold: true, children: `\xD7${rule.count}` }) }),
212
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, wrap: "truncate-end", children: originForRule(rule.name, summary.sections) })
233
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, wrap: "truncate-end", children: originForRule(rule.name, summary.sections, enabled) })
213
234
  ] }, i)),
214
- /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, wrap: "truncate-end", children: "\u2192 install node9 + enable shields above" }) })
235
+ /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, wrap: "truncate-end", children: footer }) })
215
236
  ] });
216
237
  }
217
238
 
@@ -219,14 +240,19 @@ function BlockedPanel({ summary, width }) {
219
240
  import { Box as Box7, Text as Text7 } from "ink";
220
241
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
221
242
  var ROW_LIMIT3 = 4;
243
+ var ENV_ROW_LIMIT = 3;
222
244
  function BlastRadiusPanel({
223
245
  blast,
224
246
  blastExposures,
225
247
  width
226
248
  }) {
227
249
  if (blastExposures === 0) return null;
250
+ const shown = Math.min(blast.reachable.length, ROW_LIMIT3) + Math.min(blast.envFindings.length, ENV_ROW_LIMIT);
228
251
  return /* @__PURE__ */ jsxs7(Box7, { borderStyle: "round", borderColor: "yellow", paddingX: 1, flexDirection: "column", width, children: [
229
- /* @__PURE__ */ jsx7(Text7, { bold: true, color: "yellow", children: "BLAST RADIUS" }),
252
+ /* @__PURE__ */ jsxs7(Text7, { bold: true, color: "yellow", children: [
253
+ "BLAST RADIUS",
254
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: topOf(shown, blastExposures) })
255
+ ] }),
230
256
  blast.reachable.slice(0, ROW_LIMIT3).map((path, i) => {
231
257
  const desc = path.description.split(" \u2014 ")[0].split(/—|--/)[0].trim();
232
258
  return /* @__PURE__ */ jsxs7(Box7, { children: [
@@ -251,19 +277,13 @@ function BlastRadiusPanel({
251
277
  // src/cli/render/ink/panels/ReviewQueuePanel.tsx
252
278
  import { Box as Box8, Text as Text8 } from "ink";
253
279
  import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
254
- function originForRule2(ruleName, sections) {
255
- for (const section of sections) {
256
- if (section.rules.some((r) => r.name === ruleName)) {
257
- if (section.sourceType === "default") return "default";
258
- if (section.sourceType === "shield") {
259
- return `needs shield:${section.shieldKey ?? section.id}`;
260
- }
261
- }
262
- }
263
- return "";
264
- }
265
280
  var ROW_LIMIT4 = 5;
266
- function ReviewQueuePanel({ summary, width }) {
281
+ function ReviewQueuePanel({
282
+ summary,
283
+ width,
284
+ enabledShields = []
285
+ }) {
286
+ const enabled = new Set(enabledShields);
267
287
  const rules = topRulesByVerdict(summary.sections, "review", ROW_LIMIT4);
268
288
  if (rules.length === 0) return null;
269
289
  return /* @__PURE__ */ jsxs8(Box8, { borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", width, children: [
@@ -271,7 +291,7 @@ function ReviewQueuePanel({ summary, width }) {
271
291
  rules.map((rule, i) => /* @__PURE__ */ jsxs8(Box8, { children: [
272
292
  /* @__PURE__ */ jsx8(Box8, { width: 22, children: /* @__PURE__ */ jsx8(Text8, { wrap: "truncate-end", children: rule.name }) }),
273
293
  /* @__PURE__ */ jsx8(Box8, { width: 6, children: /* @__PURE__ */ jsx8(Text8, { bold: true, children: `\xD7${rule.count}` }) }),
274
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, wrap: "truncate-end", children: originForRule2(rule.name, summary.sections) })
294
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, wrap: "truncate-end", children: originForRule(rule.name, summary.sections, enabled) })
275
295
  ] }, i)),
276
296
  /* @__PURE__ */ jsxs8(Box8, { children: [
277
297
  /* @__PURE__ */ jsx8(Text8, { dimColor: true, wrap: "truncate-end", children: "\u2192 " }),
@@ -304,7 +324,10 @@ function AgentLoopsPanel({ loopFindings, width }) {
304
324
  const toolEntries = [...byTool.entries()].sort((a, b) => b[1] - a[1]).slice(0, TOOL_ROW_LIMIT);
305
325
  const topStuck = [...loopFindings].sort((a, b) => b.count - a.count).slice(0, STUCK_ROW_LIMIT);
306
326
  return /* @__PURE__ */ jsxs9(Box9, { borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", width, children: [
307
- /* @__PURE__ */ jsx9(Text9, { bold: true, children: "AGENT LOOPS" }),
327
+ /* @__PURE__ */ jsxs9(Text9, { bold: true, children: [
328
+ "AGENT LOOPS",
329
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: topOf(toolEntries.length, byTool.size) })
330
+ ] }),
308
331
  toolEntries.map(([tool, repeats]) => {
309
332
  const pct = totalRepeats > 0 ? Math.round(repeats / totalRepeats * 100) : 0;
310
333
  return /* @__PURE__ */ jsxs9(Box9, { children: [
@@ -1651,9 +1674,9 @@ var LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
1651
1674
 
1652
1675
  // src/cli/render/ink/panels/ShieldsPanel.tsx
1653
1676
  import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
1654
- function ShieldsPanel({ summary, blastScore, width }) {
1677
+ function ShieldsPanel({ summary, width, enabledShields = [] }) {
1678
+ const enabled = new Set(enabledShields);
1655
1679
  const impacts = rollupByShield(summary.sections);
1656
- const exposed = Math.max(0, 100 - blastScore);
1657
1680
  const ranked = [...impacts].sort((a, b) => {
1658
1681
  const aDiscount = PROTECTIVE_SHIELD_DISCOUNTS[a.shieldName] ?? 0;
1659
1682
  const bDiscount = PROTECTIVE_SHIELD_DISCOUNTS[b.shieldName] ?? 0;
@@ -1662,30 +1685,30 @@ function ShieldsPanel({ summary, blastScore, width }) {
1662
1685
  });
1663
1686
  const hitShields = ranked.filter((i) => i.totalCatches > 0);
1664
1687
  const hitNames = new Set(hitShields.map((i) => i.shieldName));
1665
- const zeroHitBuiltins = Object.keys(BUILTIN_SHIELDS).filter((name) => !hitNames.has(name)).sort();
1666
- const topRec = hitShields.find((r) => (PROTECTIVE_SHIELD_DISCOUNTS[r.shieldName] ?? 0) > 0);
1667
- const topRecBonus = topRec ? Math.round(exposed * (PROTECTIVE_SHIELD_DISCOUNTS[topRec.shieldName] ?? 0)) : 0;
1688
+ const zeroHitAll = Object.keys(BUILTIN_SHIELDS).filter((name) => !hitNames.has(name)).sort();
1689
+ const zeroHitBuiltins = zeroHitAll.filter((name) => !enabled.has(name));
1690
+ const zeroHitEnabled = zeroHitAll.filter((name) => enabled.has(name));
1691
+ const topRec = hitShields.find(
1692
+ (r) => (PROTECTIVE_SHIELD_DISCOUNTS[r.shieldName] ?? 0) > 0 && !enabled.has(r.shieldName)
1693
+ );
1694
+ const allCovered = !topRec && hitShields.some(
1695
+ (r) => (PROTECTIVE_SHIELD_DISCOUNTS[r.shieldName] ?? 0) > 0 && enabled.has(r.shieldName)
1696
+ );
1668
1697
  return /* @__PURE__ */ jsxs10(Box10, { borderStyle: "round", borderColor: "cyan", paddingX: 1, flexDirection: "column", width, children: [
1669
1698
  /* @__PURE__ */ jsx10(Text10, { bold: true, color: "cyan", children: "SHIELDS" }),
1670
1699
  hitShields.map((impact) => {
1671
1700
  const discount = PROTECTIVE_SHIELD_DISCOUNTS[impact.shieldName] ?? 0;
1672
- const bonus = Math.round(exposed * discount);
1701
+ const isOn = enabled.has(impact.shieldName);
1673
1702
  const noun = `op${impact.totalCatches !== 1 ? "s" : ""}`;
1674
1703
  return /* @__PURE__ */ jsxs10(Box10, { children: [
1675
1704
  /* @__PURE__ */ jsx10(Box10, { width: 16, children: /* @__PURE__ */ jsx10(Text10, { bold: true, color: discount > 0 ? "cyan" : void 0, dimColor: discount === 0, children: impact.shieldName }) }),
1676
1705
  /* @__PURE__ */ jsx10(Box10, { width: 20, children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: `catches ${impact.totalCatches} ${noun}` }) }),
1677
- bonus > 0 ? /* @__PURE__ */ jsx10(
1678
- Text10,
1679
- {
1680
- bold: true,
1681
- color: "green",
1682
- children: `\u2192 +${bonus} pts (${blastScore} \u2192 ${blastScore + bonus})`
1683
- }
1684
- ) : null
1706
+ isOn ? /* @__PURE__ */ jsx10(Text10, { bold: true, color: "green", children: `\u2713 enabled` }) : discount > 0 ? /* @__PURE__ */ jsx10(Text10, { bold: true, color: "green", children: `\u2192 blocks these reads in-path` }) : null
1685
1707
  ] }, impact.shieldName);
1686
1708
  }),
1709
+ zeroHitEnabled.length > 0 ? /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, wrap: "truncate-end", children: "\u2713 active: " + zeroHitEnabled.join(" \xB7 ") + " (no hits in this history)" }) }) : null,
1687
1710
  zeroHitBuiltins.length > 0 ? /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, wrap: "truncate-end", children: zeroHitBuiltins.join(" \xB7 ") + " (no hits \u2014 install proactively)" }) }) : null,
1688
- topRec ? /* @__PURE__ */ jsx10(Box10, { children: /* @__PURE__ */ jsx10(Text10, { color: "cyan", bold: true, children: `\u2192 node9 shield enable ${topRec.shieldName} (start here \u2014 +${topRecBonus} pts)` }) }) : null
1711
+ topRec ? /* @__PURE__ */ jsx10(Box10, { children: /* @__PURE__ */ jsx10(Text10, { color: "cyan", bold: true, children: `\u2192 node9 shield enable ${topRec.shieldName} (start here \u2014 widest coverage)` }) }) : allCovered ? /* @__PURE__ */ jsx10(Box10, { children: /* @__PURE__ */ jsx10(Text10, { color: "green", bold: true, children: "\u2713 recommended shields are enabled" }) }) : null
1689
1712
  ] });
1690
1713
  }
1691
1714
 
@@ -1708,7 +1731,7 @@ function StaticScorecard({ input, rangeLabel, now }) {
1708
1731
  parts.push(`${leakCount} secret${leakCount !== 1 ? "s" : ""} leaked`);
1709
1732
  }
1710
1733
  if (blockedCount > 0) {
1711
- parts.push(`${blockedCount} op${blockedCount !== 1 ? "s" : ""} blocked`);
1734
+ parts.push(`${blockedCount} op${blockedCount !== 1 ? "s" : ""} would be blocked`);
1712
1735
  }
1713
1736
  return `Critical (${parts.join(" + ")})`;
1714
1737
  })();
@@ -1723,14 +1746,27 @@ function StaticScorecard({ input, rangeLabel, now }) {
1723
1746
  /* @__PURE__ */ jsx11(SeverityBand, { label: criticalLabel, width }),
1724
1747
  /* @__PURE__ */ jsxs11(Box11, { flexDirection: "row", gap: 1, children: [
1725
1748
  /* @__PURE__ */ jsx11(LeaksPanel, { summary, width: halfWidth, now }),
1726
- /* @__PURE__ */ jsx11(BlockedPanel, { summary, width: halfWidth })
1749
+ /* @__PURE__ */ jsx11(
1750
+ BlockedPanel,
1751
+ {
1752
+ summary,
1753
+ width: halfWidth,
1754
+ enabledShields: input.enabledShields
1755
+ }
1756
+ )
1727
1757
  ] })
1728
1758
  ] }) : null,
1729
1759
  input.blastExposures > 0 ? /* @__PURE__ */ jsxs11(Fragment2, { children: [
1730
1760
  /* @__PURE__ */ jsx11(
1731
1761
  SeverityBand,
1732
1762
  {
1733
- label: `High (${input.blastExposures} path${input.blastExposures !== 1 ? "s" : ""} reachable on disk)`,
1763
+ label: (() => {
1764
+ const paths = input.blast.reachable.length;
1765
+ const envs = input.blast.envFindings.length;
1766
+ const pathPart = `${paths} path${paths !== 1 ? "s" : ""}`;
1767
+ const envPart = envs > 0 ? ` + ${envs} env var${envs !== 1 ? "s" : ""}` : "";
1768
+ return `High (${pathPart}${envPart} reachable)`;
1769
+ })(),
1734
1770
  width
1735
1771
  }
1736
1772
  ),
@@ -1747,23 +1783,33 @@ function StaticScorecard({ input, rangeLabel, now }) {
1747
1783
  const reviewCount = input.reviewCount;
1748
1784
  const loopCount = input.scan.loopFindings.length;
1749
1785
  if (reviewCount === 0 && loopCount === 0) return null;
1750
- const { wastePct } = computeLoopWaste(input.scan.loopFindings, input.scan.totalToolCalls);
1786
+ const { wastePct, wastedCalls } = computeLoopWaste(
1787
+ input.scan.loopFindings,
1788
+ input.scan.totalToolCalls
1789
+ );
1751
1790
  const parts = [];
1752
1791
  if (reviewCount > 0) parts.push(`${reviewCount} op${reviewCount !== 1 ? "s" : ""} flagged`);
1753
1792
  if (loopCount > 0)
1754
1793
  parts.push(
1755
- `${loopCount} loop${loopCount !== 1 ? "s" : ""}${wastePct > 0 ? ` \xB7 ${wastePct}% wasted` : ""}`
1794
+ `${loopCount} loop${loopCount !== 1 ? "s" : ""}${wastedCalls > 0 ? ` \xB7 ${wastedCalls} wasted calls (${wastePct}% of all calls)` : ""}`
1756
1795
  );
1757
1796
  return /* @__PURE__ */ jsxs11(Fragment2, { children: [
1758
1797
  /* @__PURE__ */ jsx11(SeverityBand, { label: `Medium (${parts.join(" \xB7 ")})`, width }),
1759
1798
  /* @__PURE__ */ jsxs11(Box11, { flexDirection: "row", gap: 1, children: [
1760
- /* @__PURE__ */ jsx11(ReviewQueuePanel, { summary: input.summary, width: halfWidth }),
1799
+ /* @__PURE__ */ jsx11(
1800
+ ReviewQueuePanel,
1801
+ {
1802
+ summary: input.summary,
1803
+ width: halfWidth,
1804
+ enabledShields: input.enabledShields
1805
+ }
1806
+ ),
1761
1807
  /* @__PURE__ */ jsx11(AgentLoopsPanel, { loopFindings: input.scan.loopFindings, width: halfWidth })
1762
1808
  ] })
1763
1809
  ] });
1764
1810
  })(),
1765
1811
  /* @__PURE__ */ jsx11(SeverityBand, { label: "Recommended action", width }),
1766
- /* @__PURE__ */ jsx11(ShieldsPanel, { summary: input.summary, blastScore: input.blast.score, width })
1812
+ /* @__PURE__ */ jsx11(ShieldsPanel, { summary: input.summary, width, enabledShields: input.enabledShields })
1767
1813
  ] });
1768
1814
  }
1769
1815
  function renderScanScorecardInk(input, rangeLabel) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "1.67.5",
3
+ "version": "2.1.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",