@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
|
|
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
|
|
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
|
|
12
|
+
* - Relative path (no leading slash): resolved against the Agent workspace
|
|
13
13
|
* e.g. "uploads/photo.jpg" → /agent/uploads/photo.jpg
|
|
14
|
-
* -
|
|
14
|
+
* - Logical absolute path starting with /agent: resolved against the Agent workspace
|
|
15
15
|
* e.g. "/agent/chart.png" → /agent/chart.png
|
|
16
|
-
* -
|
|
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
|
|
29
|
+
const LOGICAL_AGENT_ROOT = "/agent";
|
|
27
30
|
|
|
28
|
-
function
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
37
|
-
if (
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
71
|
+
const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
54
72
|
|
|
55
|
-
if (
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
process.exit(0);
|
|
59
|
-
}
|
|
73
|
+
if (isMain) {
|
|
74
|
+
const input = process.argv[2];
|
|
75
|
+
const resolved = resolveAgentPath(input);
|
|
60
76
|
|
|
61
|
-
|
|
62
|
-
process.stdout.write(
|
|
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.
|
|
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=${
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
4035
|
-
realpath as
|
|
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
|
|
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) =>
|
|
4053
|
-
|
|
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 =
|
|
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:
|
|
4070
|
-
space:
|
|
4071
|
-
system:
|
|
4072
|
-
temporary:
|
|
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 =
|
|
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,
|
|
4084
|
-
ensureVolumeLink(this.descriptor.sharedVolume,
|
|
4085
|
-
ensureVolumeLink(this.systemVolume,
|
|
4086
|
-
ensureVolumeLink(this.descriptor.temporaryVolume,
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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")
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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(
|
|
4724
|
-
const systemSkillsLink =
|
|
4725
|
-
const systemScriptsLink =
|
|
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(
|
|
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
|
|
4769
|
-
realpath as
|
|
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
|
|
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 =
|
|
5114
|
+
const registryDirectory = path14.join(canonicalRoot, REGISTRY_DIRECTORY);
|
|
4799
5115
|
await ensurePrivateDirectory2(registryDirectory);
|
|
4800
|
-
const registryPath =
|
|
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
|
|
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
|
|
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 =
|
|
4877
|
-
const agentRoot =
|
|
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:
|
|
4883
|
-
sharedVolume:
|
|
4884
|
-
stateVolume:
|
|
4885
|
-
temporaryVolume:
|
|
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
|
-
|
|
5247
|
+
assertContained2(storageRoot, volume);
|
|
4932
5248
|
await ensurePrivateDirectory2(volume);
|
|
4933
|
-
const canonical = await
|
|
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
|
|
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
|
|
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 =
|
|
5033
|
-
return relative !== "" && relative !== ".." && !relative.startsWith(`..${
|
|
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
|
|
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(
|
|
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(
|
|
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",
|