@wolpertingerlabs/drawlatch 1.0.0-alpha.9.4 → 1.0.0-alpha.9.5

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.
@@ -43,7 +43,14 @@ export declare class IngestorManager {
43
43
  private readonly config;
44
44
  /** Active ingestor instances, keyed by `callerAlias:connectionAlias:instanceId`. */
45
45
  private ingestors;
46
- constructor(config: RemoteServerConfig);
46
+ /**
47
+ * Optional config loader for hot-reload support. When provided, `startOne()`
48
+ * uses it to get fresh config from disk instead of the constructor snapshot.
49
+ */
50
+ private configLoader;
51
+ constructor(config: RemoteServerConfig, configLoader?: () => RemoteServerConfig);
52
+ /** Return fresh config if a loader is available, otherwise the constructor snapshot. */
53
+ private getConfig;
47
54
  /**
48
55
  * Start ingestors for all callers whose connections have an `ingestor` config.
49
56
  * Called once when the remote server starts listening.
@@ -48,8 +48,18 @@ export class IngestorManager {
48
48
  config;
49
49
  /** Active ingestor instances, keyed by `callerAlias:connectionAlias:instanceId`. */
50
50
  ingestors = new Map();
51
- constructor(config) {
51
+ /**
52
+ * Optional config loader for hot-reload support. When provided, `startOne()`
53
+ * uses it to get fresh config from disk instead of the constructor snapshot.
54
+ */
55
+ configLoader;
56
+ constructor(config, configLoader) {
52
57
  this.config = config;
58
+ this.configLoader = configLoader;
59
+ }
60
+ /** Return fresh config if a loader is available, otherwise the constructor snapshot. */
61
+ getConfig() {
62
+ return this.configLoader ? this.configLoader() : this.config;
53
63
  }
54
64
  /**
55
65
  * Start ingestors for all callers whose connections have an `ingestor` config.
@@ -228,7 +238,11 @@ export class IngestorManager {
228
238
  * When omitted, starts the default instance (or all instances if listenerInstances is defined).
229
239
  */
230
240
  async startOne(callerAlias, connectionAlias, instanceId) {
231
- const callerConfig = this.config.callers[callerAlias];
241
+ // Get fresh config (from disk in production, or constructor snapshot in tests)
242
+ // so we pick up changes made by tool handlers (e.g. set_connection_enabled,
243
+ // set_listener_params, set_secrets) without requiring a server restart.
244
+ const config = this.getConfig();
245
+ const callerConfig = config.callers[callerAlias];
232
246
  if (!callerConfig) {
233
247
  return {
234
248
  success: false,
@@ -244,7 +258,7 @@ export class IngestorManager {
244
258
  error: `Caller does not have connection: ${connectionAlias}`,
245
259
  };
246
260
  }
247
- const rawRoutes = resolveCallerRoutes(this.config, callerAlias);
261
+ const rawRoutes = resolveCallerRoutes(config, callerAlias);
248
262
  const callerEnvResolved = resolveSecrets(callerConfig.env ?? {});
249
263
  const resolvedRoutes = resolveRoutes(rawRoutes, callerEnvResolved, callerAlias);
250
264
  const rawRoute = rawRoutes[connectionIndex];
@@ -258,7 +272,7 @@ export class IngestorManager {
258
272
  }
259
273
  // If a specific instanceId is given, start just that one
260
274
  if (instanceId) {
261
- return this.startOneInstance(callerAlias, connectionAlias, instanceId, rawRoute, resolvedRoute);
275
+ return this.startOneInstance(callerAlias, connectionAlias, instanceId, rawRoute, resolvedRoute, config);
262
276
  }
263
277
  // If listenerInstances is defined, start all instances
264
278
  const instances = callerConfig.listenerInstances?.[connectionAlias];
@@ -267,15 +281,15 @@ export class IngestorManager {
267
281
  for (const [instId, instOverrides] of Object.entries(instances)) {
268
282
  if (instOverrides.disabled)
269
283
  continue;
270
- results.push(await this.startOneInstance(callerAlias, connectionAlias, instId, rawRoute, resolvedRoute));
284
+ results.push(await this.startOneInstance(callerAlias, connectionAlias, instId, rawRoute, resolvedRoute, config));
271
285
  }
272
286
  return results;
273
287
  }
274
288
  // Single default instance
275
- return this.startOneInstance(callerAlias, connectionAlias, undefined, rawRoute, resolvedRoute);
289
+ return this.startOneInstance(callerAlias, connectionAlias, undefined, rawRoute, resolvedRoute, config);
276
290
  }
277
291
  /** Internal: start a single specific instance. */
278
- async startOneInstance(callerAlias, connectionAlias, instanceId, rawRoute, resolvedRoute) {
292
+ async startOneInstance(callerAlias, connectionAlias, instanceId, rawRoute, resolvedRoute, config) {
279
293
  const key = makeKey(callerAlias, connectionAlias, instanceId ?? DEFAULT_INSTANCE_ID);
280
294
  // If already running, return current status
281
295
  const existing = this.ingestors.get(key);
@@ -289,10 +303,10 @@ export class IngestorManager {
289
303
  // Remove stopped instance to recreate
290
304
  this.ingestors.delete(key);
291
305
  }
292
- const callerConfig = this.config.callers[callerAlias];
306
+ const callerCfg = config.callers[callerAlias];
293
307
  const overrides = instanceId
294
- ? callerConfig.listenerInstances?.[connectionAlias]?.[instanceId]
295
- : callerConfig.ingestorOverrides?.[connectionAlias];
308
+ ? callerCfg.listenerInstances?.[connectionAlias]?.[instanceId]
309
+ : callerCfg.ingestorOverrides?.[connectionAlias];
296
310
  const effectiveConfig = IngestorManager.mergeIngestorConfig(rawRoute.ingestor, overrides);
297
311
  // Apply instance params
298
312
  const instanceSecrets = { ...resolvedRoute.secrets };
@@ -1030,8 +1030,14 @@ export function createApp(options = {}) {
1030
1030
  const ownKeys = options.ownKeys ?? loadKeyBundle(getServerKeysDir());
1031
1031
  const authorizedPeers = options.authorizedPeers ?? loadCallerPeers(config.callers);
1032
1032
  rateLimitPerMinute = config.rateLimitPerMinute;
1033
- // Create or use the provided ingestor manager
1034
- const ingestorManager = options.ingestorManager ?? new IngestorManager(config);
1033
+ // Create or use the provided ingestor manager.
1034
+ // When config is loaded from disk (production), pass loadRemoteConfig as the
1035
+ // config loader so startOne()/restartOne() read fresh config, picking up
1036
+ // changes made by tool handlers without requiring a server restart.
1037
+ // When config is injected via options (tests), omit the loader so the
1038
+ // IngestorManager uses the injected config snapshot.
1039
+ const configLoader = options.config ? undefined : loadRemoteConfig;
1040
+ const ingestorManager = options.ingestorManager ?? new IngestorManager(config, configLoader);
1035
1041
  app.locals.ingestorManager = ingestorManager;
1036
1042
  // Log connector and caller summary
1037
1043
  const connectorCount = config.connectors?.length ?? 0;
@@ -1076,9 +1082,12 @@ export function createApp(options = {}) {
1076
1082
  // Look up the caller alias by matching the returned PublicKeyBundle
1077
1083
  const matchedPeer = authorizedPeers.find((p) => p.keys === initiatorPubKey);
1078
1084
  const callerAlias = matchedPeer?.alias ?? 'unknown';
1085
+ // Reload config from disk so new sessions pick up changes made by tool
1086
+ // handlers (e.g. set_connection_enabled, set_secrets) without a restart.
1087
+ const freshConfig = options.config ?? loadRemoteConfig();
1079
1088
  // Resolve per-caller routes (with optional env overrides)
1080
- const callerRoutes = resolveCallerRoutes(config, callerAlias);
1081
- const caller = config.callers[callerAlias];
1089
+ const callerRoutes = resolveCallerRoutes(freshConfig, callerAlias);
1090
+ const caller = freshConfig.callers[callerAlias];
1082
1091
  const callerEnvResolved = resolveSecrets(caller.env ?? {});
1083
1092
  const callerResolvedRoutes = resolveRoutes(callerRoutes, callerEnvResolved, callerAlias);
1084
1093
  // Store pending handshake for the finish step
@@ -1316,19 +1325,21 @@ export function createApp(options = {}) {
1316
1325
  res.status(400).json({ error: 'INVALID_PAYLOAD', detail: `Invalid public keys: ${msg}` });
1317
1326
  return;
1318
1327
  }
1328
+ // Reload config from disk so we don't clobber changes made since startup
1329
+ const freshConfig = options.config ?? loadRemoteConfig();
1319
1330
  // Register caller in config if not already present
1320
- if (!(callerAlias in config.callers)) {
1321
- config.callers[callerAlias] = {
1331
+ if (!(callerAlias in freshConfig.callers)) {
1332
+ freshConfig.callers[callerAlias] = {
1322
1333
  connections: [],
1323
1334
  };
1324
- saveRemoteConfig(config);
1335
+ saveRemoteConfig(freshConfig);
1325
1336
  console.log(`[sync] Registered new caller "${callerAlias}" (0 connections — configure manually)`);
1326
1337
  }
1327
1338
  else {
1328
1339
  console.log(`[sync] Caller "${callerAlias}" already exists, updated peer keys`);
1329
1340
  }
1330
1341
  // Reload authorized peers so the new caller can connect immediately
1331
- const newPeer = loadCallerPeers({ [callerAlias]: config.callers[callerAlias] });
1342
+ const newPeer = loadCallerPeers({ [callerAlias]: freshConfig.callers[callerAlias] });
1332
1343
  for (const p of newPeer) {
1333
1344
  if (!authorizedPeers.find((existing) => existing.alias === p.alias)) {
1334
1345
  authorizedPeers.push(p);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wolpertingerlabs/drawlatch",
3
- "version": "1.0.0-alpha.9.4",
3
+ "version": "1.0.0-alpha.9.5",
4
4
  "description": "Encrypted MCP proxy with mutual authentication. Local MCP server forwards requests through an encrypted channel to a remote secrets-holding server.",
5
5
  "type": "module",
6
6
  "main": "./dist/mcp/server.js",