@buda-ai/connector 0.1.10 → 0.1.12

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.
@@ -73,7 +73,7 @@ To show a file preview card (thumbnail + download button) inline in the current
73
73
 
74
74
  1. Call the `send-file` script with the file path (relative to `/agent`):
75
75
  ```bash
76
- node /system/.agents/skills/buda-machine-info/scripts/send-file.mjs uploads/photo.jpg
76
+ node "${BUDA_SYSTEM_ROOT:-/system}/.agents/skills/buda-machine-info/scripts/send-file.mjs" uploads/photo.jpg
77
77
  ```
78
78
  2. The script returns a `[send:uploads/photo.jpg]` marker if the file exists, or an empty string if it does not.
79
79
  3. Embed the returned marker directly in your reply — do not explain it to the user.
@@ -82,4 +82,3 @@ To show a file preview card (thumbnail + download button) inline in the current
82
82
  - Always call the script first to verify the file exists. Never emit `[send:…]` manually.
83
83
  - Path is relative to `/agent` (no leading slash): `uploads/photo.jpg` → `/agent/uploads/photo.jpg`.
84
84
  - You may also pass the full `/agent/…` path — the script normalises it automatically.
85
-
@@ -2,18 +2,20 @@
2
2
  /**
3
3
  * send-file.mjs
4
4
  *
5
- * Resolves a file path relative to /agent and returns a [send:] marker
5
+ * Resolves a file path relative to the Agent workspace and returns a [send:] marker
6
6
  * if the file exists, or an empty string if it does not.
7
7
  *
8
8
  * Usage:
9
9
  * node scripts/send-file.mjs <path>
10
10
  *
11
11
  * Path rules (mirrors the system prompt convention):
12
- * - Relative path (no leading slash): resolved against /agent
12
+ * - Relative path (no leading slash): resolved against the Agent workspace
13
13
  * e.g. "uploads/photo.jpg" → /agent/uploads/photo.jpg
14
- * - Absolute path starting with /agent: used as-is
14
+ * - Logical absolute path starting with /agent: resolved against the Agent workspace
15
15
  * e.g. "/agent/chart.png" → /agent/chart.png
16
- * - Any other absolute path: used as-is (escape hatch)
16
+ * - Connector physical absolute path: accepted only when it is inside BUDA_AGENT_ROOT
17
+ * - Connector paths outside the Agent workspace: rejected
18
+ * - Without BUDA_AGENT_ROOT, the existing Sandbox /agent behavior is preserved
17
19
  *
18
20
  * Output:
19
21
  * [send:uploads/photo.jpg] — file exists (path is relative to /agent)
@@ -21,42 +23,60 @@
21
23
  */
22
24
 
23
25
  import { existsSync } from "node:fs";
24
- import { isAbsolute, resolve } from "node:path";
26
+ import { isAbsolute, relative, resolve, sep } from "node:path";
27
+ import { fileURLToPath } from "node:url";
25
28
 
26
- const AGENT_ROOT = "/agent";
29
+ const LOGICAL_AGENT_ROOT = "/agent";
27
30
 
28
- function resolveAgentPath(input) {
31
+ function resolveSandboxPath(input) {
32
+ if (input.startsWith(`${LOGICAL_AGENT_ROOT}/`)) {
33
+ return { abs: input, rel: input.slice(`${LOGICAL_AGENT_ROOT}/`.length) };
34
+ }
35
+ if (isAbsolute(input)) {
36
+ return { abs: input, rel: input };
37
+ }
38
+ return { abs: resolve(LOGICAL_AGENT_ROOT, input), rel: input };
39
+ }
40
+
41
+ function resolveConnectorPath(input, agentRoot) {
29
42
  if (!input) return null;
43
+ const physicalRoot = resolve(agentRoot);
44
+ let absolutePath;
30
45
 
31
- // Strip a leading /agent prefix so the returned marker stays relative
32
- if (input.startsWith("/agent/")) {
33
- return { abs: input, rel: input.slice("/agent/".length) };
46
+ if (input === LOGICAL_AGENT_ROOT || input.startsWith(`${LOGICAL_AGENT_ROOT}/`)) {
47
+ const logicalRelative = input.slice(LOGICAL_AGENT_ROOT.length).replace(/^\/+/, "");
48
+ absolutePath = resolve(physicalRoot, logicalRelative);
49
+ } else {
50
+ absolutePath = isAbsolute(input) ? resolve(input) : resolve(physicalRoot, input);
34
51
  }
35
52
 
36
- // Other absolute paths — use as-is, keep the full path in the marker
37
- if (isAbsolute(input)) {
38
- return { abs: input, rel: input };
53
+ const relativePath = relative(physicalRoot, absolutePath);
54
+ if (
55
+ !relativePath ||
56
+ relativePath === ".." ||
57
+ relativePath.startsWith(`..${sep}`) ||
58
+ isAbsolute(relativePath)
59
+ ) {
60
+ return null;
39
61
  }
40
62
 
41
- // Relative path — resolve against /agent
42
- const abs = resolve(AGENT_ROOT, input);
43
- return { abs, rel: input };
63
+ return { abs: absolutePath, rel: relativePath.split(sep).join("/") };
44
64
  }
45
65
 
46
- const input = process.argv[2];
47
-
48
- if (!input) {
49
- // No argument — print nothing (empty result)
50
- process.exit(0);
66
+ export function resolveAgentPath(input, agentRoot = process.env.BUDA_AGENT_ROOT) {
67
+ if (!input) return null;
68
+ return agentRoot ? resolveConnectorPath(input, agentRoot) : resolveSandboxPath(input);
51
69
  }
52
70
 
53
- const resolved = resolveAgentPath(input);
71
+ const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
54
72
 
55
- if (!resolved || !existsSync(resolved.abs)) {
56
- // File does not exist — return empty string
57
- process.stdout.write("");
58
- process.exit(0);
59
- }
73
+ if (isMain) {
74
+ const input = process.argv[2];
75
+ const resolved = resolveAgentPath(input);
60
76
 
61
- // File exists — emit the [send:] marker
62
- process.stdout.write(`[send:${resolved.rel}]`);
77
+ if (!resolved || !existsSync(resolved.abs)) {
78
+ process.stdout.write("");
79
+ } else {
80
+ process.stdout.write(`[send:${resolved.rel}]`);
81
+ }
82
+ }
package/dist/cli.js CHANGED
@@ -517,7 +517,7 @@ import path2 from "path";
517
517
  // package.json
518
518
  var package_default = {
519
519
  name: "@buda-ai/connector",
520
- version: "0.1.10",
520
+ version: "0.1.12",
521
521
  private: false,
522
522
  type: "module",
523
523
  publishConfig: {
@@ -584,6 +584,7 @@ var writeMetadata = async (metadataPath, metadata) => {
584
584
  };
585
585
  var publicStatus = (metadata) => ({
586
586
  connectorId: metadata.connectorId,
587
+ ...typeof metadata.heartbeatHealthy === "boolean" ? { heartbeatHealthy: metadata.heartbeatHealthy } : {},
587
588
  idle: metadata.idle,
588
589
  instanceId: metadata.instanceId,
589
590
  ...metadata.lastHeartbeatAt ? { lastHeartbeatAt: metadata.lastHeartbeatAt } : {},
@@ -640,6 +641,7 @@ async function startDaemonControlServer(options) {
640
641
  return;
641
642
  }
642
643
  metadata.idle = options.isIdle?.() ?? metadata.idle;
644
+ metadata.heartbeatHealthy = options.isHeartbeatHealthy?.() ?? metadata.heartbeatHealthy;
643
645
  metadata.lastHeartbeatAt = options.getLastHeartbeatAt?.() ?? metadata.lastHeartbeatAt;
644
646
  metadata.relayConnected = options.isRelayConnected?.() ?? metadata.relayConnected;
645
647
  if (request.command === "persist") {
@@ -682,6 +684,7 @@ async function startDaemonControlServer(options) {
682
684
  throw new Error("Failed to bind daemon control port");
683
685
  metadata = {
684
686
  connectorId: options.config.connectorId,
687
+ ...options.isHeartbeatHealthy ? { heartbeatHealthy: options.isHeartbeatHealthy() } : {},
685
688
  idle: options.isIdle?.() ?? true,
686
689
  instanceId: randomUUID3(),
687
690
  mode: options.mode,
@@ -2678,11 +2681,11 @@ var streamDaemonLogs = (options) => {
2678
2681
  var toDashboardConnector = (config, status) => ({
2679
2682
  agent: status ? status.idle ? "Ready" : "Busy" : "Stopped",
2680
2683
  connectorId: config.connectorId,
2681
- heartbeat: status?.lastHeartbeatAt ? `OK ${new Date(status.lastHeartbeatAt).toLocaleTimeString("en-GB", { hour12: false })}` : status ? "Waiting" : "Stopped",
2684
+ heartbeat: status?.heartbeatHealthy === false ? "Failed" : status?.lastHeartbeatAt ? `OK ${new Date(status.lastHeartbeatAt).toLocaleTimeString("en-GB", { hour12: false })}` : status ? "Waiting" : "Stopped",
2682
2685
  relay: status?.relayConnected ? "Connected" : "Disconnected",
2683
2686
  serverUrl: config.serverUrl,
2684
2687
  spaceId: config.spaceId ?? "Unknown",
2685
- status: status ? status.relayConnected ? "Online" : "Running" : "Stopped"
2688
+ status: status ? status.relayConnected && status.heartbeatHealthy !== false ? "Online" : "Running" : "Stopped"
2686
2689
  });
2687
2690
  async function readLocalConnectorDashboardRows(configRoot = getConfigRoot()) {
2688
2691
  const configs = await listDeviceConfigs(configRoot);
@@ -3193,17 +3196,13 @@ var proxyConnectorTerminalTunnel = async (input) => {
3193
3196
  headers: { "X-Buda-Local-Terminal-Token": input.localToken },
3194
3197
  handshakeTimeout: TERMINAL_HANDSHAKE_TIMEOUT_MS
3195
3198
  });
3196
- try {
3197
- await Promise.all([connectSocket(cloud), connectSocket(local)]);
3198
- } catch (error) {
3199
- cloud.terminate();
3200
- local.terminate();
3201
- throw error;
3202
- }
3203
3199
  let bridgeAccepted = false;
3200
+ let activityStarted = false;
3204
3201
  let activityEnded = false;
3202
+ let cloudCloseAfterOpen = null;
3203
+ let localCloseAfterOpen = null;
3205
3204
  const endActivity = () => {
3206
- if (activityEnded) return;
3205
+ if (!activityStarted || activityEnded) return;
3207
3206
  activityEnded = true;
3208
3207
  input.onActivityEnd?.();
3209
3208
  };
@@ -3251,24 +3250,53 @@ var proxyConnectorTerminalTunnel = async (input) => {
3251
3250
  });
3252
3251
  cloud.once("close", (code, reason) => {
3253
3252
  endActivity();
3253
+ const normalizedCode = normalizeTerminalCloseCode(code);
3254
+ const normalizedReason = truncateTerminalCloseReason(reason.toString());
3254
3255
  if (!bridgeAccepted) {
3255
3256
  void writeLog(
3256
3257
  "warn",
3257
- `terminal cloud tunnel closed before readiness session=${input.sessionId} code=${code} reason=${truncateTerminalCloseReason(reason.toString())}`
3258
+ `terminal cloud tunnel closed before readiness session=${input.sessionId} code=${code} reason=${normalizedReason}`
3258
3259
  );
3259
3260
  }
3260
3261
  if (local.readyState === WebSocket.OPEN) {
3261
- local.close(normalizeTerminalCloseCode(code), truncateTerminalCloseReason(reason.toString()));
3262
+ local.close(normalizedCode, normalizedReason);
3263
+ } else if (local.readyState === WebSocket.CONNECTING) {
3264
+ localCloseAfterOpen = { code: normalizedCode, reason: normalizedReason };
3262
3265
  }
3263
3266
  });
3264
3267
  local.once("close", (code, reason) => {
3265
3268
  endActivity();
3269
+ const normalizedClose = {
3270
+ code: normalizeTerminalCloseCode(code),
3271
+ reason: truncateTerminalCloseReason(reason.toString())
3272
+ };
3266
3273
  if (cloud.readyState === WebSocket.OPEN) {
3267
- cloud.close(normalizeTerminalCloseCode(code), truncateTerminalCloseReason(reason.toString()));
3274
+ cloud.close(normalizedClose.code, normalizedClose.reason);
3275
+ } else if (cloud.readyState === WebSocket.CONNECTING) {
3276
+ cloudCloseAfterOpen = normalizedClose;
3277
+ }
3278
+ });
3279
+ cloud.once("open", () => {
3280
+ if (cloudCloseAfterOpen && cloud.readyState === WebSocket.OPEN) {
3281
+ cloud.close(cloudCloseAfterOpen.code, cloudCloseAfterOpen.reason);
3282
+ }
3283
+ });
3284
+ local.once("open", () => {
3285
+ if (localCloseAfterOpen && local.readyState === WebSocket.OPEN) {
3286
+ local.close(localCloseAfterOpen.code, localCloseAfterOpen.reason);
3268
3287
  }
3269
3288
  });
3270
3289
  cloud.once("error", () => local.terminate());
3271
3290
  local.once("error", () => cloud.terminate());
3291
+ try {
3292
+ await Promise.all([connectSocket(cloud), connectSocket(local)]);
3293
+ } catch (error) {
3294
+ cloud.terminate();
3295
+ local.terminate();
3296
+ throw error;
3297
+ }
3298
+ if (cloud.readyState !== WebSocket.OPEN || local.readyState !== WebSocket.OPEN) return;
3299
+ activityStarted = true;
3272
3300
  input.onActivityStart?.();
3273
3301
  cloud.send(JSON.stringify({ type: "bridge-ready" }));
3274
3302
  await writeLog(
@@ -3857,7 +3885,8 @@ var corrupt = (message) => new LegacyWorkspaceMigrationError("MIGRATION_CORRUPT"
3857
3885
  var errorMessage = (error) => error instanceof Error ? error.message : String(error);
3858
3886
 
3859
3887
  // src/relay.ts
3860
- import path11 from "path";
3888
+ import { realpathSync } from "fs";
3889
+ import path12 from "path";
3861
3890
  import { createNextHandler } from "@bunny-agent/daemon/nextjs";
3862
3891
  import WebSocket2 from "ws";
3863
3892
 
@@ -4024,6 +4053,219 @@ function isNonEmptyString(value) {
4024
4053
  return typeof value === "string" && value.trim().length > 0;
4025
4054
  }
4026
4055
 
4056
+ // src/html-preview.ts
4057
+ import { readFile as readFile8, realpath as realpath2, stat as stat4 } from "fs/promises";
4058
+ import { createServer as createServer2 } from "http";
4059
+ import path10 from "path";
4060
+ var PREVIEW_HOST = "127.0.0.1";
4061
+ var MIN_PREVIEW_PORT = 3100;
4062
+ var MAX_PREVIEW_PORT = 3900;
4063
+ var MIME_TYPES = {
4064
+ ".avif": "image/avif",
4065
+ ".css": "text/css; charset=utf-8",
4066
+ ".gif": "image/gif",
4067
+ ".htm": "text/html; charset=utf-8",
4068
+ ".html": "text/html; charset=utf-8",
4069
+ ".ico": "image/x-icon",
4070
+ ".jpeg": "image/jpeg",
4071
+ ".jpg": "image/jpeg",
4072
+ ".js": "text/javascript; charset=utf-8",
4073
+ ".json": "application/json; charset=utf-8",
4074
+ ".mjs": "text/javascript; charset=utf-8",
4075
+ ".png": "image/png",
4076
+ ".svg": "image/svg+xml",
4077
+ ".txt": "text/plain; charset=utf-8",
4078
+ ".webp": "image/webp",
4079
+ ".woff": "font/woff",
4080
+ ".woff2": "font/woff2"
4081
+ };
4082
+ var HtmlPreviewServerRegistry = class {
4083
+ servers = /* @__PURE__ */ new Map();
4084
+ async open(input) {
4085
+ const htmlFile = await resolveWorkspaceFile(input);
4086
+ const folder = path10.dirname(htmlFile);
4087
+ let pending = this.servers.get(folder);
4088
+ if (!pending) {
4089
+ pending = startStaticServer(folder);
4090
+ this.servers.set(folder, pending);
4091
+ void pending.catch(() => {
4092
+ if (this.servers.get(folder) === pending) this.servers.delete(folder);
4093
+ });
4094
+ }
4095
+ const { port } = await pending;
4096
+ const previewUrl = new URL(`http://${PREVIEW_HOST}:${port}`);
4097
+ previewUrl.pathname = `/${path10.basename(htmlFile)}`;
4098
+ return {
4099
+ url: previewUrl.toString(),
4100
+ port,
4101
+ htmlPath: previewUrl.pathname
4102
+ };
4103
+ }
4104
+ async closeAll() {
4105
+ const pending = [...this.servers.values()];
4106
+ this.servers.clear();
4107
+ const entries = await Promise.all(pending.map((entry) => entry.catch(() => void 0)));
4108
+ await Promise.all(
4109
+ entries.map(
4110
+ (entry) => new Promise((resolve) => {
4111
+ if (!entry?.server.listening) return resolve();
4112
+ entry.server.close(() => resolve());
4113
+ })
4114
+ )
4115
+ );
4116
+ }
4117
+ };
4118
+ var handleHtmlPreviewRequest = async (request, mounts, registry) => {
4119
+ if (request.method !== "POST") {
4120
+ return Response.json({ error: "HTML preview requires POST." }, { status: 405 });
4121
+ }
4122
+ if (!request.body || typeof request.body !== "object" || Array.isArray(request.body)) {
4123
+ return Response.json({ error: "Invalid HTML preview request." }, { status: 400 });
4124
+ }
4125
+ const body = request.body;
4126
+ if (typeof body.filePath !== "string" || body.filePath.length === 0) {
4127
+ return Response.json({ error: "HTML preview requires a file path." }, { status: 400 });
4128
+ }
4129
+ if (body.rootVolume !== "agent" && body.rootVolume !== "space") {
4130
+ return Response.json({ error: "HTML preview requires a valid root volume." }, { status: 400 });
4131
+ }
4132
+ try {
4133
+ return Response.json(
4134
+ await registry.open({ filePath: body.filePath, rootVolume: body.rootVolume, mounts })
4135
+ );
4136
+ } catch (error) {
4137
+ return Response.json(
4138
+ { error: error instanceof Error ? error.message : "Unable to open HTML preview." },
4139
+ { status: 400 }
4140
+ );
4141
+ }
4142
+ };
4143
+ var resolveWorkspaceFile = async (input) => {
4144
+ const virtualPath = input.filePath.replaceAll("\\", "/");
4145
+ if (virtualPath.includes("\0")) throw new Error("HTML preview path contains invalid data.");
4146
+ const mountPath = path10.resolve(input.mounts[input.rootVolume]);
4147
+ const root = await realpath2(mountPath);
4148
+ let relativePath = virtualPath;
4149
+ let explicitVolume;
4150
+ for (const volume of ["agent", "space"]) {
4151
+ const prefix = `/${volume}`;
4152
+ if (virtualPath === prefix || virtualPath.startsWith(`${prefix}/`)) {
4153
+ explicitVolume = volume;
4154
+ relativePath = virtualPath.slice(prefix.length).replace(/^\/+/, "");
4155
+ break;
4156
+ }
4157
+ }
4158
+ if (explicitVolume && explicitVolume !== input.rootVolume) {
4159
+ throw new Error("HTML preview path does not match the requested root volume.");
4160
+ }
4161
+ if (!explicitVolume && (path10.isAbsolute(virtualPath) || /^[A-Za-z]:\//.test(virtualPath))) {
4162
+ const physicalPath = path10.normalize(virtualPath);
4163
+ const physicalRoot = [mountPath, root].find(
4164
+ (candidateRoot) => isContained(candidateRoot, physicalPath)
4165
+ );
4166
+ if (!physicalRoot) {
4167
+ throw new Error("HTML preview path must stay inside the requested workspace volume.");
4168
+ }
4169
+ relativePath = path10.relative(physicalRoot, physicalPath).split(path10.sep).join("/");
4170
+ }
4171
+ const segments = relativePath.split("/").filter(Boolean);
4172
+ if (segments.length === 0 || segments.some((segment) => segment === "." || segment === "..")) {
4173
+ throw new Error("HTML preview path must not contain traversal segments.");
4174
+ }
4175
+ const candidate = path10.resolve(root, ...segments);
4176
+ assertContained(root, candidate);
4177
+ const canonicalFile = await realpath2(candidate);
4178
+ assertContained(root, canonicalFile);
4179
+ const fileStat = await stat4(canonicalFile);
4180
+ if (!fileStat.isFile()) throw new Error("HTML preview path must identify a file.");
4181
+ if (![".htm", ".html"].includes(path10.extname(canonicalFile).toLowerCase())) {
4182
+ throw new Error("HTML preview path must identify an HTML file.");
4183
+ }
4184
+ return canonicalFile;
4185
+ };
4186
+ var startStaticServer = async (folder) => {
4187
+ for (let port = MIN_PREVIEW_PORT; port <= MAX_PREVIEW_PORT; port += 1) {
4188
+ const server = createServer2((request, response) => {
4189
+ void serveStaticFile(folder, request.method ?? "GET", request.url ?? "/").then((result) => {
4190
+ response.writeHead(result.status, result.headers);
4191
+ response.end(request.method === "HEAD" ? void 0 : result.body);
4192
+ }).catch(() => {
4193
+ response.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
4194
+ response.end("HTML preview failed.");
4195
+ });
4196
+ });
4197
+ const listening = await listen(server, port);
4198
+ if (listening) {
4199
+ server.unref();
4200
+ return { server, port };
4201
+ }
4202
+ }
4203
+ throw new Error(
4204
+ `No free HTML preview port is available (${MIN_PREVIEW_PORT}-${MAX_PREVIEW_PORT}).`
4205
+ );
4206
+ };
4207
+ var listen = (server, port) => new Promise((resolve, reject) => {
4208
+ const onError = (error) => {
4209
+ server.removeListener("listening", onListening);
4210
+ if (error.code === "EADDRINUSE") resolve(false);
4211
+ else reject(error);
4212
+ };
4213
+ const onListening = () => {
4214
+ server.removeListener("error", onError);
4215
+ resolve(true);
4216
+ };
4217
+ server.once("error", onError);
4218
+ server.once("listening", onListening);
4219
+ server.listen(port, PREVIEW_HOST);
4220
+ });
4221
+ var serveStaticFile = async (folder, method, requestUrl) => {
4222
+ if (method !== "GET" && method !== "HEAD") {
4223
+ return { status: 405, headers: { Allow: "GET, HEAD" } };
4224
+ }
4225
+ let decodedPath;
4226
+ try {
4227
+ decodedPath = decodeURIComponent(requestUrl.split("?", 1)[0] ?? "/").replaceAll("\\", "/");
4228
+ } catch {
4229
+ return { status: 400, headers: {} };
4230
+ }
4231
+ if (decodedPath.includes("\0")) return { status: 400, headers: {} };
4232
+ const segments = decodedPath.split("/").filter(Boolean);
4233
+ if (segments.some((segment) => segment === "." || segment === "..")) {
4234
+ return { status: 403, headers: {} };
4235
+ }
4236
+ try {
4237
+ const candidate = path10.resolve(folder, ...segments);
4238
+ assertContained(folder, candidate);
4239
+ let canonicalFile = await realpath2(candidate);
4240
+ assertContained(folder, canonicalFile);
4241
+ if ((await stat4(canonicalFile)).isDirectory()) {
4242
+ canonicalFile = await realpath2(path10.join(canonicalFile, "index.html"));
4243
+ assertContained(folder, canonicalFile);
4244
+ }
4245
+ if (!(await stat4(canonicalFile)).isFile()) throw new Error("Not a file");
4246
+ const body = method === "HEAD" ? void 0 : await readFile8(canonicalFile);
4247
+ return {
4248
+ status: 200,
4249
+ headers: {
4250
+ "Cache-Control": "no-store",
4251
+ "Content-Type": MIME_TYPES[path10.extname(canonicalFile).toLowerCase()] ?? "application/octet-stream",
4252
+ "X-Content-Type-Options": "nosniff"
4253
+ },
4254
+ body
4255
+ };
4256
+ } catch {
4257
+ return { status: 404, headers: { "Content-Type": "text/plain; charset=utf-8" } };
4258
+ }
4259
+ };
4260
+ var assertContained = (root, candidate) => {
4261
+ if (isContained(root, candidate)) return;
4262
+ throw new Error("HTML preview path escapes its allowed root.");
4263
+ };
4264
+ var isContained = (root, candidate) => {
4265
+ const relative = path10.relative(root, candidate);
4266
+ return relative === "" || !relative.startsWith("..") && !path10.isAbsolute(relative);
4267
+ };
4268
+
4027
4269
  // src/workspace-volume-lifecycle.ts
4028
4270
  import { constants as constants3 } from "fs";
4029
4271
  import {
@@ -4031,13 +4273,13 @@ import {
4031
4273
  lstat as lstat2,
4032
4274
  mkdir as mkdir9,
4033
4275
  readdir as readdir5,
4034
- readFile as readFile8,
4035
- realpath as realpath2,
4276
+ readFile as readFile9,
4277
+ realpath as realpath3,
4036
4278
  rm as rm3,
4037
4279
  symlink as symlink2,
4038
4280
  writeFile as writeFile4
4039
4281
  } from "fs/promises";
4040
- import path10 from "path";
4282
+ import path11 from "path";
4041
4283
  var MOUNT_DIRECTORY = "mount";
4042
4284
  var ARCHIVE_MARKER = "archived.json";
4043
4285
  var AGENT_CONTEXT_TEMPLATE_FILES = [
@@ -4049,8 +4291,8 @@ var AGENT_CONTEXT_TEMPLATE_FILES = [
4049
4291
  "BOOTSTRAP.md",
4050
4292
  "MEMORY.md"
4051
4293
  ];
4052
- var deriveSystemVolume = (descriptor) => path10.join(
4053
- path10.dirname(path10.dirname(path10.dirname(path10.dirname(descriptor.sharedVolume)))),
4294
+ var deriveSystemVolume = (descriptor) => path11.join(
4295
+ path11.dirname(path11.dirname(path11.dirname(path11.dirname(descriptor.sharedVolume)))),
4054
4296
  "system"
4055
4297
  );
4056
4298
  var WorkspaceVolumeLifecycle = class {
@@ -4062,14 +4304,14 @@ var WorkspaceVolumeLifecycle = class {
4062
4304
  async mount() {
4063
4305
  await this.ensureVolumes();
4064
4306
  await this.bootstrapAgentTemplates((await readBundledManifest()).sharedDir);
4065
- const mountRoot = path10.join(this.descriptor.stateVolume, MOUNT_DIRECTORY);
4307
+ const mountRoot = path11.join(this.descriptor.stateVolume, MOUNT_DIRECTORY);
4066
4308
  await mkdir9(mountRoot, { recursive: true, mode: 448 });
4067
4309
  const mounts = {
4068
4310
  mountRoot,
4069
- agent: path10.join(mountRoot, "agent"),
4070
- space: path10.join(mountRoot, "space"),
4071
- system: path10.join(mountRoot, "system"),
4072
- temporary: path10.join(mountRoot, "tmp")
4311
+ agent: path11.join(mountRoot, "agent"),
4312
+ space: path11.join(mountRoot, "space"),
4313
+ system: path11.join(mountRoot, "system"),
4314
+ temporary: path11.join(mountRoot, "tmp")
4073
4315
  };
4074
4316
  await Promise.all([
4075
4317
  ensureVolumeLink(this.descriptor.privateVolume, mounts.agent),
@@ -4077,18 +4319,18 @@ var WorkspaceVolumeLifecycle = class {
4077
4319
  ensureVolumeLink(this.systemVolume, mounts.system),
4078
4320
  ensureVolumeLink(this.descriptor.temporaryVolume, mounts.temporary)
4079
4321
  ]);
4080
- const daemonVolumes = path10.join(mountRoot, "volumes");
4322
+ const daemonVolumes = path11.join(mountRoot, "volumes");
4081
4323
  await mkdir9(daemonVolumes, { recursive: true, mode: 448 });
4082
4324
  await Promise.all([
4083
- ensureVolumeLink(this.descriptor.privateVolume, path10.join(daemonVolumes, "agent")),
4084
- ensureVolumeLink(this.descriptor.sharedVolume, path10.join(daemonVolumes, "space")),
4085
- ensureVolumeLink(this.systemVolume, path10.join(daemonVolumes, "system")),
4086
- ensureVolumeLink(this.descriptor.temporaryVolume, path10.join(daemonVolumes, "tmp"))
4325
+ ensureVolumeLink(this.descriptor.privateVolume, path11.join(daemonVolumes, "agent")),
4326
+ ensureVolumeLink(this.descriptor.sharedVolume, path11.join(daemonVolumes, "space")),
4327
+ ensureVolumeLink(this.systemVolume, path11.join(daemonVolumes, "system")),
4328
+ ensureVolumeLink(this.descriptor.temporaryVolume, path11.join(daemonVolumes, "tmp"))
4087
4329
  ]);
4088
4330
  return mounts;
4089
4331
  }
4090
4332
  async unmount() {
4091
- await rm3(path10.join(this.descriptor.stateVolume, MOUNT_DIRECTORY), {
4333
+ await rm3(path11.join(this.descriptor.stateVolume, MOUNT_DIRECTORY), {
4092
4334
  recursive: true,
4093
4335
  force: true
4094
4336
  });
@@ -4097,33 +4339,33 @@ var WorkspaceVolumeLifecycle = class {
4097
4339
  await this.unmount();
4098
4340
  await mkdir9(this.descriptor.stateVolume, { recursive: true, mode: 448 });
4099
4341
  await writeFile4(
4100
- path10.join(this.descriptor.stateVolume, ARCHIVE_MARKER),
4342
+ path11.join(this.descriptor.stateVolume, ARCHIVE_MARKER),
4101
4343
  `${JSON.stringify({ archivedAt: (/* @__PURE__ */ new Date()).toISOString() })}
4102
4344
  `,
4103
4345
  { encoding: "utf8", mode: 384 }
4104
4346
  );
4105
4347
  }
4106
4348
  async restoreAgent() {
4107
- await rm3(path10.join(this.descriptor.stateVolume, ARCHIVE_MARKER), { force: true });
4349
+ await rm3(path11.join(this.descriptor.stateVolume, ARCHIVE_MARKER), { force: true });
4108
4350
  return this.mount();
4109
4351
  }
4110
4352
  async deleteAgent(options) {
4111
4353
  requireConfirmation(options);
4112
4354
  await this.unmount();
4113
- const agentRoot = path10.dirname(this.descriptor.privateVolume);
4355
+ const agentRoot = path11.dirname(this.descriptor.privateVolume);
4114
4356
  await rm3(agentRoot, { recursive: true, force: true });
4115
4357
  }
4116
4358
  async archiveSpace() {
4117
4359
  await mkdir9(this.descriptor.sharedVolume, { recursive: true, mode: 448 });
4118
4360
  await writeFile4(
4119
- path10.join(this.descriptor.sharedVolume, ARCHIVE_MARKER),
4361
+ path11.join(this.descriptor.sharedVolume, ARCHIVE_MARKER),
4120
4362
  `${JSON.stringify({ archivedAt: (/* @__PURE__ */ new Date()).toISOString() })}
4121
4363
  `,
4122
4364
  { encoding: "utf8", mode: 384 }
4123
4365
  );
4124
4366
  }
4125
4367
  async restoreSpace() {
4126
- await rm3(path10.join(this.descriptor.sharedVolume, ARCHIVE_MARKER), { force: true });
4368
+ await rm3(path11.join(this.descriptor.sharedVolume, ARCHIVE_MARKER), { force: true });
4127
4369
  }
4128
4370
  async deleteSpace(options) {
4129
4371
  requireConfirmation(options);
@@ -4139,7 +4381,7 @@ var WorkspaceVolumeLifecycle = class {
4139
4381
  );
4140
4382
  for (const filename of AGENT_CONTEXT_TEMPLATE_FILES) {
4141
4383
  if (!entries.has(filename)) continue;
4142
- const destination = path10.join(this.descriptor.privateVolume, filename);
4384
+ const destination = path11.join(this.descriptor.privateVolume, filename);
4143
4385
  try {
4144
4386
  await lstat2(destination);
4145
4387
  skipped.push(filename);
@@ -4147,7 +4389,7 @@ var WorkspaceVolumeLifecycle = class {
4147
4389
  if (!isMissingFile(error)) throw error;
4148
4390
  try {
4149
4391
  await copyFile2(
4150
- path10.join(templateDirectory, filename),
4392
+ path11.join(templateDirectory, filename),
4151
4393
  destination,
4152
4394
  constants3.COPYFILE_EXCL
4153
4395
  );
@@ -4170,7 +4412,7 @@ var WorkspaceVolumeLifecycle = class {
4170
4412
  ]) {
4171
4413
  await mkdir9(volume, { recursive: true, mode: 448 });
4172
4414
  const info = await lstat2(volume);
4173
- if (info.isSymbolicLink() || !info.isDirectory() || await realpath2(volume) !== volume) {
4415
+ if (info.isSymbolicLink() || !info.isDirectory() || await realpath3(volume) !== volume) {
4174
4416
  throw new Error("Workspace volume must be a canonical real directory");
4175
4417
  }
4176
4418
  }
@@ -4183,7 +4425,7 @@ var isMissingFile = (error) => error instanceof Error && "code" in error && erro
4183
4425
  var ensureVolumeLink = async (target, linkPath) => {
4184
4426
  try {
4185
4427
  const info = await lstat2(linkPath);
4186
- if (!info.isSymbolicLink() || await realpath2(linkPath) !== target) {
4428
+ if (!info.isSymbolicLink() || await realpath3(linkPath) !== target) {
4187
4429
  throw new Error("Workspace mount link does not match its canonical volume");
4188
4430
  }
4189
4431
  } catch (error) {
@@ -4193,7 +4435,7 @@ var ensureVolumeLink = async (target, linkPath) => {
4193
4435
  } catch (createError) {
4194
4436
  if (!isAlreadyExists3(createError)) throw createError;
4195
4437
  const info = await lstat2(linkPath);
4196
- if (!info.isSymbolicLink() || await realpath2(linkPath) !== target) {
4438
+ if (!info.isSymbolicLink() || await realpath3(linkPath) !== target) {
4197
4439
  throw new Error("Workspace mount link raced with an invalid target");
4198
4440
  }
4199
4441
  }
@@ -4255,6 +4497,7 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
4255
4497
  let currentInflight = /* @__PURE__ */ new Map();
4256
4498
  let activeTerminalTunnels = 0;
4257
4499
  const replayGuard = new ConnectorExecutionReplayGuard();
4500
+ const htmlPreviews = new HtmlPreviewServerRegistry();
4258
4501
  const clearTimers = () => {
4259
4502
  if (pingTimer) {
4260
4503
  clearInterval(pingTimer);
@@ -4361,13 +4604,15 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
4361
4604
  onTerminalClose: () => {
4362
4605
  activeTerminalTunnels = Math.max(0, activeTerminalTunnels - 1);
4363
4606
  }
4364
- }
4607
+ },
4608
+ htmlPreviews
4365
4609
  );
4366
4610
  });
4367
4611
  ws.on("close", (code) => {
4368
4612
  detach();
4369
4613
  if (!shouldReconnectConnectorRelay(code)) {
4370
4614
  clearTimers();
4615
+ void htmlPreviews.closeAll();
4371
4616
  if (code === CONNECTOR_SOCKET_REPLACED_CLOSE_CODE) {
4372
4617
  stopped = true;
4373
4618
  void writeLog("warn", "relay replaced by another process for this connector");
@@ -4427,10 +4672,11 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
4427
4672
  }
4428
4673
  clearTimers();
4429
4674
  socket?.close();
4675
+ void htmlPreviews.closeAll();
4430
4676
  }
4431
4677
  };
4432
4678
  }
4433
- async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedConnectorId, relayProtocol, replayGuard, workspaceRegistry, serverUrl, localDaemonBaseUrl, activity) {
4679
+ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedConnectorId, relayProtocol, replayGuard, workspaceRegistry, serverUrl, localDaemonBaseUrl, activity, htmlPreviews) {
4434
4680
  if (!socket || socket.readyState !== WebSocket2.OPEN) return;
4435
4681
  let message;
4436
4682
  try {
@@ -4534,6 +4780,7 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
4534
4780
  const requestPath = message.request.path;
4535
4781
  const isBrowserRequest = requestPath.startsWith("/browser/") || requestPath === "/browser";
4536
4782
  const isWorkspaceDescriptorRequest = requestPath === "/workspace/descriptor";
4783
+ const isHtmlPreviewRequest = new URL(requestPath, "http://connector.workspace").pathname === "/api/preview/html";
4537
4784
  const isAgentRequest = isBrowserRequest || requestPath.startsWith("/api/") || isWorkspaceDescriptorRequest;
4538
4785
  if (isAgentRequest && (!executionIdentity || !workspaceRegistry)) {
4539
4786
  throw new ConnectorRelayProtocolError("IDENTITY_INVALID");
@@ -4566,6 +4813,8 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
4566
4813
  workdirRoot: mounts?.mountRoot
4567
4814
  }
4568
4815
  );
4816
+ } else if (isHtmlPreviewRequest && mounts && htmlPreviews) {
4817
+ response = await handleHtmlPreviewRequest(message.request, mounts, htmlPreviews);
4569
4818
  } else if (descriptor && mounts) {
4570
4819
  response = await handleWorkspaceDaemonRequest(message.request, mounts, controller.signal);
4571
4820
  } else {
@@ -4606,7 +4855,8 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
4606
4855
  });
4607
4856
  if (response.body) {
4608
4857
  const reader = response.body.getReader();
4609
- const decoder = new TextDecoder();
4858
+ const binaryResponse = getHeaderValue(message.request.headers, "x-buda-relay-response-encoding") === "base64";
4859
+ const decoder = binaryResponse ? null : new TextDecoder();
4610
4860
  const cancelReader = () => {
4611
4861
  void reader.cancel().catch(() => {
4612
4862
  });
@@ -4620,10 +4870,11 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
4620
4870
  sendRelayMessage(socket, {
4621
4871
  type: "chunk",
4622
4872
  requestId: message.requestId,
4623
- data: decoder.decode(value, { stream: true })
4873
+ data: binaryResponse ? Buffer.from(value).toString("base64") : decoder?.decode(value, { stream: true }) ?? "",
4874
+ ...binaryResponse ? { encoding: "base64" } : {}
4624
4875
  });
4625
4876
  }
4626
- const tail = decoder.decode();
4877
+ const tail = decoder?.decode() ?? "";
4627
4878
  if (tail) {
4628
4879
  sendRelayMessage(socket, {
4629
4880
  type: "chunk",
@@ -4663,6 +4914,29 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
4663
4914
  var handleWorkspaceDaemonRequest = async (request, mounts, signal) => {
4664
4915
  const normalized = normalizeWorkspaceRequest(request, mounts);
4665
4916
  const handler = createNextHandler({ root: mounts.mountRoot });
4917
+ if (getHeaderValue(normalized.headers, "x-buda-relay-body-encoding") === "base64") {
4918
+ if (new URL(normalized.path, "http://connector.workspace").pathname !== "/api/fs/write-stream") {
4919
+ throw new Error("Base64 relay bodies are only supported for filesystem write streams.");
4920
+ }
4921
+ if (typeof normalized.body !== "string") {
4922
+ throw new Error("Invalid base64 filesystem write body.");
4923
+ }
4924
+ const content = Buffer.from(normalized.body, "base64");
4925
+ if (content.toString("base64") !== normalized.body) {
4926
+ throw new Error("Invalid base64 filesystem write content.");
4927
+ }
4928
+ const headers = new Headers(normalized.headers);
4929
+ headers.delete("x-buda-relay-body-encoding");
4930
+ headers.set("Content-Type", "application/octet-stream");
4931
+ return handler(
4932
+ new Request(new URL(normalized.path, "http://connector.workspace"), {
4933
+ method: normalized.method,
4934
+ headers,
4935
+ body: content,
4936
+ signal
4937
+ })
4938
+ );
4939
+ }
4666
4940
  return handler(
4667
4941
  new Request(new URL(normalized.path, "http://connector.workspace"), {
4668
4942
  method: normalized.method,
@@ -4673,18 +4947,60 @@ var handleWorkspaceDaemonRequest = async (request, mounts, signal) => {
4673
4947
  );
4674
4948
  };
4675
4949
  var normalizeWorkspaceRequest = (request, mounts) => {
4676
- if (request.path !== "/api/coding/run") return request;
4950
+ if (request.path !== "/api/coding/run") {
4951
+ return normalizeConnectorFilesystemRequest(request, mounts);
4952
+ }
4677
4953
  if (!request.body || typeof request.body !== "object" || Array.isArray(request.body)) {
4678
4954
  throw new ConnectorRelayProtocolError("WORKSPACE_MISMATCH");
4679
4955
  }
4680
4956
  const body = request.body;
4681
4957
  const cwd = typeof body.cwd === "string" ? body.cwd : "/agent";
4682
- const normalized = path11.posix.normalize(cwd);
4958
+ const normalized = path12.posix.normalize(cwd);
4683
4959
  const [mount, relative] = normalized === "/agent" || normalized.startsWith("/agent/") ? [mounts.agent, normalized.slice("/agent".length)] : normalized === "/space" || normalized.startsWith("/space/") ? [mounts.space, normalized.slice("/space".length)] : [];
4684
4960
  if (!mount || relative === void 0) {
4685
4961
  throw new ConnectorRelayProtocolError("WORKSPACE_MISMATCH");
4686
4962
  }
4687
- return { ...request, body: { ...body, cwd: path11.join(mount, relative) } };
4963
+ const env = body.env && typeof body.env === "object" && !Array.isArray(body.env) ? body.env : {};
4964
+ return {
4965
+ ...request,
4966
+ body: {
4967
+ ...body,
4968
+ cwd: path12.join(mount, relative),
4969
+ env: {
4970
+ ...env,
4971
+ BUDA_AGENT_ROOT: mounts.agent,
4972
+ BUDA_SPACE_ROOT: mounts.space,
4973
+ BUDA_SYSTEM_ROOT: mounts.system
4974
+ }
4975
+ }
4976
+ };
4977
+ };
4978
+ var normalizeConnectorFilesystemRequest = (request, mounts) => {
4979
+ const url = new URL(request.path, "http://connector.workspace");
4980
+ if (!url.pathname.startsWith("/api/fs/")) return request;
4981
+ const filePath = url.searchParams.get("path");
4982
+ if (!filePath) return request;
4983
+ const mount = url.searchParams.get("volume") === "space" ? mounts.space : mounts.agent;
4984
+ const roots = [mount];
4985
+ try {
4986
+ const canonicalRoot = realpathSync(mount);
4987
+ if (canonicalRoot !== mount) roots.push(canonicalRoot);
4988
+ } catch {
4989
+ }
4990
+ const normalizedFilePath = path12.normalize(filePath);
4991
+ for (const root of roots) {
4992
+ const normalizedRoot = path12.normalize(root);
4993
+ const candidates = [normalizedRoot, normalizedRoot.replace(/^[/\\]+/, "")];
4994
+ for (const candidate of candidates) {
4995
+ if (normalizedFilePath !== candidate && !normalizedFilePath.startsWith(`${candidate}${path12.sep}`)) {
4996
+ continue;
4997
+ }
4998
+ const relativePath = path12.relative(candidate, normalizedFilePath).split(path12.sep).join("/");
4999
+ url.searchParams.set("path", relativePath);
5000
+ return { ...request, path: `${url.pathname}${url.search}` };
5001
+ }
5002
+ }
5003
+ return request;
4688
5004
  };
4689
5005
  function sendRelayError(socket, requestId, error) {
4690
5006
  sendRelayMessage(socket, {
@@ -4716,13 +5032,13 @@ function getHeaderValue(headers, name) {
4716
5032
 
4717
5033
  // src/workdir-layout.ts
4718
5034
  import { lstat as lstat3, mkdir as mkdir10, symlink as symlink3, unlink as unlink6 } from "fs/promises";
4719
- import path12 from "path";
5035
+ import path13 from "path";
4720
5036
  async function provisionWorkdirLayout(workdirRoot) {
4721
5037
  const manifest = await readBundledManifest();
4722
5038
  await mkdir10(workdirRoot, { recursive: true });
4723
- await mkdir10(path12.join(workdirRoot, "system", ".agents"), { recursive: true });
4724
- const systemSkillsLink = path12.join(workdirRoot, "system", ".agents", "skills");
4725
- const systemScriptsLink = path12.join(workdirRoot, "system", "scripts");
5039
+ await mkdir10(path13.join(workdirRoot, "system", ".agents"), { recursive: true });
5040
+ const systemSkillsLink = path13.join(workdirRoot, "system", ".agents", "skills");
5041
+ const systemScriptsLink = path13.join(workdirRoot, "system", "scripts");
4726
5042
  await ensureSymlink(manifest.systemSkillsDir, systemSkillsLink);
4727
5043
  await ensureSymlink(manifest.scriptsDir, systemScriptsLink);
4728
5044
  await writeLog("info", "workdir layout provisioned", {
@@ -4754,7 +5070,7 @@ async function ensureSymlink(target, linkPath) {
4754
5070
  }
4755
5071
  } catch {
4756
5072
  }
4757
- await mkdir10(path12.dirname(linkPath), { recursive: true });
5073
+ await mkdir10(path13.dirname(linkPath), { recursive: true });
4758
5074
  await symlink3(target, linkPath, "dir");
4759
5075
  }
4760
5076
 
@@ -4765,13 +5081,13 @@ import {
4765
5081
  lstat as lstat4,
4766
5082
  mkdir as mkdir11,
4767
5083
  open as open3,
4768
- readFile as readFile9,
4769
- realpath as realpath3,
5084
+ readFile as readFile10,
5085
+ realpath as realpath4,
4770
5086
  rename as rename6,
4771
5087
  statfs,
4772
5088
  unlink as unlink7
4773
5089
  } from "fs/promises";
4774
- import path13 from "path";
5090
+ import path14 from "path";
4775
5091
  var REGISTRY_VERSION = 1;
4776
5092
  var REGISTRY_DIRECTORY = ".buda";
4777
5093
  var REGISTRY_FILENAME = "workspace-registry.json";
@@ -4795,13 +5111,13 @@ var LocalWorkspaceRegistry = class _LocalWorkspaceRegistry {
4795
5111
  await mkdir11(storageRoot, { recursive: true, mode: 448 });
4796
5112
  await rejectUnsafeFilesystem(storageRoot);
4797
5113
  const canonicalRoot = await canonicalDirectory(storageRoot, "storage root");
4798
- const registryDirectory = path13.join(canonicalRoot, REGISTRY_DIRECTORY);
5114
+ const registryDirectory = path14.join(canonicalRoot, REGISTRY_DIRECTORY);
4799
5115
  await ensurePrivateDirectory2(registryDirectory);
4800
- const registryPath = path13.join(registryDirectory, REGISTRY_FILENAME);
5116
+ const registryPath = path14.join(registryDirectory, REGISTRY_FILENAME);
4801
5117
  const document = await withExclusiveFileLock(registryPath, async () => {
4802
5118
  let current;
4803
5119
  try {
4804
- current = parseRegistry(await readFile9(registryPath, "utf8"));
5120
+ current = parseRegistry(await readFile10(registryPath, "utf8"));
4805
5121
  } catch (error) {
4806
5122
  if (isMissingFile2(error)) {
4807
5123
  current = { version: REGISTRY_VERSION, storageRoot: canonicalRoot, workspaces: {} };
@@ -4866,23 +5182,23 @@ var LocalWorkspaceRegistry = class _LocalWorkspaceRegistry {
4866
5182
  };
4867
5183
  var readRegistryDocument = async (registryPath) => {
4868
5184
  try {
4869
- return parseRegistry(await readFile9(registryPath, "utf8"));
5185
+ return parseRegistry(await readFile10(registryPath, "utf8"));
4870
5186
  } catch (error) {
4871
5187
  if (error instanceof WorkspaceRegistryError) throw error;
4872
5188
  throw corrupt2(`Cannot read local workspace registry: ${errorMessage2(error)}`);
4873
5189
  }
4874
5190
  };
4875
5191
  var deriveWorkspace = (storageRoot, spaceId, agentId) => {
4876
- const spaceRoot = path13.join(storageRoot, REGISTRY_DIRECTORY, "spaces", spaceId);
4877
- const agentRoot = path13.join(spaceRoot, "agents", agentId);
5192
+ const spaceRoot = path14.join(storageRoot, REGISTRY_DIRECTORY, "spaces", spaceId);
5193
+ const agentRoot = path14.join(spaceRoot, "agents", agentId);
4878
5194
  return {
4879
5195
  workspaceKey: `${spaceId}:${agentId}`,
4880
5196
  spaceId,
4881
5197
  agentId,
4882
- privateVolume: path13.join(agentRoot, "workspace"),
4883
- sharedVolume: path13.join(spaceRoot, "shared"),
4884
- stateVolume: path13.join(agentRoot, "state"),
4885
- temporaryVolume: path13.join(agentRoot, "tmp")
5198
+ privateVolume: path14.join(agentRoot, "workspace"),
5199
+ sharedVolume: path14.join(spaceRoot, "shared"),
5200
+ stateVolume: path14.join(agentRoot, "state"),
5201
+ temporaryVolume: path14.join(agentRoot, "tmp")
4886
5202
  };
4887
5203
  };
4888
5204
  var parseRegistry = (raw) => {
@@ -4928,9 +5244,9 @@ var reconcileWorkspace = async (workspace, storageRoot) => {
4928
5244
  workspace.temporaryVolume
4929
5245
  ];
4930
5246
  for (const volume of volumes) {
4931
- assertContained(storageRoot, volume);
5247
+ assertContained2(storageRoot, volume);
4932
5248
  await ensurePrivateDirectory2(volume);
4933
- const canonical = await realpath3(volume);
5249
+ const canonical = await realpath4(volume);
4934
5250
  if (canonical !== volume) throw corrupt2("Workspace path resolves through a symbolic link");
4935
5251
  }
4936
5252
  };
@@ -4976,7 +5292,7 @@ var canonicalDirectory = async (directory, label) => {
4976
5292
  if (info.isSymbolicLink() || !info.isDirectory()) {
4977
5293
  throw new WorkspaceRegistryError("STORAGE_UNSAFE", `${label} must be a real directory`);
4978
5294
  }
4979
- return realpath3(directory);
5295
+ return realpath4(directory);
4980
5296
  };
4981
5297
  var rejectUnsafeFilesystem = async (storageRoot) => {
4982
5298
  const stats = await statfs(storageRoot, { bigint: true });
@@ -5025,12 +5341,12 @@ var validateStoredId = (label, value) => {
5025
5341
  throw corrupt2(errorMessage2(error));
5026
5342
  }
5027
5343
  };
5028
- var assertContained = (root, candidate) => {
5344
+ var assertContained2 = (root, candidate) => {
5029
5345
  if (!isPathInside2(root, candidate)) throw corrupt2("Workspace path escapes the storage root");
5030
5346
  };
5031
5347
  var isPathInside2 = (parent, child) => {
5032
- const relative = path13.relative(parent, child);
5033
- return relative !== "" && relative !== ".." && !relative.startsWith(`..${path13.sep}`) && !path13.isAbsolute(relative);
5348
+ const relative = path14.relative(parent, child);
5349
+ return relative !== "" && relative !== ".." && !relative.startsWith(`..${path14.sep}`) && !path14.isAbsolute(relative);
5034
5350
  };
5035
5351
  var toDescriptor = (workspace) => ({
5036
5352
  workspaceKey: workspace.workspaceKey,
@@ -5202,7 +5518,7 @@ async function runOnboard(launchConfig, options = {}) {
5202
5518
 
5203
5519
  // src/runtime.ts
5204
5520
  import { mkdir as mkdir12 } from "fs/promises";
5205
- import path14 from "path";
5521
+ import path15 from "path";
5206
5522
  var RECOVERED_DAEMON_STOP_TIMEOUT_MS = 1e4;
5207
5523
  var RECOVERED_DAEMON_STOP_POLL_MS = 100;
5208
5524
  var wait3 = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
@@ -5258,7 +5574,7 @@ async function runConnectorDaemon(launchConfig, options = {}) {
5258
5574
  config.connectorId,
5259
5575
  configRoot
5260
5576
  );
5261
- await mkdir12(path14.dirname(daemonLockTarget), { recursive: true, mode: 448 });
5577
+ await mkdir12(path15.dirname(daemonLockTarget), { recursive: true, mode: 448 });
5262
5578
  let daemonLock = await acquireExclusiveFileLock(daemonLockTarget, { timeoutMs: 0 }).catch(() => {
5263
5579
  throw new Error(
5264
5580
  `Another Buda Connector daemon is already running for this profile on ${config.serverUrl}. Stop it before starting another instance.`
@@ -5268,6 +5584,7 @@ async function runConnectorDaemon(launchConfig, options = {}) {
5268
5584
  let bunnyDaemon;
5269
5585
  let relayProtocol;
5270
5586
  let control;
5587
+ let heartbeatHealthy = true;
5271
5588
  let lastHeartbeatAt;
5272
5589
  try {
5273
5590
  await migrateLegacyWorkspaceRoot(config.workdirRoot);
@@ -5291,7 +5608,7 @@ async function runConnectorDaemon(launchConfig, options = {}) {
5291
5608
  registration.connectorId,
5292
5609
  configRoot
5293
5610
  );
5294
- await mkdir12(path14.dirname(registeredLockTarget), { recursive: true, mode: 448 });
5611
+ await mkdir12(path15.dirname(registeredLockTarget), { recursive: true, mode: 448 });
5295
5612
  const registeredLock = await acquireExclusiveFileLock(registeredLockTarget, {
5296
5613
  timeoutMs: 0
5297
5614
  }).catch(() => {
@@ -5346,6 +5663,7 @@ async function runConnectorDaemon(launchConfig, options = {}) {
5346
5663
  };
5347
5664
  interval = setInterval(() => {
5348
5665
  heartbeatConnector(config, launchConfig, activeBunnyDaemon).then(({ relayProtocol: nextRelayProtocol }) => {
5666
+ heartbeatHealthy = true;
5349
5667
  lastHeartbeatAt = (/* @__PURE__ */ new Date()).toISOString();
5350
5668
  relay.updateRelayProtocol(nextRelayProtocol);
5351
5669
  relay.resumeAfterAccessRestore();
@@ -5353,6 +5671,7 @@ async function runConnectorDaemon(launchConfig, options = {}) {
5353
5671
  heartbeat: `OK ${(/* @__PURE__ */ new Date()).toLocaleTimeString("en-GB", { hour12: false })}`
5354
5672
  });
5355
5673
  }).catch((error) => {
5674
+ heartbeatHealthy = false;
5356
5675
  if (error instanceof ConnectorDeletedError) {
5357
5676
  void writeLog("warn", "connector deleted remotely; stopping daemon");
5358
5677
  void shutdown({ unregister: false });
@@ -5368,6 +5687,7 @@ async function runConnectorDaemon(launchConfig, options = {}) {
5368
5687
  config,
5369
5688
  configRoot,
5370
5689
  getLastHeartbeatAt: () => lastHeartbeatAt,
5690
+ isHeartbeatHealthy: () => heartbeatHealthy,
5371
5691
  isIdle: () => relay.isIdle(),
5372
5692
  isRelayConnected: () => relay.isConnected(),
5373
5693
  mode: options.mode ?? "persistent",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buda-ai/connector",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "publishConfig": {