@node9/proxy 1.10.3 → 1.11.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.
package/dist/index.js CHANGED
@@ -261,6 +261,11 @@ var ConfigFileSchema = import_zod.z.object({
261
261
  enabled: import_zod.z.boolean().optional(),
262
262
  threshold: import_zod.z.number().min(2).optional(),
263
263
  windowSeconds: import_zod.z.number().min(10).optional()
264
+ }).optional(),
265
+ skillPinning: import_zod.z.object({
266
+ enabled: import_zod.z.boolean().optional(),
267
+ mode: import_zod.z.enum(["warn", "block"]).optional(),
268
+ roots: import_zod.z.array(import_zod.z.string()).optional()
264
269
  }).optional()
265
270
  }).optional(),
266
271
  environments: import_zod.z.record(import_zod.z.object({ requireApproval: import_zod.z.boolean().optional() })).optional()
@@ -618,7 +623,8 @@ var DEFAULT_CONFIG = {
618
623
  }
619
624
  ],
620
625
  dlp: { enabled: true, scanIgnoredTools: true },
621
- loopDetection: { enabled: true, threshold: 5, windowSeconds: 120 }
626
+ loopDetection: { enabled: true, threshold: 5, windowSeconds: 120 },
627
+ skillPinning: { enabled: false, mode: "warn", roots: [] }
622
628
  },
623
629
  environments: {}
624
630
  };
@@ -741,7 +747,11 @@ function getConfig(cwd) {
741
747
  ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
742
748
  },
743
749
  dlp: { ...DEFAULT_CONFIG.policy.dlp },
744
- loopDetection: { ...DEFAULT_CONFIG.policy.loopDetection }
750
+ loopDetection: { ...DEFAULT_CONFIG.policy.loopDetection },
751
+ skillPinning: {
752
+ ...DEFAULT_CONFIG.policy.skillPinning,
753
+ roots: [...DEFAULT_CONFIG.policy.skillPinning.roots]
754
+ }
745
755
  };
746
756
  const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
747
757
  const applyLayer = (source) => {
@@ -794,6 +804,16 @@ function getConfig(cwd) {
794
804
  if (ld.windowSeconds !== void 0)
795
805
  mergedPolicy.loopDetection.windowSeconds = ld.windowSeconds;
796
806
  }
807
+ if (p.skillPinning && typeof p.skillPinning === "object") {
808
+ const sp = p.skillPinning;
809
+ if (sp.enabled !== void 0) mergedPolicy.skillPinning.enabled = sp.enabled;
810
+ if (sp.mode !== void 0) mergedPolicy.skillPinning.mode = sp.mode;
811
+ if (Array.isArray(sp.roots)) {
812
+ for (const r of sp.roots) {
813
+ if (typeof r === "string" && r.length > 0) mergedPolicy.skillPinning.roots.push(r);
814
+ }
815
+ }
816
+ }
797
817
  const envs = source.environments || {};
798
818
  for (const [envName, envConfig] of Object.entries(envs)) {
799
819
  if (envConfig && typeof envConfig === "object") {
@@ -845,6 +865,7 @@ function getConfig(cwd) {
845
865
  mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
846
866
  mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
847
867
  mergedPolicy.ignoredTools = [...new Set(mergedPolicy.ignoredTools)];
868
+ mergedPolicy.skillPinning.roots = [...new Set(mergedPolicy.skillPinning.roots)];
848
869
  mergedPolicy.snapshot.tools = [...new Set(mergedPolicy.snapshot.tools)];
849
870
  mergedPolicy.snapshot.onlyPaths = [...new Set(mergedPolicy.snapshot.onlyPaths)];
850
871
  mergedPolicy.snapshot.ignorePaths = [...new Set(mergedPolicy.snapshot.ignorePaths)];
@@ -1890,6 +1911,15 @@ var import_path9 = __toESM(require("path"));
1890
1911
  var import_os6 = __toESM(require("os"));
1891
1912
  var PAUSED_FILE = import_path9.default.join(import_os6.default.homedir(), ".node9", "PAUSED");
1892
1913
  var TRUST_FILE = import_path9.default.join(import_os6.default.homedir(), ".node9", "trust.json");
1914
+ function extractCommandPattern(toolName, args) {
1915
+ const lower = toolName.toLowerCase();
1916
+ if (lower !== "bash" && lower !== "execute_bash" && lower !== "shell") return void 0;
1917
+ const a = args;
1918
+ const cmd = typeof a?.["command"] === "string" ? a["command"].trim() : "";
1919
+ if (!cmd) return void 0;
1920
+ const words = cmd.split(/\s+/);
1921
+ return words.slice(0, 2).join(" ");
1922
+ }
1893
1923
  function checkPause() {
1894
1924
  try {
1895
1925
  if (!import_fs7.default.existsSync(PAUSED_FILE)) return { paused: false };
@@ -1913,7 +1943,7 @@ function atomicWriteSync(filePath, data, options) {
1913
1943
  import_fs7.default.writeFileSync(tmpPath, data, options);
1914
1944
  import_fs7.default.renameSync(tmpPath, filePath);
1915
1945
  }
1916
- function getActiveTrustSession(toolName) {
1946
+ function getActiveTrustSession(toolName, args) {
1917
1947
  try {
1918
1948
  if (!import_fs7.default.existsSync(TRUST_FILE)) return false;
1919
1949
  const trust = JSON.parse(import_fs7.default.readFileSync(TRUST_FILE, "utf-8"));
@@ -1922,12 +1952,20 @@ function getActiveTrustSession(toolName) {
1922
1952
  if (active.length !== trust.entries.length) {
1923
1953
  import_fs7.default.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
1924
1954
  }
1925
- return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
1955
+ return active.some((e) => {
1956
+ if (!(e.tool === toolName || matchesPattern(toolName, e.tool))) return false;
1957
+ if (e.commandPattern) {
1958
+ const actual = extractCommandPattern(toolName, args) ?? "";
1959
+ return actual === e.commandPattern || actual.startsWith(e.commandPattern + " ");
1960
+ }
1961
+ return true;
1962
+ });
1926
1963
  } catch {
1927
1964
  return false;
1928
1965
  }
1929
1966
  }
1930
- function writeTrustSession(toolName, durationMs) {
1967
+ function writeTrustSession(toolName, durationMs, args) {
1968
+ const commandPattern = extractCommandPattern(toolName, args);
1931
1969
  try {
1932
1970
  let trust = { entries: [] };
1933
1971
  try {
@@ -1937,8 +1975,14 @@ function writeTrustSession(toolName, durationMs) {
1937
1975
  } catch {
1938
1976
  }
1939
1977
  const now = Date.now();
1940
- trust.entries = trust.entries.filter((e) => e.tool !== toolName && e.expiry > now);
1941
- trust.entries.push({ tool: toolName, expiry: now + durationMs });
1978
+ trust.entries = trust.entries.filter(
1979
+ (e) => !(e.tool === toolName && e.commandPattern === commandPattern) && e.expiry > now
1980
+ );
1981
+ trust.entries.push({
1982
+ tool: toolName,
1983
+ ...commandPattern && { commandPattern },
1984
+ expiry: now + durationMs
1985
+ });
1942
1986
  atomicWriteSync(TRUST_FILE, JSON.stringify(trust, null, 2));
1943
1987
  } catch (err) {
1944
1988
  if (process.env.NODE9_DEBUG === "1") {
@@ -2900,12 +2944,6 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2900
2944
  };
2901
2945
  }
2902
2946
  }
2903
- if (getActiveTrustSession(toolName)) {
2904
- if (approvers.cloud && creds?.apiKey)
2905
- await auditLocalAllow(toolName, args, "trust", creds, meta);
2906
- if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta, hashAuditArgs);
2907
- return { approved: true, checkedBy: "trust" };
2908
- }
2909
2947
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
2910
2948
  if (policyResult.decision === "allow") {
2911
2949
  if (approvers.cloud && creds?.apiKey)
@@ -2987,6 +3025,12 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2987
3025
  if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta, hashAuditArgs);
2988
3026
  return { approved: true };
2989
3027
  }
3028
+ if (!taintWarning && getActiveTrustSession(toolName, args)) {
3029
+ if (approvers.cloud && creds?.apiKey)
3030
+ await auditLocalAllow(toolName, args, "trust", creds, meta);
3031
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta, hashAuditArgs);
3032
+ return { approved: true, checkedBy: "trust" };
3033
+ }
2990
3034
  if (taintWarning) {
2991
3035
  explainableLabel = "\u{1F534} Node9 Taint (Exfiltration Prevention)";
2992
3036
  riskMetadata = computeRiskMetadata(
@@ -3119,7 +3163,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3119
3163
  riskMetadata?.ruleDescription
3120
3164
  );
3121
3165
  if (decision === "always_allow") {
3122
- writeTrustSession(toolName, 36e5);
3166
+ writeTrustSession(toolName, 36e5, args);
3123
3167
  return { approved: true, checkedBy: "trust" };
3124
3168
  }
3125
3169
  const isApproved = decision === "allow";
package/dist/index.mjs CHANGED
@@ -231,6 +231,11 @@ var ConfigFileSchema = z.object({
231
231
  enabled: z.boolean().optional(),
232
232
  threshold: z.number().min(2).optional(),
233
233
  windowSeconds: z.number().min(10).optional()
234
+ }).optional(),
235
+ skillPinning: z.object({
236
+ enabled: z.boolean().optional(),
237
+ mode: z.enum(["warn", "block"]).optional(),
238
+ roots: z.array(z.string()).optional()
234
239
  }).optional()
235
240
  }).optional(),
236
241
  environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
@@ -588,7 +593,8 @@ var DEFAULT_CONFIG = {
588
593
  }
589
594
  ],
590
595
  dlp: { enabled: true, scanIgnoredTools: true },
591
- loopDetection: { enabled: true, threshold: 5, windowSeconds: 120 }
596
+ loopDetection: { enabled: true, threshold: 5, windowSeconds: 120 },
597
+ skillPinning: { enabled: false, mode: "warn", roots: [] }
592
598
  },
593
599
  environments: {}
594
600
  };
@@ -711,7 +717,11 @@ function getConfig(cwd) {
711
717
  ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
712
718
  },
713
719
  dlp: { ...DEFAULT_CONFIG.policy.dlp },
714
- loopDetection: { ...DEFAULT_CONFIG.policy.loopDetection }
720
+ loopDetection: { ...DEFAULT_CONFIG.policy.loopDetection },
721
+ skillPinning: {
722
+ ...DEFAULT_CONFIG.policy.skillPinning,
723
+ roots: [...DEFAULT_CONFIG.policy.skillPinning.roots]
724
+ }
715
725
  };
716
726
  const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
717
727
  const applyLayer = (source) => {
@@ -764,6 +774,16 @@ function getConfig(cwd) {
764
774
  if (ld.windowSeconds !== void 0)
765
775
  mergedPolicy.loopDetection.windowSeconds = ld.windowSeconds;
766
776
  }
777
+ if (p.skillPinning && typeof p.skillPinning === "object") {
778
+ const sp = p.skillPinning;
779
+ if (sp.enabled !== void 0) mergedPolicy.skillPinning.enabled = sp.enabled;
780
+ if (sp.mode !== void 0) mergedPolicy.skillPinning.mode = sp.mode;
781
+ if (Array.isArray(sp.roots)) {
782
+ for (const r of sp.roots) {
783
+ if (typeof r === "string" && r.length > 0) mergedPolicy.skillPinning.roots.push(r);
784
+ }
785
+ }
786
+ }
767
787
  const envs = source.environments || {};
768
788
  for (const [envName, envConfig] of Object.entries(envs)) {
769
789
  if (envConfig && typeof envConfig === "object") {
@@ -815,6 +835,7 @@ function getConfig(cwd) {
815
835
  mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
816
836
  mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
817
837
  mergedPolicy.ignoredTools = [...new Set(mergedPolicy.ignoredTools)];
838
+ mergedPolicy.skillPinning.roots = [...new Set(mergedPolicy.skillPinning.roots)];
818
839
  mergedPolicy.snapshot.tools = [...new Set(mergedPolicy.snapshot.tools)];
819
840
  mergedPolicy.snapshot.onlyPaths = [...new Set(mergedPolicy.snapshot.onlyPaths)];
820
841
  mergedPolicy.snapshot.ignorePaths = [...new Set(mergedPolicy.snapshot.ignorePaths)];
@@ -1860,6 +1881,15 @@ import path9 from "path";
1860
1881
  import os6 from "os";
1861
1882
  var PAUSED_FILE = path9.join(os6.homedir(), ".node9", "PAUSED");
1862
1883
  var TRUST_FILE = path9.join(os6.homedir(), ".node9", "trust.json");
1884
+ function extractCommandPattern(toolName, args) {
1885
+ const lower = toolName.toLowerCase();
1886
+ if (lower !== "bash" && lower !== "execute_bash" && lower !== "shell") return void 0;
1887
+ const a = args;
1888
+ const cmd = typeof a?.["command"] === "string" ? a["command"].trim() : "";
1889
+ if (!cmd) return void 0;
1890
+ const words = cmd.split(/\s+/);
1891
+ return words.slice(0, 2).join(" ");
1892
+ }
1863
1893
  function checkPause() {
1864
1894
  try {
1865
1895
  if (!fs7.existsSync(PAUSED_FILE)) return { paused: false };
@@ -1883,7 +1913,7 @@ function atomicWriteSync(filePath, data, options) {
1883
1913
  fs7.writeFileSync(tmpPath, data, options);
1884
1914
  fs7.renameSync(tmpPath, filePath);
1885
1915
  }
1886
- function getActiveTrustSession(toolName) {
1916
+ function getActiveTrustSession(toolName, args) {
1887
1917
  try {
1888
1918
  if (!fs7.existsSync(TRUST_FILE)) return false;
1889
1919
  const trust = JSON.parse(fs7.readFileSync(TRUST_FILE, "utf-8"));
@@ -1892,12 +1922,20 @@ function getActiveTrustSession(toolName) {
1892
1922
  if (active.length !== trust.entries.length) {
1893
1923
  fs7.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
1894
1924
  }
1895
- return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
1925
+ return active.some((e) => {
1926
+ if (!(e.tool === toolName || matchesPattern(toolName, e.tool))) return false;
1927
+ if (e.commandPattern) {
1928
+ const actual = extractCommandPattern(toolName, args) ?? "";
1929
+ return actual === e.commandPattern || actual.startsWith(e.commandPattern + " ");
1930
+ }
1931
+ return true;
1932
+ });
1896
1933
  } catch {
1897
1934
  return false;
1898
1935
  }
1899
1936
  }
1900
- function writeTrustSession(toolName, durationMs) {
1937
+ function writeTrustSession(toolName, durationMs, args) {
1938
+ const commandPattern = extractCommandPattern(toolName, args);
1901
1939
  try {
1902
1940
  let trust = { entries: [] };
1903
1941
  try {
@@ -1907,8 +1945,14 @@ function writeTrustSession(toolName, durationMs) {
1907
1945
  } catch {
1908
1946
  }
1909
1947
  const now = Date.now();
1910
- trust.entries = trust.entries.filter((e) => e.tool !== toolName && e.expiry > now);
1911
- trust.entries.push({ tool: toolName, expiry: now + durationMs });
1948
+ trust.entries = trust.entries.filter(
1949
+ (e) => !(e.tool === toolName && e.commandPattern === commandPattern) && e.expiry > now
1950
+ );
1951
+ trust.entries.push({
1952
+ tool: toolName,
1953
+ ...commandPattern && { commandPattern },
1954
+ expiry: now + durationMs
1955
+ });
1912
1956
  atomicWriteSync(TRUST_FILE, JSON.stringify(trust, null, 2));
1913
1957
  } catch (err) {
1914
1958
  if (process.env.NODE9_DEBUG === "1") {
@@ -2870,12 +2914,6 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2870
2914
  };
2871
2915
  }
2872
2916
  }
2873
- if (getActiveTrustSession(toolName)) {
2874
- if (approvers.cloud && creds?.apiKey)
2875
- await auditLocalAllow(toolName, args, "trust", creds, meta);
2876
- if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta, hashAuditArgs);
2877
- return { approved: true, checkedBy: "trust" };
2878
- }
2879
2917
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
2880
2918
  if (policyResult.decision === "allow") {
2881
2919
  if (approvers.cloud && creds?.apiKey)
@@ -2957,6 +2995,12 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2957
2995
  if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta, hashAuditArgs);
2958
2996
  return { approved: true };
2959
2997
  }
2998
+ if (!taintWarning && getActiveTrustSession(toolName, args)) {
2999
+ if (approvers.cloud && creds?.apiKey)
3000
+ await auditLocalAllow(toolName, args, "trust", creds, meta);
3001
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta, hashAuditArgs);
3002
+ return { approved: true, checkedBy: "trust" };
3003
+ }
2960
3004
  if (taintWarning) {
2961
3005
  explainableLabel = "\u{1F534} Node9 Taint (Exfiltration Prevention)";
2962
3006
  riskMetadata = computeRiskMetadata(
@@ -3089,7 +3133,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3089
3133
  riskMetadata?.ruleDescription
3090
3134
  );
3091
3135
  if (decision === "always_allow") {
3092
- writeTrustSession(toolName, 36e5);
3136
+ writeTrustSession(toolName, 36e5, args);
3093
3137
  return { approved: true, checkedBy: "trust" };
3094
3138
  }
3095
3139
  const isApproved = decision === "allow";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "1.10.3",
3
+ "version": "1.11.0",
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",