@node9/proxy 1.19.4 → 1.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -118,12 +118,14 @@ function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
118
118
  function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
119
119
  const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
120
120
  const testRun = isTestCall(toolName, args) || process.env.NODE9_TESTING === "1" ? { testRun: true } : {};
121
+ const ruleNameField = meta?.ruleName ? { ruleName: meta.ruleName } : {};
121
122
  appendToLog(LOCAL_AUDIT_LOG, {
122
123
  ts: (/* @__PURE__ */ new Date()).toISOString(),
123
124
  tool: toolName,
124
125
  ...argsField,
125
126
  decision,
126
127
  checkedBy,
128
+ ...ruleNameField,
127
129
  ...testRun,
128
130
  agent: meta?.agent,
129
131
  mcpServer: meta?.mcpServer,
@@ -1089,15 +1091,50 @@ var SENSITIVE_PATH_RULES = [
1089
1091
  match: (p) => /(^|[\\/])\.aws[\\/]/i.test(p)
1090
1092
  },
1091
1093
  {
1094
+ // Mirrors the JSON shield's `.env` pattern (project-jail.json's
1095
+ // review-read-env-any-tool) so the AST FS-op path catches the
1096
+ // same set the regex shield does — including Next.js / Vite's
1097
+ // `.env.<env>.local` double-suffix overrides which are commonly
1098
+ // gitignored AND commonly contain real secrets.
1099
+ //
1100
+ // Intentional non-matches (dev fixtures): .env.example, .env.sample,
1101
+ // .env.template, .env.test, .envrc. See shields.test.ts:983-995
1102
+ // for the canonical test-asserted contract.
1092
1103
  rule: "shield:project-jail:block-read-env",
1093
1104
  reason: "Reading .env files is blocked by project-jail shield",
1094
- match: (p) => /(?:^|[\\/])\.env(?:\.local|\.production|\.staging)?$/i.test(p)
1105
+ match: (p) => /(?:^|[\\/])\.env(?:\.(?:local|production|staging|development|production\.local|staging\.local|development\.local))?$/i.test(
1106
+ p
1107
+ )
1095
1108
  },
1096
1109
  {
1097
- rule: "shield:project-jail:block-read-credentials",
1098
- reason: "Reading credential files is blocked by project-jail shield",
1099
- match: (p) => /(?:credentials\.json|\.netrc|\.npmrc|\.docker[\\/]config\.json|gcloud[\\/]credentials)$/i.test(
1100
- p
1110
+ // verdict: 'review' (not 'block') is a deliberate design choice
1111
+ // documented in commit 29327a8. SSH keys and AWS credentials are
1112
+ // cryptographic material with no legitimate read use-case for
1113
+ // an AI agent → hard `block`. But .netrc / .npmrc / .docker /
1114
+ // .kube / gcloud are CONFIG files that hold tokens AND have
1115
+ // legitimate diagnostic reads ("which registry am I configured
1116
+ // for", "what cluster am I on"). Hard-blocking those creates
1117
+ // friction without much safety win because the review gate
1118
+ // still catches genuine exfiltration attempts.
1119
+ //
1120
+ // The review gate FAILS CLOSED on timeout (daemon.approvalTimeoutMs
1121
+ // returns a deny verdict via the orchestrator's timeout branch),
1122
+ // so a stuck or unattended approval does NOT silently grant
1123
+ // credential access. If the threat model demands strict block,
1124
+ // a future per-shield strict-mode toggle is the right fix —
1125
+ // not a regex-level upgrade here.
1126
+ rule: "shield:project-jail:review-read-credentials",
1127
+ reason: "Reading credential files requires approval (project-jail shield)",
1128
+ verdict: "review",
1129
+ match: (p) => (
1130
+ // .kube/config holds Kubernetes cluster credentials and was
1131
+ // flagged as missing by the node9-pr-agent review (the comment
1132
+ // above mentioned .kube but the regex didn't include it — a
1133
+ // textbook code-comment vs code drift). The JSON shield's
1134
+ // review-read-credentials-any-tool already had it. Now aligned.
1135
+ /(?:credentials\.json|\.netrc|\.npmrc|\.docker[\\/]config\.json|gcloud[\\/]credentials|\.kube[\\/]config)$/i.test(
1136
+ p
1137
+ )
1101
1138
  )
1102
1139
  }
1103
1140
  ];
@@ -1116,7 +1153,7 @@ var AST_FS_REGEX_RULES = /* @__PURE__ */ new Set([
1116
1153
  "shield:project-jail:block-read-ssh",
1117
1154
  "shield:project-jail:block-read-aws",
1118
1155
  "shield:project-jail:block-read-env",
1119
- "shield:project-jail:block-read-credentials"
1156
+ "shield:project-jail:review-read-credentials"
1120
1157
  ]);
1121
1158
  function isProtectedHomePath(rawPath) {
1122
1159
  let p = rawPath.replace(/^\$HOME[\\/]?|^\$\{HOME\}[\\/]?/, "~/");
@@ -1228,7 +1265,12 @@ function analyzeFsOperationImpl(command) {
1228
1265
  for (const p of paths) {
1229
1266
  for (const sp of SENSITIVE_PATH_RULES) {
1230
1267
  if (sp.match(p)) {
1231
- result = { ruleName: sp.rule, verdict: "block", reason: sp.reason, path: p };
1268
+ result = {
1269
+ ruleName: sp.rule,
1270
+ verdict: sp.verdict ?? "block",
1271
+ reason: sp.reason,
1272
+ path: p
1273
+ };
1232
1274
  return false;
1233
1275
  }
1234
1276
  }
@@ -2512,7 +2554,7 @@ var project_jail_default = {
2512
2554
  {
2513
2555
  field: "command",
2514
2556
  op: "matches",
2515
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*[\\/\\\\]\\.ssh[\\/\\\\]",
2557
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*?\\.ssh[\\/\\\\]",
2516
2558
  flags: "i"
2517
2559
  }
2518
2560
  ],
@@ -2526,7 +2568,7 @@ var project_jail_default = {
2526
2568
  {
2527
2569
  field: "command",
2528
2570
  op: "matches",
2529
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*[\\/\\\\]\\.aws[\\/\\\\]",
2571
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*?\\.aws[\\/\\\\]",
2530
2572
  flags: "i"
2531
2573
  }
2532
2574
  ],
@@ -2548,7 +2590,7 @@ var project_jail_default = {
2548
2590
  reason: "Reading .env files is blocked by project-jail shield"
2549
2591
  },
2550
2592
  {
2551
- name: "shield:project-jail:block-read-credentials",
2593
+ name: "shield:project-jail:review-read-credentials",
2552
2594
  tool: "bash",
2553
2595
  conditions: [
2554
2596
  {
@@ -2558,8 +2600,64 @@ var project_jail_default = {
2558
2600
  flags: "i"
2559
2601
  }
2560
2602
  ],
2603
+ verdict: "review",
2604
+ reason: "Reading credential files requires approval (project-jail shield)"
2605
+ },
2606
+ {
2607
+ name: "shield:project-jail:block-read-ssh-any-tool",
2608
+ tool: "*",
2609
+ conditions: [
2610
+ {
2611
+ field: "file_path",
2612
+ op: "matches",
2613
+ value: "(^|[\\/\\\\])\\.ssh[\\/\\\\]",
2614
+ flags: "i"
2615
+ }
2616
+ ],
2617
+ verdict: "block",
2618
+ reason: "Reading SSH private keys is blocked by project-jail shield"
2619
+ },
2620
+ {
2621
+ name: "shield:project-jail:block-read-aws-any-tool",
2622
+ tool: "*",
2623
+ conditions: [
2624
+ {
2625
+ field: "file_path",
2626
+ op: "matches",
2627
+ value: "(^|[\\/\\\\])\\.aws[\\/\\\\]",
2628
+ flags: "i"
2629
+ }
2630
+ ],
2561
2631
  verdict: "block",
2562
- reason: "Reading credential files is blocked by project-jail shield"
2632
+ reason: "Reading AWS credentials is blocked by project-jail shield"
2633
+ },
2634
+ {
2635
+ name: "shield:project-jail:review-read-env-any-tool",
2636
+ tool: "*",
2637
+ conditions: [
2638
+ {
2639
+ field: "file_path",
2640
+ op: "matches",
2641
+ value: "(^|[\\/\\\\])\\.env(\\.(local|production|staging|development|production\\.local|staging\\.local|development\\.local))?$",
2642
+ flags: "i"
2643
+ }
2644
+ ],
2645
+ verdict: "review",
2646
+ reason: "Reading .env files requires approval (project-jail shield)"
2647
+ },
2648
+ {
2649
+ name: "shield:project-jail:review-read-credentials-any-tool",
2650
+ tool: "*",
2651
+ conditions: [
2652
+ {
2653
+ field: "file_path",
2654
+ op: "matches",
2655
+ value: ".*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials|\\.kube[\\/\\\\]config)",
2656
+ flags: "i"
2657
+ }
2658
+ ],
2659
+ verdict: "review",
2660
+ reason: "Reading credential files requires approval (project-jail shield)"
2563
2661
  }
2564
2662
  ],
2565
2663
  dangerousWords: []
@@ -4664,7 +4762,10 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
4664
4762
  args,
4665
4763
  "deny",
4666
4764
  "smart-rule-block-override",
4667
- meta,
4765
+ // Same rationale as the smart-rule-block path above —
4766
+ // pass the specific rule name so [2] SHIELDS can
4767
+ // attribute this override-block to its owning shield.
4768
+ { ...meta, ruleName: policyResult.ruleName },
4668
4769
  hashAuditArgs
4669
4770
  );
4670
4771
  if (approvers.cloud && creds?.apiKey)
@@ -4694,7 +4795,20 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
4694
4795
  }
4695
4796
  } else {
4696
4797
  if (!isManual)
4697
- appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
4798
+ appendLocalAudit(
4799
+ toolName,
4800
+ args,
4801
+ "deny",
4802
+ "smart-rule-block",
4803
+ // Include policyResult.ruleName so the [2] Report SHIELDS
4804
+ // panel can attribute this block to its specific shield
4805
+ // (e.g. `shield:project-jail:block-read-ssh`) via the
4806
+ // rule→shield map. checkedBy stays as the generic
4807
+ // `smart-rule-block` for backward compat with existing
4808
+ // log readers.
4809
+ { ...meta, ruleName: policyResult.ruleName },
4810
+ hashAuditArgs
4811
+ );
4698
4812
  if (approvers.cloud && creds?.apiKey)
4699
4813
  auditLocalAllow(toolName, args, "smart-rule-block", creds, meta, void 0, false, {
4700
4814
  ruleName: policyResult.ruleName,
package/dist/index.mjs CHANGED
@@ -98,12 +98,14 @@ function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
98
98
  function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
99
99
  const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
100
100
  const testRun = isTestCall(toolName, args) || process.env.NODE9_TESTING === "1" ? { testRun: true } : {};
101
+ const ruleNameField = meta?.ruleName ? { ruleName: meta.ruleName } : {};
101
102
  appendToLog(LOCAL_AUDIT_LOG, {
102
103
  ts: (/* @__PURE__ */ new Date()).toISOString(),
103
104
  tool: toolName,
104
105
  ...argsField,
105
106
  decision,
106
107
  checkedBy,
108
+ ...ruleNameField,
107
109
  ...testRun,
108
110
  agent: meta?.agent,
109
111
  mcpServer: meta?.mcpServer,
@@ -1059,15 +1061,50 @@ var SENSITIVE_PATH_RULES = [
1059
1061
  match: (p) => /(^|[\\/])\.aws[\\/]/i.test(p)
1060
1062
  },
1061
1063
  {
1064
+ // Mirrors the JSON shield's `.env` pattern (project-jail.json's
1065
+ // review-read-env-any-tool) so the AST FS-op path catches the
1066
+ // same set the regex shield does — including Next.js / Vite's
1067
+ // `.env.<env>.local` double-suffix overrides which are commonly
1068
+ // gitignored AND commonly contain real secrets.
1069
+ //
1070
+ // Intentional non-matches (dev fixtures): .env.example, .env.sample,
1071
+ // .env.template, .env.test, .envrc. See shields.test.ts:983-995
1072
+ // for the canonical test-asserted contract.
1062
1073
  rule: "shield:project-jail:block-read-env",
1063
1074
  reason: "Reading .env files is blocked by project-jail shield",
1064
- match: (p) => /(?:^|[\\/])\.env(?:\.local|\.production|\.staging)?$/i.test(p)
1075
+ match: (p) => /(?:^|[\\/])\.env(?:\.(?:local|production|staging|development|production\.local|staging\.local|development\.local))?$/i.test(
1076
+ p
1077
+ )
1065
1078
  },
1066
1079
  {
1067
- rule: "shield:project-jail:block-read-credentials",
1068
- reason: "Reading credential files is blocked by project-jail shield",
1069
- match: (p) => /(?:credentials\.json|\.netrc|\.npmrc|\.docker[\\/]config\.json|gcloud[\\/]credentials)$/i.test(
1070
- p
1080
+ // verdict: 'review' (not 'block') is a deliberate design choice
1081
+ // documented in commit 29327a8. SSH keys and AWS credentials are
1082
+ // cryptographic material with no legitimate read use-case for
1083
+ // an AI agent → hard `block`. But .netrc / .npmrc / .docker /
1084
+ // .kube / gcloud are CONFIG files that hold tokens AND have
1085
+ // legitimate diagnostic reads ("which registry am I configured
1086
+ // for", "what cluster am I on"). Hard-blocking those creates
1087
+ // friction without much safety win because the review gate
1088
+ // still catches genuine exfiltration attempts.
1089
+ //
1090
+ // The review gate FAILS CLOSED on timeout (daemon.approvalTimeoutMs
1091
+ // returns a deny verdict via the orchestrator's timeout branch),
1092
+ // so a stuck or unattended approval does NOT silently grant
1093
+ // credential access. If the threat model demands strict block,
1094
+ // a future per-shield strict-mode toggle is the right fix —
1095
+ // not a regex-level upgrade here.
1096
+ rule: "shield:project-jail:review-read-credentials",
1097
+ reason: "Reading credential files requires approval (project-jail shield)",
1098
+ verdict: "review",
1099
+ match: (p) => (
1100
+ // .kube/config holds Kubernetes cluster credentials and was
1101
+ // flagged as missing by the node9-pr-agent review (the comment
1102
+ // above mentioned .kube but the regex didn't include it — a
1103
+ // textbook code-comment vs code drift). The JSON shield's
1104
+ // review-read-credentials-any-tool already had it. Now aligned.
1105
+ /(?:credentials\.json|\.netrc|\.npmrc|\.docker[\\/]config\.json|gcloud[\\/]credentials|\.kube[\\/]config)$/i.test(
1106
+ p
1107
+ )
1071
1108
  )
1072
1109
  }
1073
1110
  ];
@@ -1086,7 +1123,7 @@ var AST_FS_REGEX_RULES = /* @__PURE__ */ new Set([
1086
1123
  "shield:project-jail:block-read-ssh",
1087
1124
  "shield:project-jail:block-read-aws",
1088
1125
  "shield:project-jail:block-read-env",
1089
- "shield:project-jail:block-read-credentials"
1126
+ "shield:project-jail:review-read-credentials"
1090
1127
  ]);
1091
1128
  function isProtectedHomePath(rawPath) {
1092
1129
  let p = rawPath.replace(/^\$HOME[\\/]?|^\$\{HOME\}[\\/]?/, "~/");
@@ -1198,7 +1235,12 @@ function analyzeFsOperationImpl(command) {
1198
1235
  for (const p of paths) {
1199
1236
  for (const sp of SENSITIVE_PATH_RULES) {
1200
1237
  if (sp.match(p)) {
1201
- result = { ruleName: sp.rule, verdict: "block", reason: sp.reason, path: p };
1238
+ result = {
1239
+ ruleName: sp.rule,
1240
+ verdict: sp.verdict ?? "block",
1241
+ reason: sp.reason,
1242
+ path: p
1243
+ };
1202
1244
  return false;
1203
1245
  }
1204
1246
  }
@@ -2482,7 +2524,7 @@ var project_jail_default = {
2482
2524
  {
2483
2525
  field: "command",
2484
2526
  op: "matches",
2485
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*[\\/\\\\]\\.ssh[\\/\\\\]",
2527
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*?\\.ssh[\\/\\\\]",
2486
2528
  flags: "i"
2487
2529
  }
2488
2530
  ],
@@ -2496,7 +2538,7 @@ var project_jail_default = {
2496
2538
  {
2497
2539
  field: "command",
2498
2540
  op: "matches",
2499
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*[\\/\\\\]\\.aws[\\/\\\\]",
2541
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*?\\.aws[\\/\\\\]",
2500
2542
  flags: "i"
2501
2543
  }
2502
2544
  ],
@@ -2518,7 +2560,7 @@ var project_jail_default = {
2518
2560
  reason: "Reading .env files is blocked by project-jail shield"
2519
2561
  },
2520
2562
  {
2521
- name: "shield:project-jail:block-read-credentials",
2563
+ name: "shield:project-jail:review-read-credentials",
2522
2564
  tool: "bash",
2523
2565
  conditions: [
2524
2566
  {
@@ -2528,8 +2570,64 @@ var project_jail_default = {
2528
2570
  flags: "i"
2529
2571
  }
2530
2572
  ],
2573
+ verdict: "review",
2574
+ reason: "Reading credential files requires approval (project-jail shield)"
2575
+ },
2576
+ {
2577
+ name: "shield:project-jail:block-read-ssh-any-tool",
2578
+ tool: "*",
2579
+ conditions: [
2580
+ {
2581
+ field: "file_path",
2582
+ op: "matches",
2583
+ value: "(^|[\\/\\\\])\\.ssh[\\/\\\\]",
2584
+ flags: "i"
2585
+ }
2586
+ ],
2587
+ verdict: "block",
2588
+ reason: "Reading SSH private keys is blocked by project-jail shield"
2589
+ },
2590
+ {
2591
+ name: "shield:project-jail:block-read-aws-any-tool",
2592
+ tool: "*",
2593
+ conditions: [
2594
+ {
2595
+ field: "file_path",
2596
+ op: "matches",
2597
+ value: "(^|[\\/\\\\])\\.aws[\\/\\\\]",
2598
+ flags: "i"
2599
+ }
2600
+ ],
2531
2601
  verdict: "block",
2532
- reason: "Reading credential files is blocked by project-jail shield"
2602
+ reason: "Reading AWS credentials is blocked by project-jail shield"
2603
+ },
2604
+ {
2605
+ name: "shield:project-jail:review-read-env-any-tool",
2606
+ tool: "*",
2607
+ conditions: [
2608
+ {
2609
+ field: "file_path",
2610
+ op: "matches",
2611
+ value: "(^|[\\/\\\\])\\.env(\\.(local|production|staging|development|production\\.local|staging\\.local|development\\.local))?$",
2612
+ flags: "i"
2613
+ }
2614
+ ],
2615
+ verdict: "review",
2616
+ reason: "Reading .env files requires approval (project-jail shield)"
2617
+ },
2618
+ {
2619
+ name: "shield:project-jail:review-read-credentials-any-tool",
2620
+ tool: "*",
2621
+ conditions: [
2622
+ {
2623
+ field: "file_path",
2624
+ op: "matches",
2625
+ value: ".*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials|\\.kube[\\/\\\\]config)",
2626
+ flags: "i"
2627
+ }
2628
+ ],
2629
+ verdict: "review",
2630
+ reason: "Reading credential files requires approval (project-jail shield)"
2533
2631
  }
2534
2632
  ],
2535
2633
  dangerousWords: []
@@ -4634,7 +4732,10 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
4634
4732
  args,
4635
4733
  "deny",
4636
4734
  "smart-rule-block-override",
4637
- meta,
4735
+ // Same rationale as the smart-rule-block path above —
4736
+ // pass the specific rule name so [2] SHIELDS can
4737
+ // attribute this override-block to its owning shield.
4738
+ { ...meta, ruleName: policyResult.ruleName },
4638
4739
  hashAuditArgs
4639
4740
  );
4640
4741
  if (approvers.cloud && creds?.apiKey)
@@ -4664,7 +4765,20 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
4664
4765
  }
4665
4766
  } else {
4666
4767
  if (!isManual)
4667
- appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
4768
+ appendLocalAudit(
4769
+ toolName,
4770
+ args,
4771
+ "deny",
4772
+ "smart-rule-block",
4773
+ // Include policyResult.ruleName so the [2] Report SHIELDS
4774
+ // panel can attribute this block to its specific shield
4775
+ // (e.g. `shield:project-jail:block-read-ssh`) via the
4776
+ // rule→shield map. checkedBy stays as the generic
4777
+ // `smart-rule-block` for backward compat with existing
4778
+ // log readers.
4779
+ { ...meta, ruleName: policyResult.ruleName },
4780
+ hashAuditArgs
4781
+ );
4668
4782
  if (approvers.cloud && creds?.apiKey)
4669
4783
  auditLocalAllow(toolName, args, "smart-rule-block", creds, meta, void 0, false, {
4670
4784
  ruleName: policyResult.ruleName,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "1.19.4",
3
+ "version": "1.20.1",
4
4
  "description": "The Sudo Command for AI Agents. Execution Security for Claude Code & MCP.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -83,6 +83,7 @@
83
83
  "react": "^19.2.6",
84
84
  "safe-regex2": "^5.1.0",
85
85
  "smol-toml": "^1.6.1",
86
+ "string-width": "^4.2.3",
86
87
  "zod": "^3.25.76"
87
88
  },
88
89
  "bundleDependencies": [
@@ -101,6 +102,7 @@
101
102
  "@types/react": "^19.2.14",
102
103
  "@vitest/coverage-v8": "4.1.2",
103
104
  "cross-env": "^10.1.0",
105
+ "ink-testing-library": "^4.0.0",
104
106
  "prettier": "^3.4.2",
105
107
  "semantic-release": "^25.0.3",
106
108
  "tsup": "^8.5.1",