@buda-ai/connector 0.1.27 → 0.1.29

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 (2) hide show
  1. package/dist/cli.js +145 -34
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -523,7 +523,7 @@ import path2 from "path";
523
523
  // package.json
524
524
  var package_default = {
525
525
  name: "@buda-ai/connector",
526
- version: "0.1.27",
526
+ version: "0.1.29",
527
527
  private: false,
528
528
  type: "module",
529
529
  publishConfig: {
@@ -2215,6 +2215,13 @@ var ConnectorDeletedError = class extends Error {
2215
2215
  this.name = "ConnectorDeletedError";
2216
2216
  }
2217
2217
  };
2218
+ var ConnectorUnauthorizedError = class extends Error {
2219
+ constructor(message) {
2220
+ super(message);
2221
+ this.name = "ConnectorUnauthorizedError";
2222
+ }
2223
+ };
2224
+ var UNREGISTER_TIMEOUT_MS = 3e3;
2218
2225
  async function resolveConnectorScope(deviceId, launchConfig) {
2219
2226
  const serverUrl = normalizeServerOrigin(getServerUrl(launchConfig));
2220
2227
  const headers = { "Content-Type": "application/json" };
@@ -2301,14 +2308,15 @@ function buildAuthHeaders(config, launchConfig) {
2301
2308
  if (config.deviceToken) headers["X-Buda-Connector-Device-Token"] = config.deviceToken;
2302
2309
  return headers;
2303
2310
  }
2304
- async function postJson(serverUrl, pathname, body, config, launchConfig) {
2311
+ async function postJson(serverUrl, pathname, body, config, launchConfig, options = {}) {
2305
2312
  const response = await fetch(`${serverUrl}${pathname}`, {
2306
2313
  method: "POST",
2307
2314
  headers: {
2308
2315
  "Content-Type": "application/json",
2309
2316
  ...buildAuthHeaders(config, launchConfig)
2310
2317
  },
2311
- body: JSON.stringify(body)
2318
+ body: JSON.stringify(body),
2319
+ ...options.timeoutMs ? { signal: AbortSignal.timeout(options.timeoutMs) } : {}
2312
2320
  });
2313
2321
  if (!response.ok) {
2314
2322
  const text2 = await response.text().catch(() => "");
@@ -2328,6 +2336,9 @@ async function postJson(serverUrl, pathname, body, config, launchConfig) {
2328
2336
  if (errorCode === "connector_deleted") {
2329
2337
  throw new ConnectorDeletedError(errorMessage3 || "Connector no longer exists");
2330
2338
  }
2339
+ if (response.status === 401) {
2340
+ throw new ConnectorUnauthorizedError(errorMessage3 || "Connector authentication failed");
2341
+ }
2331
2342
  throw new Error(`${pathname} failed: ${response.status} ${errorMessage3}`);
2332
2343
  }
2333
2344
  return await response.json();
@@ -2403,7 +2414,8 @@ async function unregisterConnector(config, launchConfig) {
2403
2414
  connectorId: config.connectorId
2404
2415
  },
2405
2416
  config,
2406
- launchConfig
2417
+ launchConfig,
2418
+ { timeoutMs: UNREGISTER_TIMEOUT_MS }
2407
2419
  ).catch((error) => {
2408
2420
  void writeLog("warn", `unregister failed: ${error instanceof Error ? error.message : error}`);
2409
2421
  });
@@ -2601,6 +2613,7 @@ async function ensureStableConnectorRuntime() {
2601
2613
  // src/daemon-client.ts
2602
2614
  var START_TIMEOUT_MS = 45e3;
2603
2615
  var BOOTSTRAP_TIMEOUT_MS = 5 * 6e4;
2616
+ var GRACEFUL_STOP_TIMEOUT_MS = 3e3;
2604
2617
  var LOCK_ONLY_RECOVERY_MAX_AGE_MS = 5 * 6e4;
2605
2618
  var POLL_INTERVAL_MS = 250;
2606
2619
  var wait2 = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
@@ -2754,6 +2767,30 @@ var stopUnresponsiveDaemonProcess = async (pid) => {
2754
2767
  if (await waitForProcessExit(pid, 3e3)) return;
2755
2768
  throw new Error(`Timed-out Connector daemon PID ${pid} could not be stopped safely`);
2756
2769
  };
2770
+ var waitForDaemonRelease = async (config, status, configRoot = getConfigRoot()) => {
2771
+ const deadline = Date.now() + GRACEFUL_STOP_TIMEOUT_MS;
2772
+ while (Date.now() < deadline) {
2773
+ if (!isProcessAlive2(status.pid)) return true;
2774
+ const current = await readPersistedDaemonStatus(config, configRoot).catch(() => null);
2775
+ if (!current || current.pid !== status.pid || current.instanceId !== status.instanceId) {
2776
+ return true;
2777
+ }
2778
+ await wait2(50);
2779
+ }
2780
+ return !isProcessAlive2(status.pid);
2781
+ };
2782
+ var stopLiveDaemon = async (config, status, configRoot = getConfigRoot(), command = "stop") => {
2783
+ const stopStatus = await requestDaemonControl(config, command, configRoot);
2784
+ if (command === "stop-if-idle" && !stopStatus.idle) return false;
2785
+ if (await waitForDaemonRelease(config, status, configRoot)) return true;
2786
+ const current = await readPersistedDaemonStatus(config, configRoot).catch(() => null);
2787
+ if (!current || current.pid !== status.pid || current.instanceId !== status.instanceId) {
2788
+ return true;
2789
+ }
2790
+ await stopUnresponsiveDaemonProcess(status.pid);
2791
+ return true;
2792
+ };
2793
+ var shouldRestartUnhealthyDaemonForEnrollment = (status, explicitEnrollment) => explicitEnrollment && status.idle && status.heartbeatHealthy === false;
2757
2794
  var readDaemonLockOwnerPid = async (config) => {
2758
2795
  const lockPath = `${getConnectorDaemonLockTarget(
2759
2796
  config.serverUrl,
@@ -2817,6 +2854,12 @@ async function ensureConnectorDaemon(launchConfig, mode, options = {}) {
2817
2854
  if (existingConfig) {
2818
2855
  const existingStatus = await readLiveStatus(existingConfig);
2819
2856
  if (existingStatus) {
2857
+ if (shouldRestartUnhealthyDaemonForEnrollment(
2858
+ existingStatus,
2859
+ Boolean(options.explicitEnrollment)
2860
+ ) && await stopLiveDaemon(existingConfig, existingStatus, getConfigRoot(), "stop-if-idle")) {
2861
+ return await startInternalDaemonWithRecovery(launchConfig, mode, void 0, options);
2862
+ }
2820
2863
  const preparedRuntime = !shouldReplaceDaemonVersion(existingStatus.version, CONNECTOR_RUNTIME_VERSION) || !existingStatus.idle ? void 0 : await ensureStableConnectorRuntime();
2821
2864
  const replaced = await stopOlderIdleDaemon(existingConfig, existingStatus);
2822
2865
  if (!replaced) {
@@ -2937,7 +2980,7 @@ async function stopLocalConnectorDaemon(connectorId, serverUrl, configRoot = get
2937
2980
  if (!config) throw new Error(`Local Connector ${connectorId} was not found`);
2938
2981
  const status = await readLiveStatus(config, configRoot);
2939
2982
  if (!status) return;
2940
- await requestDaemonControl(config, "stop", configRoot);
2983
+ await stopLiveDaemon(config, status, configRoot);
2941
2984
  }
2942
2985
  async function stopAllLocalConnectorDaemons(configRoot = getConfigRoot()) {
2943
2986
  const configs = await listDeviceConfigs(configRoot);
@@ -2950,7 +2993,7 @@ ${config.connectorId}`, config);
2950
2993
  const results = await Promise.allSettled(
2951
2994
  [...identities.values()].map(async (config) => {
2952
2995
  const status = await readLiveStatus(config, configRoot);
2953
- if (status) await requestDaemonControl(config, "stop", configRoot);
2996
+ if (status) await stopLiveDaemon(config, status, configRoot);
2954
2997
  })
2955
2998
  );
2956
2999
  const failures = results.filter((result) => result.status === "rejected");
@@ -4661,6 +4704,7 @@ var isAlreadyExists3 = (error) => error instanceof Error && "code" in error && e
4661
4704
  // src/relay.ts
4662
4705
  var RELAY_PING_INTERVAL_MS = 2e4;
4663
4706
  var RELAY_PONG_TIMEOUT_MS = 1e4;
4707
+ var RELAY_ACTIVE_REQUEST_PONG_TIMEOUT_LIMIT = 2;
4664
4708
  var RELAY_HANDSHAKE_TIMEOUT_MS = 1e4;
4665
4709
  var RELAY_RECONNECT_MAX_MS = 3e4;
4666
4710
  var RELAY_RECONNECT_JITTER = 0.2;
@@ -4766,6 +4810,7 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
4766
4810
  socket = ws;
4767
4811
  const inflight = /* @__PURE__ */ new Map();
4768
4812
  currentInflight = inflight;
4813
+ let consecutivePongTimeouts = 0;
4769
4814
  let detached = false;
4770
4815
  const detach = () => {
4771
4816
  if (detached) return;
@@ -4778,6 +4823,7 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
4778
4823
  void writeLog("info", `relay connected ${relayUrl}`);
4779
4824
  hadOpenConnection = true;
4780
4825
  reconnectDelayMs = RELAY_RECONNECT_INTERVAL_MS;
4826
+ consecutivePongTimeouts = 0;
4781
4827
  pingTimer = setInterval(() => {
4782
4828
  if (ws.readyState !== WebSocket2.OPEN) return;
4783
4829
  try {
@@ -4789,18 +4835,33 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
4789
4835
  }
4790
4836
  if (pongTimer) clearTimeout(pongTimer);
4791
4837
  pongTimer = setTimeout(() => {
4838
+ pongTimer = null;
4839
+ consecutivePongTimeouts += 1;
4840
+ if (inflight.size > 0 && consecutivePongTimeouts < RELAY_ACTIVE_REQUEST_PONG_TIMEOUT_LIMIT) {
4841
+ void writeLog(
4842
+ "warn",
4843
+ `relay pong timeout deferred while ${inflight.size} request(s) are active`
4844
+ );
4845
+ return;
4846
+ }
4792
4847
  void writeLog("warn", "relay pong timeout, terminating");
4793
4848
  ws.terminate();
4794
4849
  }, RELAY_PONG_TIMEOUT_MS);
4795
4850
  }, RELAY_PING_INTERVAL_MS);
4796
4851
  });
4797
4852
  ws.on("pong", () => {
4853
+ consecutivePongTimeouts = 0;
4798
4854
  if (pongTimer) {
4799
4855
  clearTimeout(pongTimer);
4800
4856
  pongTimer = null;
4801
4857
  }
4802
4858
  });
4803
4859
  ws.on("message", (raw) => {
4860
+ consecutivePongTimeouts = 0;
4861
+ if (pongTimer) {
4862
+ clearTimeout(pongTimer);
4863
+ pongTimer = null;
4864
+ }
4804
4865
  void handleRelayMessage(
4805
4866
  ws,
4806
4867
  bunnyDaemon,
@@ -4976,7 +5037,7 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
4976
5037
  });
4977
5038
  executionIdentity = message.executionIdentity;
4978
5039
  } catch (error) {
4979
- sendRelayError(socket, message.requestId, error);
5040
+ await sendRelayError(socket, message.requestId, error).catch(() => void 0);
4980
5041
  return;
4981
5042
  }
4982
5043
  }
@@ -5062,7 +5123,7 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
5062
5123
  durationMs: Date.now() - codingRunStartedAt
5063
5124
  });
5064
5125
  }
5065
- sendRelayMessage(socket, {
5126
+ await sendRelayMessage(socket, {
5066
5127
  type: "start",
5067
5128
  requestId: message.requestId,
5068
5129
  status: response.status,
@@ -5082,7 +5143,7 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
5082
5143
  while (true) {
5083
5144
  const { value, done } = await reader.read();
5084
5145
  if (done) break;
5085
- sendRelayMessage(socket, {
5146
+ await sendRelayMessage(socket, {
5086
5147
  type: "chunk",
5087
5148
  requestId: message.requestId,
5088
5149
  data: binaryResponse ? Buffer.from(value).toString("base64") : decoder?.decode(value, { stream: true }) ?? "",
@@ -5091,7 +5152,7 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
5091
5152
  }
5092
5153
  const tail = decoder?.decode() ?? "";
5093
5154
  if (tail) {
5094
- sendRelayMessage(socket, {
5155
+ await sendRelayMessage(socket, {
5095
5156
  type: "chunk",
5096
5157
  requestId: message.requestId,
5097
5158
  data: tail
@@ -5102,7 +5163,7 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
5102
5163
  reader.releaseLock();
5103
5164
  }
5104
5165
  }
5105
- sendRelayMessage(socket, {
5166
+ await sendRelayMessage(socket, {
5106
5167
  type: "end",
5107
5168
  requestId: message.requestId
5108
5169
  });
@@ -5114,14 +5175,16 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
5114
5175
  });
5115
5176
  }
5116
5177
  } catch (error) {
5178
+ controller.abort();
5117
5179
  if (codingRunTrace && codingRunStartedAt !== void 0) {
5118
5180
  await writeLog("error", "relay request coding run error", {
5119
5181
  ...codingRunTrace,
5120
5182
  status: codingRunStatus,
5121
- durationMs: Date.now() - codingRunStartedAt
5183
+ durationMs: Date.now() - codingRunStartedAt,
5184
+ error: formatConnectorRelayError(error)
5122
5185
  });
5123
5186
  }
5124
- sendRelayError(socket, message.requestId, error);
5187
+ await sendRelayError(socket, message.requestId, error).catch(() => void 0);
5125
5188
  } finally {
5126
5189
  inflight.delete(message.requestId);
5127
5190
  }
@@ -5175,17 +5238,20 @@ var normalizeWorkspaceRequest = (request, mounts) => {
5175
5238
  if (!mount || relative === void 0) {
5176
5239
  throw new ConnectorRelayProtocolError("WORKSPACE_MISMATCH");
5177
5240
  }
5178
- const env = body.env && typeof body.env === "object" && !Array.isArray(body.env) ? body.env : {};
5241
+ const systemEnv = body.systemEnv && typeof body.systemEnv === "object" && !Array.isArray(body.systemEnv) ? body.systemEnv : {};
5242
+ const workspaceEnv = {
5243
+ BUDA_AGENT_ROOT: mounts.agent,
5244
+ BUDA_SPACE_ROOT: mounts.space,
5245
+ BUDA_SYSTEM_ROOT: mounts.system
5246
+ };
5179
5247
  return {
5180
5248
  ...request,
5181
5249
  body: {
5182
5250
  ...body,
5183
5251
  cwd: path12.join(mount, relative),
5184
- env: {
5185
- ...env,
5186
- BUDA_AGENT_ROOT: mounts.agent,
5187
- BUDA_SPACE_ROOT: mounts.space,
5188
- BUDA_SYSTEM_ROOT: mounts.system
5252
+ systemEnv: {
5253
+ ...systemEnv,
5254
+ ...workspaceEnv
5189
5255
  }
5190
5256
  }
5191
5257
  };
@@ -5217,17 +5283,24 @@ var normalizeConnectorFilesystemRequest = (request, mounts) => {
5217
5283
  }
5218
5284
  return request;
5219
5285
  };
5220
- function sendRelayError(socket, requestId, error) {
5221
- sendRelayMessage(socket, {
5286
+ async function sendRelayError(socket, requestId, error) {
5287
+ await sendRelayMessage(socket, {
5222
5288
  type: "error",
5223
5289
  requestId,
5224
5290
  error: error instanceof Error ? error.message : String(error),
5225
5291
  ...error instanceof ConnectorRelayProtocolError ? { code: error.code } : {}
5226
5292
  });
5227
5293
  }
5228
- function sendRelayMessage(socket, message) {
5229
- if (socket.readyState !== WebSocket2.OPEN) return;
5230
- socket.send(JSON.stringify(message));
5294
+ async function sendRelayMessage(socket, message) {
5295
+ if (socket.readyState !== WebSocket2.OPEN) {
5296
+ throw new Error(`Connector relay socket closed before ${message.type} could be sent.`);
5297
+ }
5298
+ await new Promise((resolve, reject) => {
5299
+ socket.send(JSON.stringify(message), (error) => {
5300
+ if (error) reject(error);
5301
+ else resolve();
5302
+ });
5303
+ });
5231
5304
  }
5232
5305
  function headersToRecord(headers) {
5233
5306
  const result = {};
@@ -5743,7 +5816,30 @@ import { mkdir as mkdir12 } from "fs/promises";
5743
5816
  import path15 from "path";
5744
5817
  var RECOVERED_DAEMON_STOP_TIMEOUT_MS = 1e4;
5745
5818
  var RECOVERED_DAEMON_STOP_POLL_MS = 100;
5819
+ var SHUTDOWN_FORCE_EXIT_MS = 1e4;
5820
+ var SHUTDOWN_STEP_TIMEOUT_MS = 2e3;
5746
5821
  var wait3 = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
5822
+ var runShutdownStep = async (label, operation) => {
5823
+ let timeout;
5824
+ try {
5825
+ await Promise.race([
5826
+ Promise.resolve().then(operation),
5827
+ new Promise((_, reject) => {
5828
+ timeout = setTimeout(
5829
+ () => reject(new Error(`${label} timed out after ${SHUTDOWN_STEP_TIMEOUT_MS}ms`)),
5830
+ SHUTDOWN_STEP_TIMEOUT_MS
5831
+ );
5832
+ })
5833
+ ]);
5834
+ } catch (error) {
5835
+ await writeLog(
5836
+ "warn",
5837
+ `${label} failed during shutdown: ${error instanceof Error ? error.message : String(error)}`
5838
+ ).catch(() => void 0);
5839
+ } finally {
5840
+ if (timeout) clearTimeout(timeout);
5841
+ }
5842
+ };
5747
5843
  async function stopRecoveredIdleDaemon(config, configRoot) {
5748
5844
  const stopStatus = await requestDaemonControl(config, "stop-if-idle", configRoot).catch(
5749
5845
  () => null
@@ -5863,18 +5959,30 @@ async function runConnectorDaemon(launchConfig, options = {}) {
5863
5959
  const shutdown = async (shutdownOptions = {}) => {
5864
5960
  if (shuttingDown) return;
5865
5961
  shuttingDown = true;
5962
+ const forceExit = options.exitOnShutdown === false ? void 0 : setTimeout(() => process.exit(0), SHUTDOWN_FORCE_EXIT_MS);
5866
5963
  if (interval) clearInterval(interval);
5867
5964
  relay.stop();
5868
5965
  try {
5869
- await closeAllBrowserSessions();
5870
- if (shutdownOptions.unregister !== false) {
5871
- await unregisterConnector(config, launchConfig);
5872
- }
5873
- await stopEmbeddedBunnyDaemon(activeBunnyDaemon);
5966
+ await Promise.all([
5967
+ runShutdownStep("browser session cleanup", closeAllBrowserSessions),
5968
+ ...shutdownOptions.unregister === false ? [] : [
5969
+ runShutdownStep(
5970
+ "connector unregister",
5971
+ () => unregisterConnector(config, launchConfig)
5972
+ )
5973
+ ],
5974
+ runShutdownStep(
5975
+ "embedded Bunny daemon shutdown",
5976
+ () => stopEmbeddedBunnyDaemon(activeBunnyDaemon)
5977
+ )
5978
+ ]);
5874
5979
  } finally {
5875
- await control?.close();
5876
- await daemonLock.release();
5877
- await restoreConsole?.();
5980
+ await Promise.all([
5981
+ runShutdownStep("control channel shutdown", async () => control?.close()),
5982
+ runShutdownStep("daemon lock release", () => daemonLock.release()),
5983
+ runShutdownStep("console capture restore", async () => restoreConsole?.())
5984
+ ]);
5985
+ if (forceExit) clearTimeout(forceExit);
5878
5986
  if (options.exitOnShutdown !== false) process.exit(0);
5879
5987
  }
5880
5988
  };
@@ -5889,8 +5997,11 @@ async function runConnectorDaemon(launchConfig, options = {}) {
5889
5997
  });
5890
5998
  }).catch((error) => {
5891
5999
  heartbeatHealthy = false;
5892
- if (error instanceof ConnectorDeletedError) {
5893
- void writeLog("warn", "connector deleted remotely; stopping daemon");
6000
+ if (error instanceof ConnectorDeletedError || error instanceof ConnectorUnauthorizedError) {
6001
+ void writeLog(
6002
+ "warn",
6003
+ error instanceof ConnectorDeletedError ? "connector deleted remotely; stopping daemon" : "connector authentication rejected remotely; stopping daemon"
6004
+ );
5894
6005
  void shutdown({ unregister: false });
5895
6006
  return;
5896
6007
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buda-ai/connector",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "publishConfig": {