@buda-ai/connector 0.1.10 → 0.1.11
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.11",
|
|
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);
|
|
@@ -3857,7 +3860,8 @@ var corrupt = (message) => new LegacyWorkspaceMigrationError("MIGRATION_CORRUPT"
|
|
|
3857
3860
|
var errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
3858
3861
|
|
|
3859
3862
|
// src/relay.ts
|
|
3860
|
-
import
|
|
3863
|
+
import { realpathSync } from "fs";
|
|
3864
|
+
import path12 from "path";
|
|
3861
3865
|
import { createNextHandler } from "@bunny-agent/daemon/nextjs";
|
|
3862
3866
|
import WebSocket2 from "ws";
|
|
3863
3867
|
|
|
@@ -4024,6 +4028,219 @@ function isNonEmptyString(value) {
|
|
|
4024
4028
|
return typeof value === "string" && value.trim().length > 0;
|
|
4025
4029
|
}
|
|
4026
4030
|
|
|
4031
|
+
// src/html-preview.ts
|
|
4032
|
+
import { readFile as readFile8, realpath as realpath2, stat as stat4 } from "fs/promises";
|
|
4033
|
+
import { createServer as createServer2 } from "http";
|
|
4034
|
+
import path10 from "path";
|
|
4035
|
+
var PREVIEW_HOST = "127.0.0.1";
|
|
4036
|
+
var MIN_PREVIEW_PORT = 3100;
|
|
4037
|
+
var MAX_PREVIEW_PORT = 3900;
|
|
4038
|
+
var MIME_TYPES = {
|
|
4039
|
+
".avif": "image/avif",
|
|
4040
|
+
".css": "text/css; charset=utf-8",
|
|
4041
|
+
".gif": "image/gif",
|
|
4042
|
+
".htm": "text/html; charset=utf-8",
|
|
4043
|
+
".html": "text/html; charset=utf-8",
|
|
4044
|
+
".ico": "image/x-icon",
|
|
4045
|
+
".jpeg": "image/jpeg",
|
|
4046
|
+
".jpg": "image/jpeg",
|
|
4047
|
+
".js": "text/javascript; charset=utf-8",
|
|
4048
|
+
".json": "application/json; charset=utf-8",
|
|
4049
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
4050
|
+
".png": "image/png",
|
|
4051
|
+
".svg": "image/svg+xml",
|
|
4052
|
+
".txt": "text/plain; charset=utf-8",
|
|
4053
|
+
".webp": "image/webp",
|
|
4054
|
+
".woff": "font/woff",
|
|
4055
|
+
".woff2": "font/woff2"
|
|
4056
|
+
};
|
|
4057
|
+
var HtmlPreviewServerRegistry = class {
|
|
4058
|
+
servers = /* @__PURE__ */ new Map();
|
|
4059
|
+
async open(input) {
|
|
4060
|
+
const htmlFile = await resolveWorkspaceFile(input);
|
|
4061
|
+
const folder = path10.dirname(htmlFile);
|
|
4062
|
+
let pending = this.servers.get(folder);
|
|
4063
|
+
if (!pending) {
|
|
4064
|
+
pending = startStaticServer(folder);
|
|
4065
|
+
this.servers.set(folder, pending);
|
|
4066
|
+
void pending.catch(() => {
|
|
4067
|
+
if (this.servers.get(folder) === pending) this.servers.delete(folder);
|
|
4068
|
+
});
|
|
4069
|
+
}
|
|
4070
|
+
const { port } = await pending;
|
|
4071
|
+
const previewUrl = new URL(`http://${PREVIEW_HOST}:${port}`);
|
|
4072
|
+
previewUrl.pathname = `/${path10.basename(htmlFile)}`;
|
|
4073
|
+
return {
|
|
4074
|
+
url: previewUrl.toString(),
|
|
4075
|
+
port,
|
|
4076
|
+
htmlPath: previewUrl.pathname
|
|
4077
|
+
};
|
|
4078
|
+
}
|
|
4079
|
+
async closeAll() {
|
|
4080
|
+
const pending = [...this.servers.values()];
|
|
4081
|
+
this.servers.clear();
|
|
4082
|
+
const entries = await Promise.all(pending.map((entry) => entry.catch(() => void 0)));
|
|
4083
|
+
await Promise.all(
|
|
4084
|
+
entries.map(
|
|
4085
|
+
(entry) => new Promise((resolve) => {
|
|
4086
|
+
if (!entry?.server.listening) return resolve();
|
|
4087
|
+
entry.server.close(() => resolve());
|
|
4088
|
+
})
|
|
4089
|
+
)
|
|
4090
|
+
);
|
|
4091
|
+
}
|
|
4092
|
+
};
|
|
4093
|
+
var handleHtmlPreviewRequest = async (request, mounts, registry) => {
|
|
4094
|
+
if (request.method !== "POST") {
|
|
4095
|
+
return Response.json({ error: "HTML preview requires POST." }, { status: 405 });
|
|
4096
|
+
}
|
|
4097
|
+
if (!request.body || typeof request.body !== "object" || Array.isArray(request.body)) {
|
|
4098
|
+
return Response.json({ error: "Invalid HTML preview request." }, { status: 400 });
|
|
4099
|
+
}
|
|
4100
|
+
const body = request.body;
|
|
4101
|
+
if (typeof body.filePath !== "string" || body.filePath.length === 0) {
|
|
4102
|
+
return Response.json({ error: "HTML preview requires a file path." }, { status: 400 });
|
|
4103
|
+
}
|
|
4104
|
+
if (body.rootVolume !== "agent" && body.rootVolume !== "space") {
|
|
4105
|
+
return Response.json({ error: "HTML preview requires a valid root volume." }, { status: 400 });
|
|
4106
|
+
}
|
|
4107
|
+
try {
|
|
4108
|
+
return Response.json(
|
|
4109
|
+
await registry.open({ filePath: body.filePath, rootVolume: body.rootVolume, mounts })
|
|
4110
|
+
);
|
|
4111
|
+
} catch (error) {
|
|
4112
|
+
return Response.json(
|
|
4113
|
+
{ error: error instanceof Error ? error.message : "Unable to open HTML preview." },
|
|
4114
|
+
{ status: 400 }
|
|
4115
|
+
);
|
|
4116
|
+
}
|
|
4117
|
+
};
|
|
4118
|
+
var resolveWorkspaceFile = async (input) => {
|
|
4119
|
+
const virtualPath = input.filePath.replaceAll("\\", "/");
|
|
4120
|
+
if (virtualPath.includes("\0")) throw new Error("HTML preview path contains invalid data.");
|
|
4121
|
+
const mountPath = path10.resolve(input.mounts[input.rootVolume]);
|
|
4122
|
+
const root = await realpath2(mountPath);
|
|
4123
|
+
let relativePath = virtualPath;
|
|
4124
|
+
let explicitVolume;
|
|
4125
|
+
for (const volume of ["agent", "space"]) {
|
|
4126
|
+
const prefix = `/${volume}`;
|
|
4127
|
+
if (virtualPath === prefix || virtualPath.startsWith(`${prefix}/`)) {
|
|
4128
|
+
explicitVolume = volume;
|
|
4129
|
+
relativePath = virtualPath.slice(prefix.length).replace(/^\/+/, "");
|
|
4130
|
+
break;
|
|
4131
|
+
}
|
|
4132
|
+
}
|
|
4133
|
+
if (explicitVolume && explicitVolume !== input.rootVolume) {
|
|
4134
|
+
throw new Error("HTML preview path does not match the requested root volume.");
|
|
4135
|
+
}
|
|
4136
|
+
if (!explicitVolume && (path10.isAbsolute(virtualPath) || /^[A-Za-z]:\//.test(virtualPath))) {
|
|
4137
|
+
const physicalPath = path10.normalize(virtualPath);
|
|
4138
|
+
const physicalRoot = [mountPath, root].find(
|
|
4139
|
+
(candidateRoot) => isContained(candidateRoot, physicalPath)
|
|
4140
|
+
);
|
|
4141
|
+
if (!physicalRoot) {
|
|
4142
|
+
throw new Error("HTML preview path must stay inside the requested workspace volume.");
|
|
4143
|
+
}
|
|
4144
|
+
relativePath = path10.relative(physicalRoot, physicalPath).split(path10.sep).join("/");
|
|
4145
|
+
}
|
|
4146
|
+
const segments = relativePath.split("/").filter(Boolean);
|
|
4147
|
+
if (segments.length === 0 || segments.some((segment) => segment === "." || segment === "..")) {
|
|
4148
|
+
throw new Error("HTML preview path must not contain traversal segments.");
|
|
4149
|
+
}
|
|
4150
|
+
const candidate = path10.resolve(root, ...segments);
|
|
4151
|
+
assertContained(root, candidate);
|
|
4152
|
+
const canonicalFile = await realpath2(candidate);
|
|
4153
|
+
assertContained(root, canonicalFile);
|
|
4154
|
+
const fileStat = await stat4(canonicalFile);
|
|
4155
|
+
if (!fileStat.isFile()) throw new Error("HTML preview path must identify a file.");
|
|
4156
|
+
if (![".htm", ".html"].includes(path10.extname(canonicalFile).toLowerCase())) {
|
|
4157
|
+
throw new Error("HTML preview path must identify an HTML file.");
|
|
4158
|
+
}
|
|
4159
|
+
return canonicalFile;
|
|
4160
|
+
};
|
|
4161
|
+
var startStaticServer = async (folder) => {
|
|
4162
|
+
for (let port = MIN_PREVIEW_PORT; port <= MAX_PREVIEW_PORT; port += 1) {
|
|
4163
|
+
const server = createServer2((request, response) => {
|
|
4164
|
+
void serveStaticFile(folder, request.method ?? "GET", request.url ?? "/").then((result) => {
|
|
4165
|
+
response.writeHead(result.status, result.headers);
|
|
4166
|
+
response.end(request.method === "HEAD" ? void 0 : result.body);
|
|
4167
|
+
}).catch(() => {
|
|
4168
|
+
response.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
|
|
4169
|
+
response.end("HTML preview failed.");
|
|
4170
|
+
});
|
|
4171
|
+
});
|
|
4172
|
+
const listening = await listen(server, port);
|
|
4173
|
+
if (listening) {
|
|
4174
|
+
server.unref();
|
|
4175
|
+
return { server, port };
|
|
4176
|
+
}
|
|
4177
|
+
}
|
|
4178
|
+
throw new Error(
|
|
4179
|
+
`No free HTML preview port is available (${MIN_PREVIEW_PORT}-${MAX_PREVIEW_PORT}).`
|
|
4180
|
+
);
|
|
4181
|
+
};
|
|
4182
|
+
var listen = (server, port) => new Promise((resolve, reject) => {
|
|
4183
|
+
const onError = (error) => {
|
|
4184
|
+
server.removeListener("listening", onListening);
|
|
4185
|
+
if (error.code === "EADDRINUSE") resolve(false);
|
|
4186
|
+
else reject(error);
|
|
4187
|
+
};
|
|
4188
|
+
const onListening = () => {
|
|
4189
|
+
server.removeListener("error", onError);
|
|
4190
|
+
resolve(true);
|
|
4191
|
+
};
|
|
4192
|
+
server.once("error", onError);
|
|
4193
|
+
server.once("listening", onListening);
|
|
4194
|
+
server.listen(port, PREVIEW_HOST);
|
|
4195
|
+
});
|
|
4196
|
+
var serveStaticFile = async (folder, method, requestUrl) => {
|
|
4197
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
4198
|
+
return { status: 405, headers: { Allow: "GET, HEAD" } };
|
|
4199
|
+
}
|
|
4200
|
+
let decodedPath;
|
|
4201
|
+
try {
|
|
4202
|
+
decodedPath = decodeURIComponent(requestUrl.split("?", 1)[0] ?? "/").replaceAll("\\", "/");
|
|
4203
|
+
} catch {
|
|
4204
|
+
return { status: 400, headers: {} };
|
|
4205
|
+
}
|
|
4206
|
+
if (decodedPath.includes("\0")) return { status: 400, headers: {} };
|
|
4207
|
+
const segments = decodedPath.split("/").filter(Boolean);
|
|
4208
|
+
if (segments.some((segment) => segment === "." || segment === "..")) {
|
|
4209
|
+
return { status: 403, headers: {} };
|
|
4210
|
+
}
|
|
4211
|
+
try {
|
|
4212
|
+
const candidate = path10.resolve(folder, ...segments);
|
|
4213
|
+
assertContained(folder, candidate);
|
|
4214
|
+
let canonicalFile = await realpath2(candidate);
|
|
4215
|
+
assertContained(folder, canonicalFile);
|
|
4216
|
+
if ((await stat4(canonicalFile)).isDirectory()) {
|
|
4217
|
+
canonicalFile = await realpath2(path10.join(canonicalFile, "index.html"));
|
|
4218
|
+
assertContained(folder, canonicalFile);
|
|
4219
|
+
}
|
|
4220
|
+
if (!(await stat4(canonicalFile)).isFile()) throw new Error("Not a file");
|
|
4221
|
+
const body = method === "HEAD" ? void 0 : await readFile8(canonicalFile);
|
|
4222
|
+
return {
|
|
4223
|
+
status: 200,
|
|
4224
|
+
headers: {
|
|
4225
|
+
"Cache-Control": "no-store",
|
|
4226
|
+
"Content-Type": MIME_TYPES[path10.extname(canonicalFile).toLowerCase()] ?? "application/octet-stream",
|
|
4227
|
+
"X-Content-Type-Options": "nosniff"
|
|
4228
|
+
},
|
|
4229
|
+
body
|
|
4230
|
+
};
|
|
4231
|
+
} catch {
|
|
4232
|
+
return { status: 404, headers: { "Content-Type": "text/plain; charset=utf-8" } };
|
|
4233
|
+
}
|
|
4234
|
+
};
|
|
4235
|
+
var assertContained = (root, candidate) => {
|
|
4236
|
+
if (isContained(root, candidate)) return;
|
|
4237
|
+
throw new Error("HTML preview path escapes its allowed root.");
|
|
4238
|
+
};
|
|
4239
|
+
var isContained = (root, candidate) => {
|
|
4240
|
+
const relative = path10.relative(root, candidate);
|
|
4241
|
+
return relative === "" || !relative.startsWith("..") && !path10.isAbsolute(relative);
|
|
4242
|
+
};
|
|
4243
|
+
|
|
4027
4244
|
// src/workspace-volume-lifecycle.ts
|
|
4028
4245
|
import { constants as constants3 } from "fs";
|
|
4029
4246
|
import {
|
|
@@ -4031,13 +4248,13 @@ import {
|
|
|
4031
4248
|
lstat as lstat2,
|
|
4032
4249
|
mkdir as mkdir9,
|
|
4033
4250
|
readdir as readdir5,
|
|
4034
|
-
readFile as
|
|
4035
|
-
realpath as
|
|
4251
|
+
readFile as readFile9,
|
|
4252
|
+
realpath as realpath3,
|
|
4036
4253
|
rm as rm3,
|
|
4037
4254
|
symlink as symlink2,
|
|
4038
4255
|
writeFile as writeFile4
|
|
4039
4256
|
} from "fs/promises";
|
|
4040
|
-
import
|
|
4257
|
+
import path11 from "path";
|
|
4041
4258
|
var MOUNT_DIRECTORY = "mount";
|
|
4042
4259
|
var ARCHIVE_MARKER = "archived.json";
|
|
4043
4260
|
var AGENT_CONTEXT_TEMPLATE_FILES = [
|
|
@@ -4049,8 +4266,8 @@ var AGENT_CONTEXT_TEMPLATE_FILES = [
|
|
|
4049
4266
|
"BOOTSTRAP.md",
|
|
4050
4267
|
"MEMORY.md"
|
|
4051
4268
|
];
|
|
4052
|
-
var deriveSystemVolume = (descriptor) =>
|
|
4053
|
-
|
|
4269
|
+
var deriveSystemVolume = (descriptor) => path11.join(
|
|
4270
|
+
path11.dirname(path11.dirname(path11.dirname(path11.dirname(descriptor.sharedVolume)))),
|
|
4054
4271
|
"system"
|
|
4055
4272
|
);
|
|
4056
4273
|
var WorkspaceVolumeLifecycle = class {
|
|
@@ -4062,14 +4279,14 @@ var WorkspaceVolumeLifecycle = class {
|
|
|
4062
4279
|
async mount() {
|
|
4063
4280
|
await this.ensureVolumes();
|
|
4064
4281
|
await this.bootstrapAgentTemplates((await readBundledManifest()).sharedDir);
|
|
4065
|
-
const mountRoot =
|
|
4282
|
+
const mountRoot = path11.join(this.descriptor.stateVolume, MOUNT_DIRECTORY);
|
|
4066
4283
|
await mkdir9(mountRoot, { recursive: true, mode: 448 });
|
|
4067
4284
|
const mounts = {
|
|
4068
4285
|
mountRoot,
|
|
4069
|
-
agent:
|
|
4070
|
-
space:
|
|
4071
|
-
system:
|
|
4072
|
-
temporary:
|
|
4286
|
+
agent: path11.join(mountRoot, "agent"),
|
|
4287
|
+
space: path11.join(mountRoot, "space"),
|
|
4288
|
+
system: path11.join(mountRoot, "system"),
|
|
4289
|
+
temporary: path11.join(mountRoot, "tmp")
|
|
4073
4290
|
};
|
|
4074
4291
|
await Promise.all([
|
|
4075
4292
|
ensureVolumeLink(this.descriptor.privateVolume, mounts.agent),
|
|
@@ -4077,18 +4294,18 @@ var WorkspaceVolumeLifecycle = class {
|
|
|
4077
4294
|
ensureVolumeLink(this.systemVolume, mounts.system),
|
|
4078
4295
|
ensureVolumeLink(this.descriptor.temporaryVolume, mounts.temporary)
|
|
4079
4296
|
]);
|
|
4080
|
-
const daemonVolumes =
|
|
4297
|
+
const daemonVolumes = path11.join(mountRoot, "volumes");
|
|
4081
4298
|
await mkdir9(daemonVolumes, { recursive: true, mode: 448 });
|
|
4082
4299
|
await Promise.all([
|
|
4083
|
-
ensureVolumeLink(this.descriptor.privateVolume,
|
|
4084
|
-
ensureVolumeLink(this.descriptor.sharedVolume,
|
|
4085
|
-
ensureVolumeLink(this.systemVolume,
|
|
4086
|
-
ensureVolumeLink(this.descriptor.temporaryVolume,
|
|
4300
|
+
ensureVolumeLink(this.descriptor.privateVolume, path11.join(daemonVolumes, "agent")),
|
|
4301
|
+
ensureVolumeLink(this.descriptor.sharedVolume, path11.join(daemonVolumes, "space")),
|
|
4302
|
+
ensureVolumeLink(this.systemVolume, path11.join(daemonVolumes, "system")),
|
|
4303
|
+
ensureVolumeLink(this.descriptor.temporaryVolume, path11.join(daemonVolumes, "tmp"))
|
|
4087
4304
|
]);
|
|
4088
4305
|
return mounts;
|
|
4089
4306
|
}
|
|
4090
4307
|
async unmount() {
|
|
4091
|
-
await rm3(
|
|
4308
|
+
await rm3(path11.join(this.descriptor.stateVolume, MOUNT_DIRECTORY), {
|
|
4092
4309
|
recursive: true,
|
|
4093
4310
|
force: true
|
|
4094
4311
|
});
|
|
@@ -4097,33 +4314,33 @@ var WorkspaceVolumeLifecycle = class {
|
|
|
4097
4314
|
await this.unmount();
|
|
4098
4315
|
await mkdir9(this.descriptor.stateVolume, { recursive: true, mode: 448 });
|
|
4099
4316
|
await writeFile4(
|
|
4100
|
-
|
|
4317
|
+
path11.join(this.descriptor.stateVolume, ARCHIVE_MARKER),
|
|
4101
4318
|
`${JSON.stringify({ archivedAt: (/* @__PURE__ */ new Date()).toISOString() })}
|
|
4102
4319
|
`,
|
|
4103
4320
|
{ encoding: "utf8", mode: 384 }
|
|
4104
4321
|
);
|
|
4105
4322
|
}
|
|
4106
4323
|
async restoreAgent() {
|
|
4107
|
-
await rm3(
|
|
4324
|
+
await rm3(path11.join(this.descriptor.stateVolume, ARCHIVE_MARKER), { force: true });
|
|
4108
4325
|
return this.mount();
|
|
4109
4326
|
}
|
|
4110
4327
|
async deleteAgent(options) {
|
|
4111
4328
|
requireConfirmation(options);
|
|
4112
4329
|
await this.unmount();
|
|
4113
|
-
const agentRoot =
|
|
4330
|
+
const agentRoot = path11.dirname(this.descriptor.privateVolume);
|
|
4114
4331
|
await rm3(agentRoot, { recursive: true, force: true });
|
|
4115
4332
|
}
|
|
4116
4333
|
async archiveSpace() {
|
|
4117
4334
|
await mkdir9(this.descriptor.sharedVolume, { recursive: true, mode: 448 });
|
|
4118
4335
|
await writeFile4(
|
|
4119
|
-
|
|
4336
|
+
path11.join(this.descriptor.sharedVolume, ARCHIVE_MARKER),
|
|
4120
4337
|
`${JSON.stringify({ archivedAt: (/* @__PURE__ */ new Date()).toISOString() })}
|
|
4121
4338
|
`,
|
|
4122
4339
|
{ encoding: "utf8", mode: 384 }
|
|
4123
4340
|
);
|
|
4124
4341
|
}
|
|
4125
4342
|
async restoreSpace() {
|
|
4126
|
-
await rm3(
|
|
4343
|
+
await rm3(path11.join(this.descriptor.sharedVolume, ARCHIVE_MARKER), { force: true });
|
|
4127
4344
|
}
|
|
4128
4345
|
async deleteSpace(options) {
|
|
4129
4346
|
requireConfirmation(options);
|
|
@@ -4139,7 +4356,7 @@ var WorkspaceVolumeLifecycle = class {
|
|
|
4139
4356
|
);
|
|
4140
4357
|
for (const filename of AGENT_CONTEXT_TEMPLATE_FILES) {
|
|
4141
4358
|
if (!entries.has(filename)) continue;
|
|
4142
|
-
const destination =
|
|
4359
|
+
const destination = path11.join(this.descriptor.privateVolume, filename);
|
|
4143
4360
|
try {
|
|
4144
4361
|
await lstat2(destination);
|
|
4145
4362
|
skipped.push(filename);
|
|
@@ -4147,7 +4364,7 @@ var WorkspaceVolumeLifecycle = class {
|
|
|
4147
4364
|
if (!isMissingFile(error)) throw error;
|
|
4148
4365
|
try {
|
|
4149
4366
|
await copyFile2(
|
|
4150
|
-
|
|
4367
|
+
path11.join(templateDirectory, filename),
|
|
4151
4368
|
destination,
|
|
4152
4369
|
constants3.COPYFILE_EXCL
|
|
4153
4370
|
);
|
|
@@ -4170,7 +4387,7 @@ var WorkspaceVolumeLifecycle = class {
|
|
|
4170
4387
|
]) {
|
|
4171
4388
|
await mkdir9(volume, { recursive: true, mode: 448 });
|
|
4172
4389
|
const info = await lstat2(volume);
|
|
4173
|
-
if (info.isSymbolicLink() || !info.isDirectory() || await
|
|
4390
|
+
if (info.isSymbolicLink() || !info.isDirectory() || await realpath3(volume) !== volume) {
|
|
4174
4391
|
throw new Error("Workspace volume must be a canonical real directory");
|
|
4175
4392
|
}
|
|
4176
4393
|
}
|
|
@@ -4183,7 +4400,7 @@ var isMissingFile = (error) => error instanceof Error && "code" in error && erro
|
|
|
4183
4400
|
var ensureVolumeLink = async (target, linkPath) => {
|
|
4184
4401
|
try {
|
|
4185
4402
|
const info = await lstat2(linkPath);
|
|
4186
|
-
if (!info.isSymbolicLink() || await
|
|
4403
|
+
if (!info.isSymbolicLink() || await realpath3(linkPath) !== target) {
|
|
4187
4404
|
throw new Error("Workspace mount link does not match its canonical volume");
|
|
4188
4405
|
}
|
|
4189
4406
|
} catch (error) {
|
|
@@ -4193,7 +4410,7 @@ var ensureVolumeLink = async (target, linkPath) => {
|
|
|
4193
4410
|
} catch (createError) {
|
|
4194
4411
|
if (!isAlreadyExists3(createError)) throw createError;
|
|
4195
4412
|
const info = await lstat2(linkPath);
|
|
4196
|
-
if (!info.isSymbolicLink() || await
|
|
4413
|
+
if (!info.isSymbolicLink() || await realpath3(linkPath) !== target) {
|
|
4197
4414
|
throw new Error("Workspace mount link raced with an invalid target");
|
|
4198
4415
|
}
|
|
4199
4416
|
}
|
|
@@ -4255,6 +4472,7 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
|
|
|
4255
4472
|
let currentInflight = /* @__PURE__ */ new Map();
|
|
4256
4473
|
let activeTerminalTunnels = 0;
|
|
4257
4474
|
const replayGuard = new ConnectorExecutionReplayGuard();
|
|
4475
|
+
const htmlPreviews = new HtmlPreviewServerRegistry();
|
|
4258
4476
|
const clearTimers = () => {
|
|
4259
4477
|
if (pingTimer) {
|
|
4260
4478
|
clearInterval(pingTimer);
|
|
@@ -4361,13 +4579,15 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
|
|
|
4361
4579
|
onTerminalClose: () => {
|
|
4362
4580
|
activeTerminalTunnels = Math.max(0, activeTerminalTunnels - 1);
|
|
4363
4581
|
}
|
|
4364
|
-
}
|
|
4582
|
+
},
|
|
4583
|
+
htmlPreviews
|
|
4365
4584
|
);
|
|
4366
4585
|
});
|
|
4367
4586
|
ws.on("close", (code) => {
|
|
4368
4587
|
detach();
|
|
4369
4588
|
if (!shouldReconnectConnectorRelay(code)) {
|
|
4370
4589
|
clearTimers();
|
|
4590
|
+
void htmlPreviews.closeAll();
|
|
4371
4591
|
if (code === CONNECTOR_SOCKET_REPLACED_CLOSE_CODE) {
|
|
4372
4592
|
stopped = true;
|
|
4373
4593
|
void writeLog("warn", "relay replaced by another process for this connector");
|
|
@@ -4427,10 +4647,11 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
|
|
|
4427
4647
|
}
|
|
4428
4648
|
clearTimers();
|
|
4429
4649
|
socket?.close();
|
|
4650
|
+
void htmlPreviews.closeAll();
|
|
4430
4651
|
}
|
|
4431
4652
|
};
|
|
4432
4653
|
}
|
|
4433
|
-
async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedConnectorId, relayProtocol, replayGuard, workspaceRegistry, serverUrl, localDaemonBaseUrl, activity) {
|
|
4654
|
+
async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedConnectorId, relayProtocol, replayGuard, workspaceRegistry, serverUrl, localDaemonBaseUrl, activity, htmlPreviews) {
|
|
4434
4655
|
if (!socket || socket.readyState !== WebSocket2.OPEN) return;
|
|
4435
4656
|
let message;
|
|
4436
4657
|
try {
|
|
@@ -4534,6 +4755,7 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
|
|
|
4534
4755
|
const requestPath = message.request.path;
|
|
4535
4756
|
const isBrowserRequest = requestPath.startsWith("/browser/") || requestPath === "/browser";
|
|
4536
4757
|
const isWorkspaceDescriptorRequest = requestPath === "/workspace/descriptor";
|
|
4758
|
+
const isHtmlPreviewRequest = new URL(requestPath, "http://connector.workspace").pathname === "/api/preview/html";
|
|
4537
4759
|
const isAgentRequest = isBrowserRequest || requestPath.startsWith("/api/") || isWorkspaceDescriptorRequest;
|
|
4538
4760
|
if (isAgentRequest && (!executionIdentity || !workspaceRegistry)) {
|
|
4539
4761
|
throw new ConnectorRelayProtocolError("IDENTITY_INVALID");
|
|
@@ -4566,6 +4788,8 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
|
|
|
4566
4788
|
workdirRoot: mounts?.mountRoot
|
|
4567
4789
|
}
|
|
4568
4790
|
);
|
|
4791
|
+
} else if (isHtmlPreviewRequest && mounts && htmlPreviews) {
|
|
4792
|
+
response = await handleHtmlPreviewRequest(message.request, mounts, htmlPreviews);
|
|
4569
4793
|
} else if (descriptor && mounts) {
|
|
4570
4794
|
response = await handleWorkspaceDaemonRequest(message.request, mounts, controller.signal);
|
|
4571
4795
|
} else {
|
|
@@ -4606,7 +4830,8 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
|
|
|
4606
4830
|
});
|
|
4607
4831
|
if (response.body) {
|
|
4608
4832
|
const reader = response.body.getReader();
|
|
4609
|
-
const
|
|
4833
|
+
const binaryResponse = getHeaderValue(message.request.headers, "x-buda-relay-response-encoding") === "base64";
|
|
4834
|
+
const decoder = binaryResponse ? null : new TextDecoder();
|
|
4610
4835
|
const cancelReader = () => {
|
|
4611
4836
|
void reader.cancel().catch(() => {
|
|
4612
4837
|
});
|
|
@@ -4620,10 +4845,11 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
|
|
|
4620
4845
|
sendRelayMessage(socket, {
|
|
4621
4846
|
type: "chunk",
|
|
4622
4847
|
requestId: message.requestId,
|
|
4623
|
-
data: decoder
|
|
4848
|
+
data: binaryResponse ? Buffer.from(value).toString("base64") : decoder?.decode(value, { stream: true }) ?? "",
|
|
4849
|
+
...binaryResponse ? { encoding: "base64" } : {}
|
|
4624
4850
|
});
|
|
4625
4851
|
}
|
|
4626
|
-
const tail = decoder
|
|
4852
|
+
const tail = decoder?.decode() ?? "";
|
|
4627
4853
|
if (tail) {
|
|
4628
4854
|
sendRelayMessage(socket, {
|
|
4629
4855
|
type: "chunk",
|
|
@@ -4663,6 +4889,29 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
|
|
|
4663
4889
|
var handleWorkspaceDaemonRequest = async (request, mounts, signal) => {
|
|
4664
4890
|
const normalized = normalizeWorkspaceRequest(request, mounts);
|
|
4665
4891
|
const handler = createNextHandler({ root: mounts.mountRoot });
|
|
4892
|
+
if (getHeaderValue(normalized.headers, "x-buda-relay-body-encoding") === "base64") {
|
|
4893
|
+
if (new URL(normalized.path, "http://connector.workspace").pathname !== "/api/fs/write-stream") {
|
|
4894
|
+
throw new Error("Base64 relay bodies are only supported for filesystem write streams.");
|
|
4895
|
+
}
|
|
4896
|
+
if (typeof normalized.body !== "string") {
|
|
4897
|
+
throw new Error("Invalid base64 filesystem write body.");
|
|
4898
|
+
}
|
|
4899
|
+
const content = Buffer.from(normalized.body, "base64");
|
|
4900
|
+
if (content.toString("base64") !== normalized.body) {
|
|
4901
|
+
throw new Error("Invalid base64 filesystem write content.");
|
|
4902
|
+
}
|
|
4903
|
+
const headers = new Headers(normalized.headers);
|
|
4904
|
+
headers.delete("x-buda-relay-body-encoding");
|
|
4905
|
+
headers.set("Content-Type", "application/octet-stream");
|
|
4906
|
+
return handler(
|
|
4907
|
+
new Request(new URL(normalized.path, "http://connector.workspace"), {
|
|
4908
|
+
method: normalized.method,
|
|
4909
|
+
headers,
|
|
4910
|
+
body: content,
|
|
4911
|
+
signal
|
|
4912
|
+
})
|
|
4913
|
+
);
|
|
4914
|
+
}
|
|
4666
4915
|
return handler(
|
|
4667
4916
|
new Request(new URL(normalized.path, "http://connector.workspace"), {
|
|
4668
4917
|
method: normalized.method,
|
|
@@ -4673,18 +4922,60 @@ var handleWorkspaceDaemonRequest = async (request, mounts, signal) => {
|
|
|
4673
4922
|
);
|
|
4674
4923
|
};
|
|
4675
4924
|
var normalizeWorkspaceRequest = (request, mounts) => {
|
|
4676
|
-
if (request.path !== "/api/coding/run")
|
|
4925
|
+
if (request.path !== "/api/coding/run") {
|
|
4926
|
+
return normalizeConnectorFilesystemRequest(request, mounts);
|
|
4927
|
+
}
|
|
4677
4928
|
if (!request.body || typeof request.body !== "object" || Array.isArray(request.body)) {
|
|
4678
4929
|
throw new ConnectorRelayProtocolError("WORKSPACE_MISMATCH");
|
|
4679
4930
|
}
|
|
4680
4931
|
const body = request.body;
|
|
4681
4932
|
const cwd = typeof body.cwd === "string" ? body.cwd : "/agent";
|
|
4682
|
-
const normalized =
|
|
4933
|
+
const normalized = path12.posix.normalize(cwd);
|
|
4683
4934
|
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
4935
|
if (!mount || relative === void 0) {
|
|
4685
4936
|
throw new ConnectorRelayProtocolError("WORKSPACE_MISMATCH");
|
|
4686
4937
|
}
|
|
4687
|
-
|
|
4938
|
+
const env = body.env && typeof body.env === "object" && !Array.isArray(body.env) ? body.env : {};
|
|
4939
|
+
return {
|
|
4940
|
+
...request,
|
|
4941
|
+
body: {
|
|
4942
|
+
...body,
|
|
4943
|
+
cwd: path12.join(mount, relative),
|
|
4944
|
+
env: {
|
|
4945
|
+
...env,
|
|
4946
|
+
BUDA_AGENT_ROOT: mounts.agent,
|
|
4947
|
+
BUDA_SPACE_ROOT: mounts.space,
|
|
4948
|
+
BUDA_SYSTEM_ROOT: mounts.system
|
|
4949
|
+
}
|
|
4950
|
+
}
|
|
4951
|
+
};
|
|
4952
|
+
};
|
|
4953
|
+
var normalizeConnectorFilesystemRequest = (request, mounts) => {
|
|
4954
|
+
const url = new URL(request.path, "http://connector.workspace");
|
|
4955
|
+
if (!url.pathname.startsWith("/api/fs/")) return request;
|
|
4956
|
+
const filePath = url.searchParams.get("path");
|
|
4957
|
+
if (!filePath) return request;
|
|
4958
|
+
const mount = url.searchParams.get("volume") === "space" ? mounts.space : mounts.agent;
|
|
4959
|
+
const roots = [mount];
|
|
4960
|
+
try {
|
|
4961
|
+
const canonicalRoot = realpathSync(mount);
|
|
4962
|
+
if (canonicalRoot !== mount) roots.push(canonicalRoot);
|
|
4963
|
+
} catch {
|
|
4964
|
+
}
|
|
4965
|
+
const normalizedFilePath = path12.normalize(filePath);
|
|
4966
|
+
for (const root of roots) {
|
|
4967
|
+
const normalizedRoot = path12.normalize(root);
|
|
4968
|
+
const candidates = [normalizedRoot, normalizedRoot.replace(/^[/\\]+/, "")];
|
|
4969
|
+
for (const candidate of candidates) {
|
|
4970
|
+
if (normalizedFilePath !== candidate && !normalizedFilePath.startsWith(`${candidate}${path12.sep}`)) {
|
|
4971
|
+
continue;
|
|
4972
|
+
}
|
|
4973
|
+
const relativePath = path12.relative(candidate, normalizedFilePath).split(path12.sep).join("/");
|
|
4974
|
+
url.searchParams.set("path", relativePath);
|
|
4975
|
+
return { ...request, path: `${url.pathname}${url.search}` };
|
|
4976
|
+
}
|
|
4977
|
+
}
|
|
4978
|
+
return request;
|
|
4688
4979
|
};
|
|
4689
4980
|
function sendRelayError(socket, requestId, error) {
|
|
4690
4981
|
sendRelayMessage(socket, {
|
|
@@ -4716,13 +5007,13 @@ function getHeaderValue(headers, name) {
|
|
|
4716
5007
|
|
|
4717
5008
|
// src/workdir-layout.ts
|
|
4718
5009
|
import { lstat as lstat3, mkdir as mkdir10, symlink as symlink3, unlink as unlink6 } from "fs/promises";
|
|
4719
|
-
import
|
|
5010
|
+
import path13 from "path";
|
|
4720
5011
|
async function provisionWorkdirLayout(workdirRoot) {
|
|
4721
5012
|
const manifest = await readBundledManifest();
|
|
4722
5013
|
await mkdir10(workdirRoot, { recursive: true });
|
|
4723
|
-
await mkdir10(
|
|
4724
|
-
const systemSkillsLink =
|
|
4725
|
-
const systemScriptsLink =
|
|
5014
|
+
await mkdir10(path13.join(workdirRoot, "system", ".agents"), { recursive: true });
|
|
5015
|
+
const systemSkillsLink = path13.join(workdirRoot, "system", ".agents", "skills");
|
|
5016
|
+
const systemScriptsLink = path13.join(workdirRoot, "system", "scripts");
|
|
4726
5017
|
await ensureSymlink(manifest.systemSkillsDir, systemSkillsLink);
|
|
4727
5018
|
await ensureSymlink(manifest.scriptsDir, systemScriptsLink);
|
|
4728
5019
|
await writeLog("info", "workdir layout provisioned", {
|
|
@@ -4754,7 +5045,7 @@ async function ensureSymlink(target, linkPath) {
|
|
|
4754
5045
|
}
|
|
4755
5046
|
} catch {
|
|
4756
5047
|
}
|
|
4757
|
-
await mkdir10(
|
|
5048
|
+
await mkdir10(path13.dirname(linkPath), { recursive: true });
|
|
4758
5049
|
await symlink3(target, linkPath, "dir");
|
|
4759
5050
|
}
|
|
4760
5051
|
|
|
@@ -4765,13 +5056,13 @@ import {
|
|
|
4765
5056
|
lstat as lstat4,
|
|
4766
5057
|
mkdir as mkdir11,
|
|
4767
5058
|
open as open3,
|
|
4768
|
-
readFile as
|
|
4769
|
-
realpath as
|
|
5059
|
+
readFile as readFile10,
|
|
5060
|
+
realpath as realpath4,
|
|
4770
5061
|
rename as rename6,
|
|
4771
5062
|
statfs,
|
|
4772
5063
|
unlink as unlink7
|
|
4773
5064
|
} from "fs/promises";
|
|
4774
|
-
import
|
|
5065
|
+
import path14 from "path";
|
|
4775
5066
|
var REGISTRY_VERSION = 1;
|
|
4776
5067
|
var REGISTRY_DIRECTORY = ".buda";
|
|
4777
5068
|
var REGISTRY_FILENAME = "workspace-registry.json";
|
|
@@ -4795,13 +5086,13 @@ var LocalWorkspaceRegistry = class _LocalWorkspaceRegistry {
|
|
|
4795
5086
|
await mkdir11(storageRoot, { recursive: true, mode: 448 });
|
|
4796
5087
|
await rejectUnsafeFilesystem(storageRoot);
|
|
4797
5088
|
const canonicalRoot = await canonicalDirectory(storageRoot, "storage root");
|
|
4798
|
-
const registryDirectory =
|
|
5089
|
+
const registryDirectory = path14.join(canonicalRoot, REGISTRY_DIRECTORY);
|
|
4799
5090
|
await ensurePrivateDirectory2(registryDirectory);
|
|
4800
|
-
const registryPath =
|
|
5091
|
+
const registryPath = path14.join(registryDirectory, REGISTRY_FILENAME);
|
|
4801
5092
|
const document = await withExclusiveFileLock(registryPath, async () => {
|
|
4802
5093
|
let current;
|
|
4803
5094
|
try {
|
|
4804
|
-
current = parseRegistry(await
|
|
5095
|
+
current = parseRegistry(await readFile10(registryPath, "utf8"));
|
|
4805
5096
|
} catch (error) {
|
|
4806
5097
|
if (isMissingFile2(error)) {
|
|
4807
5098
|
current = { version: REGISTRY_VERSION, storageRoot: canonicalRoot, workspaces: {} };
|
|
@@ -4866,23 +5157,23 @@ var LocalWorkspaceRegistry = class _LocalWorkspaceRegistry {
|
|
|
4866
5157
|
};
|
|
4867
5158
|
var readRegistryDocument = async (registryPath) => {
|
|
4868
5159
|
try {
|
|
4869
|
-
return parseRegistry(await
|
|
5160
|
+
return parseRegistry(await readFile10(registryPath, "utf8"));
|
|
4870
5161
|
} catch (error) {
|
|
4871
5162
|
if (error instanceof WorkspaceRegistryError) throw error;
|
|
4872
5163
|
throw corrupt2(`Cannot read local workspace registry: ${errorMessage2(error)}`);
|
|
4873
5164
|
}
|
|
4874
5165
|
};
|
|
4875
5166
|
var deriveWorkspace = (storageRoot, spaceId, agentId) => {
|
|
4876
|
-
const spaceRoot =
|
|
4877
|
-
const agentRoot =
|
|
5167
|
+
const spaceRoot = path14.join(storageRoot, REGISTRY_DIRECTORY, "spaces", spaceId);
|
|
5168
|
+
const agentRoot = path14.join(spaceRoot, "agents", agentId);
|
|
4878
5169
|
return {
|
|
4879
5170
|
workspaceKey: `${spaceId}:${agentId}`,
|
|
4880
5171
|
spaceId,
|
|
4881
5172
|
agentId,
|
|
4882
|
-
privateVolume:
|
|
4883
|
-
sharedVolume:
|
|
4884
|
-
stateVolume:
|
|
4885
|
-
temporaryVolume:
|
|
5173
|
+
privateVolume: path14.join(agentRoot, "workspace"),
|
|
5174
|
+
sharedVolume: path14.join(spaceRoot, "shared"),
|
|
5175
|
+
stateVolume: path14.join(agentRoot, "state"),
|
|
5176
|
+
temporaryVolume: path14.join(agentRoot, "tmp")
|
|
4886
5177
|
};
|
|
4887
5178
|
};
|
|
4888
5179
|
var parseRegistry = (raw) => {
|
|
@@ -4928,9 +5219,9 @@ var reconcileWorkspace = async (workspace, storageRoot) => {
|
|
|
4928
5219
|
workspace.temporaryVolume
|
|
4929
5220
|
];
|
|
4930
5221
|
for (const volume of volumes) {
|
|
4931
|
-
|
|
5222
|
+
assertContained2(storageRoot, volume);
|
|
4932
5223
|
await ensurePrivateDirectory2(volume);
|
|
4933
|
-
const canonical = await
|
|
5224
|
+
const canonical = await realpath4(volume);
|
|
4934
5225
|
if (canonical !== volume) throw corrupt2("Workspace path resolves through a symbolic link");
|
|
4935
5226
|
}
|
|
4936
5227
|
};
|
|
@@ -4976,7 +5267,7 @@ var canonicalDirectory = async (directory, label) => {
|
|
|
4976
5267
|
if (info.isSymbolicLink() || !info.isDirectory()) {
|
|
4977
5268
|
throw new WorkspaceRegistryError("STORAGE_UNSAFE", `${label} must be a real directory`);
|
|
4978
5269
|
}
|
|
4979
|
-
return
|
|
5270
|
+
return realpath4(directory);
|
|
4980
5271
|
};
|
|
4981
5272
|
var rejectUnsafeFilesystem = async (storageRoot) => {
|
|
4982
5273
|
const stats = await statfs(storageRoot, { bigint: true });
|
|
@@ -5025,12 +5316,12 @@ var validateStoredId = (label, value) => {
|
|
|
5025
5316
|
throw corrupt2(errorMessage2(error));
|
|
5026
5317
|
}
|
|
5027
5318
|
};
|
|
5028
|
-
var
|
|
5319
|
+
var assertContained2 = (root, candidate) => {
|
|
5029
5320
|
if (!isPathInside2(root, candidate)) throw corrupt2("Workspace path escapes the storage root");
|
|
5030
5321
|
};
|
|
5031
5322
|
var isPathInside2 = (parent, child) => {
|
|
5032
|
-
const relative =
|
|
5033
|
-
return relative !== "" && relative !== ".." && !relative.startsWith(`..${
|
|
5323
|
+
const relative = path14.relative(parent, child);
|
|
5324
|
+
return relative !== "" && relative !== ".." && !relative.startsWith(`..${path14.sep}`) && !path14.isAbsolute(relative);
|
|
5034
5325
|
};
|
|
5035
5326
|
var toDescriptor = (workspace) => ({
|
|
5036
5327
|
workspaceKey: workspace.workspaceKey,
|
|
@@ -5202,7 +5493,7 @@ async function runOnboard(launchConfig, options = {}) {
|
|
|
5202
5493
|
|
|
5203
5494
|
// src/runtime.ts
|
|
5204
5495
|
import { mkdir as mkdir12 } from "fs/promises";
|
|
5205
|
-
import
|
|
5496
|
+
import path15 from "path";
|
|
5206
5497
|
var RECOVERED_DAEMON_STOP_TIMEOUT_MS = 1e4;
|
|
5207
5498
|
var RECOVERED_DAEMON_STOP_POLL_MS = 100;
|
|
5208
5499
|
var wait3 = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
@@ -5258,7 +5549,7 @@ async function runConnectorDaemon(launchConfig, options = {}) {
|
|
|
5258
5549
|
config.connectorId,
|
|
5259
5550
|
configRoot
|
|
5260
5551
|
);
|
|
5261
|
-
await mkdir12(
|
|
5552
|
+
await mkdir12(path15.dirname(daemonLockTarget), { recursive: true, mode: 448 });
|
|
5262
5553
|
let daemonLock = await acquireExclusiveFileLock(daemonLockTarget, { timeoutMs: 0 }).catch(() => {
|
|
5263
5554
|
throw new Error(
|
|
5264
5555
|
`Another Buda Connector daemon is already running for this profile on ${config.serverUrl}. Stop it before starting another instance.`
|
|
@@ -5268,6 +5559,7 @@ async function runConnectorDaemon(launchConfig, options = {}) {
|
|
|
5268
5559
|
let bunnyDaemon;
|
|
5269
5560
|
let relayProtocol;
|
|
5270
5561
|
let control;
|
|
5562
|
+
let heartbeatHealthy = true;
|
|
5271
5563
|
let lastHeartbeatAt;
|
|
5272
5564
|
try {
|
|
5273
5565
|
await migrateLegacyWorkspaceRoot(config.workdirRoot);
|
|
@@ -5291,7 +5583,7 @@ async function runConnectorDaemon(launchConfig, options = {}) {
|
|
|
5291
5583
|
registration.connectorId,
|
|
5292
5584
|
configRoot
|
|
5293
5585
|
);
|
|
5294
|
-
await mkdir12(
|
|
5586
|
+
await mkdir12(path15.dirname(registeredLockTarget), { recursive: true, mode: 448 });
|
|
5295
5587
|
const registeredLock = await acquireExclusiveFileLock(registeredLockTarget, {
|
|
5296
5588
|
timeoutMs: 0
|
|
5297
5589
|
}).catch(() => {
|
|
@@ -5346,6 +5638,7 @@ async function runConnectorDaemon(launchConfig, options = {}) {
|
|
|
5346
5638
|
};
|
|
5347
5639
|
interval = setInterval(() => {
|
|
5348
5640
|
heartbeatConnector(config, launchConfig, activeBunnyDaemon).then(({ relayProtocol: nextRelayProtocol }) => {
|
|
5641
|
+
heartbeatHealthy = true;
|
|
5349
5642
|
lastHeartbeatAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5350
5643
|
relay.updateRelayProtocol(nextRelayProtocol);
|
|
5351
5644
|
relay.resumeAfterAccessRestore();
|
|
@@ -5353,6 +5646,7 @@ async function runConnectorDaemon(launchConfig, options = {}) {
|
|
|
5353
5646
|
heartbeat: `OK ${(/* @__PURE__ */ new Date()).toLocaleTimeString("en-GB", { hour12: false })}`
|
|
5354
5647
|
});
|
|
5355
5648
|
}).catch((error) => {
|
|
5649
|
+
heartbeatHealthy = false;
|
|
5356
5650
|
if (error instanceof ConnectorDeletedError) {
|
|
5357
5651
|
void writeLog("warn", "connector deleted remotely; stopping daemon");
|
|
5358
5652
|
void shutdown({ unregister: false });
|
|
@@ -5368,6 +5662,7 @@ async function runConnectorDaemon(launchConfig, options = {}) {
|
|
|
5368
5662
|
config,
|
|
5369
5663
|
configRoot,
|
|
5370
5664
|
getLastHeartbeatAt: () => lastHeartbeatAt,
|
|
5665
|
+
isHeartbeatHealthy: () => heartbeatHealthy,
|
|
5371
5666
|
isIdle: () => relay.isIdle(),
|
|
5372
5667
|
isRelayConnected: () => relay.isConnected(),
|
|
5373
5668
|
mode: options.mode ?? "persistent",
|