@juspay/neurolink 9.91.1 → 9.92.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.
@@ -855,7 +855,7 @@ function readPrimaryFromRouting(routing) {
855
855
  }
856
856
  /** Best-effort detection of a running proxy. Mirrors `proxy status` semantics
857
857
  * without importing the proxy module. */
858
- function detectRunningProxyPid() {
858
+ function detectRunningProxyState() {
859
859
  try {
860
860
  const stateFile = path.join(NEUROLINK_CONFIG_DIR, "proxy-state.json");
861
861
  if (!fs.existsSync(stateFile)) {
@@ -866,12 +866,33 @@ function detectRunningProxyPid() {
866
866
  return undefined;
867
867
  }
868
868
  process.kill(parsed.pid, 0);
869
- return parsed.pid;
869
+ return parsed;
870
870
  }
871
871
  catch {
872
872
  return undefined;
873
873
  }
874
874
  }
875
+ function reportRunningProxyConfigUpdate(filePath) {
876
+ const state = detectRunningProxyState();
877
+ if (!state) {
878
+ return;
879
+ }
880
+ logger.always("");
881
+ const editedPath = path.resolve(filePath);
882
+ const watchedPath = state.configFile
883
+ ? path.resolve(state.configFile)
884
+ : undefined;
885
+ if (watchedPath === editedPath) {
886
+ logger.always(chalk.green(`✓ Running proxy PID ${state.pid} is watching this file and will apply the change automatically.`));
887
+ logger.always(chalk.gray(" Verify the active generation with `neurolink proxy status`."));
888
+ return;
889
+ }
890
+ if (watchedPath) {
891
+ logger.always(chalk.yellow(`⚠ Running proxy PID ${state.pid} watches ${watchedPath}, not ${editedPath}; this change will not affect that process.`));
892
+ return;
893
+ }
894
+ logger.always(chalk.yellow(`⚠ Running proxy PID ${state.pid} does not report hot-reload support. Restart it to pick up the change.`));
895
+ }
875
896
  /**
876
897
  * Handle the set-primary subcommand
877
898
  * `neurolink auth set-primary <email> [--config <path>]`
@@ -910,13 +931,7 @@ export async function handleSetPrimary(argv) {
910
931
  " `neurolink auth login --add` to add it. The proxy will fall\n" +
911
932
  " back to the first enabled account until then."));
912
933
  }
913
- // Restart hint
914
- const pid = detectRunningProxyPid();
915
- if (pid) {
916
- logger.always("");
917
- logger.always(chalk.yellow(`⚠ A proxy is currently running (PID ${pid}). Restart it to pick\n` +
918
- " up the change: `neurolink proxy stop && neurolink proxy start`."));
919
- }
934
+ reportRunningProxyConfigUpdate(filePath);
920
935
  }
921
936
  catch (err) {
922
937
  logger.error(chalk.red("Failed to set primary account:"));
@@ -991,12 +1006,7 @@ export async function handleClearPrimary(argv) {
991
1006
  await writeProxyConfigFile(filePath, doc);
992
1007
  logger.always(chalk.green(`✓ Cleared primary account (was: ${before})`));
993
1008
  logger.always(chalk.green(`✓ Saved to ${filePath}`));
994
- const pid = detectRunningProxyPid();
995
- if (pid) {
996
- logger.always("");
997
- logger.always(chalk.yellow(`⚠ A proxy is currently running (PID ${pid}). Restart it to pick\n` +
998
- " up the change: `neurolink proxy stop && neurolink proxy start`."));
999
- }
1009
+ reportRunningProxyConfigUpdate(filePath);
1000
1010
  }
1001
1011
  catch (err) {
1002
1012
  logger.error(chalk.red("Failed to clear primary account:"));
@@ -10,12 +10,12 @@
10
10
  * (generate/stream), with an optional ModelRouter for model remapping.
11
11
  */
12
12
  import type { CommandModule } from "yargs";
13
- import type { AccountAllowlist, LoadedProxyConfig, ProxyGuardArgs, ProxyNeurolinkRuntime, ProxyStartArgs, ProxyStartStrategy, ProxyStatusArgs, ProxyTelemetryArgs } from "../../lib/types/index.js";
14
- import type { ModelRouter } from "../../lib/proxy/modelRouter.js";
13
+ import type { AccountAllowlist, LoadedProxyConfig, ModelRouterInterface, ProxyGuardArgs, ProxyNeurolinkRuntime, ProxyStartArgs, ProxyStartStrategy, ProxyStatusArgs, ProxyTelemetryArgs } from "../../lib/types/index.js";
14
+ import { ProxyRuntimeConfigStore } from "../../lib/proxy/runtimeConfig.js";
15
15
  export declare function mapClaudeErrorTypeToStatus(errorType?: string): number;
16
16
  export declare function createProxyStartApp(params: {
17
17
  neurolink: ProxyNeurolinkRuntime["neurolink"];
18
- modelRouter: ModelRouter | undefined;
18
+ modelRouter: ModelRouterInterface | undefined;
19
19
  strategy: ProxyStartStrategy;
20
20
  passthrough: boolean;
21
21
  port: number;
@@ -23,6 +23,7 @@ export declare function createProxyStartApp(params: {
23
23
  proxyConfig: LoadedProxyConfig | null;
24
24
  primaryAccountKey: string | undefined;
25
25
  accountAllowlist: AccountAllowlist | undefined;
26
+ runtimeConfigStore?: ProxyRuntimeConfigStore;
26
27
  }): Promise<{
27
28
  app: import("hono").Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
28
29
  readiness: import("../../lib/types/proxy.js").ProxyReadinessState;
@@ -18,6 +18,7 @@ import { buildProxyHealthResponse, createProxyReadinessState, markProxyReady, wa
18
18
  import { logger } from "../../lib/utils/logger.js";
19
19
  import { formatUptime, isProcessRunning, StateFileManager, } from "../utils/serverUtils.js";
20
20
  import { configureProxyKeepAliveDispatcher } from "../../lib/proxy/proxyDispatcher.js";
21
+ import { ProxyRuntimeConfigStore } from "../../lib/proxy/runtimeConfig.js";
21
22
  import { anthropicAccountKeysEqual, createAccountAllowlist, ENV_ANTHROPIC_ACCOUNT_KEY, isAccountAllowed, LEGACY_ANTHROPIC_ACCOUNT_KEY, normalizeAnthropicAccountKey, shouldLoadFallbackCredential, } from "../../lib/proxy/accountSelection.js";
22
23
  import { beginProxyRequest, getProxyActivitySnapshot, trackProxyResponse, } from "../../lib/proxy/proxyActivity.js";
23
24
  import { describeInstallFailure, getGlobalInstallArgs, resolveGlobalInstaller, validateInstalledVersion, } from "../../lib/proxy/globalInstaller.js";
@@ -709,107 +710,6 @@ async function createProxyNeurolinkRuntime(logsDir) {
709
710
  cleanupLogs(7, 500);
710
711
  return { neurolink, cleanupLogs };
711
712
  }
712
- async function loadProxyStartConfiguration(argv, spinner) {
713
- const configPath = argv.config ?? join(homedir(), ".neurolink", "proxy-config.yaml");
714
- let proxyConfig = null;
715
- try {
716
- const { loadProxyConfig } = await import("../../lib/proxy/proxyConfig.js");
717
- proxyConfig = (await loadProxyConfig(configPath));
718
- if (spinner) {
719
- spinner.text = `Loaded proxy config from ${configPath}`;
720
- }
721
- }
722
- catch (configError) {
723
- const isNotFound = configError instanceof Error &&
724
- "code" in configError &&
725
- configError.code === "ENOENT";
726
- if (argv.config || !isNotFound) {
727
- if (spinner) {
728
- spinner.fail(chalk.red(`Failed to load proxy config: ${configPath}`));
729
- }
730
- throw configError;
731
- }
732
- }
733
- const strategy = (argv.strategy ??
734
- proxyConfig?.routing?.strategy ??
735
- "fill-first");
736
- let modelRouter;
737
- if (proxyConfig?.routing) {
738
- const { ModelRouter } = await import("../../lib/proxy/modelRouter.js");
739
- modelRouter = new ModelRouter({
740
- strategy,
741
- modelMappings: proxyConfig.routing.modelMappings ?? [],
742
- fallbackChain: proxyConfig.routing.fallbackChain ?? [],
743
- passthroughModels: proxyConfig.routing.passthroughModels,
744
- });
745
- }
746
- const primaryAccountKey = await resolveBootPrimaryAccountKey(proxyConfig?.routing?.primaryAccount);
747
- const accountAllowlist = await resolveBootAccountAllowlist(proxyConfig?.routing?.accountAllowlist);
748
- if (primaryAccountKey &&
749
- !isAccountAllowed(primaryAccountKey, accountAllowlist)) {
750
- throw new Error(`Configured routing.primaryAccount=${proxyConfig?.routing?.primaryAccount} is excluded by routing.accountAllowlist`);
751
- }
752
- return {
753
- configPath,
754
- proxyConfig,
755
- strategy,
756
- modelRouter,
757
- passthrough: argv.passthrough ?? false,
758
- primaryAccountKey,
759
- accountAllowlist,
760
- };
761
- }
762
- async function resolveBootAccountAllowlist(configuredAccounts) {
763
- const allowlist = createAccountAllowlist(configuredAccounts);
764
- if (allowlist === undefined) {
765
- return undefined;
766
- }
767
- if (allowlist.size === 0) {
768
- logger.warn("[proxy] routing.accountAllowlist is empty; all stored Anthropic credentials are denied");
769
- return allowlist;
770
- }
771
- try {
772
- const { tokenStore } = await import("../../lib/auth/tokenStore.js");
773
- const known = new Set((await tokenStore.listByPrefix("anthropic:")).map(normalizeAnthropicAccountKey));
774
- known.add(LEGACY_ANTHROPIC_ACCOUNT_KEY);
775
- known.add(ENV_ANTHROPIC_ACCOUNT_KEY);
776
- for (const key of allowlist) {
777
- if (!known.has(key)) {
778
- logger.warn(`[proxy] WARN: routing.accountAllowlist entry ${key} is allowed but not currently authenticated`);
779
- }
780
- }
781
- }
782
- catch (err) {
783
- logger.debug(`[proxy] could not validate account allowlist against token store: ${err instanceof Error ? err.message : String(err)}`);
784
- }
785
- return allowlist;
786
- }
787
- /** Resolve the operator's configured primary email to a stable token-store
788
- * key (anthropic:<email>). Cross-checks the token store and emits a one-time
789
- * startup warning if the configured account isn't authenticated — but still
790
- * returns the key so it activates automatically once the user runs
791
- * `auth login --add`. */
792
- async function resolveBootPrimaryAccountKey(primaryEmail) {
793
- const trimmed = primaryEmail?.trim();
794
- if (!trimmed) {
795
- return undefined;
796
- }
797
- const key = normalizeAnthropicAccountKey(trimmed);
798
- try {
799
- const { tokenStore } = await import("../../lib/auth/tokenStore.js");
800
- const known = await tokenStore.listByPrefix("anthropic:");
801
- if (!known.some((knownKey) => anthropicAccountKeysEqual(knownKey, key))) {
802
- logger.warn(`[proxy] WARN: configured routing.primaryAccount=${trimmed} not ` +
803
- `found in token store; falling back to first enabled account. ` +
804
- `Run \`neurolink auth login --add\` to authenticate it, or ` +
805
- `\`neurolink auth clear-primary\` to remove the setting.`);
806
- }
807
- }
808
- catch (err) {
809
- logger.debug(`[proxy] could not validate primary account against token store: ${err instanceof Error ? err.message : String(err)}`);
810
- }
811
- return key;
812
- }
813
713
  export async function createProxyStartApp(params) {
814
714
  const { createClaudeProxyRoutes } = await import("../../lib/server/routes/claudeProxyRoutes.js");
815
715
  const { createOpenAIProxyRoutes } = await import("../../lib/server/routes/openaiProxyRoutes.js");
@@ -904,8 +804,17 @@ export async function createProxyStartApp(params) {
904
804
  throw error;
905
805
  }
906
806
  });
907
- const routeGroup = createClaudeProxyRoutes(params.modelRouter, "", params.strategy, params.passthrough, params.primaryAccountKey, params.accountAllowlist);
908
- const openaiRouteGroup = createOpenAIProxyRoutes(params.modelRouter, "", params.port);
807
+ const runtimeConfigStore = params.runtimeConfigStore;
808
+ const runtimeConfigProvider = runtimeConfigStore
809
+ ? () => runtimeConfigStore.getSnapshot()
810
+ : undefined;
811
+ const routeGroup = createClaudeProxyRoutes(params.modelRouter, "", params.strategy, params.passthrough, params.primaryAccountKey, runtimeConfigProvider
812
+ ? {
813
+ accountAllowlist: params.accountAllowlist,
814
+ runtimeConfigProvider,
815
+ }
816
+ : params.accountAllowlist);
817
+ const openaiRouteGroup = createOpenAIProxyRoutes(params.modelRouter, "", params.port, runtimeConfigProvider);
909
818
  const allProxyRoutes = [...routeGroup.routes, ...openaiRouteGroup.routes];
910
819
  for (const route of allProxyRoutes) {
911
820
  const method = route.method.toLowerCase();
@@ -1032,12 +941,31 @@ export async function createProxyStartApp(params) {
1032
941
  return c.json(result ?? {});
1033
942
  });
1034
943
  }
1035
- app.get("/health", (c) => c.json(buildProxyHealthResponse(readiness, {
1036
- strategy: params.strategy,
1037
- passthrough: params.passthrough,
1038
- version: PROXY_VERSION,
1039
- })));
944
+ app.get("/health", (c) => {
945
+ const runtimeConfig = params.runtimeConfigStore?.getSnapshot();
946
+ return c.json(buildProxyHealthResponse(readiness, {
947
+ strategy: runtimeConfig ? runtimeConfig.strategy : params.strategy,
948
+ passthrough: runtimeConfig
949
+ ? runtimeConfig.passthrough
950
+ : params.passthrough,
951
+ version: PROXY_VERSION,
952
+ }));
953
+ });
1040
954
  app.get("/status", async (c) => {
955
+ const runtimeConfig = params.runtimeConfigStore?.getSnapshot();
956
+ const runtimeConfigStatus = params.runtimeConfigStore?.getStatus();
957
+ const activeStrategy = runtimeConfig
958
+ ? runtimeConfig.strategy
959
+ : params.strategy;
960
+ const activePassthrough = runtimeConfig
961
+ ? runtimeConfig.passthrough
962
+ : params.passthrough;
963
+ const activeProxyConfig = runtimeConfig
964
+ ? runtimeConfig.proxyConfig
965
+ : params.proxyConfig;
966
+ const activeAccountAllowlist = runtimeConfig
967
+ ? runtimeConfig.accountAllowlist
968
+ : params.accountAllowlist;
1041
969
  const { getStats } = await import("../../lib/proxy/usageStats.js");
1042
970
  const { loadAccountCooldowns } = await import("../../lib/proxy/accountCooldown.js");
1043
971
  const stats = getStats();
@@ -1056,11 +984,11 @@ export async function createProxyStartApp(params) {
1056
984
  }
1057
985
  const now = Date.now();
1058
986
  const health = buildProxyHealthResponse(readiness, {
1059
- strategy: params.strategy,
1060
- passthrough: params.passthrough,
987
+ strategy: activeStrategy,
988
+ passthrough: activePassthrough,
1061
989
  version: PROXY_VERSION,
1062
990
  });
1063
- const primaryAccount = await resolveStatusPrimaryAccount(params.proxyConfig);
991
+ const primaryAccount = await resolveStatusPrimaryAccount(activeProxyConfig);
1064
992
  return c.json({
1065
993
  status: "running",
1066
994
  ready: health.ready,
@@ -1069,7 +997,7 @@ export async function createProxyStartApp(params) {
1069
997
  pid: process.pid,
1070
998
  port: params.port,
1071
999
  host: params.host,
1072
- strategy: params.strategy,
1000
+ strategy: activeStrategy,
1073
1001
  uptime: process.uptime(),
1074
1002
  version: PROXY_VERSION,
1075
1003
  health,
@@ -1134,14 +1062,30 @@ export async function createProxyStartApp(params) {
1134
1062
  }
1135
1063
  : null,
1136
1064
  },
1137
- config: params.proxyConfig
1065
+ config: params.runtimeConfigStore
1138
1066
  ? {
1139
- hasRouting: !!params.proxyConfig.routing,
1140
- accountAllowlist: params.accountAllowlist
1141
- ? [...params.accountAllowlist]
1067
+ hasRouting: !!activeProxyConfig?.routing,
1068
+ accountAllowlist: activeAccountAllowlist
1069
+ ? [...activeAccountAllowlist]
1142
1070
  : null,
1071
+ generation: runtimeConfig?.generation ?? null,
1072
+ loadedAt: runtimeConfig?.loadedAt ?? null,
1073
+ hash: runtimeConfig?.configHash ?? null,
1074
+ watching: runtimeConfigStatus?.watching ?? false,
1075
+ lastReloadAttemptAt: runtimeConfigStatus?.lastReloadAttemptAt ?? null,
1076
+ lastReloadAt: runtimeConfigStatus?.lastReloadAt ?? null,
1077
+ lastReloadSource: runtimeConfigStatus?.lastReloadSource ?? null,
1078
+ lastReloadError: runtimeConfigStatus?.lastReloadError ?? null,
1079
+ consecutiveFailures: runtimeConfigStatus?.consecutiveFailures ?? 0,
1143
1080
  }
1144
- : null,
1081
+ : params.proxyConfig
1082
+ ? {
1083
+ hasRouting: !!params.proxyConfig.routing,
1084
+ accountAllowlist: params.accountAllowlist
1085
+ ? [...params.accountAllowlist]
1086
+ : null,
1087
+ }
1088
+ : null,
1145
1089
  });
1146
1090
  });
1147
1091
  return { app, readiness };
@@ -1321,13 +1265,13 @@ async function refreshProxyTokensInBackground(accountAllowlist) {
1321
1265
  // non-fatal
1322
1266
  }
1323
1267
  }
1324
- function startProxyBackgroundMaintenance(cleanupLogs, accountAllowlist) {
1268
+ function startProxyBackgroundMaintenance(cleanupLogs, getAccountAllowlist) {
1325
1269
  const refreshInterval = setInterval(() => {
1326
1270
  if (backgroundRefreshInProgress) {
1327
1271
  return;
1328
1272
  }
1329
1273
  backgroundRefreshInProgress = true;
1330
- void refreshProxyTokensInBackground(accountAllowlist)
1274
+ void refreshProxyTokensInBackground(getAccountAllowlist())
1331
1275
  .catch((error) => {
1332
1276
  logger.debug(`[proxy] background token refresh cycle failed: ${error instanceof Error ? error.message : String(error)}`);
1333
1277
  })
@@ -1385,6 +1329,7 @@ function registerProxyShutdownHandlers(params) {
1385
1329
  clearInterval(params.refreshInterval);
1386
1330
  clearInterval(params.logCleanupInterval);
1387
1331
  params.updaterSupervisor?.stop();
1332
+ params.stopRuntimeConfig?.();
1388
1333
  logger.always(`\nShutting down proxy (${signal})...`);
1389
1334
  let exitCode = signal === "SIGINT" ? 0 : 1;
1390
1335
  try {
@@ -1485,15 +1430,29 @@ async function startProxyRuntime(params) {
1485
1430
  })
1486
1431
  : undefined;
1487
1432
  const updaterPid = updaterSupervisor?.currentPid();
1488
- const fallbackChain = params.proxyConfig?.routing?.fallbackChain?.map((entry) => ({
1433
+ const initialRuntimeConfig = params.runtimeConfigStore?.getSnapshot();
1434
+ const activeStrategy = initialRuntimeConfig
1435
+ ? initialRuntimeConfig.strategy
1436
+ : params.strategy;
1437
+ const activeProxyConfig = initialRuntimeConfig
1438
+ ? initialRuntimeConfig.proxyConfig
1439
+ : params.proxyConfig;
1440
+ const activeAccountAllowlist = initialRuntimeConfig
1441
+ ? initialRuntimeConfig.accountAllowlist
1442
+ : params.accountAllowlist;
1443
+ const activePassthrough = initialRuntimeConfig
1444
+ ? initialRuntimeConfig.passthrough
1445
+ : params.passthrough;
1446
+ const fallbackChain = activeProxyConfig?.routing?.fallbackChain?.map((entry) => ({
1489
1447
  provider: entry.provider,
1490
1448
  model: entry.model,
1491
1449
  }));
1450
+ const initialConfigStatus = params.runtimeConfigStore?.getStatus();
1492
1451
  saveProxyState({
1493
1452
  pid: process.pid,
1494
1453
  port: params.port,
1495
1454
  host: params.host,
1496
- strategy: params.strategy,
1455
+ strategy: activeStrategy,
1497
1456
  startTime: new Date().toISOString(),
1498
1457
  ready: true,
1499
1458
  readyAt: params.readiness.readyAtMs
@@ -1503,28 +1462,73 @@ async function startProxyRuntime(params) {
1503
1462
  statusPath: "/status",
1504
1463
  envFile: params.loadedEnvFile,
1505
1464
  fallbackChain,
1506
- accountAllowlist: params.accountAllowlist
1507
- ? [...params.accountAllowlist]
1465
+ accountAllowlist: activeAccountAllowlist
1466
+ ? [...activeAccountAllowlist]
1508
1467
  : undefined,
1509
1468
  guardPid,
1510
1469
  updaterPid,
1511
1470
  managedBy: managedByLaunchd ? "launchd" : "manual",
1512
- passthrough: params.passthrough,
1471
+ passthrough: activePassthrough,
1472
+ configGeneration: initialRuntimeConfig?.generation,
1473
+ configLoadedAt: initialRuntimeConfig?.loadedAt,
1474
+ lastConfigReloadError: initialConfigStatus?.lastReloadError,
1475
+ configFile: initialConfigStatus?.configPath,
1513
1476
  });
1477
+ const persistRuntimeConfig = (snapshot) => {
1478
+ const state = loadProxyState();
1479
+ if (!state || state.pid !== process.pid) {
1480
+ return;
1481
+ }
1482
+ const status = params.runtimeConfigStore?.getStatus();
1483
+ const currentFallbackChain = snapshot.proxyConfig?.routing?.fallbackChain?.map((entry) => ({
1484
+ provider: entry.provider,
1485
+ model: entry.model,
1486
+ }));
1487
+ saveProxyState({
1488
+ ...state,
1489
+ strategy: snapshot.strategy,
1490
+ fallbackChain: currentFallbackChain,
1491
+ accountAllowlist: snapshot.accountAllowlist
1492
+ ? [...snapshot.accountAllowlist]
1493
+ : undefined,
1494
+ passthrough: snapshot.passthrough,
1495
+ configGeneration: snapshot.generation,
1496
+ configLoadedAt: snapshot.loadedAt,
1497
+ lastConfigReloadError: status?.lastReloadError,
1498
+ });
1499
+ };
1500
+ let stopRuntimeConfig;
1501
+ if (params.runtimeConfigStore) {
1502
+ const runtimeConfigStore = params.runtimeConfigStore;
1503
+ const unsubscribeReload = runtimeConfigStore.subscribeReload(() => {
1504
+ persistRuntimeConfig(runtimeConfigStore.getSnapshot());
1505
+ });
1506
+ const reloadOnSighup = () => {
1507
+ void runtimeConfigStore.reload("sighup");
1508
+ };
1509
+ runtimeConfigStore.startWatching();
1510
+ process.on("SIGHUP", reloadOnSighup);
1511
+ stopRuntimeConfig = () => {
1512
+ process.off("SIGHUP", reloadOnSighup);
1513
+ unsubscribeReload();
1514
+ runtimeConfigStore.stopWatching();
1515
+ };
1516
+ logger.always(`[proxy] watching configuration generation ${initialRuntimeConfig?.generation ?? 1}; send SIGHUP to reload immediately`);
1517
+ }
1514
1518
  if (params.spinner) {
1515
1519
  params.spinner.succeed(chalk.green("Claude proxy started successfully"));
1516
1520
  }
1517
1521
  const isDev = params.argv.dev ?? false;
1518
1522
  const normalizedHost = params.host === "0.0.0.0" ? "localhost" : params.host;
1519
1523
  const url = `http://${normalizedHost}:${params.port}`;
1520
- printProxyBanner(url, params.strategy);
1524
+ printProxyBanner(url, activeStrategy);
1521
1525
  if (isDev) {
1522
1526
  logger.always(` ${chalk.bold("Mode:")} ${chalk.magenta("dev (isolated — state in .neurolink-dev/)")}`);
1523
1527
  }
1524
1528
  else {
1525
- logger.always(` ${chalk.bold("Mode:")} ${chalk.cyan(params.passthrough ? "passthrough" : "full")}`);
1529
+ logger.always(` ${chalk.bold("Mode:")} ${chalk.cyan(activePassthrough ? "passthrough" : "full")}`);
1526
1530
  }
1527
- if (params.passthrough) {
1531
+ if (activePassthrough) {
1528
1532
  logger.always(chalk.yellow(" ! Passthrough mode forwards client auth directly to Anthropic"));
1529
1533
  logger.always(chalk.dim(" Stored proxy OAuth/API credentials are ignored; clients need their own valid Anthropic auth."));
1530
1534
  }
@@ -1554,13 +1558,16 @@ async function startProxyRuntime(params) {
1554
1558
  else {
1555
1559
  logger.always(chalk.dim(" ⊘ Dev mode: skipping client auto-configuration"));
1556
1560
  }
1557
- const maintenance = startProxyBackgroundMaintenance(params.cleanupLogs, params.accountAllowlist);
1561
+ const maintenance = startProxyBackgroundMaintenance(params.cleanupLogs, () => params.runtimeConfigStore
1562
+ ? params.runtimeConfigStore.getSnapshot().accountAllowlist
1563
+ : params.accountAllowlist);
1558
1564
  registerProxyShutdownHandlers({
1559
1565
  server,
1560
1566
  host: params.host,
1561
1567
  port: params.port,
1562
1568
  isDev,
1563
1569
  updaterSupervisor,
1570
+ stopRuntimeConfig,
1564
1571
  ...maintenance,
1565
1572
  });
1566
1573
  }
@@ -1587,13 +1594,34 @@ async function startProxyCommandHandler(argv) {
1587
1594
  if (!isDev) {
1588
1595
  await ensureProxyStartAllowed(spinner);
1589
1596
  }
1597
+ const baseEnv = { ...process.env };
1598
+ const envResolution = resolveProxyEnvFile({
1599
+ explicitEnvFile: argv.envFile,
1600
+ env: baseEnv,
1601
+ });
1590
1602
  const loadedEnvFile = await loadProxyStartEnv(argv, spinner);
1591
1603
  // Reuse upstream TCP connections (longer keep-alive + bounded pool) instead
1592
1604
  // of opening a new flow per request — cuts outbound flow churn through host
1593
1605
  // content-filters. Runs once, after env load so it can be tuned via env.
1594
1606
  configureProxyKeepAliveDispatcher();
1595
1607
  const { neurolink, cleanupLogs } = await createProxyNeurolinkRuntime(devPaths?.logsDir);
1596
- const { proxyConfig, strategy, modelRouter, passthrough, primaryAccountKey, accountAllowlist, } = await loadProxyStartConfiguration(argv, spinner);
1608
+ const configPath = argv.config
1609
+ ? resolve(argv.config)
1610
+ : join(homedir(), ".neurolink", "proxy-config.yaml");
1611
+ const runtimeConfigStore = await ProxyRuntimeConfigStore.create({
1612
+ configPath,
1613
+ configRequired: Boolean(argv.config),
1614
+ envFilePath: envResolution.path ?? join(homedir(), ".neurolink", ".env"),
1615
+ envFileRequired: envResolution.required,
1616
+ baseEnv,
1617
+ strategyOverride: argv.strategy,
1618
+ passthrough: argv.passthrough ?? false,
1619
+ });
1620
+ const initialConfig = runtimeConfigStore.getSnapshot();
1621
+ const { proxyConfig, strategy, modelRouter, passthrough, primaryAccountKey, accountAllowlist, } = initialConfig;
1622
+ if (spinner && proxyConfig) {
1623
+ spinner.text = `Loaded proxy config from ${configPath}`;
1624
+ }
1597
1625
  if (spinner) {
1598
1626
  spinner.text = "Configuring server...";
1599
1627
  }
@@ -1609,6 +1637,7 @@ async function startProxyCommandHandler(argv) {
1609
1637
  proxyConfig,
1610
1638
  primaryAccountKey,
1611
1639
  accountAllowlist,
1640
+ runtimeConfigStore,
1612
1641
  });
1613
1642
  await initializeProxyOpenTelemetry();
1614
1643
  if (spinner) {
@@ -1627,6 +1656,7 @@ async function startProxyCommandHandler(argv) {
1627
1656
  loadedEnvFile,
1628
1657
  passthrough,
1629
1658
  cleanupLogs,
1659
+ runtimeConfigStore,
1630
1660
  });
1631
1661
  }
1632
1662
  catch (error) {
@@ -1810,6 +1840,9 @@ export const proxyStatusCommand = {
1810
1840
  envFile: null,
1811
1841
  fallbackChain: null,
1812
1842
  accountAllowlist: null,
1843
+ configGeneration: null,
1844
+ configLoadedAt: null,
1845
+ lastConfigReloadError: null,
1813
1846
  autoUpdateEnabled: isProxyAutoUpdateEnabled(),
1814
1847
  updaterPid: null,
1815
1848
  updaterRunning: false,
@@ -1830,6 +1863,9 @@ export const proxyStatusCommand = {
1830
1863
  status.envFile = state.envFile ?? null;
1831
1864
  status.fallbackChain = state.fallbackChain ?? null;
1832
1865
  status.accountAllowlist = state.accountAllowlist ?? null;
1866
+ status.configGeneration = state.configGeneration ?? null;
1867
+ status.configLoadedAt = state.configLoadedAt ?? null;
1868
+ status.lastConfigReloadError = state.lastConfigReloadError ?? null;
1833
1869
  status.updaterPid = state.updaterPid ?? null;
1834
1870
  status.updaterRunning = state.updaterPid
1835
1871
  ? isProcessRunning(state.updaterPid)
@@ -1837,12 +1873,24 @@ export const proxyStatusCommand = {
1837
1873
  }
1838
1874
  // Fetch live stats before rendering (JSON or text)
1839
1875
  let liveStats = null;
1876
+ let liveConfig = null;
1840
1877
  if (status.running && status.url) {
1841
1878
  try {
1842
1879
  const statusResp = await fetch(`${status.url}/status`);
1843
1880
  if (statusResp.ok) {
1844
1881
  const statusData = (await statusResp.json());
1845
1882
  liveStats = statusData.stats;
1883
+ liveConfig = statusData.config;
1884
+ if (typeof liveConfig?.generation === "number") {
1885
+ status.configGeneration = liveConfig.generation;
1886
+ }
1887
+ if (typeof liveConfig?.loadedAt === "string") {
1888
+ status.configLoadedAt = liveConfig.loadedAt;
1889
+ }
1890
+ status.lastConfigReloadError =
1891
+ typeof liveConfig?.lastReloadError === "string"
1892
+ ? liveConfig.lastReloadError
1893
+ : null;
1846
1894
  }
1847
1895
  }
1848
1896
  catch {
@@ -1850,7 +1898,7 @@ export const proxyStatusCommand = {
1850
1898
  }
1851
1899
  }
1852
1900
  if (argv.format === "json") {
1853
- logger.always(JSON.stringify({ ...status, stats: liveStats }, null, 2));
1901
+ logger.always(JSON.stringify({ ...status, stats: liveStats, config: liveConfig }, null, 2));
1854
1902
  return;
1855
1903
  }
1856
1904
  // Text format
@@ -1864,6 +1912,15 @@ export const proxyStatusCommand = {
1864
1912
  logger.always(` ${chalk.bold("URL:")} ${chalk.cyan(status.url)}`);
1865
1913
  logger.always(` ${chalk.bold("Strategy:")} ${chalk.cyan(status.strategy)}`);
1866
1914
  logger.always(` ${chalk.bold("Mode:")} ${chalk.cyan(status.mode ?? "full")}`);
1915
+ if (status.configGeneration !== null) {
1916
+ logger.always(` ${chalk.bold("Config:")} ${chalk.cyan(`generation ${status.configGeneration}`)}` +
1917
+ (status.configLoadedAt
1918
+ ? chalk.gray(` (${status.configLoadedAt})`)
1919
+ : ""));
1920
+ }
1921
+ if (status.lastConfigReloadError) {
1922
+ logger.always(` ${chalk.bold("Config error:")} ${chalk.red(status.lastConfigReloadError)}`);
1923
+ }
1867
1924
  logger.always(` ${chalk.bold("Started:")} ${chalk.cyan(status.startTime)}`);
1868
1925
  logger.always(` ${chalk.bold("Uptime:")} ${chalk.cyan(formatUptime(status.uptime ?? 0))}`);
1869
1926
  logger.always(` ${chalk.bold("Auto-update:")} ${status.autoUpdateEnabled ? chalk.green(status.updaterRunning ? `enabled (PID ${status.updaterPid})` : "enabled (worker unavailable)") : chalk.yellow("disabled")}`);
@@ -550,6 +550,11 @@ export class BaseProvider {
550
550
  getToolPolicy(options) {
551
551
  return resolveToolPolicy({
552
552
  options: {
553
+ // Defense-in-depth: both current call sites already zero the tool
554
+ // record via shouldUseTools before the gate runs, but forwarding
555
+ // disableTools makes the gate self-sufficient for any future call
556
+ // site that forgets the upstream check.
557
+ disableTools: options.disableTools,
553
558
  toolFilter: options.toolFilter,
554
559
  excludeTools: options.excludeTools,
555
560
  enabledToolNames: options.enabledToolNames,
@@ -550,6 +550,11 @@ export class BaseProvider {
550
550
  getToolPolicy(options) {
551
551
  return resolveToolPolicy({
552
552
  options: {
553
+ // Defense-in-depth: both current call sites already zero the tool
554
+ // record via shouldUseTools before the gate runs, but forwarding
555
+ // disableTools makes the gate self-sufficient for any future call
556
+ // site that forgets the upstream check.
557
+ disableTools: options.disableTools,
553
558
  toolFilter: options.toolFilter,
554
559
  excludeTools: options.excludeTools,
555
560
  enabledToolNames: options.enabledToolNames,
@@ -5968,14 +5968,19 @@ Current user's request: ${currentInput}`;
5968
5968
  instanceConfig: this.toolsConfig,
5969
5969
  builtinToolNames: Object.keys(directAgentTools),
5970
5970
  });
5971
- const record = {};
5971
+ // Null prototype + own-property checks: a tool named "__proto__" must
5972
+ // become an own entry — with a plain `{}`, `"__proto__" in record` is
5973
+ // truthy via the prototype and the tool would be silently dropped from
5974
+ // the listing while surviving the native gate. Matches the hardening on
5975
+ // every other record in the tool-resolution pipeline.
5976
+ const record = Object.create(null);
5972
5977
  for (const tool of tools) {
5973
- if (!(tool.name in record)) {
5978
+ if (!Object.hasOwn(record, tool.name)) {
5974
5979
  record[tool.name] = tool;
5975
5980
  }
5976
5981
  }
5977
5982
  const gated = applyToolGate(record, policy);
5978
- const filtered = tools.filter((t) => t.name in gated);
5983
+ const filtered = tools.filter((t) => Object.hasOwn(gated, t.name));
5979
5984
  if (filtered.length !== tools.length) {
5980
5985
  logger.debug(`Tool info filtering applied for system prompt`, {
5981
5986
  beforeCount: tools.length,