@node9/proxy 1.10.0 → 1.10.2

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
@@ -238,7 +238,8 @@ var ConfigFileSchema = import_zod.z.object({
238
238
  slackEnabled: import_zod.z.boolean().optional(),
239
239
  enableTrustSessions: import_zod.z.boolean().optional(),
240
240
  allowGlobalPause: import_zod.z.boolean().optional(),
241
- auditHashArgs: import_zod.z.boolean().optional()
241
+ auditHashArgs: import_zod.z.boolean().optional(),
242
+ agentPolicy: import_zod.z.enum(["require_approval", "block_on_rules"]).optional()
242
243
  }).optional(),
243
244
  policy: import_zod.z.object({
244
245
  sandboxPaths: import_zod.z.array(import_zod.z.string()).optional(),
@@ -1989,19 +1990,44 @@ function getInternalToken() {
1989
1990
  function isDaemonRunning() {
1990
1991
  const pidFile = import_path10.default.join(import_os7.default.homedir(), ".node9", "daemon.pid");
1991
1992
  if (import_fs8.default.existsSync(pidFile)) {
1993
+ let pid;
1994
+ let port;
1992
1995
  try {
1993
- const { pid, port } = JSON.parse(import_fs8.default.readFileSync(pidFile, "utf-8"));
1994
- if (port !== DAEMON_PORT) return false;
1995
- process.kill(pid, 0);
1996
- return true;
1996
+ const data = JSON.parse(import_fs8.default.readFileSync(pidFile, "utf-8"));
1997
+ pid = data.pid;
1998
+ port = data.port;
1997
1999
  } catch {
1998
2000
  return false;
1999
2001
  }
2002
+ if (port !== DAEMON_PORT) {
2003
+ return false;
2004
+ }
2005
+ const MAX_PID = 4194304;
2006
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || pid > MAX_PID) {
2007
+ return false;
2008
+ }
2009
+ try {
2010
+ process.kill(pid, 0);
2011
+ } catch (err) {
2012
+ if (err instanceof Error && "code" in err && err.code === "ESRCH") {
2013
+ try {
2014
+ import_fs8.default.unlinkSync(pidFile);
2015
+ } catch {
2016
+ }
2017
+ }
2018
+ return false;
2019
+ }
2020
+ const r = (0, import_child_process.spawnSync)("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
2021
+ encoding: "utf8",
2022
+ timeout: 300
2023
+ });
2024
+ if (r.status === 0 && (r.stdout ?? "").includes(`:${DAEMON_PORT}`)) return true;
2025
+ return false;
2000
2026
  }
2001
2027
  try {
2002
2028
  const r = (0, import_child_process.spawnSync)("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
2003
2029
  encoding: "utf8",
2004
- timeout: 500
2030
+ timeout: 300
2005
2031
  });
2006
2032
  return r.status === 0 && (r.stdout ?? "").includes(`:${DAEMON_PORT}`);
2007
2033
  } catch {
@@ -2321,11 +2347,12 @@ ${smartTruncate(str, 500)}`
2321
2347
  function escapePango(text) {
2322
2348
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2323
2349
  }
2324
- function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
2350
+ function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1, ruleDescription) {
2325
2351
  const lines = [];
2326
2352
  if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
2327
2353
  lines.push(`\u{1F916} ${agent || "AI Agent"} | \u{1F527} ${toolName}`);
2328
2354
  lines.push(`\u{1F6E1}\uFE0F ${explainableLabel || "Security Policy"}`);
2355
+ if (ruleDescription) lines.push(`\u2139 ${ruleDescription}`);
2329
2356
  lines.push("");
2330
2357
  lines.push(formattedArgs);
2331
2358
  if (allowCount >= 3) {
@@ -2338,7 +2365,7 @@ function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, loc
2338
2365
  }
2339
2366
  return lines.join("\n");
2340
2367
  }
2341
- function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
2368
+ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1, ruleDescription) {
2342
2369
  const lines = [];
2343
2370
  if (locked) {
2344
2371
  lines.push('<span foreground="red" weight="bold">\u26A0\uFE0F LOCKED BY ADMIN POLICY</span>');
@@ -2348,6 +2375,7 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
2348
2375
  `<b>\u{1F916} ${escapePango(agent || "AI Agent")}</b> | <b>\u{1F527} <tt>${escapePango(toolName)}</tt></b>`
2349
2376
  );
2350
2377
  lines.push(`<i>\u{1F6E1}\uFE0F ${escapePango(explainableLabel || "Security Policy")}</i>`);
2378
+ if (ruleDescription) lines.push(`<i>\u2139 ${escapePango(ruleDescription)}</i>`);
2351
2379
  lines.push("");
2352
2380
  lines.push(`<tt>${escapePango(formattedArgs)}</tt>`);
2353
2381
  if (allowCount >= 3) {
@@ -2364,7 +2392,7 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
2364
2392
  }
2365
2393
  return lines.join("\n");
2366
2394
  }
2367
- async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord, allowCount = 1) {
2395
+ async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord, allowCount = 1, ruleDescription) {
2368
2396
  if (isTestEnv()) return "deny";
2369
2397
  const { message: formattedArgs, intent } = formatArgs(args, matchedField, matchedWord);
2370
2398
  const intentLabel = intent === "EDIT" ? "Code Edit" : "Action Approval";
@@ -2375,7 +2403,8 @@ async function askNativePopup(toolName, args, agent, explainableLabel, locked =
2375
2403
  agent,
2376
2404
  explainableLabel,
2377
2405
  locked,
2378
- allowCount
2406
+ allowCount,
2407
+ ruleDescription
2379
2408
  );
2380
2409
  return new Promise((resolve) => {
2381
2410
  let childProcess = null;
@@ -2409,7 +2438,8 @@ end run`;
2409
2438
  agent,
2410
2439
  explainableLabel,
2411
2440
  locked,
2412
- allowCount
2441
+ allowCount,
2442
+ ruleDescription
2413
2443
  );
2414
2444
  const argsList = [
2415
2445
  locked ? "--info" : "--question",
@@ -2477,7 +2507,7 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
2477
2507
  }).catch(() => {
2478
2508
  });
2479
2509
  }
2480
- async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
2510
+ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPolicy, forceReview) {
2481
2511
  const controller = new AbortController();
2482
2512
  const timeout = setTimeout(() => controller.abort(), 1e4);
2483
2513
  if (!creds.apiKey) throw new Error("Node9 API Key is missing");
@@ -2522,7 +2552,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
2522
2552
  platform: import_os8.default.platform()
2523
2553
  },
2524
2554
  ...riskMetadata && { riskMetadata },
2525
- ...ciContext && { ciContext }
2555
+ ...ciContext && { ciContext },
2556
+ ...agentPolicy && { policy: agentPolicy },
2557
+ ...forceReview && { forceReview: true }
2526
2558
  }),
2527
2559
  signal: controller.signal
2528
2560
  });
@@ -2914,6 +2946,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2914
2946
  policyMatchedWord,
2915
2947
  policyResult.ruleName
2916
2948
  );
2949
+ if (policyRuleDescription) riskMetadata.ruleDescription = policyRuleDescription.slice(0, 200);
2917
2950
  const persistent = policyResult.ruleName ? null : getPersistentDecision(toolName);
2918
2951
  if (persistent === "allow") {
2919
2952
  if (approvers.cloud && creds?.apiKey)
@@ -2948,9 +2981,18 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2948
2981
  }
2949
2982
  let cloudRequestId = null;
2950
2983
  const cloudEnforced = approvers.cloud && !!creds?.apiKey;
2951
- if (cloudEnforced && !localSmartRuleMatched && !options?.localSmartRuleMatched) {
2984
+ const forceReview = localSmartRuleMatched === true || options?.localSmartRuleMatched === true || void 0;
2985
+ if (cloudEnforced) {
2952
2986
  try {
2953
- const initResult = await initNode9SaaS(toolName, args, creds, meta, riskMetadata);
2987
+ const initResult = await initNode9SaaS(
2988
+ toolName,
2989
+ args,
2990
+ creds,
2991
+ meta,
2992
+ riskMetadata,
2993
+ config.settings.agentPolicy,
2994
+ forceReview
2995
+ );
2954
2996
  if (!initResult.pending) {
2955
2997
  if (initResult.shadowMode) {
2956
2998
  return { approved: true, checkedBy: "cloud" };
@@ -2965,9 +3007,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2965
3007
  };
2966
3008
  }
2967
3009
  }
2968
- if (!localSmartRuleMatched && !options?.localSmartRuleMatched) {
2969
- cloudRequestId = initResult.requestId || null;
2970
- }
3010
+ if (initResult.pending) cloudRequestId = initResult.requestId || null;
2971
3011
  if (!taintWarning) explainableLabel = "Organization Policy (SaaS)";
2972
3012
  } catch {
2973
3013
  }
@@ -3024,7 +3064,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3024
3064
  }
3025
3065
  }
3026
3066
  }
3027
- if (cloudEnforced && cloudRequestId && !localSmartRuleMatched && !options?.localSmartRuleMatched) {
3067
+ if (cloudEnforced && cloudRequestId) {
3028
3068
  racePromises.push(
3029
3069
  (async () => {
3030
3070
  try {
@@ -3056,7 +3096,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3056
3096
  signal,
3057
3097
  policyMatchedField,
3058
3098
  policyMatchedWord,
3059
- daemonAllowCount
3099
+ daemonAllowCount,
3100
+ riskMetadata?.ruleDescription
3060
3101
  );
3061
3102
  if (decision === "always_allow") {
3062
3103
  writeTrustSession(toolName, 36e5);
package/dist/index.mjs CHANGED
@@ -208,7 +208,8 @@ var ConfigFileSchema = z.object({
208
208
  slackEnabled: z.boolean().optional(),
209
209
  enableTrustSessions: z.boolean().optional(),
210
210
  allowGlobalPause: z.boolean().optional(),
211
- auditHashArgs: z.boolean().optional()
211
+ auditHashArgs: z.boolean().optional(),
212
+ agentPolicy: z.enum(["require_approval", "block_on_rules"]).optional()
212
213
  }).optional(),
213
214
  policy: z.object({
214
215
  sandboxPaths: z.array(z.string()).optional(),
@@ -1959,19 +1960,44 @@ function getInternalToken() {
1959
1960
  function isDaemonRunning() {
1960
1961
  const pidFile = path10.join(os7.homedir(), ".node9", "daemon.pid");
1961
1962
  if (fs8.existsSync(pidFile)) {
1963
+ let pid;
1964
+ let port;
1962
1965
  try {
1963
- const { pid, port } = JSON.parse(fs8.readFileSync(pidFile, "utf-8"));
1964
- if (port !== DAEMON_PORT) return false;
1965
- process.kill(pid, 0);
1966
- return true;
1966
+ const data = JSON.parse(fs8.readFileSync(pidFile, "utf-8"));
1967
+ pid = data.pid;
1968
+ port = data.port;
1967
1969
  } catch {
1968
1970
  return false;
1969
1971
  }
1972
+ if (port !== DAEMON_PORT) {
1973
+ return false;
1974
+ }
1975
+ const MAX_PID = 4194304;
1976
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || pid > MAX_PID) {
1977
+ return false;
1978
+ }
1979
+ try {
1980
+ process.kill(pid, 0);
1981
+ } catch (err) {
1982
+ if (err instanceof Error && "code" in err && err.code === "ESRCH") {
1983
+ try {
1984
+ fs8.unlinkSync(pidFile);
1985
+ } catch {
1986
+ }
1987
+ }
1988
+ return false;
1989
+ }
1990
+ const r = spawnSync("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
1991
+ encoding: "utf8",
1992
+ timeout: 300
1993
+ });
1994
+ if (r.status === 0 && (r.stdout ?? "").includes(`:${DAEMON_PORT}`)) return true;
1995
+ return false;
1970
1996
  }
1971
1997
  try {
1972
1998
  const r = spawnSync("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
1973
1999
  encoding: "utf8",
1974
- timeout: 500
2000
+ timeout: 300
1975
2001
  });
1976
2002
  return r.status === 0 && (r.stdout ?? "").includes(`:${DAEMON_PORT}`);
1977
2003
  } catch {
@@ -2291,11 +2317,12 @@ ${smartTruncate(str, 500)}`
2291
2317
  function escapePango(text) {
2292
2318
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2293
2319
  }
2294
- function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
2320
+ function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1, ruleDescription) {
2295
2321
  const lines = [];
2296
2322
  if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
2297
2323
  lines.push(`\u{1F916} ${agent || "AI Agent"} | \u{1F527} ${toolName}`);
2298
2324
  lines.push(`\u{1F6E1}\uFE0F ${explainableLabel || "Security Policy"}`);
2325
+ if (ruleDescription) lines.push(`\u2139 ${ruleDescription}`);
2299
2326
  lines.push("");
2300
2327
  lines.push(formattedArgs);
2301
2328
  if (allowCount >= 3) {
@@ -2308,7 +2335,7 @@ function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, loc
2308
2335
  }
2309
2336
  return lines.join("\n");
2310
2337
  }
2311
- function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
2338
+ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1, ruleDescription) {
2312
2339
  const lines = [];
2313
2340
  if (locked) {
2314
2341
  lines.push('<span foreground="red" weight="bold">\u26A0\uFE0F LOCKED BY ADMIN POLICY</span>');
@@ -2318,6 +2345,7 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
2318
2345
  `<b>\u{1F916} ${escapePango(agent || "AI Agent")}</b> | <b>\u{1F527} <tt>${escapePango(toolName)}</tt></b>`
2319
2346
  );
2320
2347
  lines.push(`<i>\u{1F6E1}\uFE0F ${escapePango(explainableLabel || "Security Policy")}</i>`);
2348
+ if (ruleDescription) lines.push(`<i>\u2139 ${escapePango(ruleDescription)}</i>`);
2321
2349
  lines.push("");
2322
2350
  lines.push(`<tt>${escapePango(formattedArgs)}</tt>`);
2323
2351
  if (allowCount >= 3) {
@@ -2334,7 +2362,7 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
2334
2362
  }
2335
2363
  return lines.join("\n");
2336
2364
  }
2337
- async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord, allowCount = 1) {
2365
+ async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord, allowCount = 1, ruleDescription) {
2338
2366
  if (isTestEnv()) return "deny";
2339
2367
  const { message: formattedArgs, intent } = formatArgs(args, matchedField, matchedWord);
2340
2368
  const intentLabel = intent === "EDIT" ? "Code Edit" : "Action Approval";
@@ -2345,7 +2373,8 @@ async function askNativePopup(toolName, args, agent, explainableLabel, locked =
2345
2373
  agent,
2346
2374
  explainableLabel,
2347
2375
  locked,
2348
- allowCount
2376
+ allowCount,
2377
+ ruleDescription
2349
2378
  );
2350
2379
  return new Promise((resolve) => {
2351
2380
  let childProcess = null;
@@ -2379,7 +2408,8 @@ end run`;
2379
2408
  agent,
2380
2409
  explainableLabel,
2381
2410
  locked,
2382
- allowCount
2411
+ allowCount,
2412
+ ruleDescription
2383
2413
  );
2384
2414
  const argsList = [
2385
2415
  locked ? "--info" : "--question",
@@ -2447,7 +2477,7 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
2447
2477
  }).catch(() => {
2448
2478
  });
2449
2479
  }
2450
- async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
2480
+ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPolicy, forceReview) {
2451
2481
  const controller = new AbortController();
2452
2482
  const timeout = setTimeout(() => controller.abort(), 1e4);
2453
2483
  if (!creds.apiKey) throw new Error("Node9 API Key is missing");
@@ -2492,7 +2522,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
2492
2522
  platform: os8.platform()
2493
2523
  },
2494
2524
  ...riskMetadata && { riskMetadata },
2495
- ...ciContext && { ciContext }
2525
+ ...ciContext && { ciContext },
2526
+ ...agentPolicy && { policy: agentPolicy },
2527
+ ...forceReview && { forceReview: true }
2496
2528
  }),
2497
2529
  signal: controller.signal
2498
2530
  });
@@ -2884,6 +2916,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2884
2916
  policyMatchedWord,
2885
2917
  policyResult.ruleName
2886
2918
  );
2919
+ if (policyRuleDescription) riskMetadata.ruleDescription = policyRuleDescription.slice(0, 200);
2887
2920
  const persistent = policyResult.ruleName ? null : getPersistentDecision(toolName);
2888
2921
  if (persistent === "allow") {
2889
2922
  if (approvers.cloud && creds?.apiKey)
@@ -2918,9 +2951,18 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2918
2951
  }
2919
2952
  let cloudRequestId = null;
2920
2953
  const cloudEnforced = approvers.cloud && !!creds?.apiKey;
2921
- if (cloudEnforced && !localSmartRuleMatched && !options?.localSmartRuleMatched) {
2954
+ const forceReview = localSmartRuleMatched === true || options?.localSmartRuleMatched === true || void 0;
2955
+ if (cloudEnforced) {
2922
2956
  try {
2923
- const initResult = await initNode9SaaS(toolName, args, creds, meta, riskMetadata);
2957
+ const initResult = await initNode9SaaS(
2958
+ toolName,
2959
+ args,
2960
+ creds,
2961
+ meta,
2962
+ riskMetadata,
2963
+ config.settings.agentPolicy,
2964
+ forceReview
2965
+ );
2924
2966
  if (!initResult.pending) {
2925
2967
  if (initResult.shadowMode) {
2926
2968
  return { approved: true, checkedBy: "cloud" };
@@ -2935,9 +2977,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2935
2977
  };
2936
2978
  }
2937
2979
  }
2938
- if (!localSmartRuleMatched && !options?.localSmartRuleMatched) {
2939
- cloudRequestId = initResult.requestId || null;
2940
- }
2980
+ if (initResult.pending) cloudRequestId = initResult.requestId || null;
2941
2981
  if (!taintWarning) explainableLabel = "Organization Policy (SaaS)";
2942
2982
  } catch {
2943
2983
  }
@@ -2994,7 +3034,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2994
3034
  }
2995
3035
  }
2996
3036
  }
2997
- if (cloudEnforced && cloudRequestId && !localSmartRuleMatched && !options?.localSmartRuleMatched) {
3037
+ if (cloudEnforced && cloudRequestId) {
2998
3038
  racePromises.push(
2999
3039
  (async () => {
3000
3040
  try {
@@ -3026,7 +3066,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3026
3066
  signal,
3027
3067
  policyMatchedField,
3028
3068
  policyMatchedWord,
3029
- daemonAllowCount
3069
+ daemonAllowCount,
3070
+ riskMetadata?.ruleDescription
3030
3071
  );
3031
3072
  if (decision === "always_allow") {
3032
3073
  writeTrustSession(toolName, 36e5);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "1.10.0",
3
+ "version": "1.10.2",
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",