@buda-ai/connector 0.1.1 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +29 -6
  2. package/agent-templates/scripts/skill-scanner.sh +125 -0
  3. package/agent-templates/shared/AGENTS.md +220 -0
  4. package/agent-templates/shared/BOOTSTRAP.md +15 -0
  5. package/agent-templates/shared/HEARTBEAT.md +5 -0
  6. package/agent-templates/shared/IDENTITY.md +29 -0
  7. package/agent-templates/shared/MEMORY.md +19 -0
  8. package/agent-templates/shared/SOUL.md +43 -0
  9. package/agent-templates/shared/TOOLS.md +85 -0
  10. package/agent-templates/shared/USER.md +23 -0
  11. package/agent-templates/system-skills/app-builder/README.md +8 -0
  12. package/agent-templates/system-skills/app-builder/SKILL.md +678 -0
  13. package/agent-templates/system-skills/app-builder/buda.app.schema.json +165 -0
  14. package/agent-templates/system-skills/app-builder/evals/evals.json +23 -0
  15. package/agent-templates/system-skills/app-builder/scripts/check-preview-url.mjs +184 -0
  16. package/agent-templates/system-skills/buda-automations/SKILL.md +175 -0
  17. package/agent-templates/system-skills/buda-automations/scripts/create-automation.sh +226 -0
  18. package/agent-templates/system-skills/buda-automations/scripts/delete-automation.sh +73 -0
  19. package/agent-templates/system-skills/buda-automations/scripts/list-automations.sh +77 -0
  20. package/agent-templates/system-skills/buda-far/SKILL.md +221 -0
  21. package/agent-templates/system-skills/buda-far/far_gen.py +1499 -0
  22. package/agent-templates/system-skills/buda-machine-info/SKILL.md +67 -0
  23. package/agent-templates/system-skills/buda-machine-info/scripts/get-info.sh +25 -0
  24. package/agent-templates/system-skills/buda-machine-info/scripts/send-file.mjs +62 -0
  25. package/agent-templates/system-skills/buda-web-preview/SKILL.md +462 -0
  26. package/agent-templates/system-skills/find-skills/SKILL.md +142 -0
  27. package/agent-templates/system-skills/skill-creator/LICENSE.txt +202 -0
  28. package/agent-templates/system-skills/skill-creator/SKILL.md +479 -0
  29. package/agent-templates/system-skills/skill-creator/agents/analyzer.md +274 -0
  30. package/agent-templates/system-skills/skill-creator/agents/comparator.md +202 -0
  31. package/agent-templates/system-skills/skill-creator/agents/grader.md +223 -0
  32. package/agent-templates/system-skills/skill-creator/assets/eval_review.html +146 -0
  33. package/agent-templates/system-skills/skill-creator/eval-viewer/generate_review.py +471 -0
  34. package/agent-templates/system-skills/skill-creator/eval-viewer/viewer.html +1325 -0
  35. package/agent-templates/system-skills/skill-creator/references/schemas.md +430 -0
  36. package/agent-templates/system-skills/skill-creator/scripts/__init__.py +0 -0
  37. package/agent-templates/system-skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  38. package/agent-templates/system-skills/skill-creator/scripts/generate_report.py +326 -0
  39. package/agent-templates/system-skills/skill-creator/scripts/improve_description.py +248 -0
  40. package/agent-templates/system-skills/skill-creator/scripts/package_skill.py +136 -0
  41. package/agent-templates/system-skills/skill-creator/scripts/quick_validate.py +103 -0
  42. package/agent-templates/system-skills/skill-creator/scripts/run_eval.py +310 -0
  43. package/agent-templates/system-skills/skill-creator/scripts/run_loop.py +332 -0
  44. package/agent-templates/system-skills/skill-creator/scripts/utils.py +47 -0
  45. package/dist/cli.js +2807 -183
  46. package/package.json +10 -6
package/dist/cli.js CHANGED
@@ -1,19 +1,180 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/config.ts
4
- import { randomUUID } from "crypto";
5
- import { mkdir, readFile, writeFile } from "fs/promises";
6
- import { homedir, hostname, platform } from "os";
4
+ import { createHash as createHash2, randomBytes, randomUUID } from "crypto";
5
+ import { chmod, mkdir, readFile as readFile2, rename, unlink as unlink2, writeFile } from "fs/promises";
6
+ import { homedir, hostname, platform as platform2 } from "os";
7
7
  import path from "path";
8
- var CONFIG_DIR = path.join(homedir(), ".buda");
9
- var CONFIG_PATH = path.join(CONFIG_DIR, "device.json");
10
- var LOG_PATH = path.join(CONFIG_DIR, "connector.log");
8
+
9
+ // src/exclusive-file-lock.ts
10
+ import { open, readFile, stat, unlink } from "fs/promises";
11
+ var DEFAULT_TIMEOUT_MS = 1e4;
12
+ var DEFAULT_STALE_MS = 12e4;
13
+ var DEFAULT_POLL_MS = 25;
14
+ async function acquireExclusiveFileLock(targetPath, options = {}) {
15
+ const lockPath = `${targetPath}.lock`;
16
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
17
+ const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
18
+ const pollMs = options.pollMs ?? DEFAULT_POLL_MS;
19
+ const deadline = Date.now() + timeoutMs;
20
+ while (true) {
21
+ let handle;
22
+ try {
23
+ handle = await open(lockPath, "wx", 384);
24
+ } catch (error) {
25
+ if (!isAlreadyExists(error)) throw error;
26
+ if (await shouldRecoverLock(lockPath, staleMs)) {
27
+ await unlink(lockPath).catch(() => void 0);
28
+ continue;
29
+ }
30
+ if (Date.now() >= deadline) {
31
+ throw new Error(`Timed out waiting for exclusive access to ${targetPath}`);
32
+ }
33
+ await wait(pollMs);
34
+ continue;
35
+ }
36
+ await handle.writeFile(`${JSON.stringify({ pid: process.pid, createdAt: Date.now() })}
37
+ `);
38
+ await handle.sync();
39
+ let released = false;
40
+ return {
41
+ release: async () => {
42
+ if (released) return;
43
+ released = true;
44
+ await handle.close();
45
+ await unlink(lockPath).catch(() => void 0);
46
+ }
47
+ };
48
+ }
49
+ }
50
+ async function withExclusiveFileLock(targetPath, operation, options = {}) {
51
+ const lock = await acquireExclusiveFileLock(targetPath, options);
52
+ try {
53
+ return await operation();
54
+ } finally {
55
+ await lock.release();
56
+ }
57
+ }
58
+ var wait = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
59
+ var isAlreadyExists = (error) => typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
60
+ var shouldRecoverLock = async (lockPath, staleMs) => {
61
+ const [raw, lockStat] = await Promise.all([
62
+ readFile(lockPath, "utf8").catch(() => ""),
63
+ stat(lockPath).catch(() => null)
64
+ ]);
65
+ if (!lockStat) return false;
66
+ try {
67
+ const metadata = JSON.parse(raw);
68
+ if (typeof metadata.pid === "number" && Number.isInteger(metadata.pid) && metadata.pid > 0) {
69
+ return !isProcessAlive(metadata.pid);
70
+ }
71
+ } catch {
72
+ }
73
+ return Date.now() - lockStat.mtimeMs >= staleMs;
74
+ };
75
+ var isProcessAlive = (pid) => {
76
+ try {
77
+ process.kill(pid, 0);
78
+ return true;
79
+ } catch (error) {
80
+ return !(typeof error === "object" && error !== null && "code" in error && error.code === "ESRCH");
81
+ }
82
+ };
83
+
84
+ // src/system-device-id.ts
85
+ import { execFileSync } from "child_process";
86
+ import { createHash } from "crypto";
87
+ import { readFileSync } from "fs";
88
+ import { platform } from "os";
89
+ var defaultDependencies = {
90
+ platform: platform(),
91
+ readFile: (filePath) => readFileSync(filePath, "utf8"),
92
+ execFile: (file, args) => execFileSync(file, args, { encoding: "utf8" })
93
+ };
94
+ var readRawSystemId = (dependencies) => {
95
+ if (dependencies.platform === "darwin") {
96
+ const output = dependencies.execFile("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"]);
97
+ return output.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/)?.[1];
98
+ }
99
+ if (dependencies.platform === "linux") {
100
+ for (const filePath of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
101
+ try {
102
+ const value = dependencies.readFile(filePath).trim();
103
+ if (value) return value;
104
+ } catch {
105
+ }
106
+ }
107
+ return void 0;
108
+ }
109
+ if (dependencies.platform === "win32") {
110
+ const output = dependencies.execFile("reg", [
111
+ "query",
112
+ String.raw`HKLM\SOFTWARE\Microsoft\Cryptography`,
113
+ "/v",
114
+ "MachineGuid"
115
+ ]);
116
+ return output.match(/MachineGuid\s+REG_SZ\s+([^\r\n]+)/i)?.[1];
117
+ }
118
+ return void 0;
119
+ };
120
+ var hashSystemDeviceId = (rawSystemId) => {
121
+ const normalized = rawSystemId.trim().toLowerCase();
122
+ if (!normalized) return void 0;
123
+ const digest = createHash("sha256").update(`buda:system-device:v1:${normalized}`, "utf8").digest("hex");
124
+ return `dvc_${digest}`;
125
+ };
126
+ var resolveSystemDeviceId = (dependencies = defaultDependencies) => {
127
+ try {
128
+ const rawSystemId = readRawSystemId(dependencies);
129
+ return rawSystemId ? hashSystemDeviceId(rawSystemId) : void 0;
130
+ } catch {
131
+ return void 0;
132
+ }
133
+ };
134
+
135
+ // src/config.ts
11
136
  var DEFAULT_SERVER_URL = "https://buda.im";
12
- var DEFAULT_WORKDIR_ROOT = path.join(CONFIG_DIR, "agents");
13
137
  var DEFAULT_BUNNY_DAEMON_HOST = "127.0.0.1";
14
138
  var HEARTBEAT_INTERVAL_MS = 5e3;
15
139
  var RELAY_RECONNECT_INTERVAL_MS = 3e3;
16
140
  var LAUNCH_CONFIG_STDIN_FLAG = "--launch-config-stdin";
141
+ function normalizeServerOrigin(serverUrl) {
142
+ return new URL(serverUrl).origin;
143
+ }
144
+ function getServerOriginKey(serverUrl) {
145
+ const origin = normalizeServerOrigin(serverUrl);
146
+ const hostnameKey = new URL(origin).hostname.replace(/[^a-z0-9.-]+/gi, "-").toLowerCase();
147
+ const digest = createHash2("sha256").update(origin).digest("hex").slice(0, 12);
148
+ return `${hostnameKey}-${digest}`;
149
+ }
150
+ function getConnectorTokenFingerprint(token) {
151
+ return createHash2("sha256").update(`buda:connector-profile:v1:${token.trim()}`, "utf8").digest("hex");
152
+ }
153
+ function getConnectorProfileKey(serverUrl, token) {
154
+ const discriminator = token?.trim() ? getConnectorTokenFingerprint(token) : "legacy-no-token";
155
+ return `${getServerOriginKey(serverUrl)}--${discriminator}`;
156
+ }
157
+ function getConnectorPaths(serverUrl, configRoot = path.join(homedir(), ".buda")) {
158
+ const scopedLegacyDir = path.join(configRoot, "servers", getServerOriginKey(serverUrl));
159
+ return {
160
+ configDir: configRoot,
161
+ configPath: path.join(configRoot, "device.json"),
162
+ logPath: path.join(configRoot, "connector.log"),
163
+ workdirRoot: path.join(configRoot, "agents"),
164
+ scopedLegacyConfigPath: path.join(scopedLegacyDir, "device.json"),
165
+ scopedLegacyWorkdirRoot: path.join(scopedLegacyDir, "agents")
166
+ };
167
+ }
168
+ function getConnectorDaemonLockTarget(serverUrl, connectorId, configRoot = path.join(homedir(), ".buda")) {
169
+ const connectorKey = connectorId.replace(/[^a-z0-9_-]+/gi, "-").toLowerCase();
170
+ return path.join(
171
+ configRoot,
172
+ "daemons",
173
+ getServerOriginKey(serverUrl),
174
+ connectorKey,
175
+ "connector-daemon"
176
+ );
177
+ }
17
178
  function getServerUrl(launchConfig) {
18
179
  return (launchConfig?.serverUrl || process.env.BUDA_CONNECTOR_SERVER_URL || process.env.BUDA_SERVER_URL || process.env.NEXT_PUBLIC_APP_URL || DEFAULT_SERVER_URL).replace(/\/$/, "");
19
180
  }
@@ -24,7 +185,7 @@ function getConnectorRelayUrl(serverUrl, connectorId) {
24
185
  return url.toString();
25
186
  }
26
187
  function getWorkdirRoot(launchConfig) {
27
- return launchConfig?.workdirRoot || process.env.BUDA_CONNECTOR_WORKDIR || DEFAULT_WORKDIR_ROOT;
188
+ return launchConfig?.workdirRoot || process.env.BUDA_CONNECTOR_WORKDIR || getConnectorPaths(getServerUrl(launchConfig)).workdirRoot;
28
189
  }
29
190
  function getBunnyDaemonHost(launchConfig) {
30
191
  return launchConfig?.bunnyDaemonHost || process.env.BUDA_CONNECTOR_BUNNY_DAEMON_HOST || DEFAULT_BUNNY_DAEMON_HOST;
@@ -36,46 +197,194 @@ function getBunnyDaemonPort(launchConfig) {
36
197
  const parsed = Number(raw);
37
198
  return Number.isInteger(parsed) && parsed >= 0 ? parsed : 0;
38
199
  }
39
- function getSpaceId(launchConfig) {
40
- return launchConfig?.spaceId || process.env.BUDA_CONNECTOR_SPACE_ID;
200
+ function getConnectorToken(launchConfig) {
201
+ return launchConfig?.token?.trim() || process.env.BUDA_CONNECTOR_TOKEN?.trim() || void 0;
41
202
  }
42
- function buildDeviceConfig(launchConfig) {
203
+ function buildDeviceConfig(launchConfig, deviceId = `dvc_${randomUUID()}`) {
43
204
  const host = hostname();
205
+ const token = getConnectorToken(launchConfig);
44
206
  return {
45
207
  connectorId: `cnr_${randomUUID()}`,
46
- deviceId: `dev_${randomUUID()}`,
208
+ deviceId,
47
209
  deviceName: host,
48
- hostLabel: `${host} (${platform()})`,
210
+ hostLabel: `${host} (${platform2()})`,
49
211
  serverUrl: getServerUrl(launchConfig),
50
212
  workdirRoot: getWorkdirRoot(launchConfig),
51
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
213
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
214
+ deviceToken: randomBytes(32).toString("base64url"),
215
+ ...token ? { token } : {}
52
216
  };
53
217
  }
54
- async function readDeviceConfig() {
218
+ function isDeviceConfig(value) {
219
+ if (!value || typeof value !== "object") return false;
220
+ const config = value;
221
+ return typeof config.connectorId === "string" && typeof config.deviceId === "string" && typeof config.serverUrl === "string";
222
+ }
223
+ async function readJsonFile(configPath) {
55
224
  try {
56
- const raw = await readFile(CONFIG_PATH, "utf8");
225
+ const raw = await readFile2(configPath, "utf8");
57
226
  return JSON.parse(raw);
58
227
  } catch {
59
228
  return null;
60
229
  }
61
230
  }
62
- async function writeDeviceConfig(config) {
63
- await mkdir(CONFIG_DIR, { recursive: true });
64
- await mkdir(config.workdirRoot, { recursive: true });
65
- await writeFile(CONFIG_PATH, `${JSON.stringify(config, null, 2)}
66
- `, { mode: 384 });
231
+ function migrateLegacyProfiles(values) {
232
+ const profiles = {};
233
+ for (const value of values) {
234
+ if (!isDeviceConfig(value)) continue;
235
+ try {
236
+ profiles[getConnectorProfileKey(value.serverUrl, value.token)] = value;
237
+ } catch {
238
+ }
239
+ }
240
+ return profiles;
67
241
  }
68
- async function ensureDeviceConfig(launchConfig) {
69
- const existing = await readDeviceConfig();
70
- if (existing) {
242
+ async function loadDeviceConfigStore(configPath) {
243
+ const parsed = await readJsonFile(configPath);
244
+ const candidate = parsed;
245
+ if (candidate?.version === 3 && candidate.profiles && typeof candidate.profiles === "object") {
71
246
  return {
72
- ...existing,
73
- serverUrl: getServerUrl(launchConfig),
74
- workdirRoot: launchConfig?.workdirRoot || process.env.BUDA_CONNECTOR_WORKDIR || existing.workdirRoot || getWorkdirRoot(launchConfig)
247
+ store: {
248
+ version: 3,
249
+ ...typeof candidate.systemDeviceId === "string" ? { systemDeviceId: candidate.systemDeviceId } : {},
250
+ profiles: candidate.profiles
251
+ },
252
+ needsWrite: false
75
253
  };
76
254
  }
77
- const config = buildDeviceConfig(launchConfig);
78
- await writeDeviceConfig(config);
255
+ if (candidate && (candidate.version === 1 || candidate.version === 2) && candidate.servers && typeof candidate.servers === "object") {
256
+ return {
257
+ store: {
258
+ version: 3,
259
+ ...typeof candidate.systemDeviceId === "string" ? { systemDeviceId: candidate.systemDeviceId } : {},
260
+ profiles: migrateLegacyProfiles(Object.values(candidate.servers))
261
+ },
262
+ needsWrite: true
263
+ };
264
+ }
265
+ if (isDeviceConfig(parsed)) {
266
+ return {
267
+ store: {
268
+ version: 3,
269
+ profiles: migrateLegacyProfiles([parsed])
270
+ },
271
+ needsWrite: true
272
+ };
273
+ }
274
+ return { store: { version: 3, profiles: {} }, needsWrite: false };
275
+ }
276
+ function normalizeStoredWorkdirRoot(config, paths) {
277
+ const configuredRoot = config.workdirRoot?.trim();
278
+ if (!configuredRoot) {
279
+ return { config: { ...config, workdirRoot: paths.workdirRoot }, changed: true };
280
+ }
281
+ const relativeToLegacyServers = path.relative(
282
+ path.join(paths.configDir, "servers"),
283
+ path.resolve(configuredRoot)
284
+ );
285
+ const segments = relativeToLegacyServers.split(path.sep);
286
+ const isServerScopedDefault = !path.isAbsolute(relativeToLegacyServers) && segments.length === 2 && segments[0] !== ".." && segments[1] === "agents";
287
+ if (!isServerScopedDefault) return { config, changed: false };
288
+ return { config: { ...config, workdirRoot: paths.workdirRoot }, changed: true };
289
+ }
290
+ async function writeDeviceConfigStore(configPath, store) {
291
+ await mkdir(path.dirname(configPath), { recursive: true });
292
+ const temporaryPath = `${configPath}.${process.pid}.${randomUUID()}.tmp`;
293
+ try {
294
+ await writeFile(temporaryPath, `${JSON.stringify(store, null, 2)}
295
+ `, { mode: 384 });
296
+ await rename(temporaryPath, configPath);
297
+ await chmod(configPath, 384);
298
+ } finally {
299
+ await unlink2(temporaryPath).catch(() => void 0);
300
+ }
301
+ }
302
+ async function readDeviceConfig(launchConfig, configRoot) {
303
+ const serverUrl = getServerUrl(launchConfig);
304
+ const paths = getConnectorPaths(serverUrl, configRoot);
305
+ const launchToken = getConnectorToken(launchConfig);
306
+ await mkdir(paths.configDir, { recursive: true });
307
+ return withExclusiveFileLock(paths.configPath, async () => {
308
+ const { store, needsWrite } = await loadDeviceConfigStore(paths.configPath);
309
+ const profileKey = getConnectorProfileKey(serverUrl, launchToken);
310
+ const storedConfig = store.profiles[profileKey];
311
+ let config = isDeviceConfig(storedConfig) ? storedConfig : null;
312
+ let normalizedWorkdirRoot = false;
313
+ if (config) {
314
+ const normalized = normalizeStoredWorkdirRoot(config, paths);
315
+ config = normalized.config;
316
+ normalizedWorkdirRoot = normalized.changed;
317
+ store.profiles[getConnectorProfileKey(config.serverUrl, config.token)] = config;
318
+ }
319
+ if (needsWrite || normalizedWorkdirRoot) {
320
+ await writeDeviceConfigStore(paths.configPath, store);
321
+ }
322
+ return config;
323
+ });
324
+ }
325
+ async function writeDeviceConfig(config, configRoot) {
326
+ const paths = getConnectorPaths(config.serverUrl, configRoot);
327
+ await mkdir(config.workdirRoot, { recursive: true });
328
+ await mkdir(paths.configDir, { recursive: true });
329
+ await withExclusiveFileLock(paths.configPath, async () => {
330
+ const { store } = await loadDeviceConfigStore(paths.configPath);
331
+ store.profiles[getConnectorProfileKey(config.serverUrl, config.token)] = config;
332
+ await writeDeviceConfigStore(paths.configPath, store);
333
+ });
334
+ }
335
+ async function getOrCreateSystemDeviceId(configRoot, resolver = resolveSystemDeviceId) {
336
+ const paths = getConnectorPaths(DEFAULT_SERVER_URL, configRoot);
337
+ await mkdir(paths.configDir, { recursive: true });
338
+ return withExclusiveFileLock(paths.configPath, async () => {
339
+ const { store, needsWrite } = await loadDeviceConfigStore(paths.configPath);
340
+ if (store.systemDeviceId) {
341
+ if (needsWrite) await writeDeviceConfigStore(paths.configPath, store);
342
+ return store.systemDeviceId;
343
+ }
344
+ const existingDeviceId = Object.values(store.profiles).find(isDeviceConfig)?.deviceId;
345
+ store.systemDeviceId = resolver() || existingDeviceId || `dvc_${randomUUID()}`;
346
+ await writeDeviceConfigStore(paths.configPath, store);
347
+ return store.systemDeviceId;
348
+ });
349
+ }
350
+ async function createDeviceConfig(launchConfig, configRoot, resolver) {
351
+ const deviceId = await getOrCreateSystemDeviceId(configRoot, resolver);
352
+ return buildDeviceConfig(launchConfig, deviceId);
353
+ }
354
+ async function ensureDeviceConfig(launchConfig, configRoot, resolver = resolveSystemDeviceId) {
355
+ const serverUrl = getServerUrl(launchConfig);
356
+ const paths = getConnectorPaths(serverUrl, configRoot);
357
+ const token = getConnectorToken(launchConfig);
358
+ const profileKey = getConnectorProfileKey(serverUrl, token);
359
+ await mkdir(paths.configDir, { recursive: true });
360
+ const config = await withExclusiveFileLock(paths.configPath, async () => {
361
+ const { store, needsWrite } = await loadDeviceConfigStore(paths.configPath);
362
+ const storedConfig = store.profiles[profileKey];
363
+ if (isDeviceConfig(storedConfig)) {
364
+ const normalized = normalizeStoredWorkdirRoot(storedConfig, paths);
365
+ if (needsWrite || normalized.changed) {
366
+ store.profiles[profileKey] = normalized.config;
367
+ await writeDeviceConfigStore(paths.configPath, store);
368
+ }
369
+ const existing = normalized.config;
370
+ const overrideToken = getConnectorToken(launchConfig);
371
+ return {
372
+ ...existing,
373
+ serverUrl,
374
+ workdirRoot: launchConfig?.workdirRoot || process.env.BUDA_CONNECTOR_WORKDIR || existing.workdirRoot || getWorkdirRoot(launchConfig),
375
+ ...overrideToken ? { token: overrideToken } : {}
376
+ };
377
+ }
378
+ if (!store.systemDeviceId) {
379
+ const existingDeviceId = Object.values(store.profiles).find(isDeviceConfig)?.deviceId;
380
+ store.systemDeviceId = resolver() || existingDeviceId || `dvc_${randomUUID()}`;
381
+ }
382
+ const created = buildDeviceConfig(launchConfig, store.systemDeviceId);
383
+ store.profiles[profileKey] = created;
384
+ await writeDeviceConfigStore(paths.configPath, store);
385
+ return created;
386
+ });
387
+ await mkdir(config.workdirRoot, { recursive: true });
79
388
  return config;
80
389
  }
81
390
  function parseLaunchConfig(raw) {
@@ -86,12 +395,15 @@ function parseLaunchConfig(raw) {
86
395
  serverUrl: typeof parsed.serverUrl === "string" ? parsed.serverUrl : void 0,
87
396
  workdirRoot: typeof parsed.workdirRoot === "string" ? parsed.workdirRoot : void 0,
88
397
  oauthToken: typeof parsed.oauthToken === "string" ? parsed.oauthToken : void 0,
89
- spaceId: typeof parsed.spaceId === "string" ? parsed.spaceId : void 0,
398
+ token: typeof parsed.token === "string" ? parsed.token : void 0,
90
399
  bunnyDaemonHost: typeof parsed.bunnyDaemonHost === "string" ? parsed.bunnyDaemonHost : void 0,
91
400
  bunnyDaemonPort
92
401
  };
93
402
  }
94
403
 
404
+ // src/onboard.ts
405
+ import { cancel, intro, isCancel, log, note, outro, spinner, text } from "@clack/prompts";
406
+
95
407
  // src/bunny-daemon.ts
96
408
  import { mkdir as mkdir3 } from "fs/promises";
97
409
  import { createDaemon } from "@bunny-agent/daemon";
@@ -108,8 +420,9 @@ async function writeLog(level, message, metadata) {
108
420
  };
109
421
  pendingLogs.push(entry);
110
422
  if (pendingLogs.length > 100) pendingLogs.splice(0, pendingLogs.length - 100);
111
- await mkdir2(CONFIG_DIR, { recursive: true }).catch(() => void 0);
112
- await appendFile(LOG_PATH, `${JSON.stringify(entry)}
423
+ const paths = getConnectorPaths(getServerUrl());
424
+ await mkdir2(paths.configDir, { recursive: true }).catch(() => void 0);
425
+ await appendFile(paths.logPath, `${JSON.stringify(entry)}
113
426
  `).catch(() => void 0);
114
427
  const line = `[buda-connector] ${message}`;
115
428
  if (level === "error") console.error(line);
@@ -165,14 +478,627 @@ async function stopEmbeddedBunnyDaemon(daemon) {
165
478
  }
166
479
 
167
480
  // src/cloud-client.ts
168
- import { platform as platform2, release } from "os";
169
- function buildPayload(config, extra) {
481
+ import { createHash as createHash4 } from "crypto";
482
+ import { platform as platform3, release } from "os";
483
+
484
+ // src/browser.ts
485
+ import { accessSync, constants, statSync } from "fs";
486
+ import { mkdir as mkdir4 } from "fs/promises";
487
+ import { platform as getPlatform, homedir as homedir2 } from "os";
488
+ import path2 from "path";
489
+ var DEFAULT_VIEWPORT = { width: 1280, height: 800 };
490
+ var MAX_SCREENCAST_DIMENSION = 2560;
491
+ var SESSION_ID_PATTERN = /^[A-Za-z0-9_.:-]{1,320}$/;
492
+ var sessions = /* @__PURE__ */ new Map();
493
+ var sessionPromises = /* @__PURE__ */ new Map();
494
+ var getBrowserMode = () => {
495
+ const configured = process.env.BUDA_CONNECTOR_BROWSER_MODE?.trim().toLowerCase();
496
+ return configured === "auto" || configured === "headed" || configured === "headless" ? configured : "headless";
497
+ };
498
+ var getSystemBrowserCandidates = (platform4, homeDirectory, env) => {
499
+ if (platform4 === "darwin") {
500
+ return [
501
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
502
+ "/Applications/Chromium.app/Contents/MacOS/Chromium",
503
+ "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
504
+ `${homeDirectory}/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`,
505
+ `${homeDirectory}/Applications/Chromium.app/Contents/MacOS/Chromium`,
506
+ `${homeDirectory}/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge`
507
+ ];
508
+ }
509
+ if (platform4 === "win32") {
510
+ const localAppData = env.LOCALAPPDATA;
511
+ const programFiles = env.PROGRAMFILES;
512
+ const programFilesX86 = env["PROGRAMFILES(X86)"];
513
+ return [
514
+ localAppData ? `${localAppData}\\Google\\Chrome\\Application\\chrome.exe` : "",
515
+ localAppData ? `${localAppData}\\Chromium\\Application\\chrome.exe` : "",
516
+ programFiles ? `${programFiles}\\Google\\Chrome\\Application\\chrome.exe` : "",
517
+ programFilesX86 ? `${programFilesX86}\\Google\\Chrome\\Application\\chrome.exe` : "",
518
+ programFiles ? `${programFiles}\\Microsoft\\Edge\\Application\\msedge.exe` : "",
519
+ programFilesX86 ? `${programFilesX86}\\Microsoft\\Edge\\Application\\msedge.exe` : ""
520
+ ];
521
+ }
522
+ return [
523
+ "/usr/bin/google-chrome",
524
+ "/usr/bin/google-chrome-stable",
525
+ "/usr/bin/chromium",
526
+ "/usr/bin/chromium-browser",
527
+ "/snap/bin/chromium",
528
+ "/usr/bin/microsoft-edge",
529
+ "/usr/bin/microsoft-edge-stable"
530
+ ];
531
+ };
532
+ var resolveBrowserAvailability = (options = {}) => {
533
+ const env = options.env ?? process.env;
534
+ const platform4 = options.platform ?? getPlatform();
535
+ const homeDirectory = options.homeDirectory ?? homedir2();
536
+ const pathIsExecutable = options.pathIsExecutable ?? ((candidate) => {
537
+ try {
538
+ if (!statSync(candidate).isFile()) return false;
539
+ accessSync(candidate, constants.X_OK);
540
+ return true;
541
+ } catch {
542
+ return false;
543
+ }
544
+ });
545
+ const configuredPath = env.BUDA_CONNECTOR_BROWSER_PATH?.trim();
546
+ if (configuredPath) {
547
+ if (pathIsExecutable(configuredPath)) {
548
+ return { available: true, executablePath: configuredPath };
549
+ }
550
+ return {
551
+ available: false,
552
+ error: `BUDA_CONNECTOR_BROWSER_PATH is missing or not executable: ${configuredPath}. Set it to a Chrome, Chromium, or Edge executable.`
553
+ };
554
+ }
555
+ const executablePath = getSystemBrowserCandidates(platform4, homeDirectory, env).find(
556
+ (candidate) => candidate && pathIsExecutable(candidate)
557
+ );
558
+ if (executablePath) {
559
+ return { available: true, executablePath };
560
+ }
561
+ return {
562
+ available: false,
563
+ error: "No supported system browser was found. Install Google Chrome, Chromium, or Microsoft Edge, or set BUDA_CONNECTOR_BROWSER_PATH to its executable. The connector browser capability will not be advertised."
564
+ };
565
+ };
566
+ async function launchPlaywright(viewport = DEFAULT_VIEWPORT) {
567
+ const availability = resolveBrowserAvailability();
568
+ if (!availability.available) throw new Error(availability.error);
569
+ const { chromium } = await import("playwright-core");
570
+ const mode = getBrowserMode();
571
+ const executablePath = availability.executablePath;
572
+ let browser = null;
573
+ if (mode !== "headless") {
574
+ try {
575
+ await writeLog("info", `launching headed system browser ${executablePath}`);
576
+ browser = await chromium.launch({
577
+ executablePath,
578
+ headless: false,
579
+ args: ["--no-sandbox", "--disable-setuid-sandbox"]
580
+ });
581
+ } catch (error) {
582
+ if (mode === "headed") throw error;
583
+ await writeLog(
584
+ "warn",
585
+ `headed browser launch failed, falling back to headless: ${error instanceof Error ? error.message : String(error)}`
586
+ );
587
+ }
588
+ }
589
+ if (!browser) {
590
+ await writeLog("info", `launching headless system browser ${executablePath}`);
591
+ browser = await chromium.launch({
592
+ executablePath,
593
+ headless: true,
594
+ args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu"]
595
+ });
596
+ }
597
+ const context = await browser.newContext({ viewport, deviceScaleFactor: 1 });
598
+ const page = await context.newPage();
599
+ const cdpSession = await context.newCDPSession(page);
600
+ return { browser, page, cdpSession };
601
+ }
602
+ var normalizeSessionId = (value) => {
603
+ if (typeof value !== "string") return null;
604
+ const normalized = value.trim();
605
+ return SESSION_ID_PATTERN.test(normalized) ? normalized : null;
606
+ };
607
+ async function ensureSession(sessionId, initialViewport = DEFAULT_VIEWPORT) {
608
+ const activeSession = sessions.get(sessionId);
609
+ if (activeSession) return activeSession;
610
+ let sessionPromise = sessionPromises.get(sessionId);
611
+ if (!sessionPromise) {
612
+ sessionPromise = (async () => {
613
+ await writeLog("info", `launching local browser session ${sessionId}`);
614
+ const { browser, page, cdpSession } = await launchPlaywright(initialViewport);
615
+ const nextSession = {
616
+ page,
617
+ browser,
618
+ cdpSession,
619
+ screencastClients: /* @__PURE__ */ new Set(),
620
+ isScreencasting: false,
621
+ screencastListenerAttached: false,
622
+ resizeQueue: Promise.resolve(),
623
+ inputQueue: Promise.resolve(),
624
+ navigationQueue: Promise.resolve(),
625
+ clientIds: /* @__PURE__ */ new Set()
626
+ };
627
+ sessions.set(sessionId, nextSession);
628
+ browser.on("disconnected", () => {
629
+ if (sessions.get(sessionId) === nextSession) sessions.delete(sessionId);
630
+ });
631
+ return nextSession;
632
+ })();
633
+ sessionPromises.set(sessionId, sessionPromise);
634
+ }
635
+ try {
636
+ return await sessionPromise;
637
+ } finally {
638
+ if (sessionPromises.get(sessionId) === sessionPromise) sessionPromises.delete(sessionId);
639
+ }
640
+ }
641
+ async function startScreencast(s) {
642
+ if (s.isScreencasting) return;
643
+ if (!s.screencastListenerAttached) {
644
+ s.screencastListenerAttached = true;
645
+ s.cdpSession.on("Page.screencastFrame", (params) => {
646
+ const metadata = params.metadata;
647
+ const viewport = s.page.viewportSize() ?? DEFAULT_VIEWPORT;
648
+ const frame = {
649
+ data: params.data,
650
+ viewportWidth: metadata.deviceWidth ?? viewport.width,
651
+ viewportHeight: metadata.deviceHeight ?? viewport.height
652
+ };
653
+ for (const listener of s.screencastClients) listener(frame);
654
+ void s.cdpSession.send("Page.screencastFrameAck", { sessionId: params.sessionId }).catch(() => {
655
+ });
656
+ });
657
+ }
658
+ await s.cdpSession.send("Page.startScreencast", {
659
+ format: "jpeg",
660
+ quality: 90,
661
+ maxWidth: MAX_SCREENCAST_DIMENSION,
662
+ maxHeight: MAX_SCREENCAST_DIMENSION,
663
+ everyNthFrame: 2
664
+ });
665
+ s.isScreencasting = true;
666
+ }
667
+ async function stopScreencast(s) {
668
+ if (!s.isScreencasting) return;
669
+ await s.cdpSession.send("Page.stopScreencast").catch(() => {
670
+ });
671
+ s.isScreencasting = false;
672
+ }
673
+ async function closeSession(sessionId) {
674
+ const activeSession = sessions.get(sessionId) ?? await sessionPromises.get(sessionId)?.catch(() => null);
675
+ if (!activeSession) return;
676
+ if (sessions.get(sessionId) === activeSession) sessions.delete(sessionId);
677
+ await stopScreencast(activeSession);
678
+ await activeSession.cdpSession.detach().catch(() => {
679
+ });
680
+ await activeSession.browser.close().catch(() => {
681
+ });
682
+ await writeLog("info", `browser session stopped ${sessionId}`);
683
+ }
684
+ async function closeAllBrowserSessions() {
685
+ const sessionIds = /* @__PURE__ */ new Set([...sessions.keys(), ...sessionPromises.keys()]);
686
+ await Promise.all([...sessionIds].map((sessionId) => closeSession(sessionId)));
687
+ }
688
+ async function resizeBrowser(s, viewport) {
689
+ const resize = s.resizeQueue.catch(() => {
690
+ }).then(async () => {
691
+ const current = s.page.viewportSize();
692
+ if (current?.width === viewport.width && current.height === viewport.height) return;
693
+ const wasScreencasting = s.isScreencasting;
694
+ if (wasScreencasting) await stopScreencast(s);
695
+ await s.page.setViewportSize(viewport);
696
+ if (wasScreencasting) await startScreencast(s);
697
+ await writeLog("info", `browser resized to ${viewport.width}x${viewport.height}`);
698
+ });
699
+ s.resizeQueue = resize;
700
+ await resize;
701
+ }
702
+ async function navigateBrowser(s, url, options) {
703
+ const navigation = s.navigationQueue.catch(() => {
704
+ }).then(async () => {
705
+ if (s.page.url() === url) return null;
706
+ return s.page.goto(url, options);
707
+ });
708
+ s.navigationQueue = navigation.then(() => {
709
+ });
710
+ return navigation;
711
+ }
712
+ var normalizeViewportDimension = (value, min, max, fallback) => {
713
+ const dimension = typeof value === "number" && Number.isFinite(value) ? Math.round(value) : fallback;
714
+ return Math.min(max, Math.max(min, dimension));
715
+ };
716
+ var normalizeTimeout = (value, fallback, max) => typeof value === "number" && Number.isFinite(value) ? Math.min(max, Math.max(1e3, Math.round(value))) : fallback;
717
+ function jsonResponse(status, body) {
718
+ return new Response(JSON.stringify(body), {
719
+ status,
720
+ headers: { "content-type": "application/json" }
721
+ });
722
+ }
723
+ var getRequestSessionId = (context) => normalizeSessionId(context.sessionId);
724
+ async function handleBrowserInput(sessionIdValue, body) {
725
+ const sessionId = normalizeSessionId(sessionIdValue);
726
+ if (!sessionId) throw new Error("A valid browser session id is required.");
727
+ const ev = body ?? {};
728
+ const s = await ensureSession(sessionId);
729
+ const input = s.inputQueue.catch(() => {
730
+ }).then(async () => {
731
+ switch (ev.type) {
732
+ case "mouseMove":
733
+ await s.cdpSession.send("Input.dispatchMouseEvent", {
734
+ type: "mouseMoved",
735
+ x: ev.x,
736
+ y: ev.y
737
+ });
738
+ return;
739
+ case "mouseDown":
740
+ await s.cdpSession.send("Input.dispatchMouseEvent", {
741
+ type: "mousePressed",
742
+ x: ev.x,
743
+ y: ev.y,
744
+ button: "left",
745
+ clickCount: 1
746
+ });
747
+ return;
748
+ case "mouseUp":
749
+ await s.cdpSession.send("Input.dispatchMouseEvent", {
750
+ type: "mouseReleased",
751
+ x: ev.x,
752
+ y: ev.y,
753
+ button: "left",
754
+ clickCount: 1
755
+ });
756
+ return;
757
+ case "keyDown":
758
+ await s.cdpSession.send("Input.dispatchKeyEvent", {
759
+ type: "keyDown",
760
+ key: ev.key,
761
+ text: ev.text ?? ""
762
+ });
763
+ return;
764
+ case "keyUp":
765
+ await s.cdpSession.send("Input.dispatchKeyEvent", {
766
+ type: "keyUp",
767
+ key: ev.key
768
+ });
769
+ return;
770
+ case "scroll":
771
+ await s.cdpSession.send("Input.dispatchMouseEvent", {
772
+ type: "mouseWheel",
773
+ x: ev.x,
774
+ y: ev.y,
775
+ deltaX: 0,
776
+ deltaY: ev.deltaY
777
+ });
778
+ return;
779
+ default:
780
+ throw new Error(`Unknown browser input event: ${String(ev.type)}`);
781
+ }
782
+ });
783
+ s.inputQueue = input;
784
+ await input;
785
+ }
786
+ var getPageResult = async (s) => ({
787
+ url: s.page.url(),
788
+ title: await s.page.title().catch(() => "")
789
+ });
790
+ var resolveScreenshotPath = (workdirRoot, filePath) => {
791
+ if (!workdirRoot) throw new Error("Connector workdir is unavailable for screenshots.");
792
+ const raw = typeof filePath === "string" ? filePath.trim() : "";
793
+ if (!raw) throw new Error("file_path is required");
794
+ const agentRoot = path2.resolve(workdirRoot, "agent");
795
+ const relativeInput = raw === "/agent" ? "" : raw.startsWith("/agent/") ? raw.slice(7) : raw;
796
+ const absolutePath = path2.resolve(agentRoot, relativeInput);
797
+ if (absolutePath === agentRoot || !absolutePath.startsWith(`${agentRoot}${path2.sep}`)) {
798
+ throw new Error("file_path must stay under the connector agent drive");
799
+ }
800
+ return {
801
+ absolutePath,
802
+ relativePath: path2.relative(agentRoot, absolutePath).split(path2.sep).join("/")
803
+ };
804
+ };
805
+ async function handleBrowserRequest(requestPath, method, body, context = {}) {
806
+ const route = new URL(requestPath, "http://connector.local").pathname.replace(
807
+ /^\/browser\/?/,
808
+ ""
809
+ );
810
+ const sessionId = getRequestSessionId(context);
811
+ if (!sessionId)
812
+ return jsonResponse(400, { ok: false, error: "valid browser session id required" });
813
+ if (route === "start" && method === "POST") {
814
+ const input = body ?? {};
815
+ const viewport = {
816
+ width: normalizeViewportDimension(input.width, 320, 2560, DEFAULT_VIEWPORT.width),
817
+ height: normalizeViewportDimension(input.height, 240, 2560, DEFAULT_VIEWPORT.height)
818
+ };
819
+ const s = await ensureSession(sessionId, viewport);
820
+ const clientId = normalizeSessionId(input.clientId);
821
+ if (clientId) s.clientIds.add(clientId);
822
+ await resizeBrowser(s, viewport);
823
+ const url = input.url?.trim() || "about:blank";
824
+ await writeLog("info", `browser viewport ${viewport.width}x${viewport.height}`);
825
+ await navigateBrowser(s, url);
826
+ await writeLog("info", `browser ready at ${s.page.url()}`);
827
+ return jsonResponse(200, { ok: true, ...await getPageResult(s) });
828
+ }
829
+ if (route === "stop" && method === "POST") {
830
+ await closeSession(sessionId);
831
+ return jsonResponse(200, { ok: true });
832
+ }
833
+ if (route === "release" && method === "POST") {
834
+ const input = body ?? {};
835
+ const clientId = normalizeSessionId(input.clientId);
836
+ if (!clientId) return jsonResponse(400, { ok: false, error: "valid client id required" });
837
+ const s = sessions.get(sessionId);
838
+ if (!s) return jsonResponse(200, { ok: true, closed: false });
839
+ s.clientIds.delete(clientId);
840
+ if (s.clientIds.size > 0) return jsonResponse(200, { ok: true, closed: false });
841
+ await closeSession(sessionId);
842
+ return jsonResponse(200, { ok: true, closed: true });
843
+ }
844
+ if (route === "navigate" && method === "POST") {
845
+ const input = body ?? {};
846
+ const url = input.url?.trim();
847
+ if (!url) return jsonResponse(400, { ok: false, error: "url required" });
848
+ const s = await ensureSession(sessionId);
849
+ const response = await navigateBrowser(s, url, {
850
+ waitUntil: input.waitUntil ?? "domcontentloaded",
851
+ timeout: normalizeTimeout(input.timeoutMs, 3e4, 6e4)
852
+ });
853
+ return jsonResponse(200, {
854
+ ok: true,
855
+ action: "navigate",
856
+ ...await getPageResult(s),
857
+ status: response?.status() ?? null
858
+ });
859
+ }
860
+ if (route === "click" && method === "POST") {
861
+ const input = body ?? {};
862
+ const selector = input.selector?.trim();
863
+ if (!selector) return jsonResponse(400, { ok: false, error: "selector required" });
864
+ const s = await ensureSession(sessionId);
865
+ const button = input.button === "right" || input.button === "middle" ? input.button : "left";
866
+ await s.page.locator(selector).click({
867
+ button,
868
+ timeout: normalizeTimeout(input.timeoutMs, 1e4, 3e4)
869
+ });
870
+ await s.page.waitForLoadState("domcontentloaded", { timeout: 5e3 }).catch(() => {
871
+ });
872
+ return jsonResponse(200, {
873
+ ok: true,
874
+ action: "click",
875
+ ...await getPageResult(s),
876
+ message: `Clicked ${selector}`
877
+ });
878
+ }
879
+ if (route === "type" && method === "POST") {
880
+ const input = body ?? {};
881
+ const selector = input.selector?.trim();
882
+ if (!selector) return jsonResponse(400, { ok: false, error: "selector required" });
883
+ const s = await ensureSession(sessionId);
884
+ const locator = s.page.locator(selector);
885
+ const text2 = typeof input.text === "string" ? input.text : String(input.text ?? "");
886
+ const timeout = normalizeTimeout(input.timeoutMs, 1e4, 3e4);
887
+ if (input.clear !== false) {
888
+ await locator.fill("", { timeout });
889
+ await locator.fill(text2, { timeout });
890
+ } else {
891
+ await locator.type(text2, { timeout });
892
+ }
893
+ return jsonResponse(200, {
894
+ ok: true,
895
+ action: "type",
896
+ ...await getPageResult(s),
897
+ message: `Typed into ${selector}`
898
+ });
899
+ }
900
+ if (route === "extract" && method === "POST") {
901
+ const input = body ?? {};
902
+ const selector = input.selector?.trim() || "body";
903
+ const s = await ensureSession(sessionId);
904
+ const locator = s.page.locator(selector).first();
905
+ const text2 = (await locator.innerText({ timeout: 1e4 }).catch(async () => s.page.locator("body").innerText({ timeout: 1e4 }))).trim();
906
+ const maxChars = typeof input.maxChars === "number" && Number.isFinite(input.maxChars) ? Math.min(2e4, Math.max(500, Math.round(input.maxChars))) : 8e3;
907
+ return jsonResponse(200, {
908
+ ok: true,
909
+ action: "extract",
910
+ ...await getPageResult(s),
911
+ text: text2.slice(0, maxChars)
912
+ });
913
+ }
914
+ if (route === "screenshot" && method === "POST") {
915
+ const input = body ?? {};
916
+ const s = await ensureSession(sessionId);
917
+ const screenshotPath = resolveScreenshotPath(context.workdirRoot, input.file_path);
918
+ await mkdir4(path2.dirname(screenshotPath.absolutePath), { recursive: true });
919
+ await s.page.screenshot({
920
+ path: screenshotPath.absolutePath,
921
+ fullPage: input.fullPage !== false
922
+ });
923
+ return jsonResponse(200, {
924
+ ok: true,
925
+ action: "screenshot",
926
+ ...await getPageResult(s),
927
+ filePath: screenshotPath.relativePath,
928
+ message: "Screenshot saved"
929
+ });
930
+ }
931
+ if (route === "resize" && method === "POST") {
932
+ const input = body ?? {};
933
+ const viewport = {
934
+ width: normalizeViewportDimension(input.width, 320, 2560, DEFAULT_VIEWPORT.width),
935
+ height: normalizeViewportDimension(input.height, 240, 2560, DEFAULT_VIEWPORT.height)
936
+ };
937
+ const s = await ensureSession(sessionId);
938
+ await resizeBrowser(s, viewport);
939
+ return jsonResponse(200, { ok: true, ...viewport });
940
+ }
941
+ if (route === "input" && method === "POST") {
942
+ await handleBrowserInput(sessionId, body);
943
+ return jsonResponse(200, { ok: true });
944
+ }
945
+ if (route === "location" && method === "GET") {
946
+ const s = sessions.get(sessionId);
947
+ if (!s) return jsonResponse(200, { ok: true, url: "", title: "" });
948
+ return jsonResponse(200, { ok: true, ...await getPageResult(s) });
949
+ }
950
+ if (route === "screencast" && method === "GET") {
951
+ const s = await ensureSession(sessionId);
952
+ let cleanup = () => {
953
+ };
954
+ const stream = new ReadableStream({
955
+ start(controller) {
956
+ const encoder = new TextEncoder();
957
+ const listener = (frame) => {
958
+ const line = `data:${JSON.stringify({
959
+ type: "frame",
960
+ ...frame,
961
+ mimeType: "image/jpeg"
962
+ })}
963
+
964
+ `;
965
+ controller.enqueue(encoder.encode(line));
966
+ };
967
+ s.screencastClients.add(listener);
968
+ void startScreencast(s).catch((error) => controller.error(error));
969
+ let checkInterval = null;
970
+ cleanup = () => {
971
+ s.screencastClients.delete(listener);
972
+ if (checkInterval) clearInterval(checkInterval);
973
+ checkInterval = null;
974
+ if (s.screencastClients.size === 0) void stopScreencast(s);
975
+ };
976
+ checkInterval = setInterval(() => {
977
+ if (sessions.get(sessionId) !== s) {
978
+ cleanup();
979
+ controller.close();
980
+ }
981
+ }, 1e3);
982
+ },
983
+ cancel() {
984
+ cleanup();
985
+ }
986
+ });
987
+ return new Response(stream, {
988
+ status: 200,
989
+ headers: {
990
+ "content-type": "text/event-stream",
991
+ "cache-control": "no-cache",
992
+ connection: "keep-alive"
993
+ }
994
+ });
995
+ }
996
+ return jsonResponse(404, { ok: false, error: "not found" });
997
+ }
998
+
999
+ // src/bundled.ts
1000
+ import { createHash as createHash3 } from "crypto";
1001
+ import { cpSync, existsSync, rmSync } from "fs";
1002
+ import { readdir, readFile as readFile3 } from "fs/promises";
1003
+ import path3 from "path";
1004
+ import { fileURLToPath } from "url";
1005
+ var PKG_ROOT = path3.resolve(fileURLToPath(import.meta.url), "../..");
1006
+ var BUNDLED_DIR = path3.join(PKG_ROOT, "agent-templates");
1007
+ var SYSTEM_SKILLS_DIR = path3.join(BUNDLED_DIR, "system-skills");
1008
+ var SCRIPTS_DIR = path3.join(BUNDLED_DIR, "scripts");
1009
+ var SHARED_DIR = path3.join(BUNDLED_DIR, "shared");
1010
+ var MONOREPO_SRC = path3.resolve(PKG_ROOT, "../buda/agent-templates");
1011
+ function syncTemplatesFromMonorepo() {
1012
+ if (!existsSync(MONOREPO_SRC)) {
1013
+ throw new Error(`Cannot sync agent-templates: source missing at ${MONOREPO_SRC}`);
1014
+ }
1015
+ rmSync(BUNDLED_DIR, { recursive: true, force: true });
1016
+ cpSync(MONOREPO_SRC, BUNDLED_DIR, {
1017
+ recursive: true,
1018
+ dereference: true,
1019
+ filter: (src) => !src.split(path3.sep).includes("extensions")
1020
+ });
1021
+ }
1022
+ ensureBundledDir();
1023
+ function ensureBundledDir() {
1024
+ const missingRequiredTemplate = !existsSync(path3.join(SHARED_DIR, "BOOTSTRAP.md"));
1025
+ if (existsSync(BUNDLED_DIR) && !missingRequiredTemplate) return;
1026
+ if (!existsSync(MONOREPO_SRC)) {
1027
+ console.warn(
1028
+ `[buda-connector] agent-templates not found at ${BUNDLED_DIR} \u2014 system skills will be empty. Reinstall @buda-ai/connector to restore.`
1029
+ );
1030
+ return;
1031
+ }
1032
+ syncTemplatesFromMonorepo();
1033
+ console.log(`[buda-connector] synced agent-templates from ${MONOREPO_SRC}`);
1034
+ }
1035
+ var cached = null;
1036
+ async function readBundledManifest() {
1037
+ if (cached) return cached;
1038
+ const systemSkillNames = (await readdir(SYSTEM_SKILLS_DIR, { withFileTypes: true }).catch(() => [])).filter((e) => e.isDirectory()).map((e) => e.name).sort();
1039
+ const version = await hashDir(BUNDLED_DIR);
1040
+ cached = {
1041
+ version,
1042
+ bundledDir: BUNDLED_DIR,
1043
+ systemSkillsDir: SYSTEM_SKILLS_DIR,
1044
+ systemSkillNames,
1045
+ scriptsDir: SCRIPTS_DIR,
1046
+ sharedDir: SHARED_DIR
1047
+ };
1048
+ return cached;
1049
+ }
1050
+ async function hashDir(dir) {
1051
+ const hash = createHash3("sha256");
1052
+ await walk(dir, async (relPath, abs) => {
1053
+ hash.update(`${relPath}:`).update(await readFile3(abs));
1054
+ });
1055
+ return hash.digest("hex");
1056
+ }
1057
+ async function walk(dir, visit, base = dir) {
1058
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
1059
+ entries.sort((a, b) => a.name.localeCompare(b.name));
1060
+ for (const entry of entries) {
1061
+ const abs = path3.join(dir, entry.name);
1062
+ if (entry.isDirectory()) {
1063
+ await walk(abs, visit, base);
1064
+ } else if (entry.isFile()) {
1065
+ await visit(path3.relative(base, abs), abs);
1066
+ }
1067
+ }
1068
+ }
1069
+
1070
+ // src/types.ts
1071
+ var CONNECTOR_RELAY_PROTOCOL_VERSION = 1;
1072
+ var CONNECTOR_EXECUTION_IDENTITY_CAPABILITY = "execution-identity:ed25519-v1";
1073
+
1074
+ // src/cloud-client.ts
1075
+ var BASE_CONNECTOR_CAPABILITIES = [
1076
+ "connector",
1077
+ "bunny-daemon",
1078
+ "coding",
1079
+ "filesystem",
1080
+ "shell",
1081
+ "git"
1082
+ ];
1083
+ var browserAvailabilityWarningLogged = false;
1084
+ var getAdvertisedCapabilities = (browserAvailability = resolveBrowserAvailability(), deviceToken) => [
1085
+ ...BASE_CONNECTOR_CAPABILITIES,
1086
+ ...browserAvailability.available ? ["browser"] : [],
1087
+ ...deviceToken ? [CONNECTOR_EXECUTION_IDENTITY_CAPABILITY] : []
1088
+ ];
1089
+ async function buildPayload(config, extra) {
1090
+ const bundle = await readBundledManifest();
1091
+ const browserAvailability = resolveBrowserAvailability();
1092
+ if (!browserAvailability.available && !browserAvailabilityWarningLogged) {
1093
+ browserAvailabilityWarningLogged = true;
1094
+ await writeLog("warn", browserAvailability.error);
1095
+ }
170
1096
  return {
171
1097
  connectorId: config.connectorId,
172
1098
  deviceId: config.deviceId,
173
1099
  deviceName: config.deviceName,
174
1100
  hostLabel: config.hostLabel,
175
- platform: `${platform2()} ${release()}`,
1101
+ platform: `${platform3()} ${release()}`,
176
1102
  version: "dev",
177
1103
  pid: process.pid,
178
1104
  workdirRoot: config.workdirRoot,
@@ -184,61 +1110,109 @@ function buildPayload(config, extra) {
184
1110
  root: extra.bunnyDaemon.root,
185
1111
  status: "online"
186
1112
  } : void 0,
187
- capabilities: ["connector", "bunny-daemon", "coding", "filesystem", "shell", "git"],
188
- ...extra?.logs?.length ? { logs: extra.logs } : {}
1113
+ paths: {
1114
+ workdir: config.workdirRoot,
1115
+ systemSkillsDir: bundle.systemSkillsDir,
1116
+ scriptsDir: bundle.scriptsDir,
1117
+ sharedDir: bundle.sharedDir
1118
+ },
1119
+ bundle: {
1120
+ version: bundle.version,
1121
+ systemSkillNames: bundle.systemSkillNames
1122
+ },
1123
+ capabilities: getAdvertisedCapabilities(browserAvailability, config.deviceToken),
1124
+ deviceCredential: config.deviceToken ? {
1125
+ version: 1,
1126
+ tokenHash: createHash4("sha256").update(config.deviceToken, "utf8").digest("base64url")
1127
+ } : void 0,
1128
+ ...extra?.logs?.length ? { logs: extra.logs } : {},
1129
+ ...extra?.recovery ? { recovery: extra.recovery } : {}
189
1130
  };
190
1131
  }
191
- async function postJson(serverUrl, pathname, body, launchConfig) {
192
- const token = launchConfig?.oauthToken?.trim();
193
- const spaceId = getSpaceId(launchConfig)?.trim();
1132
+ function buildAuthHeaders(config, launchConfig) {
1133
+ const headers = {
1134
+ "X-Buda-Connector-Id": config.connectorId
1135
+ };
1136
+ const token = config.token?.trim() || getConnectorToken(launchConfig);
1137
+ if (token) headers["X-Buda-Connector-Token"] = token;
1138
+ if (config.deviceToken) headers["X-Buda-Connector-Device-Token"] = config.deviceToken;
1139
+ return headers;
1140
+ }
1141
+ async function postJson(serverUrl, pathname, body, config, launchConfig) {
194
1142
  const response = await fetch(`${serverUrl}${pathname}`, {
195
1143
  method: "POST",
196
1144
  headers: {
197
1145
  "Content-Type": "application/json",
198
- ...token ? { Authorization: `Bearer ${token}` } : {},
199
- ...!token && spaceId ? { "X-Buda-Space-Id": spaceId } : {}
1146
+ ...buildAuthHeaders(config, launchConfig)
200
1147
  },
201
1148
  body: JSON.stringify(body)
202
1149
  });
203
1150
  if (!response.ok) {
204
- const text = await response.text().catch(() => "");
205
- throw new Error(`${pathname} failed: ${response.status} ${text}`);
1151
+ const text2 = await response.text().catch(() => "");
1152
+ let errorCode;
1153
+ let errorMessage3 = text2;
1154
+ try {
1155
+ const payload = JSON.parse(text2);
1156
+ errorCode = typeof payload.code === "string" ? payload.code : void 0;
1157
+ errorMessage3 = typeof payload.error === "string" ? payload.error : text2;
1158
+ } catch {
1159
+ }
1160
+ if (errorCode === "device_revoked") {
1161
+ throw new Error(
1162
+ `This device is prohibited/revoked and cannot reconnect. Contact support. (${errorMessage3})`
1163
+ );
1164
+ }
1165
+ throw new Error(`${pathname} failed: ${response.status} ${errorMessage3}`);
206
1166
  }
207
1167
  return await response.json();
208
1168
  }
209
1169
  function formatConnectorOwner(connector) {
210
1170
  return connector.userEmail ?? connector.userId ?? "anonymous";
211
1171
  }
212
- async function registerConnector(config, launchConfig, bunnyDaemon) {
1172
+ async function warnIfStaleBundle(result) {
1173
+ if (!result.bundle?.staleClient) return;
1174
+ await writeLog(
1175
+ "warn",
1176
+ `connector bundle out of date \u2014 server expects ${result.bundle.expectedVersion ?? "newer"}; run \`pnpm up @buda-ai/connector\``
1177
+ );
1178
+ }
1179
+ async function registerConnector(config, launchConfig, bunnyDaemon, options) {
213
1180
  const result = await postJson(
214
1181
  config.serverUrl,
215
1182
  "/api/connectors/register",
216
- buildPayload(config, { bunnyDaemon }),
1183
+ await buildPayload(config, {
1184
+ bunnyDaemon,
1185
+ ...options?.recoverMissingProfile ? { recovery: "missing_local_profile" } : {}
1186
+ }),
1187
+ config,
217
1188
  launchConfig
218
1189
  );
219
1190
  await writeLog(
220
1191
  "info",
221
1192
  `registered connector=${result.connector.connectorId} status=${result.connector.status} owner=${formatConnectorOwner(result.connector)}`
222
1193
  );
1194
+ await warnIfStaleBundle(result);
1195
+ return {
1196
+ connectorId: result.connector.connectorId,
1197
+ relayProtocol: result.relayProtocol
1198
+ };
223
1199
  }
224
1200
  async function heartbeatConnector(config, launchConfig, bunnyDaemon) {
225
1201
  const logs = takePendingLogs();
226
1202
  const result = await postJson(
227
1203
  config.serverUrl,
228
1204
  "/api/connectors/heartbeat",
229
- buildPayload(config, {
1205
+ await buildPayload(config, {
230
1206
  bunnyDaemon,
231
1207
  logs
232
1208
  }),
1209
+ config,
233
1210
  launchConfig
234
1211
  ).catch((error) => {
235
1212
  restorePendingLogs(logs);
236
1213
  throw error;
237
1214
  });
238
- await writeLog(
239
- "info",
240
- `heartbeat connector=${result.connector.connectorId} status=${result.connector.status} owner=${formatConnectorOwner(result.connector)}`
241
- );
1215
+ return { relayProtocol: result.relayProtocol };
242
1216
  }
243
1217
  async function unregisterConnector(config, launchConfig) {
244
1218
  await postJson(
@@ -247,160 +1221,1260 @@ async function unregisterConnector(config, launchConfig) {
247
1221
  {
248
1222
  connectorId: config.connectorId
249
1223
  },
1224
+ config,
250
1225
  launchConfig
251
1226
  ).catch((error) => {
252
1227
  void writeLog("warn", `unregister failed: ${error instanceof Error ? error.message : error}`);
253
1228
  });
254
1229
  }
255
1230
 
256
- // ../../packages/relaylib/src/protocol.ts
257
- var utf8Encoder = new TextEncoder();
258
- function bytesToBase64(bytes) {
259
- if (typeof Buffer !== "undefined") {
260
- return Buffer.from(bytes).toString("base64");
1231
+ // src/legacy-workspace-migration.ts
1232
+ import { createHash as createHash5, randomUUID as randomUUID2 } from "crypto";
1233
+ import { constants as constants2, createReadStream } from "fs";
1234
+ import {
1235
+ chmod as chmod2,
1236
+ copyFile,
1237
+ lstat,
1238
+ mkdir as mkdir5,
1239
+ open as open2,
1240
+ readdir as readdir2,
1241
+ readFile as readFile4,
1242
+ readlink,
1243
+ realpath,
1244
+ rename as rename2,
1245
+ rm,
1246
+ symlink,
1247
+ unlink as unlink3
1248
+ } from "fs/promises";
1249
+ import path4 from "path";
1250
+ var MIGRATION_VERSION = 1;
1251
+ var MIGRATION_DIRECTORY = path4.join(".buda", "migrations", "legacy-workspace-v1");
1252
+ var JOURNAL_FILENAME = "journal.json";
1253
+ var IMMUTABLE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
1254
+ var LEGACY_TEMPLATE_FILES = [
1255
+ "AGENTS.md",
1256
+ "HEARTBEAT.md",
1257
+ "IDENTITY.md",
1258
+ "MEMORY.md",
1259
+ "SOUL.md",
1260
+ "TOOLS.md",
1261
+ "USER.md"
1262
+ ];
1263
+ var JOURNAL_STATUSES = /* @__PURE__ */ new Set([
1264
+ "planned",
1265
+ "migrating",
1266
+ "complete",
1267
+ "rolling-back",
1268
+ "rolled-back"
1269
+ ]);
1270
+ var ITEM_PHASES = /* @__PURE__ */ new Set([
1271
+ "pending",
1272
+ "copying",
1273
+ "copied",
1274
+ "verified",
1275
+ "committed",
1276
+ "rolled-back"
1277
+ ]);
1278
+ var LegacyWorkspaceMigrationError = class extends Error {
1279
+ constructor(code, message) {
1280
+ super(message);
1281
+ this.code = code;
1282
+ this.name = "LegacyWorkspaceMigrationError";
261
1283
  }
262
- let binary = "";
263
- for (const byte of bytes) {
264
- binary += String.fromCharCode(byte);
1284
+ recoverable = true;
1285
+ };
1286
+ var migrateLegacyWorkspaceRoot = async (storageRoot, options = {}) => {
1287
+ await mkdir5(storageRoot, { recursive: true, mode: 448 });
1288
+ const rootInfo = await lstat(storageRoot);
1289
+ if (rootInfo.isSymbolicLink() || !rootInfo.isDirectory()) {
1290
+ throw new LegacyWorkspaceMigrationError(
1291
+ "MIGRATION_SOURCE_UNSAFE",
1292
+ "Legacy migration storage root must be a real directory"
1293
+ );
265
1294
  }
266
- return btoa(binary);
267
- }
268
-
269
- // ../../packages/relaylib/src/relay-client.ts
270
- function attachRelayClient(socket, handler, options = {}) {
271
- const chunkEncoding = options.chunkEncoding ?? "base64";
272
- socket.on("message", (raw) => {
273
- let message;
274
- try {
275
- message = JSON.parse(raw.toString());
276
- } catch {
1295
+ const canonicalRoot = await realpath(storageRoot);
1296
+ const migrationDirectory = path4.join(canonicalRoot, MIGRATION_DIRECTORY);
1297
+ await ensurePrivateDirectoryWithin(canonicalRoot, migrationDirectory);
1298
+ const journalPath = path4.join(migrationDirectory, JOURNAL_FILENAME);
1299
+ let journal = await readJournal(journalPath);
1300
+ if (journal) validateJournalPaths(journal, canonicalRoot, migrationDirectory);
1301
+ if (journal?.status === "complete") {
1302
+ await verifyCommittedItems(journal);
1303
+ return toResult(journal);
1304
+ }
1305
+ if (journal) await checkpoint(options, "restart", null);
1306
+ if (journal?.status === "rolling-back") {
1307
+ await resumeRollback(journalPath, journal, options);
1308
+ journal = void 0;
1309
+ }
1310
+ if (journal?.status === "rolled-back") journal = void 0;
1311
+ if (!journal) {
1312
+ journal = await createJournal(canonicalRoot, migrationDirectory, options.owner);
1313
+ await writeJournal(journalPath, journal);
1314
+ } else {
1315
+ assertOwnerMatches(journal, options.owner);
1316
+ }
1317
+ if (journal.status === "planned") {
1318
+ journal.status = "migrating";
1319
+ await writeJournal(journalPath, journal);
1320
+ }
1321
+ for (const item of journal.items) {
1322
+ await migrateItem(canonicalRoot, journalPath, journal, item, options);
1323
+ }
1324
+ journal.status = "complete";
1325
+ journal.completedAt = (/* @__PURE__ */ new Date()).toISOString();
1326
+ await writeJournal(journalPath, journal);
1327
+ return toResult(journal);
1328
+ };
1329
+ var migrateItem = async (storageRoot, journalPath, journal, item, options) => {
1330
+ if (item.phase === "committed") return;
1331
+ if (await exists(item.target)) {
1332
+ if (!await exists(item.staging) && ["verified", "copied", "copying"].includes(item.phase)) {
1333
+ await verifyTree(item.source, item.target);
1334
+ item.phase = "committed";
1335
+ await writeJournal(journalPath, journal);
277
1336
  return;
278
1337
  }
279
- if (message.type !== "request") return;
280
- void handleRequest(socket, message.requestId, message.request, handler, chunkEncoding);
281
- });
282
- }
283
- async function handleRequest(socket, requestId, request, handler, chunkEncoding) {
284
- const send = (message) => {
285
- if (socket.readyState === 1) socket.send(JSON.stringify(message));
1338
+ throw new LegacyWorkspaceMigrationError(
1339
+ "MIGRATION_TARGET_CONFLICT",
1340
+ `Legacy migration target already exists for ${item.id}`
1341
+ );
1342
+ }
1343
+ if (item.phase === "pending" || item.phase === "copying") {
1344
+ item.phase = "copying";
1345
+ await writeJournal(journalPath, journal);
1346
+ await checkpoint(options, "before-copy", item.id);
1347
+ await rm(item.staging, { recursive: true, force: true });
1348
+ await ensurePrivateDirectoryWithin(storageRoot, path4.dirname(item.staging));
1349
+ await copyTree(item.source, item.staging, () => checkpoint(options, "during-copy", item.id));
1350
+ await checkpoint(options, "after-copy", item.id);
1351
+ item.phase = "copied";
1352
+ await writeJournal(journalPath, journal);
1353
+ }
1354
+ if (item.phase === "copied") {
1355
+ await checkpoint(options, "before-verify", item.id);
1356
+ try {
1357
+ await verifyTree(item.source, item.staging);
1358
+ } catch (error) {
1359
+ await rm(item.staging, { recursive: true, force: true });
1360
+ item.phase = "copying";
1361
+ await writeJournal(journalPath, journal);
1362
+ throw error;
1363
+ }
1364
+ await checkpoint(options, "after-verify", item.id);
1365
+ item.phase = "verified";
1366
+ await writeJournal(journalPath, journal);
1367
+ }
1368
+ if (item.phase === "verified") {
1369
+ await checkpoint(options, "before-rename", item.id);
1370
+ await ensurePrivateDirectoryWithin(storageRoot, path4.dirname(item.target));
1371
+ await rename2(item.staging, item.target);
1372
+ await syncDirectory(path4.dirname(item.target));
1373
+ await checkpoint(options, "after-rename", item.id);
1374
+ item.phase = "committed";
1375
+ await writeJournal(journalPath, journal);
1376
+ }
1377
+ };
1378
+ var resumeRollback = async (journalPath, journal, options) => {
1379
+ for (const item of [...journal.items].reverse()) {
1380
+ if (item.phase === "rolled-back") continue;
1381
+ await checkpoint(options, "before-rollback", item.id);
1382
+ if (!await exists(item.source)) {
1383
+ throw new LegacyWorkspaceMigrationError(
1384
+ "MIGRATION_VERIFY_FAILED",
1385
+ `Cannot roll back ${item.id}: the preserved legacy source is missing`
1386
+ );
1387
+ }
1388
+ await rm(item.staging, { recursive: true, force: true });
1389
+ await rm(item.target, { recursive: true, force: true });
1390
+ await checkpoint(options, "after-rollback", item.id);
1391
+ item.phase = "rolled-back";
1392
+ await writeJournal(journalPath, journal);
1393
+ }
1394
+ journal.status = "rolled-back";
1395
+ delete journal.completedAt;
1396
+ await writeJournal(journalPath, journal);
1397
+ };
1398
+ var createJournal = async (storageRoot, migrationDirectory, owner) => {
1399
+ if (owner) validateOwner(owner);
1400
+ const ownership = owner ? "explicit-owner" : "shared-legacy";
1401
+ const label = owner ? `Legacy Connector root assigned to ${owner.spaceId}/${owner.agentId}` : "Shared legacy Connector root (ownership unassigned)";
1402
+ const sources = await inventoryLegacySources(storageRoot);
1403
+ const stagingRoot = path4.join(migrationDirectory, "staging", randomUUID2());
1404
+ const items = sources.map(({ id, source }) => ({
1405
+ id,
1406
+ source,
1407
+ target: deriveTarget(storageRoot, id, owner),
1408
+ staging: path4.join(stagingRoot, id),
1409
+ phase: "pending"
1410
+ }));
1411
+ assertDistinctTargets(items);
1412
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1413
+ return {
1414
+ version: MIGRATION_VERSION,
1415
+ status: "planned",
1416
+ ownership,
1417
+ label,
1418
+ ...owner ? { owner } : {},
1419
+ createdAt: now,
1420
+ updatedAt: now,
1421
+ items
286
1422
  };
287
- let response;
288
- try {
289
- response = await handler(request);
290
- } catch (error) {
291
- send({ type: "error", requestId, error: errorMessage(error) });
1423
+ };
1424
+ var inventoryLegacySources = async (storageRoot) => {
1425
+ const rootEntries = await readdir2(storageRoot, { withFileTypes: true });
1426
+ const templateNames = new Set(LEGACY_TEMPLATE_FILES);
1427
+ for (const entry of rootEntries) {
1428
+ if (entry.name.endsWith(".md")) templateNames.add(entry.name);
1429
+ }
1430
+ const candidates = [
1431
+ { id: "agent", source: path4.join(storageRoot, "agent") },
1432
+ { id: "space", source: path4.join(storageRoot, "space") },
1433
+ { id: "logs", source: path4.join(storageRoot, "logs") },
1434
+ { id: "connector-log", source: path4.join(storageRoot, "connector.log") },
1435
+ { id: "runtime-config", source: path4.join(storageRoot, "runtime-config") },
1436
+ { id: "runtime-config-hidden", source: path4.join(storageRoot, ".runtime-config") },
1437
+ { id: "runtime-config-file", source: path4.join(storageRoot, "runtime-config.json") },
1438
+ { id: "runtime-config-hidden-file", source: path4.join(storageRoot, ".runtime-config.json") },
1439
+ { id: "bunny-runtime", source: path4.join(storageRoot, ".bunny") },
1440
+ ...[...templateNames].sort().map((name) => ({
1441
+ id: `template-${name}`,
1442
+ source: path4.join(storageRoot, name)
1443
+ }))
1444
+ ];
1445
+ const found = [];
1446
+ for (const candidate of candidates) {
1447
+ if (!await exists(candidate.source)) continue;
1448
+ const info = await lstat(candidate.source);
1449
+ if (info.isSymbolicLink()) {
1450
+ throw new LegacyWorkspaceMigrationError(
1451
+ "MIGRATION_SOURCE_UNSAFE",
1452
+ `Legacy migration source ${candidate.id} must not be a symbolic link`
1453
+ );
1454
+ }
1455
+ if (info.isDirectory() && (await readdir2(candidate.source)).length === 0) continue;
1456
+ found.push(candidate);
1457
+ }
1458
+ return found;
1459
+ };
1460
+ var deriveTarget = (storageRoot, id, owner) => {
1461
+ if (!owner) return path4.join(storageRoot, ".buda", "legacy", "shared", id);
1462
+ const spaceRoot = path4.join(storageRoot, ".buda", "spaces", owner.spaceId);
1463
+ const agentRoot = path4.join(spaceRoot, "agents", owner.agentId);
1464
+ if (id === "agent") return path4.join(agentRoot, "workspace", "legacy-connector-root");
1465
+ if (id === "space") return path4.join(spaceRoot, "shared", "legacy-connector-root");
1466
+ if (id.startsWith("template-")) {
1467
+ return path4.join(spaceRoot, "shared", "legacy-templates", id.slice("template-".length));
1468
+ }
1469
+ return path4.join(agentRoot, "state", "legacy-connector-root", id);
1470
+ };
1471
+ var copyTree = async (source, destination, onEntryCopied) => {
1472
+ const info = await lstat(source);
1473
+ if (info.isDirectory()) {
1474
+ await mkdir5(destination, { mode: info.mode & 4095 });
1475
+ const entries = (await readdir2(source)).sort();
1476
+ for (const entry of entries) {
1477
+ await copyTree(path4.join(source, entry), path4.join(destination, entry), onEntryCopied);
1478
+ }
1479
+ await chmod2(destination, info.mode & 4095);
292
1480
  return;
293
1481
  }
294
- send({
295
- type: "start",
296
- requestId,
297
- status: response.status,
298
- headers: headersToRecord(response.headers)
1482
+ if (info.isFile()) {
1483
+ await copyFile(source, destination, constants2.COPYFILE_EXCL);
1484
+ await chmod2(destination, info.mode & 4095);
1485
+ await onEntryCopied();
1486
+ return;
1487
+ }
1488
+ if (info.isSymbolicLink()) {
1489
+ await symlink(await readlink(source), destination);
1490
+ await onEntryCopied();
1491
+ return;
1492
+ }
1493
+ throw new LegacyWorkspaceMigrationError(
1494
+ "MIGRATION_SOURCE_UNSAFE",
1495
+ "Legacy migration supports only files, directories, and symbolic links"
1496
+ );
1497
+ };
1498
+ var verifyTree = async (source, destination) => {
1499
+ const [sourceInfo, destinationInfo] = await Promise.all([lstat(source), lstat(destination)]);
1500
+ if (sourceInfo.isDirectory() !== destinationInfo.isDirectory()) {
1501
+ throw verifyFailure("entry types differ");
1502
+ }
1503
+ if (sourceInfo.isFile() !== destinationInfo.isFile()) throw verifyFailure("entry types differ");
1504
+ if (sourceInfo.isSymbolicLink() !== destinationInfo.isSymbolicLink()) {
1505
+ throw verifyFailure("entry types differ");
1506
+ }
1507
+ if (!sourceInfo.isSymbolicLink() && (sourceInfo.mode & 4095) !== (destinationInfo.mode & 4095)) {
1508
+ throw verifyFailure("permission bits differ");
1509
+ }
1510
+ if (sourceInfo.isDirectory()) {
1511
+ const [sourceEntries, destinationEntries] = await Promise.all([
1512
+ readdir2(source),
1513
+ readdir2(destination)
1514
+ ]);
1515
+ sourceEntries.sort();
1516
+ destinationEntries.sort();
1517
+ if (JSON.stringify(sourceEntries) !== JSON.stringify(destinationEntries)) {
1518
+ throw verifyFailure("directory entries differ");
1519
+ }
1520
+ for (const entry of sourceEntries) {
1521
+ await verifyTree(path4.join(source, entry), path4.join(destination, entry));
1522
+ }
1523
+ return;
1524
+ }
1525
+ if (sourceInfo.isFile()) {
1526
+ if (sourceInfo.size !== destinationInfo.size) throw verifyFailure("file sizes differ");
1527
+ const [sourceHash, destinationHash] = await Promise.all([
1528
+ hashFile(source),
1529
+ hashFile(destination)
1530
+ ]);
1531
+ if (sourceHash !== destinationHash) throw verifyFailure("file bytes differ");
1532
+ return;
1533
+ }
1534
+ if (await readlink(source) !== await readlink(destination)) {
1535
+ throw verifyFailure("symbolic link targets differ");
1536
+ }
1537
+ };
1538
+ var hashFile = async (filePath) => {
1539
+ const hash = createHash5("sha256");
1540
+ await new Promise((resolve, reject) => {
1541
+ const stream = createReadStream(filePath);
1542
+ stream.on("data", (chunk) => hash.update(chunk));
1543
+ stream.once("error", reject);
1544
+ stream.once("end", resolve);
299
1545
  });
1546
+ return hash.digest("hex");
1547
+ };
1548
+ var readJournal = async (journalPath) => {
1549
+ let raw;
300
1550
  try {
301
- const body = response.body;
302
- if (body) {
303
- const reader = body.getReader();
304
- if (chunkEncoding === "utf8") {
305
- const decoder = new TextDecoder();
306
- while (true) {
307
- const { done, value } = await reader.read();
308
- if (done) break;
309
- const text = decoder.decode(value, { stream: true });
310
- if (text) send({ type: "chunk", requestId, data: text });
311
- }
312
- const tail = decoder.decode();
313
- if (tail) send({ type: "chunk", requestId, data: tail });
314
- } else {
315
- while (true) {
316
- const { done, value } = await reader.read();
317
- if (done) break;
318
- if (value && value.length > 0) {
319
- send({
320
- type: "chunk",
321
- requestId,
322
- encoding: "base64",
323
- data: bytesToBase64(value)
324
- });
325
- }
326
- }
327
- }
1551
+ raw = await readFile4(journalPath, "utf8");
1552
+ } catch (error) {
1553
+ if (isMissing(error)) return void 0;
1554
+ throw error;
1555
+ }
1556
+ let value;
1557
+ try {
1558
+ value = JSON.parse(raw);
1559
+ } catch {
1560
+ throw corrupt("Legacy migration journal contains invalid JSON");
1561
+ }
1562
+ if (!isJournal(value)) throw corrupt("Legacy migration journal is malformed");
1563
+ await chmod2(journalPath, 384);
1564
+ return value;
1565
+ };
1566
+ var writeJournal = async (journalPath, journal) => {
1567
+ journal.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1568
+ await ensurePrivateDirectory(path4.dirname(journalPath));
1569
+ const temporaryPath = `${journalPath}.${process.pid}.${randomUUID2()}.tmp`;
1570
+ const handle = await open2(
1571
+ temporaryPath,
1572
+ constants2.O_CREAT | constants2.O_EXCL | constants2.O_WRONLY,
1573
+ 384
1574
+ );
1575
+ try {
1576
+ await handle.writeFile(`${JSON.stringify(journal, null, 2)}
1577
+ `, "utf8");
1578
+ await handle.sync();
1579
+ } finally {
1580
+ await handle.close();
1581
+ }
1582
+ try {
1583
+ await rename2(temporaryPath, journalPath);
1584
+ await chmod2(journalPath, 384);
1585
+ await syncDirectory(path4.dirname(journalPath));
1586
+ } finally {
1587
+ await unlink3(temporaryPath).catch(() => void 0);
1588
+ }
1589
+ };
1590
+ var syncDirectory = async (directory) => {
1591
+ const handle = await open2(directory, constants2.O_RDONLY);
1592
+ try {
1593
+ await handle.sync();
1594
+ } finally {
1595
+ await handle.close();
1596
+ }
1597
+ };
1598
+ var ensurePrivateDirectory = async (directory) => {
1599
+ await mkdir5(directory, { mode: 448 }).catch((error) => {
1600
+ if (!isAlreadyExists2(error)) throw error;
1601
+ });
1602
+ const info = await lstat(directory);
1603
+ if (info.isSymbolicLink() || !info.isDirectory()) {
1604
+ throw new LegacyWorkspaceMigrationError(
1605
+ "MIGRATION_SOURCE_UNSAFE",
1606
+ "Legacy migration control path must be a real directory"
1607
+ );
1608
+ }
1609
+ await chmod2(directory, 448);
1610
+ };
1611
+ var ensurePrivateDirectoryWithin = async (storageRoot, directory) => {
1612
+ if (directory !== storageRoot && !isPathInside(storageRoot, directory)) {
1613
+ throw corrupt("Legacy migration control path escapes protected storage");
1614
+ }
1615
+ const relative = path4.relative(storageRoot, directory);
1616
+ let current = storageRoot;
1617
+ for (const segment of relative.split(path4.sep).filter(Boolean)) {
1618
+ current = path4.join(current, segment);
1619
+ await ensurePrivateDirectory(current);
1620
+ }
1621
+ };
1622
+ var checkpoint = async (options, stage, itemId) => options.faultInjector?.({ stage, itemId });
1623
+ var assertOwnerMatches = (journal, owner) => {
1624
+ if (!owner) return;
1625
+ const expected = owner ? `${owner.spaceId}:${owner.agentId}` : void 0;
1626
+ const actual = journal.owner ? `${journal.owner.spaceId}:${journal.owner.agentId}` : void 0;
1627
+ if (expected !== actual) {
1628
+ throw new LegacyWorkspaceMigrationError(
1629
+ "MIGRATION_TARGET_CONFLICT",
1630
+ "Legacy migration ownership cannot change while a journal is active; roll back first"
1631
+ );
1632
+ }
1633
+ };
1634
+ var validateJournalPaths = (journal, storageRoot, migrationDirectory) => {
1635
+ validateJournalOwnership(journal);
1636
+ const stagingParents = /* @__PURE__ */ new Set();
1637
+ const itemIds = /* @__PURE__ */ new Set();
1638
+ const sources = /* @__PURE__ */ new Set();
1639
+ const targets = /* @__PURE__ */ new Set();
1640
+ const stagingPaths = /* @__PURE__ */ new Set();
1641
+ for (const item of journal.items) {
1642
+ const sourceRelative = sourceRelativePath(item.id);
1643
+ if (!sourceRelative || item.source !== path4.join(storageRoot, sourceRelative)) {
1644
+ throw corrupt("Legacy migration journal contains an unexpected source path");
1645
+ }
1646
+ if (item.target !== deriveTarget(storageRoot, item.id, journal.owner)) {
1647
+ throw corrupt("Legacy migration journal target is not canonical");
328
1648
  }
329
- send({ type: "end", requestId });
1649
+ if (!isPathInside(migrationDirectory, item.staging)) {
1650
+ throw corrupt("Legacy migration journal staging path escapes the migration directory");
1651
+ }
1652
+ const stagingParent = path4.dirname(item.staging);
1653
+ if (path4.dirname(stagingParent) !== path4.join(migrationDirectory, "staging") || item.staging !== path4.join(stagingParent, item.id)) {
1654
+ throw corrupt("Legacy migration journal staging path is not canonical");
1655
+ }
1656
+ stagingParents.add(stagingParent);
1657
+ if (itemIds.has(item.id) || sources.has(item.source) || targets.has(item.target) || stagingPaths.has(item.staging)) {
1658
+ throw corrupt("Legacy migration journal contains duplicate items");
1659
+ }
1660
+ itemIds.add(item.id);
1661
+ sources.add(item.source);
1662
+ targets.add(item.target);
1663
+ stagingPaths.add(item.staging);
1664
+ }
1665
+ if (stagingParents.size > 1) throw corrupt("Legacy migration journal has multiple staging roots");
1666
+ validateJournalState(journal);
1667
+ };
1668
+ var sourceRelativePath = (id) => {
1669
+ const known = {
1670
+ agent: "agent",
1671
+ space: "space",
1672
+ logs: "logs",
1673
+ "connector-log": "connector.log",
1674
+ "runtime-config": "runtime-config",
1675
+ "runtime-config-hidden": ".runtime-config",
1676
+ "runtime-config-file": "runtime-config.json",
1677
+ "runtime-config-hidden-file": ".runtime-config.json",
1678
+ "bunny-runtime": ".bunny"
1679
+ };
1680
+ if (known[id]) return known[id];
1681
+ if (!id.startsWith("template-")) return null;
1682
+ const name = id.slice("template-".length);
1683
+ return name.endsWith(".md") && path4.basename(name) === name ? name : null;
1684
+ };
1685
+ var validateJournalOwnership = (journal) => {
1686
+ if (journal.ownership === "explicit-owner") {
1687
+ if (!journal.owner) throw corrupt("Explicit legacy migration ownership is missing");
1688
+ validateOwner(journal.owner);
1689
+ return;
1690
+ }
1691
+ if (journal.ownership !== "shared-legacy" || journal.owner) {
1692
+ throw corrupt("Legacy migration ownership is inconsistent");
1693
+ }
1694
+ };
1695
+ var validateJournalState = (journal) => {
1696
+ if (journal.status === "planned" && journal.items.some((item) => item.phase !== "pending")) {
1697
+ throw corrupt("Planned legacy migration contains advanced items");
1698
+ }
1699
+ if (journal.status === "migrating" && journal.items.some((item) => item.phase === "rolled-back")) {
1700
+ throw corrupt("Active legacy migration contains rolled-back items");
1701
+ }
1702
+ if (journal.status === "complete" && journal.items.some((item) => item.phase !== "committed")) {
1703
+ throw corrupt("Completed legacy migration contains unfinished items");
1704
+ }
1705
+ if (journal.status === "rolled-back" && journal.items.some((item) => item.phase !== "rolled-back")) {
1706
+ throw corrupt("Rolled-back legacy migration contains active items");
1707
+ }
1708
+ };
1709
+ var verifyCommittedItems = async (journal) => {
1710
+ try {
1711
+ for (const item of journal.items) await verifyTree(item.source, item.target);
1712
+ } catch (error) {
1713
+ if (error instanceof LegacyWorkspaceMigrationError) throw error;
1714
+ throw verifyFailure(`committed data is unavailable: ${errorMessage(error)}`);
1715
+ }
1716
+ };
1717
+ var validateOwner = (owner) => {
1718
+ if (!IMMUTABLE_ID_PATTERN.test(owner.spaceId) || !IMMUTABLE_ID_PATTERN.test(owner.agentId)) {
1719
+ throw new LegacyWorkspaceMigrationError(
1720
+ "MIGRATION_SOURCE_UNSAFE",
1721
+ "Legacy migration owner must use immutable Space and Agent IDs"
1722
+ );
1723
+ }
1724
+ };
1725
+ var assertDistinctTargets = (items) => {
1726
+ const targets = /* @__PURE__ */ new Set();
1727
+ for (const item of items) {
1728
+ if (targets.has(item.target)) throw corrupt("Legacy migration targets collide");
1729
+ targets.add(item.target);
1730
+ }
1731
+ };
1732
+ var toResult = (journal) => ({
1733
+ status: "complete",
1734
+ ownership: journal.ownership,
1735
+ label: journal.label,
1736
+ migratedItems: journal.items.map((item) => item.id),
1737
+ rollbackAvailable: true
1738
+ });
1739
+ var isJournal = (value) => {
1740
+ if (!value || typeof value !== "object") return false;
1741
+ const journal = value;
1742
+ return journal.version === MIGRATION_VERSION && typeof journal.status === "string" && JOURNAL_STATUSES.has(journal.status) && (journal.ownership === "explicit-owner" || journal.ownership === "shared-legacy") && typeof journal.label === "string" && typeof journal.createdAt === "string" && typeof journal.updatedAt === "string" && Array.isArray(journal.items) && journal.items.every(
1743
+ (item) => item && typeof item === "object" && typeof item.id === "string" && typeof item.source === "string" && typeof item.target === "string" && typeof item.staging === "string" && typeof item.phase === "string" && ITEM_PHASES.has(item.phase)
1744
+ );
1745
+ };
1746
+ var exists = async (filePath) => {
1747
+ try {
1748
+ await lstat(filePath);
1749
+ return true;
330
1750
  } catch (error) {
331
- send({ type: "error", requestId, error: errorMessage(error) });
1751
+ if (isMissing(error)) return false;
1752
+ throw error;
1753
+ }
1754
+ };
1755
+ var isPathInside = (parent, child) => {
1756
+ const relative = path4.relative(parent, child);
1757
+ return relative !== "" && relative !== ".." && !relative.startsWith(`..${path4.sep}`) && !path4.isAbsolute(relative);
1758
+ };
1759
+ var isMissing = (error) => error instanceof Error && "code" in error && error.code === "ENOENT";
1760
+ var isAlreadyExists2 = (error) => error instanceof Error && "code" in error && error.code === "EEXIST";
1761
+ var verifyFailure = (detail) => new LegacyWorkspaceMigrationError(
1762
+ "MIGRATION_VERIFY_FAILED",
1763
+ `Legacy migration verification failed: ${detail}`
1764
+ );
1765
+ var corrupt = (message) => new LegacyWorkspaceMigrationError("MIGRATION_CORRUPT", message);
1766
+ var errorMessage = (error) => error instanceof Error ? error.message : String(error);
1767
+
1768
+ // src/relay.ts
1769
+ import path6 from "path";
1770
+ import { createNextHandler } from "@bunny-agent/daemon/nextjs";
1771
+ import WebSocket from "ws";
1772
+
1773
+ // src/execution-identity.ts
1774
+ import {
1775
+ createHash as createHash6,
1776
+ createPublicKey,
1777
+ timingSafeEqual,
1778
+ verify as verifySignature
1779
+ } from "crypto";
1780
+ var MAX_ISSUED_AT_FUTURE_SKEW_MS = 3e4;
1781
+ var ERROR_MESSAGES = {
1782
+ IDENTITY_INVALID: "The Connector execution identity is invalid.",
1783
+ IDENTITY_EXPIRED: "The Connector execution identity has expired or is not yet valid.",
1784
+ WORKSPACE_MISMATCH: "The Connector execution identity targets a different workspace.",
1785
+ REPLAY_REJECTED: "The Connector execution identity has already been used.",
1786
+ PROTOCOL_UNSUPPORTED: "The Connector execution identity protocol is unsupported."
1787
+ };
1788
+ var ConnectorRelayProtocolError = class extends Error {
1789
+ code;
1790
+ constructor(code) {
1791
+ super(ERROR_MESSAGES[code]);
1792
+ this.name = "ConnectorRelayProtocolError";
1793
+ this.code = code;
332
1794
  }
1795
+ };
1796
+ var ConnectorExecutionReplayGuard = class {
1797
+ nonces = /* @__PURE__ */ new Map();
1798
+ runIds = /* @__PURE__ */ new Map();
1799
+ assertUnusedAndRemember(identity, expiresAt, now) {
1800
+ this.prune(now);
1801
+ if (this.nonces.has(identity.nonce) || this.runIds.has(identity.runId)) {
1802
+ throw new ConnectorRelayProtocolError("REPLAY_REJECTED");
1803
+ }
1804
+ this.nonces.set(identity.nonce, expiresAt);
1805
+ this.runIds.set(identity.runId, expiresAt);
1806
+ }
1807
+ prune(now) {
1808
+ for (const [nonce, expiresAt] of this.nonces) {
1809
+ if (expiresAt <= now) this.nonces.delete(nonce);
1810
+ }
1811
+ for (const [runId, expiresAt] of this.runIds) {
1812
+ if (expiresAt <= now) this.runIds.delete(runId);
1813
+ }
1814
+ }
1815
+ };
1816
+ function canonicalizeConnectorExecutionIdentity(identity) {
1817
+ return Buffer.from(
1818
+ JSON.stringify([
1819
+ identity.version,
1820
+ identity.keyId,
1821
+ identity.connectorId,
1822
+ identity.spaceId,
1823
+ identity.agentId,
1824
+ identity.runId,
1825
+ identity.issuedAt,
1826
+ identity.expiresAt,
1827
+ identity.nonce,
1828
+ identity.requestDigest
1829
+ ]),
1830
+ "utf8"
1831
+ );
333
1832
  }
334
- function headersToRecord(headers) {
335
- const record = {};
336
- headers.forEach((value, key) => {
337
- record[key] = value;
338
- });
339
- return record;
1833
+ function verifyConnectorExecutionIdentity(options) {
1834
+ const now = options.now ?? Date.now();
1835
+ const protocol = assertSupportedProtocol(options.protocolVersion, options.protocol);
1836
+ const identity = parseExecutionIdentity(options.executionIdentity);
1837
+ if (identity.version !== CONNECTOR_RELAY_PROTOCOL_VERSION) {
1838
+ throw new ConnectorRelayProtocolError("PROTOCOL_UNSUPPORTED");
1839
+ }
1840
+ if (identity.keyId !== protocol.keyId) {
1841
+ throw new ConnectorRelayProtocolError("IDENTITY_INVALID");
1842
+ }
1843
+ if (identity.connectorId !== options.expectedConnectorId) {
1844
+ throw new ConnectorRelayProtocolError("WORKSPACE_MISMATCH");
1845
+ }
1846
+ const issuedAt = Date.parse(identity.issuedAt);
1847
+ const expiresAt = Date.parse(identity.expiresAt);
1848
+ if (!Number.isFinite(issuedAt) || !Number.isFinite(expiresAt) || expiresAt <= issuedAt) {
1849
+ throw new ConnectorRelayProtocolError("IDENTITY_INVALID");
1850
+ }
1851
+ if (issuedAt > now + MAX_ISSUED_AT_FUTURE_SKEW_MS || expiresAt <= now) {
1852
+ throw new ConnectorRelayProtocolError("IDENTITY_EXPIRED");
1853
+ }
1854
+ const signature = decodeSignature(identity.signature);
1855
+ let verified = false;
1856
+ try {
1857
+ const publicKey = createPublicKey({ key: protocol.publicKeyJwk, format: "jwk" });
1858
+ if (publicKey.asymmetricKeyType !== "ed25519") {
1859
+ throw new ConnectorRelayProtocolError("PROTOCOL_UNSUPPORTED");
1860
+ }
1861
+ verified = verifySignature(
1862
+ null,
1863
+ canonicalizeConnectorExecutionIdentity(identity),
1864
+ publicKey,
1865
+ signature
1866
+ );
1867
+ } catch (error) {
1868
+ if (error instanceof ConnectorRelayProtocolError) throw error;
1869
+ throw new ConnectorRelayProtocolError("PROTOCOL_UNSUPPORTED");
1870
+ }
1871
+ if (!verified) throw new ConnectorRelayProtocolError("IDENTITY_INVALID");
1872
+ const actualRequestDigest = digestConnectorRelayRequest(options.request);
1873
+ if (!safeDigestEqual(identity.requestDigest, actualRequestDigest)) {
1874
+ throw new ConnectorRelayProtocolError("IDENTITY_INVALID");
1875
+ }
1876
+ options.replayGuard.assertUnusedAndRemember(identity, expiresAt, now);
1877
+ return identity;
1878
+ }
1879
+ function assertSupportedProtocol(protocolVersion, protocol) {
1880
+ if (protocolVersion !== CONNECTOR_RELAY_PROTOCOL_VERSION || !protocol || protocol.version !== CONNECTOR_RELAY_PROTOCOL_VERSION || protocol.capability !== CONNECTOR_EXECUTION_IDENTITY_CAPABILITY || protocol.algorithm !== "Ed25519" || !isNonEmptyString(protocol.keyId) || !protocol.publicKeyJwk) {
1881
+ throw new ConnectorRelayProtocolError("PROTOCOL_UNSUPPORTED");
1882
+ }
1883
+ return protocol;
1884
+ }
1885
+ function parseExecutionIdentity(value) {
1886
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1887
+ throw new ConnectorRelayProtocolError("PROTOCOL_UNSUPPORTED");
1888
+ }
1889
+ const identity = value;
1890
+ const requiredStringFields = [
1891
+ "keyId",
1892
+ "connectorId",
1893
+ "spaceId",
1894
+ "agentId",
1895
+ "runId",
1896
+ "issuedAt",
1897
+ "expiresAt",
1898
+ "nonce",
1899
+ "requestDigest",
1900
+ "signature"
1901
+ ];
1902
+ if (requiredStringFields.some((field) => !isNonEmptyString(identity[field]))) {
1903
+ throw new ConnectorRelayProtocolError("IDENTITY_INVALID");
1904
+ }
1905
+ if (typeof identity.version !== "number") {
1906
+ throw new ConnectorRelayProtocolError("IDENTITY_INVALID");
1907
+ }
1908
+ return identity;
1909
+ }
1910
+ function digestConnectorRelayRequest(request) {
1911
+ const serialized = JSON.stringify(request);
1912
+ if (serialized === void 0) {
1913
+ throw new ConnectorRelayProtocolError("IDENTITY_INVALID");
1914
+ }
1915
+ return createHash6("sha256").update(serialized, "utf8").digest("base64url");
1916
+ }
1917
+ function safeDigestEqual(expected, actual) {
1918
+ const expectedBytes = Buffer.from(expected, "utf8");
1919
+ const actualBytes = Buffer.from(actual, "utf8");
1920
+ return expectedBytes.length === actualBytes.length && timingSafeEqual(expectedBytes, actualBytes);
1921
+ }
1922
+ function decodeSignature(value) {
1923
+ if (!/^[A-Za-z0-9_-]+$/.test(value)) {
1924
+ throw new ConnectorRelayProtocolError("IDENTITY_INVALID");
1925
+ }
1926
+ const signature = Buffer.from(value, "base64url");
1927
+ if (signature.length !== 64) {
1928
+ throw new ConnectorRelayProtocolError("IDENTITY_INVALID");
1929
+ }
1930
+ return signature;
340
1931
  }
341
- function errorMessage(error) {
342
- return error instanceof Error ? error.message : String(error);
1932
+ function isNonEmptyString(value) {
1933
+ return typeof value === "string" && value.trim().length > 0;
343
1934
  }
344
1935
 
1936
+ // src/workspace-volume-lifecycle.ts
1937
+ import { constants as constants3 } from "fs";
1938
+ import {
1939
+ copyFile as copyFile2,
1940
+ lstat as lstat2,
1941
+ mkdir as mkdir6,
1942
+ readdir as readdir3,
1943
+ readFile as readFile5,
1944
+ realpath as realpath2,
1945
+ rm as rm2,
1946
+ symlink as symlink2,
1947
+ writeFile as writeFile2
1948
+ } from "fs/promises";
1949
+ import path5 from "path";
1950
+ var MOUNT_DIRECTORY = "mount";
1951
+ var ARCHIVE_MARKER = "archived.json";
1952
+ var AGENT_CONTEXT_TEMPLATE_FILES = [
1953
+ "AGENTS.md",
1954
+ "SOUL.md",
1955
+ "IDENTITY.md",
1956
+ "USER.md",
1957
+ "TOOLS.md",
1958
+ "BOOTSTRAP.md",
1959
+ "MEMORY.md"
1960
+ ];
1961
+ var deriveSystemVolume = (descriptor) => path5.join(
1962
+ path5.dirname(path5.dirname(path5.dirname(path5.dirname(descriptor.sharedVolume)))),
1963
+ "system"
1964
+ );
1965
+ var WorkspaceVolumeLifecycle = class {
1966
+ constructor(descriptor) {
1967
+ this.descriptor = descriptor;
1968
+ this.systemVolume = deriveSystemVolume(descriptor);
1969
+ }
1970
+ systemVolume;
1971
+ async mount() {
1972
+ await this.ensureVolumes();
1973
+ await this.bootstrapAgentTemplates((await readBundledManifest()).sharedDir);
1974
+ const mountRoot = path5.join(this.descriptor.stateVolume, MOUNT_DIRECTORY);
1975
+ await mkdir6(mountRoot, { recursive: true, mode: 448 });
1976
+ const mounts = {
1977
+ mountRoot,
1978
+ agent: path5.join(mountRoot, "agent"),
1979
+ space: path5.join(mountRoot, "space"),
1980
+ system: path5.join(mountRoot, "system"),
1981
+ temporary: path5.join(mountRoot, "tmp")
1982
+ };
1983
+ await Promise.all([
1984
+ ensureVolumeLink(this.descriptor.privateVolume, mounts.agent),
1985
+ ensureVolumeLink(this.descriptor.sharedVolume, mounts.space),
1986
+ ensureVolumeLink(this.systemVolume, mounts.system),
1987
+ ensureVolumeLink(this.descriptor.temporaryVolume, mounts.temporary)
1988
+ ]);
1989
+ const daemonVolumes = path5.join(mountRoot, "volumes");
1990
+ await mkdir6(daemonVolumes, { recursive: true, mode: 448 });
1991
+ await Promise.all([
1992
+ ensureVolumeLink(this.descriptor.privateVolume, path5.join(daemonVolumes, "agent")),
1993
+ ensureVolumeLink(this.descriptor.sharedVolume, path5.join(daemonVolumes, "space")),
1994
+ ensureVolumeLink(this.systemVolume, path5.join(daemonVolumes, "system")),
1995
+ ensureVolumeLink(this.descriptor.temporaryVolume, path5.join(daemonVolumes, "tmp"))
1996
+ ]);
1997
+ return mounts;
1998
+ }
1999
+ async unmount() {
2000
+ await rm2(path5.join(this.descriptor.stateVolume, MOUNT_DIRECTORY), {
2001
+ recursive: true,
2002
+ force: true
2003
+ });
2004
+ }
2005
+ async archiveAgent() {
2006
+ await this.unmount();
2007
+ await mkdir6(this.descriptor.stateVolume, { recursive: true, mode: 448 });
2008
+ await writeFile2(
2009
+ path5.join(this.descriptor.stateVolume, ARCHIVE_MARKER),
2010
+ `${JSON.stringify({ archivedAt: (/* @__PURE__ */ new Date()).toISOString() })}
2011
+ `,
2012
+ { encoding: "utf8", mode: 384 }
2013
+ );
2014
+ }
2015
+ async restoreAgent() {
2016
+ await rm2(path5.join(this.descriptor.stateVolume, ARCHIVE_MARKER), { force: true });
2017
+ return this.mount();
2018
+ }
2019
+ async deleteAgent(options) {
2020
+ requireConfirmation(options);
2021
+ await this.unmount();
2022
+ const agentRoot = path5.dirname(this.descriptor.privateVolume);
2023
+ await rm2(agentRoot, { recursive: true, force: true });
2024
+ }
2025
+ async archiveSpace() {
2026
+ await mkdir6(this.descriptor.sharedVolume, { recursive: true, mode: 448 });
2027
+ await writeFile2(
2028
+ path5.join(this.descriptor.sharedVolume, ARCHIVE_MARKER),
2029
+ `${JSON.stringify({ archivedAt: (/* @__PURE__ */ new Date()).toISOString() })}
2030
+ `,
2031
+ { encoding: "utf8", mode: 384 }
2032
+ );
2033
+ }
2034
+ async restoreSpace() {
2035
+ await rm2(path5.join(this.descriptor.sharedVolume, ARCHIVE_MARKER), { force: true });
2036
+ }
2037
+ async deleteSpace(options) {
2038
+ requireConfirmation(options);
2039
+ await this.unmount();
2040
+ await rm2(this.descriptor.sharedVolume, { recursive: true, force: true });
2041
+ }
2042
+ async bootstrapAgentTemplates(templateDirectory) {
2043
+ await mkdir6(this.descriptor.privateVolume, { recursive: true, mode: 448 });
2044
+ const bootstrapped = [];
2045
+ const skipped = [];
2046
+ const entries = new Set(
2047
+ (await readdir3(templateDirectory, { withFileTypes: true }).catch(() => [])).filter((entry) => entry.isFile()).map((entry) => entry.name)
2048
+ );
2049
+ for (const filename of AGENT_CONTEXT_TEMPLATE_FILES) {
2050
+ if (!entries.has(filename)) continue;
2051
+ const destination = path5.join(this.descriptor.privateVolume, filename);
2052
+ try {
2053
+ await lstat2(destination);
2054
+ skipped.push(filename);
2055
+ } catch (error) {
2056
+ if (!isMissingFile(error)) throw error;
2057
+ try {
2058
+ await copyFile2(
2059
+ path5.join(templateDirectory, filename),
2060
+ destination,
2061
+ constants3.COPYFILE_EXCL
2062
+ );
2063
+ bootstrapped.push(filename);
2064
+ } catch (copyError) {
2065
+ if (!isAlreadyExists3(copyError)) throw copyError;
2066
+ skipped.push(filename);
2067
+ }
2068
+ }
2069
+ }
2070
+ return { bootstrapped, skipped };
2071
+ }
2072
+ async ensureVolumes() {
2073
+ for (const volume of [
2074
+ this.descriptor.privateVolume,
2075
+ this.descriptor.sharedVolume,
2076
+ this.descriptor.stateVolume,
2077
+ this.descriptor.temporaryVolume,
2078
+ this.systemVolume
2079
+ ]) {
2080
+ await mkdir6(volume, { recursive: true, mode: 448 });
2081
+ const info = await lstat2(volume);
2082
+ if (info.isSymbolicLink() || !info.isDirectory() || await realpath2(volume) !== volume) {
2083
+ throw new Error("Workspace volume must be a canonical real directory");
2084
+ }
2085
+ }
2086
+ }
2087
+ };
2088
+ var requireConfirmation = (options) => {
2089
+ if (options.confirmed !== true) throw new Error("Workspace deletion requires confirmation");
2090
+ };
2091
+ var isMissingFile = (error) => error instanceof Error && "code" in error && error.code === "ENOENT";
2092
+ var ensureVolumeLink = async (target, linkPath) => {
2093
+ try {
2094
+ const info = await lstat2(linkPath);
2095
+ if (!info.isSymbolicLink() || await realpath2(linkPath) !== target) {
2096
+ throw new Error("Workspace mount link does not match its canonical volume");
2097
+ }
2098
+ } catch (error) {
2099
+ if (!isMissingFile(error)) throw error;
2100
+ try {
2101
+ await symlink2(target, linkPath, "dir");
2102
+ } catch (createError) {
2103
+ if (!isAlreadyExists3(createError)) throw createError;
2104
+ const info = await lstat2(linkPath);
2105
+ if (!info.isSymbolicLink() || await realpath2(linkPath) !== target) {
2106
+ throw new Error("Workspace mount link raced with an invalid target");
2107
+ }
2108
+ }
2109
+ }
2110
+ };
2111
+ var isAlreadyExists3 = (error) => error instanceof Error && "code" in error && error.code === "EEXIST";
2112
+
345
2113
  // src/relay.ts
346
- import WebSocket from "ws";
347
- function startConnectorRelay(config, bunnyDaemon, launchConfig) {
2114
+ var RELAY_PING_INTERVAL_MS = 2e4;
2115
+ var RELAY_PONG_TIMEOUT_MS = 1e4;
2116
+ var RELAY_RECONNECT_MAX_MS = 3e4;
2117
+ var RELAY_RECONNECT_JITTER = 0.2;
2118
+ var RELAY_FAST_RECONNECT_MS = 500;
2119
+ var CONNECTOR_SOCKET_REPLACED_CLOSE_CODE = 4001;
2120
+ var CONNECTOR_SOCKET_REVOKED_CLOSE_CODE = 4003;
2121
+ var shouldReconnectConnectorRelay = (closeCode) => closeCode !== CONNECTOR_SOCKET_REPLACED_CLOSE_CODE && closeCode !== CONNECTOR_SOCKET_REVOKED_CLOSE_CODE;
2122
+ var getConnectorRelayReconnectBaseDelay = (hadOpenConnection, backoffMs) => hadOpenConnection ? RELAY_FAST_RECONNECT_MS : backoffMs;
2123
+ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegistry, launchConfig) {
348
2124
  let socket = null;
349
2125
  let reconnectTimer = null;
2126
+ let pingTimer = null;
2127
+ let pongTimer = null;
350
2128
  let stopped = false;
351
- const handler = createRelayHandler(bunnyDaemon);
2129
+ let reconnectDelayMs = RELAY_RECONNECT_INTERVAL_MS;
2130
+ let hadOpenConnection = false;
2131
+ let activeRelayProtocol = relayProtocol;
2132
+ const replayGuard = new ConnectorExecutionReplayGuard();
2133
+ const clearTimers = () => {
2134
+ if (pingTimer) {
2135
+ clearInterval(pingTimer);
2136
+ pingTimer = null;
2137
+ }
2138
+ if (pongTimer) {
2139
+ clearTimeout(pongTimer);
2140
+ pongTimer = null;
2141
+ }
2142
+ };
2143
+ const nextReconnectDelay = () => {
2144
+ const reconnectingEstablishedSocket = hadOpenConnection;
2145
+ const base = getConnectorRelayReconnectBaseDelay(
2146
+ reconnectingEstablishedSocket,
2147
+ reconnectDelayMs
2148
+ );
2149
+ hadOpenConnection = false;
2150
+ reconnectDelayMs = reconnectingEstablishedSocket ? RELAY_RECONNECT_INTERVAL_MS : Math.min(reconnectDelayMs * 2, RELAY_RECONNECT_MAX_MS);
2151
+ const jitter = base * RELAY_RECONNECT_JITTER * (Math.random() * 2 - 1);
2152
+ return Math.max(reconnectingEstablishedSocket ? 250 : 500, Math.round(base + jitter));
2153
+ };
2154
+ const scheduleReconnect = (reason) => {
2155
+ if (stopped || reconnectTimer) return;
2156
+ const delay = nextReconnectDelay();
2157
+ void writeLog("info", `relay reconnecting in ${delay}ms (${reason})`);
2158
+ clearTimers();
2159
+ reconnectTimer = setTimeout(() => {
2160
+ reconnectTimer = null;
2161
+ try {
2162
+ connect();
2163
+ } catch (error) {
2164
+ void writeLog(
2165
+ "warn",
2166
+ `relay reconnect threw: ${error instanceof Error ? error.message : error}`
2167
+ );
2168
+ scheduleReconnect("connect-throw");
2169
+ }
2170
+ }, delay);
2171
+ };
352
2172
  const connect = () => {
353
2173
  if (stopped) return;
354
2174
  const relayUrl = getConnectorRelayUrl(config.serverUrl, config.connectorId);
355
- const token = launchConfig?.oauthToken?.trim();
356
- socket = new WebSocket(relayUrl, {
357
- headers: token ? { Authorization: `Bearer ${token}` } : void 0
2175
+ const oauthToken = launchConfig?.oauthToken?.trim();
2176
+ const token = config.token?.trim() || getConnectorToken(launchConfig);
2177
+ const headers = {};
2178
+ if (oauthToken) headers.Authorization = `Bearer ${oauthToken}`;
2179
+ if (token) headers["X-Buda-Connector-Token"] = token;
2180
+ headers["X-Buda-Connector-Id"] = config.connectorId;
2181
+ if (config.deviceToken) headers["X-Buda-Connector-Device-Token"] = config.deviceToken;
2182
+ const ws = new WebSocket(relayUrl, {
2183
+ headers: Object.keys(headers).length > 0 ? headers : void 0
358
2184
  });
359
- socket.on("open", () => {
2185
+ socket = ws;
2186
+ const inflight = /* @__PURE__ */ new Map();
2187
+ let detached = false;
2188
+ const detach = () => {
2189
+ if (detached) return;
2190
+ detached = true;
2191
+ ws.removeAllListeners();
2192
+ for (const controller of inflight.values()) controller.abort();
2193
+ inflight.clear();
2194
+ };
2195
+ ws.on("open", () => {
360
2196
  void writeLog("info", `relay connected ${relayUrl}`);
2197
+ hadOpenConnection = true;
2198
+ reconnectDelayMs = RELAY_RECONNECT_INTERVAL_MS;
2199
+ pingTimer = setInterval(() => {
2200
+ if (ws.readyState !== WebSocket.OPEN) return;
2201
+ try {
2202
+ ws.ping();
2203
+ } catch (error) {
2204
+ void writeLog(
2205
+ "warn",
2206
+ `relay ping failed: ${error instanceof Error ? error.message : error}`
2207
+ );
2208
+ ws.terminate();
2209
+ return;
2210
+ }
2211
+ if (pongTimer) clearTimeout(pongTimer);
2212
+ pongTimer = setTimeout(() => {
2213
+ void writeLog("warn", "relay pong timeout, terminating");
2214
+ ws.terminate();
2215
+ }, RELAY_PONG_TIMEOUT_MS);
2216
+ }, RELAY_PING_INTERVAL_MS);
2217
+ });
2218
+ ws.on("pong", () => {
2219
+ if (pongTimer) {
2220
+ clearTimeout(pongTimer);
2221
+ pongTimer = null;
2222
+ }
2223
+ });
2224
+ ws.on("message", (raw) => {
2225
+ void handleRelayMessage(
2226
+ ws,
2227
+ bunnyDaemon,
2228
+ raw.toString(),
2229
+ inflight,
2230
+ config.connectorId,
2231
+ activeRelayProtocol,
2232
+ replayGuard,
2233
+ workspaceRegistry
2234
+ );
361
2235
  });
362
- attachRelayClient(socket, handler, { chunkEncoding: "utf8" });
363
- socket.on("close", () => {
2236
+ ws.on("close", (code) => {
2237
+ detach();
2238
+ if (!shouldReconnectConnectorRelay(code)) {
2239
+ stopped = true;
2240
+ void writeLog(
2241
+ "warn",
2242
+ code === CONNECTOR_SOCKET_REPLACED_CLOSE_CODE ? "relay replaced by another process for this connector" : "relay access revoked; restore access and restart the Connector to reconnect"
2243
+ );
2244
+ return;
2245
+ }
364
2246
  void writeLog("warn", "relay disconnected");
365
- if (stopped) return;
366
- reconnectTimer = setTimeout(connect, RELAY_RECONNECT_INTERVAL_MS);
2247
+ scheduleReconnect("close");
367
2248
  });
368
- socket.on("error", (error) => {
2249
+ ws.on("error", (error) => {
369
2250
  void writeLog("warn", `relay error: ${error instanceof Error ? error.message : error}`);
2251
+ detach();
2252
+ scheduleReconnect("error");
370
2253
  });
371
2254
  };
372
- connect();
2255
+ try {
2256
+ connect();
2257
+ } catch (error) {
2258
+ void writeLog(
2259
+ "warn",
2260
+ `relay initial connect threw: ${error instanceof Error ? error.message : error}`
2261
+ );
2262
+ scheduleReconnect("connect-throw");
2263
+ }
373
2264
  return {
2265
+ updateRelayProtocol(nextRelayProtocol) {
2266
+ const previousKeyId = activeRelayProtocol?.keyId;
2267
+ activeRelayProtocol = nextRelayProtocol;
2268
+ if (nextRelayProtocol?.keyId && nextRelayProtocol.keyId !== previousKeyId) {
2269
+ void writeLog("info", `relay protocol key updated ${nextRelayProtocol.keyId}`);
2270
+ }
2271
+ },
374
2272
  stop() {
375
2273
  stopped = true;
376
- if (reconnectTimer) clearTimeout(reconnectTimer);
2274
+ if (reconnectTimer) {
2275
+ clearTimeout(reconnectTimer);
2276
+ reconnectTimer = null;
2277
+ }
2278
+ clearTimers();
377
2279
  socket?.close();
378
2280
  }
379
2281
  };
380
2282
  }
381
- function createRelayHandler(bunnyDaemon) {
382
- const allowedOrigin = new URL(bunnyDaemon.baseUrl).origin;
383
- return async (request) => {
384
- const requestUrl = new URL(request.path, bunnyDaemon.baseUrl);
385
- if (requestUrl.origin !== allowedOrigin) {
386
- throw new Error("Relay request must target the embedded bunny daemon.");
387
- }
388
- await writeLog("info", `relay request ${request.method} ${requestUrl.pathname}`, {
389
- actorEmail: getHeaderValue(request.headers, "x-buda-relay-actor-email") || null,
390
- actorId: getHeaderValue(request.headers, "x-buda-relay-actor-id") || null,
391
- source: getHeaderValue(request.headers, "x-buda-relay-source") || "unknown",
392
- action: getHeaderValue(request.headers, "x-buda-relay-action") || "unknown",
393
- target: requestUrl.toString()
2283
+ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedConnectorId, relayProtocol, replayGuard, workspaceRegistry) {
2284
+ if (!socket || socket.readyState !== WebSocket.OPEN) return;
2285
+ let message;
2286
+ try {
2287
+ message = JSON.parse(raw);
2288
+ } catch {
2289
+ await writeLog("warn", "relay received invalid json");
2290
+ return;
2291
+ }
2292
+ if (message.type === "abort") {
2293
+ const controller2 = inflight.get(message.requestId);
2294
+ if (controller2) {
2295
+ void writeLog("info", `relay abort ${message.requestId}`);
2296
+ controller2.abort();
2297
+ }
2298
+ return;
2299
+ }
2300
+ if (message.type === "event") {
2301
+ await writeLog("warn", "relay rejected unsigned browser input event");
2302
+ return;
2303
+ }
2304
+ if (message.type !== "request") return;
2305
+ const hasProtocolVersion = message.protocolVersion !== void 0;
2306
+ const hasExecutionIdentity = message.executionIdentity !== void 0;
2307
+ let executionIdentity;
2308
+ if (hasProtocolVersion || hasExecutionIdentity) {
2309
+ try {
2310
+ verifyConnectorExecutionIdentity({
2311
+ protocolVersion: message.protocolVersion,
2312
+ executionIdentity: message.executionIdentity,
2313
+ request: message.request,
2314
+ protocol: relayProtocol,
2315
+ expectedConnectorId,
2316
+ replayGuard
2317
+ });
2318
+ executionIdentity = message.executionIdentity;
2319
+ } catch (error) {
2320
+ sendRelayError(socket, message.requestId, error);
2321
+ return;
2322
+ }
2323
+ }
2324
+ const controller = new AbortController();
2325
+ inflight.set(message.requestId, controller);
2326
+ try {
2327
+ const requestPath = message.request.path;
2328
+ const isBrowserRequest = requestPath.startsWith("/browser/") || requestPath === "/browser";
2329
+ const isWorkspaceDescriptorRequest = requestPath === "/workspace/descriptor";
2330
+ const isAgentRequest = isBrowserRequest || requestPath.startsWith("/api/") || isWorkspaceDescriptorRequest;
2331
+ if (isAgentRequest && (!executionIdentity || !workspaceRegistry)) {
2332
+ throw new ConnectorRelayProtocolError("IDENTITY_INVALID");
2333
+ }
2334
+ const descriptor = executionIdentity ? await workspaceRegistry?.resolve(executionIdentity.spaceId, executionIdentity.agentId) : void 0;
2335
+ if (descriptor && (descriptor.spaceId !== executionIdentity?.spaceId || descriptor.agentId !== executionIdentity.agentId)) {
2336
+ throw new ConnectorRelayProtocolError("WORKSPACE_MISMATCH");
2337
+ }
2338
+ const mounts = descriptor ? await new WorkspaceVolumeLifecycle(descriptor).mount() : void 0;
2339
+ let response;
2340
+ if (isWorkspaceDescriptorRequest && descriptor) {
2341
+ response = Response.json({
2342
+ ok: true,
2343
+ data: {
2344
+ workspaceKey: descriptor.workspaceKey,
2345
+ workspacePath: descriptor.privateVolume,
2346
+ sharedPath: descriptor.sharedVolume,
2347
+ systemPath: deriveSystemVolume(descriptor),
2348
+ isolationMode: descriptor.isolationMode
2349
+ }
2350
+ });
2351
+ } else if (isBrowserRequest) {
2352
+ await writeLog("info", `relay browser ${message.request.method} ${requestPath}`);
2353
+ response = await handleBrowserRequest(
2354
+ requestPath,
2355
+ message.request.method,
2356
+ message.request.body,
2357
+ {
2358
+ sessionId: `${descriptor?.workspaceKey}:${getHeaderValue(message.request.headers, "x-buda-browser-session-id") ?? "default"}`,
2359
+ workdirRoot: mounts?.mountRoot
2360
+ }
2361
+ );
2362
+ } else if (descriptor && mounts) {
2363
+ response = await handleWorkspaceDaemonRequest(message.request, mounts, controller.signal);
2364
+ } else {
2365
+ const requestUrl = new URL(requestPath, bunnyDaemon.baseUrl);
2366
+ if (requestUrl.origin !== new URL(bunnyDaemon.baseUrl).origin) {
2367
+ throw new Error("Relay request must target the embedded bunny daemon.");
2368
+ }
2369
+ await writeLog("info", `relay request ${message.request.method} ${requestUrl.pathname}`, {
2370
+ actorEmail: getHeaderValue(message.request.headers, "x-buda-relay-actor-email") || null,
2371
+ actorId: getHeaderValue(message.request.headers, "x-buda-relay-actor-id") || null,
2372
+ source: getHeaderValue(message.request.headers, "x-buda-relay-source") || "unknown",
2373
+ action: getHeaderValue(message.request.headers, "x-buda-relay-action") || "unknown",
2374
+ target: requestUrl.toString()
2375
+ });
2376
+ response = await fetch(requestUrl, {
2377
+ method: message.request.method,
2378
+ headers: {
2379
+ "Content-Type": "application/json",
2380
+ ...message.request.headers ?? {}
2381
+ },
2382
+ body: message.request.body === void 0 ? void 0 : JSON.stringify(message.request.body),
2383
+ signal: controller.signal
2384
+ });
2385
+ }
2386
+ sendRelayMessage(socket, {
2387
+ type: "start",
2388
+ requestId: message.requestId,
2389
+ status: response.status,
2390
+ headers: headersToRecord(response.headers)
394
2391
  });
395
- return fetch(requestUrl, {
396
- method: request.method,
397
- headers: {
398
- "Content-Type": "application/json",
399
- ...request.headers ?? {}
400
- },
401
- body: request.body === void 0 ? void 0 : JSON.stringify(request.body)
2392
+ if (response.body) {
2393
+ const reader = response.body.getReader();
2394
+ const decoder = new TextDecoder();
2395
+ const cancelReader = () => {
2396
+ void reader.cancel().catch(() => {
2397
+ });
2398
+ };
2399
+ controller.signal.addEventListener("abort", cancelReader, { once: true });
2400
+ if (controller.signal.aborted) cancelReader();
2401
+ try {
2402
+ while (true) {
2403
+ const { value, done } = await reader.read();
2404
+ if (done) break;
2405
+ sendRelayMessage(socket, {
2406
+ type: "chunk",
2407
+ requestId: message.requestId,
2408
+ data: decoder.decode(value, { stream: true })
2409
+ });
2410
+ }
2411
+ const tail = decoder.decode();
2412
+ if (tail) {
2413
+ sendRelayMessage(socket, {
2414
+ type: "chunk",
2415
+ requestId: message.requestId,
2416
+ data: tail
2417
+ });
2418
+ }
2419
+ } finally {
2420
+ controller.signal.removeEventListener("abort", cancelReader);
2421
+ reader.releaseLock();
2422
+ }
2423
+ }
2424
+ sendRelayMessage(socket, {
2425
+ type: "end",
2426
+ requestId: message.requestId
402
2427
  });
403
- };
2428
+ } catch (error) {
2429
+ sendRelayError(socket, message.requestId, error);
2430
+ } finally {
2431
+ inflight.delete(message.requestId);
2432
+ }
2433
+ }
2434
+ var handleWorkspaceDaemonRequest = async (request, mounts, signal) => {
2435
+ const normalized = normalizeWorkspaceRequest(request, mounts);
2436
+ const handler = createNextHandler({ root: mounts.mountRoot });
2437
+ return handler(
2438
+ new Request(new URL(normalized.path, "http://connector.workspace"), {
2439
+ method: normalized.method,
2440
+ headers: normalized.headers,
2441
+ body: normalized.body === void 0 ? void 0 : JSON.stringify(normalized.body),
2442
+ signal
2443
+ })
2444
+ );
2445
+ };
2446
+ var normalizeWorkspaceRequest = (request, mounts) => {
2447
+ if (request.path !== "/api/coding/run") return request;
2448
+ if (!request.body || typeof request.body !== "object" || Array.isArray(request.body)) {
2449
+ throw new ConnectorRelayProtocolError("WORKSPACE_MISMATCH");
2450
+ }
2451
+ const body = request.body;
2452
+ const cwd = typeof body.cwd === "string" ? body.cwd : "/agent";
2453
+ const normalized = path6.posix.normalize(cwd);
2454
+ 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)] : [];
2455
+ if (!mount || relative === void 0) {
2456
+ throw new ConnectorRelayProtocolError("WORKSPACE_MISMATCH");
2457
+ }
2458
+ return { ...request, body: { ...body, cwd: path6.join(mount, relative) } };
2459
+ };
2460
+ function sendRelayError(socket, requestId, error) {
2461
+ sendRelayMessage(socket, {
2462
+ type: "error",
2463
+ requestId,
2464
+ error: error instanceof Error ? error.message : String(error),
2465
+ ...error instanceof ConnectorRelayProtocolError ? { code: error.code } : {}
2466
+ });
2467
+ }
2468
+ function sendRelayMessage(socket, message) {
2469
+ if (socket.readyState !== WebSocket.OPEN) return;
2470
+ socket.send(JSON.stringify(message));
2471
+ }
2472
+ function headersToRecord(headers) {
2473
+ const result = {};
2474
+ headers.forEach((value, key) => {
2475
+ result[key] = value;
2476
+ });
2477
+ return result;
404
2478
  }
405
2479
  function getHeaderValue(headers, name) {
406
2480
  if (!headers) return void 0;
@@ -411,30 +2485,460 @@ function getHeaderValue(headers, name) {
411
2485
  return void 0;
412
2486
  }
413
2487
 
414
- // src/runtime.ts
415
- async function runNewConnector(launchConfig) {
416
- const config = buildDeviceConfig(launchConfig);
417
- await writeDeviceConfig(config);
418
- await registerConnector(config, launchConfig);
419
- await writeLog("info", `wrote ${CONFIG_PATH}`);
420
- await writeLog("info", `workdir root ${config.workdirRoot}`);
421
- await writeLog("info", `log file ${LOG_PATH}`);
2488
+ // src/workdir-layout.ts
2489
+ import { lstat as lstat3, mkdir as mkdir7, symlink as symlink3, unlink as unlink4 } from "fs/promises";
2490
+ import path7 from "path";
2491
+ async function provisionWorkdirLayout(workdirRoot) {
2492
+ const manifest = await readBundledManifest();
2493
+ await mkdir7(workdirRoot, { recursive: true });
2494
+ await mkdir7(path7.join(workdirRoot, "system", ".agents"), { recursive: true });
2495
+ const systemSkillsLink = path7.join(workdirRoot, "system", ".agents", "skills");
2496
+ const systemScriptsLink = path7.join(workdirRoot, "system", "scripts");
2497
+ await ensureSymlink(manifest.systemSkillsDir, systemSkillsLink);
2498
+ await ensureSymlink(manifest.scriptsDir, systemScriptsLink);
2499
+ await writeLog("info", "workdir layout provisioned", {
2500
+ workdirRoot,
2501
+ systemSkillsLink,
2502
+ systemScriptsLink,
2503
+ bundleVersion: manifest.version
2504
+ });
2505
+ return {
2506
+ workdirRoot,
2507
+ systemSkillsLink,
2508
+ systemScriptsLink,
2509
+ bundleVersion: manifest.version
2510
+ };
422
2511
  }
423
- async function runConnectorDaemon(launchConfig) {
424
- const config = await ensureDeviceConfig(launchConfig);
425
- const bunnyDaemon = await startEmbeddedBunnyDaemon(config, launchConfig);
2512
+ async function ensureSymlink(target, linkPath) {
426
2513
  try {
427
- await registerConnector(config, launchConfig, bunnyDaemon);
2514
+ const existing = await lstat3(linkPath);
2515
+ if (existing.isSymbolicLink()) {
2516
+ await unlink4(linkPath);
2517
+ } else if (existing.isDirectory()) {
2518
+ await writeLog(
2519
+ "warn",
2520
+ `skipping symlink ${linkPath}: existing directory at path; remove it to enable bundle`
2521
+ );
2522
+ return;
2523
+ } else {
2524
+ await unlink4(linkPath);
2525
+ }
2526
+ } catch {
2527
+ }
2528
+ await mkdir7(path7.dirname(linkPath), { recursive: true });
2529
+ await symlink3(target, linkPath, "dir");
2530
+ }
2531
+
2532
+ // src/workspace-registry.ts
2533
+ import { constants as constants4 } from "fs";
2534
+ import {
2535
+ chmod as chmod3,
2536
+ lstat as lstat4,
2537
+ mkdir as mkdir8,
2538
+ open as open3,
2539
+ readFile as readFile6,
2540
+ realpath as realpath3,
2541
+ rename as rename3,
2542
+ statfs,
2543
+ unlink as unlink5
2544
+ } from "fs/promises";
2545
+ import path8 from "path";
2546
+ var REGISTRY_VERSION = 1;
2547
+ var REGISTRY_DIRECTORY = ".buda";
2548
+ var REGISTRY_FILENAME = "workspace-registry.json";
2549
+ var IMMUTABLE_ID_PATTERN2 = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
2550
+ var UNSAFE_FILESYSTEM_TYPES = /* @__PURE__ */ new Set([26985, 20859, 4283649346, 4266872130]);
2551
+ var WorkspaceRegistryError = class extends Error {
2552
+ constructor(code, message) {
2553
+ super(message);
2554
+ this.code = code;
2555
+ this.name = "WorkspaceRegistryError";
2556
+ }
2557
+ recoverable = true;
2558
+ };
2559
+ var LocalWorkspaceRegistry = class _LocalWorkspaceRegistry {
2560
+ constructor(registryPath, storageRoot, document) {
2561
+ this.registryPath = registryPath;
2562
+ this.storageRoot = storageRoot;
2563
+ this.document = document;
2564
+ }
2565
+ static async open(storageRoot) {
2566
+ await mkdir8(storageRoot, { recursive: true, mode: 448 });
2567
+ await rejectUnsafeFilesystem(storageRoot);
2568
+ const canonicalRoot = await canonicalDirectory(storageRoot, "storage root");
2569
+ const registryDirectory = path8.join(canonicalRoot, REGISTRY_DIRECTORY);
2570
+ await ensurePrivateDirectory2(registryDirectory);
2571
+ const registryPath = path8.join(registryDirectory, REGISTRY_FILENAME);
2572
+ const document = await withExclusiveFileLock(registryPath, async () => {
2573
+ let current;
2574
+ try {
2575
+ current = parseRegistry(await readFile6(registryPath, "utf8"));
2576
+ } catch (error) {
2577
+ if (isMissingFile2(error)) {
2578
+ current = { version: REGISTRY_VERSION, storageRoot: canonicalRoot, workspaces: {} };
2579
+ await writeRegistryAtomically(registryPath, current);
2580
+ } else if (error instanceof WorkspaceRegistryError) {
2581
+ throw error;
2582
+ } else {
2583
+ throw corrupt2(`Cannot read local workspace registry: ${errorMessage2(error)}`);
2584
+ }
2585
+ }
2586
+ if (current.storageRoot !== canonicalRoot) {
2587
+ throw corrupt2("Registry storage root does not match the selected storage root");
2588
+ }
2589
+ await chmod3(registryPath, 384);
2590
+ await reconcileDocument(current, canonicalRoot);
2591
+ return current;
2592
+ });
2593
+ await chmod3(registryPath, 384);
2594
+ return new _LocalWorkspaceRegistry(registryPath, canonicalRoot, document);
2595
+ }
2596
+ async resolve(spaceId, agentId) {
2597
+ validateImmutableId("spaceId", spaceId);
2598
+ validateImmutableId("agentId", agentId);
2599
+ return withExclusiveFileLock(this.registryPath, async () => {
2600
+ const document = await readRegistryDocument(this.registryPath);
2601
+ if (document.storageRoot !== this.storageRoot) {
2602
+ throw corrupt2("Registry storage root does not match the selected storage root");
2603
+ }
2604
+ await reconcileDocument(document, this.storageRoot);
2605
+ const expected = deriveWorkspace(this.storageRoot, spaceId, agentId);
2606
+ const existing = document.workspaces[expected.workspaceKey];
2607
+ if (existing) {
2608
+ assertWorkspaceMatches(existing, expected);
2609
+ await reconcileWorkspace(existing, this.storageRoot);
2610
+ this.document = document;
2611
+ return toDescriptor(existing);
2612
+ }
2613
+ assertNoCollision(document.workspaces, expected);
2614
+ await Promise.all([
2615
+ ensurePrivateDirectory2(expected.privateVolume),
2616
+ ensurePrivateDirectory2(expected.sharedVolume),
2617
+ ensurePrivateDirectory2(expected.stateVolume),
2618
+ ensurePrivateDirectory2(expected.temporaryVolume)
2619
+ ]);
2620
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2621
+ const workspace = { ...expected, createdAt: now, verifiedAt: now };
2622
+ document.workspaces[workspace.workspaceKey] = workspace;
2623
+ await writeRegistryAtomically(this.registryPath, document);
2624
+ this.document = document;
2625
+ return toDescriptor(workspace);
2626
+ });
2627
+ }
2628
+ sanitizedMetadata() {
2629
+ return Object.values(this.document.workspaces).map((workspace) => ({
2630
+ workspaceKey: workspace.workspaceKey,
2631
+ spaceId: workspace.spaceId,
2632
+ agentId: workspace.agentId,
2633
+ isolationMode: "limited",
2634
+ verifiedAt: workspace.verifiedAt
2635
+ }));
2636
+ }
2637
+ };
2638
+ var readRegistryDocument = async (registryPath) => {
2639
+ try {
2640
+ return parseRegistry(await readFile6(registryPath, "utf8"));
428
2641
  } catch (error) {
429
- await stopEmbeddedBunnyDaemon(bunnyDaemon);
2642
+ if (error instanceof WorkspaceRegistryError) throw error;
2643
+ throw corrupt2(`Cannot read local workspace registry: ${errorMessage2(error)}`);
2644
+ }
2645
+ };
2646
+ var deriveWorkspace = (storageRoot, spaceId, agentId) => {
2647
+ const spaceRoot = path8.join(storageRoot, REGISTRY_DIRECTORY, "spaces", spaceId);
2648
+ const agentRoot = path8.join(spaceRoot, "agents", agentId);
2649
+ return {
2650
+ workspaceKey: `${spaceId}:${agentId}`,
2651
+ spaceId,
2652
+ agentId,
2653
+ privateVolume: path8.join(agentRoot, "workspace"),
2654
+ sharedVolume: path8.join(spaceRoot, "shared"),
2655
+ stateVolume: path8.join(agentRoot, "state"),
2656
+ temporaryVolume: path8.join(agentRoot, "tmp")
2657
+ };
2658
+ };
2659
+ var parseRegistry = (raw) => {
2660
+ let value;
2661
+ try {
2662
+ value = JSON.parse(raw);
2663
+ } catch {
2664
+ throw corrupt2("Local workspace registry contains invalid JSON");
2665
+ }
2666
+ if (!isRecord(value) || value.version !== REGISTRY_VERSION) {
2667
+ throw corrupt2("Local workspace registry has an unsupported version");
2668
+ }
2669
+ if (typeof value.storageRoot !== "string" || !isRecord(value.workspaces)) {
2670
+ throw corrupt2("Local workspace registry is missing required fields");
2671
+ }
2672
+ for (const workspace of Object.values(value.workspaces)) {
2673
+ if (!isRegistryWorkspace(workspace)) {
2674
+ throw corrupt2("Local workspace registry contains a malformed workspace entry");
2675
+ }
2676
+ }
2677
+ return value;
2678
+ };
2679
+ var reconcileDocument = async (document, storageRoot) => {
2680
+ const seenPrivateVolumes = /* @__PURE__ */ new Set();
2681
+ for (const [key, workspace] of Object.entries(document.workspaces)) {
2682
+ validateStoredId("spaceId", workspace.spaceId);
2683
+ validateStoredId("agentId", workspace.agentId);
2684
+ const expected = deriveWorkspace(storageRoot, workspace.spaceId, workspace.agentId);
2685
+ if (key !== workspace.workspaceKey) throw corrupt2("Workspace registry key is inconsistent");
2686
+ if (seenPrivateVolumes.has(workspace.privateVolume)) {
2687
+ throw collision("Two Agents map to the same private workspace");
2688
+ }
2689
+ seenPrivateVolumes.add(workspace.privateVolume);
2690
+ assertWorkspaceMatches(workspace, expected);
2691
+ await reconcileWorkspace(workspace, storageRoot);
2692
+ }
2693
+ };
2694
+ var reconcileWorkspace = async (workspace, storageRoot) => {
2695
+ const volumes = [
2696
+ workspace.privateVolume,
2697
+ workspace.sharedVolume,
2698
+ workspace.stateVolume,
2699
+ workspace.temporaryVolume
2700
+ ];
2701
+ for (const volume of volumes) {
2702
+ assertContained(storageRoot, volume);
2703
+ await ensurePrivateDirectory2(volume);
2704
+ const canonical = await realpath3(volume);
2705
+ if (canonical !== volume) throw corrupt2("Workspace path resolves through a symbolic link");
2706
+ }
2707
+ };
2708
+ var assertWorkspaceMatches = (actual, expected) => {
2709
+ for (const field of [
2710
+ "workspaceKey",
2711
+ "spaceId",
2712
+ "agentId",
2713
+ "privateVolume",
2714
+ "sharedVolume",
2715
+ "stateVolume",
2716
+ "temporaryVolume"
2717
+ ]) {
2718
+ if (actual[field] !== expected[field]) {
2719
+ throw corrupt2(`Workspace registry ${field} does not match its canonical value`);
2720
+ }
2721
+ }
2722
+ };
2723
+ var assertNoCollision = (workspaces, candidate) => {
2724
+ for (const existing of Object.values(workspaces)) {
2725
+ if (existing.privateVolume === candidate.privateVolume) {
2726
+ throw collision("Two Agents map to the same private workspace");
2727
+ }
2728
+ for (const left of [existing.privateVolume, existing.sharedVolume]) {
2729
+ for (const right of [candidate.privateVolume, candidate.sharedVolume]) {
2730
+ if (left !== right && (isPathInside2(left, right) || isPathInside2(right, left))) {
2731
+ throw collision("Workspace mappings overlap");
2732
+ }
2733
+ }
2734
+ }
2735
+ }
2736
+ };
2737
+ var ensurePrivateDirectory2 = async (directory) => {
2738
+ await mkdir8(directory, { recursive: true, mode: 448 });
2739
+ const info = await lstat4(directory);
2740
+ if (info.isSymbolicLink() || !info.isDirectory()) {
2741
+ throw corrupt2("Workspace registry path is not a real directory");
2742
+ }
2743
+ await chmod3(directory, 448);
2744
+ };
2745
+ var canonicalDirectory = async (directory, label) => {
2746
+ const info = await lstat4(directory);
2747
+ if (info.isSymbolicLink() || !info.isDirectory()) {
2748
+ throw new WorkspaceRegistryError("STORAGE_UNSAFE", `${label} must be a real directory`);
2749
+ }
2750
+ return realpath3(directory);
2751
+ };
2752
+ var rejectUnsafeFilesystem = async (storageRoot) => {
2753
+ const stats = await statfs(storageRoot, { bigint: true });
2754
+ const type = Number(stats.type);
2755
+ if (UNSAFE_FILESYSTEM_TYPES.has(type)) {
2756
+ throw new WorkspaceRegistryError(
2757
+ "STORAGE_UNSAFE",
2758
+ "Storage root uses a network filesystem that cannot guarantee workspace isolation"
2759
+ );
2760
+ }
2761
+ };
2762
+ var writeRegistryAtomically = async (registryPath, document) => {
2763
+ const temporaryPath = `${registryPath}.${process.pid}.${Date.now()}.tmp`;
2764
+ const handle = await open3(
2765
+ temporaryPath,
2766
+ constants4.O_CREAT | constants4.O_EXCL | constants4.O_WRONLY,
2767
+ 384
2768
+ );
2769
+ try {
2770
+ await handle.writeFile(`${JSON.stringify(document, null, 2)}
2771
+ `, "utf8");
2772
+ await handle.sync();
2773
+ } finally {
2774
+ await handle.close();
2775
+ }
2776
+ try {
2777
+ await rename3(temporaryPath, registryPath);
2778
+ await chmod3(registryPath, 384);
2779
+ } catch (error) {
2780
+ await unlink5(temporaryPath).catch(() => void 0);
430
2781
  throw error;
431
2782
  }
432
- const relay = startConnectorRelay(config, bunnyDaemon, launchConfig);
2783
+ };
2784
+ var validateImmutableId = (label, value) => {
2785
+ if (!IMMUTABLE_ID_PATTERN2.test(value)) {
2786
+ throw new WorkspaceRegistryError(
2787
+ "WORKSPACE_INVALID",
2788
+ `${label} must contain only letters, numbers, underscores, or hyphens`
2789
+ );
2790
+ }
2791
+ };
2792
+ var validateStoredId = (label, value) => {
2793
+ try {
2794
+ validateImmutableId(label, value);
2795
+ } catch (error) {
2796
+ throw corrupt2(errorMessage2(error));
2797
+ }
2798
+ };
2799
+ var assertContained = (root, candidate) => {
2800
+ if (!isPathInside2(root, candidate)) throw corrupt2("Workspace path escapes the storage root");
2801
+ };
2802
+ var isPathInside2 = (parent, child) => {
2803
+ const relative = path8.relative(parent, child);
2804
+ return relative !== "" && relative !== ".." && !relative.startsWith(`..${path8.sep}`) && !path8.isAbsolute(relative);
2805
+ };
2806
+ var toDescriptor = (workspace) => ({
2807
+ workspaceKey: workspace.workspaceKey,
2808
+ spaceId: workspace.spaceId,
2809
+ agentId: workspace.agentId,
2810
+ privateVolume: workspace.privateVolume,
2811
+ sharedVolume: workspace.sharedVolume,
2812
+ stateVolume: workspace.stateVolume,
2813
+ temporaryVolume: workspace.temporaryVolume,
2814
+ isolationMode: "limited"
2815
+ });
2816
+ var isRegistryWorkspace = (value) => isRecord(value) && [
2817
+ "workspaceKey",
2818
+ "spaceId",
2819
+ "agentId",
2820
+ "privateVolume",
2821
+ "sharedVolume",
2822
+ "stateVolume",
2823
+ "temporaryVolume",
2824
+ "createdAt",
2825
+ "verifiedAt"
2826
+ ].every((field) => typeof value[field] === "string");
2827
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2828
+ var isMissingFile2 = (error) => isRecord(error) && "code" in error && error.code === "ENOENT";
2829
+ var errorMessage2 = (error) => error instanceof Error ? error.message : String(error);
2830
+ var corrupt2 = (message) => new WorkspaceRegistryError("REGISTRY_CORRUPT", message);
2831
+ var collision = (message) => new WorkspaceRegistryError("WORKSPACE_COLLISION", message);
2832
+
2833
+ // src/onboard.ts
2834
+ async function runOnboard(launchConfig) {
2835
+ intro("Welcome to Buda Connector");
2836
+ const existing = await readDeviceConfig(launchConfig);
2837
+ let relayProtocol;
2838
+ if (existing) {
2839
+ log.info(`Already registered: ${existing.connectorId}`);
2840
+ log.info(`Server: ${existing.serverUrl}`);
2841
+ log.info(`Workdir: ${existing.workdirRoot}`);
2842
+ log.step("Starting daemon with existing config...");
2843
+ } else {
2844
+ log.step("First-time setup");
2845
+ const serverUrl = launchConfig?.serverUrl || await (async () => {
2846
+ const answer = await text({
2847
+ message: "Server URL",
2848
+ placeholder: DEFAULT_SERVER_URL,
2849
+ defaultValue: DEFAULT_SERVER_URL
2850
+ });
2851
+ if (isCancel(answer)) {
2852
+ cancel("Setup cancelled.");
2853
+ process.exit(0);
2854
+ }
2855
+ return answer;
2856
+ })();
2857
+ const oauthToken = launchConfig?.oauthToken || await (async () => {
2858
+ const answer = await text({
2859
+ message: "Auth token (from buda.im \u2192 Settings \u2192 Connectors)",
2860
+ placeholder: "paste your token here",
2861
+ validate: (value) => {
2862
+ if (!value || !value.trim()) return "Token is required";
2863
+ }
2864
+ });
2865
+ if (isCancel(answer)) {
2866
+ cancel("Setup cancelled.");
2867
+ process.exit(0);
2868
+ }
2869
+ return answer;
2870
+ })();
2871
+ launchConfig = { ...launchConfig, serverUrl, oauthToken };
2872
+ const s2 = spinner();
2873
+ s2.start("Registering connector...");
2874
+ const config2 = await createDeviceConfig(launchConfig);
2875
+ const paths2 = getConnectorPaths(config2.serverUrl);
2876
+ await migrateLegacyWorkspaceRoot(config2.workdirRoot);
2877
+ await provisionWorkdirLayout(config2.workdirRoot);
2878
+ try {
2879
+ const registration = await registerConnector(config2, launchConfig, void 0, {
2880
+ recoverMissingProfile: true
2881
+ });
2882
+ config2.connectorId = registration.connectorId;
2883
+ relayProtocol = registration.relayProtocol;
2884
+ await writeDeviceConfig(config2);
2885
+ } catch (error) {
2886
+ s2.stop("Registration failed");
2887
+ log.error(error instanceof Error ? error.message : String(error));
2888
+ process.exit(1);
2889
+ }
2890
+ s2.stop("Registered successfully");
2891
+ note(
2892
+ [
2893
+ `Connector ID: ${config2.connectorId}`,
2894
+ `Config: ${paths2.configPath}`,
2895
+ `Workdir: ${config2.workdirRoot}`
2896
+ ].join("\n"),
2897
+ "Device info"
2898
+ );
2899
+ }
2900
+ const config = await ensureDeviceConfig(launchConfig);
2901
+ const paths = getConnectorPaths(config.serverUrl);
2902
+ await migrateLegacyWorkspaceRoot(config.workdirRoot);
2903
+ await provisionWorkdirLayout(config.workdirRoot);
2904
+ const workspaceRegistry = await LocalWorkspaceRegistry.open(config.workdirRoot);
2905
+ const s = spinner();
2906
+ s.start("Starting daemon...");
2907
+ const bunnyDaemon = await startEmbeddedBunnyDaemon(config, launchConfig);
2908
+ if (!existing) {
2909
+ } else {
2910
+ try {
2911
+ relayProtocol = (await registerConnector(config, launchConfig, bunnyDaemon)).relayProtocol;
2912
+ } catch (error) {
2913
+ await stopEmbeddedBunnyDaemon(bunnyDaemon);
2914
+ s.stop("Failed to register");
2915
+ log.error(error instanceof Error ? error.message : String(error));
2916
+ process.exit(1);
2917
+ }
2918
+ }
2919
+ const relay = startConnectorRelay(
2920
+ config,
2921
+ bunnyDaemon,
2922
+ relayProtocol,
2923
+ workspaceRegistry,
2924
+ launchConfig
2925
+ );
2926
+ s.stop("Daemon running");
433
2927
  await writeLog("info", `server ${config.serverUrl}`);
434
- await writeLog("info", `config ${CONFIG_PATH}`);
2928
+ await writeLog("info", `config ${paths.configPath}`);
435
2929
  await writeLog("info", `workdir root ${config.workdirRoot}`);
436
- await writeLog("info", `log file ${LOG_PATH}`);
437
2930
  await writeLog("info", `bunny daemon ${bunnyDaemon.baseUrl}`);
2931
+ await writeLog("info", `log file ${paths.logPath}`);
2932
+ note(
2933
+ [
2934
+ `Server: ${config.serverUrl}`,
2935
+ `Bunny daemon: ${bunnyDaemon.baseUrl}`,
2936
+ `Relay: connected`,
2937
+ `Logs: ${paths.logPath}`
2938
+ ].join("\n"),
2939
+ "Status"
2940
+ );
2941
+ outro("Buda Connector is running. Press Ctrl+C to stop.");
438
2942
  const interval = setInterval(() => {
439
2943
  heartbeatConnector(config, launchConfig, bunnyDaemon).catch((error) => {
440
2944
  void writeLog(
@@ -454,10 +2958,117 @@ async function runConnectorDaemon(launchConfig) {
454
2958
  process.on("SIGTERM", shutdown);
455
2959
  }
456
2960
 
2961
+ // src/runtime.ts
2962
+ import { mkdir as mkdir9 } from "fs/promises";
2963
+ import path9 from "path";
2964
+ async function runNewConnector(launchConfig) {
2965
+ const existing = await readDeviceConfig(launchConfig);
2966
+ const config = await ensureDeviceConfig(launchConfig);
2967
+ const paths = getConnectorPaths(config.serverUrl);
2968
+ await migrateLegacyWorkspaceRoot(config.workdirRoot);
2969
+ await provisionWorkdirLayout(config.workdirRoot);
2970
+ await LocalWorkspaceRegistry.open(config.workdirRoot);
2971
+ const registration = await registerConnector(config, launchConfig, void 0, {
2972
+ recoverMissingProfile: !existing
2973
+ });
2974
+ config.connectorId = registration.connectorId;
2975
+ await writeDeviceConfig(config);
2976
+ await writeLog("info", `wrote ${paths.configPath}`);
2977
+ await writeLog("info", `workdir root ${config.workdirRoot}`);
2978
+ await writeLog("info", `log file ${paths.logPath}`);
2979
+ }
2980
+ async function runConnectorDaemon(launchConfig) {
2981
+ const existing = await readDeviceConfig(launchConfig);
2982
+ const config = await ensureDeviceConfig(launchConfig);
2983
+ const paths = getConnectorPaths(config.serverUrl);
2984
+ const daemonLockTarget = getConnectorDaemonLockTarget(config.serverUrl, config.connectorId);
2985
+ await mkdir9(path9.dirname(daemonLockTarget), { recursive: true, mode: 448 });
2986
+ let daemonLock = await acquireExclusiveFileLock(daemonLockTarget, { timeoutMs: 0 }).catch(() => {
2987
+ throw new Error(
2988
+ `Another Buda Connector daemon is already running for this profile on ${config.serverUrl}. Stop it before starting another instance.`
2989
+ );
2990
+ });
2991
+ let bunnyDaemon;
2992
+ let relayProtocol;
2993
+ try {
2994
+ await migrateLegacyWorkspaceRoot(config.workdirRoot);
2995
+ await provisionWorkdirLayout(config.workdirRoot);
2996
+ const workspaceRegistry = await LocalWorkspaceRegistry.open(config.workdirRoot);
2997
+ bunnyDaemon = await startEmbeddedBunnyDaemon(config, launchConfig);
2998
+ const activeBunnyDaemon = bunnyDaemon;
2999
+ const registration = await registerConnector(config, launchConfig, activeBunnyDaemon, {
3000
+ recoverMissingProfile: !existing
3001
+ });
3002
+ const connectorIdChanged = config.connectorId !== registration.connectorId;
3003
+ if (connectorIdChanged) {
3004
+ const registeredLockTarget = getConnectorDaemonLockTarget(
3005
+ config.serverUrl,
3006
+ registration.connectorId
3007
+ );
3008
+ await mkdir9(path9.dirname(registeredLockTarget), { recursive: true, mode: 448 });
3009
+ const registeredLock = await acquireExclusiveFileLock(registeredLockTarget, {
3010
+ timeoutMs: 0
3011
+ }).catch(() => {
3012
+ throw new Error(
3013
+ `Another Buda Connector daemon is already running for this profile on ${config.serverUrl}. Stop it before starting another instance.`
3014
+ );
3015
+ });
3016
+ await daemonLock.release();
3017
+ daemonLock = registeredLock;
3018
+ }
3019
+ config.connectorId = registration.connectorId;
3020
+ relayProtocol = registration.relayProtocol;
3021
+ if (!existing || connectorIdChanged) await writeDeviceConfig(config);
3022
+ const relay = startConnectorRelay(
3023
+ config,
3024
+ activeBunnyDaemon,
3025
+ relayProtocol,
3026
+ workspaceRegistry,
3027
+ launchConfig
3028
+ );
3029
+ await writeLog("info", `server ${config.serverUrl}`);
3030
+ await writeLog("info", `config ${paths.configPath}`);
3031
+ await writeLog("info", `workdir root ${config.workdirRoot}`);
3032
+ await writeLog("info", `log file ${paths.logPath}`);
3033
+ await writeLog("info", `bunny daemon ${activeBunnyDaemon.baseUrl}`);
3034
+ const interval = setInterval(() => {
3035
+ heartbeatConnector(config, launchConfig, activeBunnyDaemon).then(({ relayProtocol: nextRelayProtocol }) => {
3036
+ relay.updateRelayProtocol(nextRelayProtocol);
3037
+ }).catch((error) => {
3038
+ void writeLog(
3039
+ "warn",
3040
+ `heartbeat failed: ${error instanceof Error ? error.message : String(error)}`
3041
+ );
3042
+ });
3043
+ }, HEARTBEAT_INTERVAL_MS);
3044
+ let shuttingDown = false;
3045
+ const shutdown = async () => {
3046
+ if (shuttingDown) return;
3047
+ shuttingDown = true;
3048
+ clearInterval(interval);
3049
+ relay.stop();
3050
+ try {
3051
+ await closeAllBrowserSessions();
3052
+ await unregisterConnector(config, launchConfig);
3053
+ await stopEmbeddedBunnyDaemon(activeBunnyDaemon);
3054
+ } finally {
3055
+ await daemonLock.release();
3056
+ process.exit(0);
3057
+ }
3058
+ };
3059
+ process.once("SIGINT", () => void shutdown());
3060
+ process.once("SIGTERM", () => void shutdown());
3061
+ } catch (error) {
3062
+ if (bunnyDaemon) await stopEmbeddedBunnyDaemon(bunnyDaemon);
3063
+ await daemonLock.release();
3064
+ throw error;
3065
+ }
3066
+ }
3067
+
457
3068
  // src/cli.ts
458
- function printUsage() {
3069
+ function printHelp() {
459
3070
  console.log(
460
- "Usage: buda-connector new | dev | daemon [--space-id <space>] [--server-url <url>] [--token <token>] [--workdir <path>] [--launch-config-stdin]\n pnpm --filter @buda-ai/connector dev\n\nCommands:\n new Create/register a new connector identity\n dev Run the connector daemon in development\n daemon Run the connector daemon for sidecar/service usage"
3071
+ "Help: buda-connector [onboard | new | dev | daemon] [--token <token>] [--server-url <url>] [--workdir <path>] [--launch-config-stdin]\n pnpm --filter @buda-ai/connector dev\n\nCommands (default: onboard in TTY, daemon otherwise):\n onboard Interactive first-time setup + start daemon\n new Create/register a new connector identity\n dev Run the connector daemon in development\n daemon Run the connector daemon for sidecar/service usage"
461
3072
  );
462
3073
  }
463
3074
  function readFlagValue(name) {
@@ -471,8 +3082,8 @@ function readCliLaunchConfig() {
471
3082
  return {
472
3083
  serverUrl: readFlagValue("--server-url"),
473
3084
  workdirRoot: readFlagValue("--workdir"),
474
- oauthToken: readFlagValue("--token"),
475
- spaceId: readFlagValue("--space-id"),
3085
+ oauthToken: readFlagValue("--oauth-token"),
3086
+ token: readFlagValue("--token"),
476
3087
  bunnyDaemonHost: readFlagValue("--bunny-daemon-host"),
477
3088
  bunnyDaemonPort: bunnyDaemonPort && Number.isInteger(Number(bunnyDaemonPort)) ? Number(bunnyDaemonPort) : void 0
478
3089
  };
@@ -509,21 +3120,34 @@ async function readLaunchConfig() {
509
3120
  });
510
3121
  }
511
3122
  async function main() {
512
- const command = process.argv[2];
513
- if (!command || process.argv.includes("--help") || process.argv.includes("-h")) {
514
- printUsage();
3123
+ if (process.argv.includes("--help") || process.argv.includes("-h")) {
3124
+ printHelp();
515
3125
  return;
516
3126
  }
3127
+ const rawCommand = process.argv[2];
3128
+ const isTTY = process.stdin.isTTY && process.stdout.isTTY;
3129
+ const defaultCommand = isTTY ? "onboard" : "daemon";
3130
+ const command = !rawCommand || rawCommand.startsWith("-") ? defaultCommand : rawCommand;
3131
+ process.on("unhandledRejection", (reason) => {
3132
+ console.error("[buda-connector] unhandledRejection:", reason);
3133
+ });
3134
+ process.on("uncaughtException", (error) => {
3135
+ console.error("[buda-connector] uncaughtException:", error);
3136
+ });
517
3137
  const launchConfig = await readLaunchConfig();
518
3138
  if (command === "new") {
519
3139
  await runNewConnector(launchConfig);
520
3140
  return;
521
3141
  }
3142
+ if (command === "onboard") {
3143
+ await runOnboard(launchConfig);
3144
+ return;
3145
+ }
522
3146
  if (command === "dev" || command === "daemon") {
523
3147
  await runConnectorDaemon(launchConfig);
524
3148
  return;
525
3149
  }
526
- printUsage();
3150
+ printHelp();
527
3151
  process.exit(1);
528
3152
  }
529
3153
  main().catch((error) => {