@node9/proxy 2.23.2 → 2.24.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.
Files changed (3) hide show
  1. package/dist/cli.js +81 -11
  2. package/dist/cli.mjs +81 -11
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -60496,11 +60496,15 @@ function injectableJobs(wf, raw, untrustedTrigger) {
60496
60496
  }
60497
60497
  return out;
60498
60498
  }
60499
+ var DEAD_SECRET_TAIL_RE = /(?<=(?:\$\{\{|\(|,)\s*)(secrets\.GITHUB_TOKEN|github\.token)(?:\s*\|\|\s*(?:'[^']*'|[A-Za-z_][A-Za-z0-9_.]*))+(?=\s*(?:\}\}|\)|,))/gi;
60500
+ function dropDeadSecretRefs(text) {
60501
+ return text.replace(DEAD_SECRET_TAIL_RE, "$1");
60502
+ }
60499
60503
  function usesPatIn(jobs) {
60500
60504
  for (const j of jobs)
60501
60505
  for (const step of j.steps ?? []) {
60502
- const gt = str(step.with?.["github_token"]);
60503
- if (/secrets\./i.test(gt) && !/secrets\.GITHUB_TOKEN/i.test(gt)) return true;
60506
+ const gt = dropDeadSecretRefs(str(step.with?.["github_token"]));
60507
+ if (/secrets\.(?!GITHUB_TOKEN\b)[A-Za-z_]/i.test(gt)) return true;
60504
60508
  }
60505
60509
  return false;
60506
60510
  }
@@ -60533,6 +60537,36 @@ function hasMetaWritePerm(wf, agentJobs) {
60533
60537
  function hasEnvDeny(steps) {
60534
60538
  return steps.some((s) => /"?mode"?\s*:\s*"?deny/i.test(str(s.with?.["settings"])));
60535
60539
  }
60540
+ var CLAUDE_CODE_ACTION_RE = /anthropics\/claude-code-action@/i;
60541
+ var FULL_OUTPUT_ACTION_RE = /anthropics\/claude-code(-base)?-action@/i;
60542
+ function pinnedBelow(uses, min) {
60543
+ const m = /@v?(\d+)\.(\d+)\.(\d+)$/.exec(uses.trim());
60544
+ if (!m) return false;
60545
+ const v = [Number(m[1]), Number(m[2]), Number(m[3])];
60546
+ for (let i = 0; i < 3; i++) if (v[i] !== min[i]) return v[i] < min[i];
60547
+ return false;
60548
+ }
60549
+ var FULL_OUTPUT_MIN = [1, 0, 16];
60550
+ var ENV_SCRUB_MIN = [1, 0, 77];
60551
+ function showsFullOutput(steps) {
60552
+ return steps.some(
60553
+ (s) => FULL_OUTPUT_ACTION_RE.test(s.uses ?? "") && !pinnedBelow(s.uses ?? "", FULL_OUTPUT_MIN) && str(s.with?.["show_full_output"]) === "true"
60554
+ );
60555
+ }
60556
+ function envScrubOptedOut(pairs, wf) {
60557
+ const wfEnv = wf.env;
60558
+ const KEY = "CLAUDE_CODE_SUBPROCESS_ENV_SCRUB";
60559
+ return pairs.some(({ job, step }) => {
60560
+ const uses = step.uses ?? "";
60561
+ if (!CLAUDE_CODE_ACTION_RE.test(uses) || pinnedBelow(uses, ENV_SCRUB_MIN)) return false;
60562
+ if (!str(step.with?.["allowed_non_write_users"]).trim()) return false;
60563
+ const holder = [step.env, job.env, wfEnv].find((e) => e != null && KEY in e);
60564
+ if (!holder) return false;
60565
+ const v = str(holder[KEY]).trim();
60566
+ if (!v || /\$\{\{/.test(v)) return false;
60567
+ return !/^(1|true|yes|on)$/i.test(v);
60568
+ });
60569
+ }
60536
60570
  function agentActionsPinned(steps) {
60537
60571
  const agent = steps.filter(isAgentStep).filter((s) => s.uses);
60538
60572
  if (agent.length === 0) return false;
@@ -60652,6 +60686,19 @@ function analyzeWorkflow(path80, content) {
60652
60686
  signals.push('allowed_non_write_users: "*" with github_token \u2014 any user can trigger the agent');
60653
60687
  if (elevated) signals.push("elevated permissions (contents/id-token: write)");
60654
60688
  if (pat) signals.push("a static PAT is exposed to the agent (recoverable via injection)");
60689
+ const scopedPairs = allSteps(wf).filter(
60690
+ (p) => isAgentStep(p.step) && (injJobs.length === 0 || injJobs.includes(p.job))
60691
+ );
60692
+ const fullOutput = showsFullOutput(scopedPairs.map((p) => p.step));
60693
+ const scrubOff = envScrubOptedOut(scopedPairs, wf);
60694
+ if (fullOutput)
60695
+ signals.push(
60696
+ "`show_full_output: true` writes every agent command and its output to the Actions log, public on a public repo; GitHub masks known secret values there, but an agent told to encode one first can publish it without any network tool"
60697
+ );
60698
+ if (scrubOff)
60699
+ signals.push(
60700
+ "`CLAUDE_CODE_SUBPROCESS_ENV_SCRUB` is switched off, so the action no longer scrubs the Anthropic key, cloud credentials and Actions runtime tokens from the agent's shell"
60701
+ );
60655
60702
  if (!gate && reach > 0) signals.push("no effective actor gate");
60656
60703
  const noExplicitPerms = wf.permissions == null && jobList(wf).every((j) => j.permissions == null);
60657
60704
  if (noExplicitPerms && reach > 0 && (broadTools || githubWriteTool))
@@ -60672,7 +60719,7 @@ function analyzeWorkflow(path80, content) {
60672
60719
  'allowed_non_write_users: "*" is set without github_token, so the action keeps its default write gate'
60673
60720
  );
60674
60721
  const title = severity === "critical" || severity === "high" ? "Injectable agent workflow \u2014 untrusted input reaches a tool-using agent with secrets" : severity === "medium" ? "Agent workflow with a risky pattern (partially mitigated)" : "Agent workflow on a privileged trigger \u2014 review the actor gate";
60675
- return {
60722
+ const finding = {
60676
60723
  check: "CI-2",
60677
60724
  // One verdict per workflow file — the finding IS the file's reachability score.
60678
60725
  rule: "CI-2.injectable-workflow",
@@ -60684,13 +60731,20 @@ function analyzeWorkflow(path80, content) {
60684
60731
  mitigations: mitigations.length ? mitigations : void 0,
60685
60732
  fix: head === "root" && privileged ? "Do not check out the untrusted PR head into the workspace root under a privileged trigger (pull_request_target/workflow_run) \u2014 check out the base ref, or isolate the head in a subdir (--add-dir). Add an actor gate and scope the agent tools." : head === "root" ? "This runs under `pull_request` (fork PRs get a read-only token), so the head checkout is low-risk today \u2014 keep it on `pull_request` (not `pull_request_target`) and keep the actor gate + scoped tools." : "Add/verify an actor gate, scope the agent tools to read-only, and env-deny secrets. See Anthropic\u2019s claude-code-action security doc."
60686
60733
  };
60734
+ const extraFix = [
60735
+ fullOutput ? "Remove `show_full_output: true` (the action documents it for debugging only)." : "",
60736
+ scrubOff ? "Remove the `CLAUDE_CODE_SUBPROCESS_ENV_SCRUB` opt-out." : ""
60737
+ ].filter(Boolean).join(" ");
60738
+ if (extraFix) finding.fix = `${finding.fix} ${extraFix}`;
60739
+ return finding;
60687
60740
  }
60688
60741
  var AGENT_FUEL_RE = /^(ANTHROPIC_API_KEY|ANTHROPIC_AUTH_TOKEN|ANTHROPIC_BASE_URL|CLAUDE_CODE_OAUTH_TOKEN|OPENAI_API_KEY|OPENAI_BASE_URL|GEMINI_API_KEY|GOOGLE_API_KEY|GITHUB_TOKEN)$/i;
60689
60742
  var FUEL_INPUT_KEYS = /^(anthropic_api_key|anthropic_auth_token|anthropic_base_url|claude_code_oauth_token|openai_api_key|openai_base_url|gemini_api_key|google_api_key)$/i;
60690
60743
  function fuelSecretNames(agentSteps) {
60691
60744
  const out = /* @__PURE__ */ new Set();
60692
60745
  const add = (v) => {
60693
- for (const m of str(v).matchAll(/secrets\.([A-Za-z_][A-Za-z0-9_]*)/gi)) out.add(m[1]);
60746
+ for (const m of dropDeadSecretRefs(str(v)).matchAll(/secrets\.([A-Za-z_][A-Za-z0-9_]*)/gi))
60747
+ out.add(m[1]);
60694
60748
  };
60695
60749
  for (const st of agentSteps) {
60696
60750
  for (const [k, v] of Object.entries(st.with ?? {})) if (FUEL_INPUT_KEYS.test(k)) add(v);
@@ -60712,7 +60766,7 @@ function agentReachableSecrets(wf, agentSteps, agentJobs) {
60712
60766
  blobs.push(str(wf.env));
60713
60767
  const fuel = fuelSecretNames(agentSteps);
60714
60768
  const found = /* @__PURE__ */ new Map();
60715
- for (const b of blobs) {
60769
+ for (const b of blobs.map(dropDeadSecretRefs)) {
60716
60770
  for (const m of b.matchAll(/secrets\.([A-Za-z_][A-Za-z0-9_]*)/gi)) {
60717
60771
  const name = m[1];
60718
60772
  if (AGENT_FUEL_RE.test(name) || fuel.has(name)) continue;
@@ -60840,19 +60894,35 @@ function analyzeAgentConfig(path80, content) {
60840
60894
  );
60841
60895
  if (broad.length > 0) {
60842
60896
  const hasBackstop = deny.some((d) => /Bash|Write|Edit/.test(d));
60897
+ const bashBackstop = deny.some((d) => /^Bash\b/.test(d));
60898
+ const bareShell = broad.some((a) => /^Bash$|^Bash\(\s*\*/.test(a));
60899
+ const high = bareShell && !bashBackstop;
60900
+ const signals = [`broad allow(s): ${broad.slice(0, 5).join(", ")}`];
60901
+ if (high)
60902
+ signals.push(
60903
+ "unrestricted `Bash` with no `deny` backstop \u2014 any command an injected instruction names runs without a prompt, for everyone who opens this repo with the agent"
60904
+ );
60905
+ else if (bareShell)
60906
+ signals.push(
60907
+ "a `Bash` deny list narrows the unrestricted `Bash` allow; it blocks only the commands it names"
60908
+ );
60909
+ if (broad.some((a) => /^Bash\(git:/.test(a)))
60910
+ signals.push(
60911
+ "`Bash(git:*)` pre-approves every git command; git can run other programs (`-c core.pager=\u2026`, `!` aliases), so this is broader than it looks"
60912
+ );
60913
+ if (broad.some((a) => /^Write|^Edit$/.test(a)))
60914
+ signals.push("`Write`/`Edit` pre-approve file changes without a prompt");
60915
+ if (!high && !bareShell && !hasBackstop) signals.push("no `deny` entry narrows these grants");
60843
60916
  findings.push({
60844
60917
  check: "CI-1",
60845
60918
  rule: "CI-1.broad-allow",
60846
60919
  // File-level: one finding per config file. Adding a SECOND broad allow makes the
60847
60920
  // same statement about the same file, so it must not read as a new finding.
60848
60921
  dimension: "toolRules",
60849
- severity: hasBackstop ? "medium" : "high",
60850
- title: hasBackstop ? "Committed agent config pre-authorizes broad tools" : "Committed agent config pre-authorizes broad tools with no deny backstop",
60922
+ severity: high ? "high" : "medium",
60923
+ title: high ? "Committed agent config pre-authorizes broad tools with no deny backstop" : "Committed agent config pre-authorizes broad tools",
60851
60924
  file: path80,
60852
- signals: [
60853
- `broad allow(s): ${broad.slice(0, 5).join(", ")}`,
60854
- hasBackstop ? "a `deny` list backstops the broad allow" : "no `deny` entry covers Bash/Write/Edit \u2014 every contributor is pre-authorized for catastrophic tools"
60855
- ],
60925
+ signals,
60856
60926
  fix: "Scope the allow-list to specific read-only subcommands (e.g. `Bash(gh pr view:*)`); avoid bare `Bash`/`git:`/`Write`, or add a `deny` backstop."
60857
60927
  });
60858
60928
  }
package/dist/cli.mjs CHANGED
@@ -60485,11 +60485,15 @@ function injectableJobs(wf, raw, untrustedTrigger) {
60485
60485
  }
60486
60486
  return out;
60487
60487
  }
60488
+ var DEAD_SECRET_TAIL_RE = /(?<=(?:\$\{\{|\(|,)\s*)(secrets\.GITHUB_TOKEN|github\.token)(?:\s*\|\|\s*(?:'[^']*'|[A-Za-z_][A-Za-z0-9_.]*))+(?=\s*(?:\}\}|\)|,))/gi;
60489
+ function dropDeadSecretRefs(text) {
60490
+ return text.replace(DEAD_SECRET_TAIL_RE, "$1");
60491
+ }
60488
60492
  function usesPatIn(jobs) {
60489
60493
  for (const j of jobs)
60490
60494
  for (const step of j.steps ?? []) {
60491
- const gt = str(step.with?.["github_token"]);
60492
- if (/secrets\./i.test(gt) && !/secrets\.GITHUB_TOKEN/i.test(gt)) return true;
60495
+ const gt = dropDeadSecretRefs(str(step.with?.["github_token"]));
60496
+ if (/secrets\.(?!GITHUB_TOKEN\b)[A-Za-z_]/i.test(gt)) return true;
60493
60497
  }
60494
60498
  return false;
60495
60499
  }
@@ -60522,6 +60526,36 @@ function hasMetaWritePerm(wf, agentJobs) {
60522
60526
  function hasEnvDeny(steps) {
60523
60527
  return steps.some((s) => /"?mode"?\s*:\s*"?deny/i.test(str(s.with?.["settings"])));
60524
60528
  }
60529
+ var CLAUDE_CODE_ACTION_RE = /anthropics\/claude-code-action@/i;
60530
+ var FULL_OUTPUT_ACTION_RE = /anthropics\/claude-code(-base)?-action@/i;
60531
+ function pinnedBelow(uses, min) {
60532
+ const m = /@v?(\d+)\.(\d+)\.(\d+)$/.exec(uses.trim());
60533
+ if (!m) return false;
60534
+ const v = [Number(m[1]), Number(m[2]), Number(m[3])];
60535
+ for (let i = 0; i < 3; i++) if (v[i] !== min[i]) return v[i] < min[i];
60536
+ return false;
60537
+ }
60538
+ var FULL_OUTPUT_MIN = [1, 0, 16];
60539
+ var ENV_SCRUB_MIN = [1, 0, 77];
60540
+ function showsFullOutput(steps) {
60541
+ return steps.some(
60542
+ (s) => FULL_OUTPUT_ACTION_RE.test(s.uses ?? "") && !pinnedBelow(s.uses ?? "", FULL_OUTPUT_MIN) && str(s.with?.["show_full_output"]) === "true"
60543
+ );
60544
+ }
60545
+ function envScrubOptedOut(pairs, wf) {
60546
+ const wfEnv = wf.env;
60547
+ const KEY = "CLAUDE_CODE_SUBPROCESS_ENV_SCRUB";
60548
+ return pairs.some(({ job, step }) => {
60549
+ const uses = step.uses ?? "";
60550
+ if (!CLAUDE_CODE_ACTION_RE.test(uses) || pinnedBelow(uses, ENV_SCRUB_MIN)) return false;
60551
+ if (!str(step.with?.["allowed_non_write_users"]).trim()) return false;
60552
+ const holder = [step.env, job.env, wfEnv].find((e) => e != null && KEY in e);
60553
+ if (!holder) return false;
60554
+ const v = str(holder[KEY]).trim();
60555
+ if (!v || /\$\{\{/.test(v)) return false;
60556
+ return !/^(1|true|yes|on)$/i.test(v);
60557
+ });
60558
+ }
60525
60559
  function agentActionsPinned(steps) {
60526
60560
  const agent = steps.filter(isAgentStep).filter((s) => s.uses);
60527
60561
  if (agent.length === 0) return false;
@@ -60641,6 +60675,19 @@ function analyzeWorkflow(path80, content) {
60641
60675
  signals.push('allowed_non_write_users: "*" with github_token \u2014 any user can trigger the agent');
60642
60676
  if (elevated) signals.push("elevated permissions (contents/id-token: write)");
60643
60677
  if (pat) signals.push("a static PAT is exposed to the agent (recoverable via injection)");
60678
+ const scopedPairs = allSteps(wf).filter(
60679
+ (p) => isAgentStep(p.step) && (injJobs.length === 0 || injJobs.includes(p.job))
60680
+ );
60681
+ const fullOutput = showsFullOutput(scopedPairs.map((p) => p.step));
60682
+ const scrubOff = envScrubOptedOut(scopedPairs, wf);
60683
+ if (fullOutput)
60684
+ signals.push(
60685
+ "`show_full_output: true` writes every agent command and its output to the Actions log, public on a public repo; GitHub masks known secret values there, but an agent told to encode one first can publish it without any network tool"
60686
+ );
60687
+ if (scrubOff)
60688
+ signals.push(
60689
+ "`CLAUDE_CODE_SUBPROCESS_ENV_SCRUB` is switched off, so the action no longer scrubs the Anthropic key, cloud credentials and Actions runtime tokens from the agent's shell"
60690
+ );
60644
60691
  if (!gate && reach > 0) signals.push("no effective actor gate");
60645
60692
  const noExplicitPerms = wf.permissions == null && jobList(wf).every((j) => j.permissions == null);
60646
60693
  if (noExplicitPerms && reach > 0 && (broadTools || githubWriteTool))
@@ -60661,7 +60708,7 @@ function analyzeWorkflow(path80, content) {
60661
60708
  'allowed_non_write_users: "*" is set without github_token, so the action keeps its default write gate'
60662
60709
  );
60663
60710
  const title = severity === "critical" || severity === "high" ? "Injectable agent workflow \u2014 untrusted input reaches a tool-using agent with secrets" : severity === "medium" ? "Agent workflow with a risky pattern (partially mitigated)" : "Agent workflow on a privileged trigger \u2014 review the actor gate";
60664
- return {
60711
+ const finding = {
60665
60712
  check: "CI-2",
60666
60713
  // One verdict per workflow file — the finding IS the file's reachability score.
60667
60714
  rule: "CI-2.injectable-workflow",
@@ -60673,13 +60720,20 @@ function analyzeWorkflow(path80, content) {
60673
60720
  mitigations: mitigations.length ? mitigations : void 0,
60674
60721
  fix: head === "root" && privileged ? "Do not check out the untrusted PR head into the workspace root under a privileged trigger (pull_request_target/workflow_run) \u2014 check out the base ref, or isolate the head in a subdir (--add-dir). Add an actor gate and scope the agent tools." : head === "root" ? "This runs under `pull_request` (fork PRs get a read-only token), so the head checkout is low-risk today \u2014 keep it on `pull_request` (not `pull_request_target`) and keep the actor gate + scoped tools." : "Add/verify an actor gate, scope the agent tools to read-only, and env-deny secrets. See Anthropic\u2019s claude-code-action security doc."
60675
60722
  };
60723
+ const extraFix = [
60724
+ fullOutput ? "Remove `show_full_output: true` (the action documents it for debugging only)." : "",
60725
+ scrubOff ? "Remove the `CLAUDE_CODE_SUBPROCESS_ENV_SCRUB` opt-out." : ""
60726
+ ].filter(Boolean).join(" ");
60727
+ if (extraFix) finding.fix = `${finding.fix} ${extraFix}`;
60728
+ return finding;
60676
60729
  }
60677
60730
  var AGENT_FUEL_RE = /^(ANTHROPIC_API_KEY|ANTHROPIC_AUTH_TOKEN|ANTHROPIC_BASE_URL|CLAUDE_CODE_OAUTH_TOKEN|OPENAI_API_KEY|OPENAI_BASE_URL|GEMINI_API_KEY|GOOGLE_API_KEY|GITHUB_TOKEN)$/i;
60678
60731
  var FUEL_INPUT_KEYS = /^(anthropic_api_key|anthropic_auth_token|anthropic_base_url|claude_code_oauth_token|openai_api_key|openai_base_url|gemini_api_key|google_api_key)$/i;
60679
60732
  function fuelSecretNames(agentSteps) {
60680
60733
  const out = /* @__PURE__ */ new Set();
60681
60734
  const add = (v) => {
60682
- for (const m of str(v).matchAll(/secrets\.([A-Za-z_][A-Za-z0-9_]*)/gi)) out.add(m[1]);
60735
+ for (const m of dropDeadSecretRefs(str(v)).matchAll(/secrets\.([A-Za-z_][A-Za-z0-9_]*)/gi))
60736
+ out.add(m[1]);
60683
60737
  };
60684
60738
  for (const st of agentSteps) {
60685
60739
  for (const [k, v] of Object.entries(st.with ?? {})) if (FUEL_INPUT_KEYS.test(k)) add(v);
@@ -60701,7 +60755,7 @@ function agentReachableSecrets(wf, agentSteps, agentJobs) {
60701
60755
  blobs.push(str(wf.env));
60702
60756
  const fuel = fuelSecretNames(agentSteps);
60703
60757
  const found = /* @__PURE__ */ new Map();
60704
- for (const b of blobs) {
60758
+ for (const b of blobs.map(dropDeadSecretRefs)) {
60705
60759
  for (const m of b.matchAll(/secrets\.([A-Za-z_][A-Za-z0-9_]*)/gi)) {
60706
60760
  const name = m[1];
60707
60761
  if (AGENT_FUEL_RE.test(name) || fuel.has(name)) continue;
@@ -60829,19 +60883,35 @@ function analyzeAgentConfig(path80, content) {
60829
60883
  );
60830
60884
  if (broad.length > 0) {
60831
60885
  const hasBackstop = deny.some((d) => /Bash|Write|Edit/.test(d));
60886
+ const bashBackstop = deny.some((d) => /^Bash\b/.test(d));
60887
+ const bareShell = broad.some((a) => /^Bash$|^Bash\(\s*\*/.test(a));
60888
+ const high = bareShell && !bashBackstop;
60889
+ const signals = [`broad allow(s): ${broad.slice(0, 5).join(", ")}`];
60890
+ if (high)
60891
+ signals.push(
60892
+ "unrestricted `Bash` with no `deny` backstop \u2014 any command an injected instruction names runs without a prompt, for everyone who opens this repo with the agent"
60893
+ );
60894
+ else if (bareShell)
60895
+ signals.push(
60896
+ "a `Bash` deny list narrows the unrestricted `Bash` allow; it blocks only the commands it names"
60897
+ );
60898
+ if (broad.some((a) => /^Bash\(git:/.test(a)))
60899
+ signals.push(
60900
+ "`Bash(git:*)` pre-approves every git command; git can run other programs (`-c core.pager=\u2026`, `!` aliases), so this is broader than it looks"
60901
+ );
60902
+ if (broad.some((a) => /^Write|^Edit$/.test(a)))
60903
+ signals.push("`Write`/`Edit` pre-approve file changes without a prompt");
60904
+ if (!high && !bareShell && !hasBackstop) signals.push("no `deny` entry narrows these grants");
60832
60905
  findings.push({
60833
60906
  check: "CI-1",
60834
60907
  rule: "CI-1.broad-allow",
60835
60908
  // File-level: one finding per config file. Adding a SECOND broad allow makes the
60836
60909
  // same statement about the same file, so it must not read as a new finding.
60837
60910
  dimension: "toolRules",
60838
- severity: hasBackstop ? "medium" : "high",
60839
- title: hasBackstop ? "Committed agent config pre-authorizes broad tools" : "Committed agent config pre-authorizes broad tools with no deny backstop",
60911
+ severity: high ? "high" : "medium",
60912
+ title: high ? "Committed agent config pre-authorizes broad tools with no deny backstop" : "Committed agent config pre-authorizes broad tools",
60840
60913
  file: path80,
60841
- signals: [
60842
- `broad allow(s): ${broad.slice(0, 5).join(", ")}`,
60843
- hasBackstop ? "a `deny` list backstops the broad allow" : "no `deny` entry covers Bash/Write/Edit \u2014 every contributor is pre-authorized for catastrophic tools"
60844
- ],
60914
+ signals,
60845
60915
  fix: "Scope the allow-list to specific read-only subcommands (e.g. `Bash(gh pr view:*)`); avoid bare `Bash`/`git:`/`Write`, or add a `deny` backstop."
60846
60916
  });
60847
60917
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "2.23.2",
3
+ "version": "2.24.0",
4
4
  "description": "IAM for your AI agents. Set what Claude Code, Codex, Gemini, Cursor and any MCP server are allowed to do, review risky actions before they run, and keep every action on the record.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",