@buda-ai/connector 0.1.6 → 0.1.8
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.
- package/README.md +46 -13
- package/agent-templates/shared/AGENTS.md +1 -1
- package/dist/cli.js +2699 -400
- package/package.json +4 -1
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/config.ts
|
|
4
|
-
import { createHash as createHash2, randomBytes, randomUUID } from "crypto";
|
|
4
|
+
import { createHash as createHash2, randomBytes, randomUUID as randomUUID2 } from "crypto";
|
|
5
5
|
import { chmod, mkdir, readFile as readFile2, rename, unlink as unlink2, writeFile } from "fs/promises";
|
|
6
6
|
import { homedir, hostname, platform as platform2 } from "os";
|
|
7
7
|
import path from "path";
|
|
8
8
|
|
|
9
9
|
// src/exclusive-file-lock.ts
|
|
10
|
+
import { randomUUID } from "crypto";
|
|
10
11
|
import { open, readFile, stat, unlink } from "fs/promises";
|
|
11
12
|
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
12
13
|
var DEFAULT_STALE_MS = 12e4;
|
|
@@ -33,8 +34,11 @@ async function acquireExclusiveFileLock(targetPath, options = {}) {
|
|
|
33
34
|
await wait(pollMs);
|
|
34
35
|
continue;
|
|
35
36
|
}
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
const ownerToken = randomUUID();
|
|
38
|
+
await handle.writeFile(
|
|
39
|
+
`${JSON.stringify({ pid: process.pid, ownerToken, createdAt: Date.now() })}
|
|
40
|
+
`
|
|
41
|
+
);
|
|
38
42
|
await handle.sync();
|
|
39
43
|
let released = false;
|
|
40
44
|
return {
|
|
@@ -42,7 +46,8 @@ async function acquireExclusiveFileLock(targetPath, options = {}) {
|
|
|
42
46
|
if (released) return;
|
|
43
47
|
released = true;
|
|
44
48
|
await handle.close();
|
|
45
|
-
await
|
|
49
|
+
const currentOwner = await readFile(lockPath, "utf8").then((raw) => JSON.parse(raw).ownerToken).catch(() => void 0);
|
|
50
|
+
if (currentOwner === ownerToken) await unlink(lockPath).catch(() => void 0);
|
|
46
51
|
}
|
|
47
52
|
};
|
|
48
53
|
}
|
|
@@ -154,12 +159,23 @@ function getConnectorProfileKey(serverUrl, token) {
|
|
|
154
159
|
const discriminator = token?.trim() ? getConnectorTokenFingerprint(token) : "legacy-no-token";
|
|
155
160
|
return `${getServerOriginKey(serverUrl)}--${discriminator}`;
|
|
156
161
|
}
|
|
162
|
+
function getConnectorIdentityKey(deviceId, serverUrl, spaceId) {
|
|
163
|
+
const normalizedSpaceId = spaceId.trim();
|
|
164
|
+
if (!deviceId.trim() || !normalizedSpaceId) {
|
|
165
|
+
throw new Error("Connector identity requires deviceId and spaceId");
|
|
166
|
+
}
|
|
167
|
+
const digest = createHash2("sha256").update(`${deviceId.trim()}
|
|
168
|
+
${normalizeServerOrigin(serverUrl)}
|
|
169
|
+
${normalizedSpaceId}`, "utf8").digest("hex").slice(0, 24);
|
|
170
|
+
const spaceKey = normalizedSpaceId.replace(/[^a-z0-9_-]+/gi, "-").toLowerCase();
|
|
171
|
+
return `${getServerOriginKey(serverUrl)}--${spaceKey}--${digest}`;
|
|
172
|
+
}
|
|
157
173
|
function getConnectorPaths(serverUrl, configRoot = path.join(homedir(), ".buda")) {
|
|
158
174
|
const scopedLegacyDir = path.join(configRoot, "servers", getServerOriginKey(serverUrl));
|
|
159
175
|
return {
|
|
160
176
|
configDir: configRoot,
|
|
161
177
|
configPath: path.join(configRoot, "device.json"),
|
|
162
|
-
logPath: path.join(configRoot, "connector.log"),
|
|
178
|
+
logPath: path.join(configRoot, "logs", "connector.log"),
|
|
163
179
|
workdirRoot: path.join(configRoot, "agents"),
|
|
164
180
|
scopedLegacyConfigPath: path.join(scopedLegacyDir, "device.json"),
|
|
165
181
|
scopedLegacyWorkdirRoot: path.join(scopedLegacyDir, "agents")
|
|
@@ -175,6 +191,17 @@ function getConnectorDaemonLockTarget(serverUrl, connectorId, configRoot = path.
|
|
|
175
191
|
"connector-daemon"
|
|
176
192
|
);
|
|
177
193
|
}
|
|
194
|
+
function getConnectorBootstrapLockTarget(launchConfig, configRoot = path.join(homedir(), ".buda")) {
|
|
195
|
+
const serverUrl = getServerUrl(launchConfig);
|
|
196
|
+
const scope = launchConfig?.spaceId?.trim() ? `space-${launchConfig.spaceId.trim()}` : getConnectorToken(launchConfig) ? `token-${getConnectorTokenFingerprint(getConnectorToken(launchConfig)).slice(0, 24)}` : "oauth-unresolved";
|
|
197
|
+
return path.join(
|
|
198
|
+
configRoot,
|
|
199
|
+
"daemons",
|
|
200
|
+
getServerOriginKey(serverUrl),
|
|
201
|
+
scope.replace(/[^a-z0-9_-]+/gi, "-").toLowerCase(),
|
|
202
|
+
"bootstrap"
|
|
203
|
+
);
|
|
204
|
+
}
|
|
178
205
|
function getServerUrl(launchConfig) {
|
|
179
206
|
return (launchConfig?.serverUrl || process.env.BUDA_CONNECTOR_SERVER_URL || process.env.BUDA_SERVER_URL || process.env.NEXT_PUBLIC_APP_URL || DEFAULT_SERVER_URL).replace(/\/$/, "");
|
|
180
207
|
}
|
|
@@ -184,6 +211,13 @@ function getConnectorRelayUrl(serverUrl, connectorId) {
|
|
|
184
211
|
url.searchParams.set("connectorId", connectorId);
|
|
185
212
|
return url.toString();
|
|
186
213
|
}
|
|
214
|
+
function getConnectorTerminalUrl(serverUrl, connectorId, sessionId) {
|
|
215
|
+
const url = new URL("/api/connectors/terminal", serverUrl);
|
|
216
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
217
|
+
url.searchParams.set("connectorId", connectorId);
|
|
218
|
+
url.searchParams.set("sessionId", sessionId);
|
|
219
|
+
return url.toString();
|
|
220
|
+
}
|
|
187
221
|
function getWorkdirRoot(launchConfig) {
|
|
188
222
|
return launchConfig?.workdirRoot || process.env.BUDA_CONNECTOR_WORKDIR || getConnectorPaths(getServerUrl(launchConfig)).workdirRoot;
|
|
189
223
|
}
|
|
@@ -200,15 +234,16 @@ function getBunnyDaemonPort(launchConfig) {
|
|
|
200
234
|
function getConnectorToken(launchConfig) {
|
|
201
235
|
return launchConfig?.token?.trim() || process.env.BUDA_CONNECTOR_TOKEN?.trim() || void 0;
|
|
202
236
|
}
|
|
203
|
-
function buildDeviceConfig(launchConfig, deviceId = `dvc_${
|
|
237
|
+
function buildDeviceConfig(launchConfig, deviceId = `dvc_${randomUUID2()}`) {
|
|
204
238
|
const host = hostname();
|
|
205
239
|
const token = getConnectorToken(launchConfig);
|
|
206
240
|
return {
|
|
207
|
-
connectorId: `cnr_${
|
|
241
|
+
connectorId: `cnr_${randomUUID2()}`,
|
|
208
242
|
deviceId,
|
|
209
243
|
deviceName: host,
|
|
210
244
|
hostLabel: `${host} (${platform2()})`,
|
|
211
245
|
serverUrl: getServerUrl(launchConfig),
|
|
246
|
+
...launchConfig?.spaceId?.trim() ? { spaceId: launchConfig.spaceId.trim() } : {},
|
|
212
247
|
workdirRoot: getWorkdirRoot(launchConfig),
|
|
213
248
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
214
249
|
deviceToken: randomBytes(32).toString("base64url"),
|
|
@@ -220,12 +255,18 @@ function isDeviceConfig(value) {
|
|
|
220
255
|
const config = value;
|
|
221
256
|
return typeof config.connectorId === "string" && typeof config.deviceId === "string" && typeof config.serverUrl === "string";
|
|
222
257
|
}
|
|
258
|
+
function isCanonicalDeviceConfig(value) {
|
|
259
|
+
return isDeviceConfig(value) && typeof value.spaceId === "string" && value.spaceId.trim().length > 0;
|
|
260
|
+
}
|
|
223
261
|
async function readJsonFile(configPath) {
|
|
224
262
|
try {
|
|
225
263
|
const raw = await readFile2(configPath, "utf8");
|
|
226
264
|
return JSON.parse(raw);
|
|
227
|
-
} catch {
|
|
228
|
-
|
|
265
|
+
} catch (error) {
|
|
266
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
throw new Error(`Invalid Connector config ${configPath}`, { cause: error });
|
|
229
270
|
}
|
|
230
271
|
}
|
|
231
272
|
function migrateLegacyProfiles(values) {
|
|
@@ -242,36 +283,68 @@ function migrateLegacyProfiles(values) {
|
|
|
242
283
|
async function loadDeviceConfigStore(configPath) {
|
|
243
284
|
const parsed = await readJsonFile(configPath);
|
|
244
285
|
const candidate = parsed;
|
|
286
|
+
if (candidate?.version === 4 && candidate.identities && typeof candidate.identities === "object" && candidate.aliases && typeof candidate.aliases === "object") {
|
|
287
|
+
const identities = candidate.identities;
|
|
288
|
+
for (const [identityKey, value] of Object.entries(identities)) {
|
|
289
|
+
if (!isCanonicalDeviceConfig(value)) {
|
|
290
|
+
throw new Error(`Invalid canonical Connector identity ${identityKey}`);
|
|
291
|
+
}
|
|
292
|
+
const expected = getConnectorIdentityKey(value.deviceId, value.serverUrl, value.spaceId);
|
|
293
|
+
if (identityKey !== expected) {
|
|
294
|
+
throw new Error(`Connector identity key does not match ${value.deviceId}/${value.spaceId}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
store: {
|
|
299
|
+
version: 4,
|
|
300
|
+
...typeof candidate.systemDeviceId === "string" ? { systemDeviceId: candidate.systemDeviceId } : {},
|
|
301
|
+
identities,
|
|
302
|
+
aliases: candidate.aliases
|
|
303
|
+
},
|
|
304
|
+
legacyProfiles: {},
|
|
305
|
+
needsWrite: false
|
|
306
|
+
};
|
|
307
|
+
}
|
|
245
308
|
if (candidate?.version === 3 && candidate.profiles && typeof candidate.profiles === "object") {
|
|
246
309
|
return {
|
|
247
310
|
store: {
|
|
248
|
-
version:
|
|
311
|
+
version: 4,
|
|
249
312
|
...typeof candidate.systemDeviceId === "string" ? { systemDeviceId: candidate.systemDeviceId } : {},
|
|
250
|
-
|
|
313
|
+
identities: {},
|
|
314
|
+
aliases: {}
|
|
251
315
|
},
|
|
316
|
+
legacyProfiles: candidate.profiles,
|
|
252
317
|
needsWrite: false
|
|
253
318
|
};
|
|
254
319
|
}
|
|
255
320
|
if (candidate && (candidate.version === 1 || candidate.version === 2) && candidate.servers && typeof candidate.servers === "object") {
|
|
256
321
|
return {
|
|
257
322
|
store: {
|
|
258
|
-
version:
|
|
323
|
+
version: 4,
|
|
259
324
|
...typeof candidate.systemDeviceId === "string" ? { systemDeviceId: candidate.systemDeviceId } : {},
|
|
260
|
-
|
|
325
|
+
identities: {},
|
|
326
|
+
aliases: {}
|
|
261
327
|
},
|
|
262
|
-
|
|
328
|
+
legacyProfiles: migrateLegacyProfiles(Object.values(candidate.servers)),
|
|
329
|
+
needsWrite: false
|
|
263
330
|
};
|
|
264
331
|
}
|
|
265
332
|
if (isDeviceConfig(parsed)) {
|
|
266
333
|
return {
|
|
267
334
|
store: {
|
|
268
|
-
version:
|
|
269
|
-
|
|
335
|
+
version: 4,
|
|
336
|
+
identities: {},
|
|
337
|
+
aliases: {}
|
|
270
338
|
},
|
|
271
|
-
|
|
339
|
+
legacyProfiles: migrateLegacyProfiles([parsed]),
|
|
340
|
+
needsWrite: false
|
|
272
341
|
};
|
|
273
342
|
}
|
|
274
|
-
return {
|
|
343
|
+
return {
|
|
344
|
+
store: { version: 4, identities: {}, aliases: {} },
|
|
345
|
+
legacyProfiles: {},
|
|
346
|
+
needsWrite: false
|
|
347
|
+
};
|
|
275
348
|
}
|
|
276
349
|
function normalizeStoredWorkdirRoot(config, paths) {
|
|
277
350
|
const configuredRoot = config.workdirRoot?.trim();
|
|
@@ -289,7 +362,7 @@ function normalizeStoredWorkdirRoot(config, paths) {
|
|
|
289
362
|
}
|
|
290
363
|
async function writeDeviceConfigStore(configPath, store) {
|
|
291
364
|
await mkdir(path.dirname(configPath), { recursive: true });
|
|
292
|
-
const temporaryPath = `${configPath}.${process.pid}.${
|
|
365
|
+
const temporaryPath = `${configPath}.${process.pid}.${randomUUID2()}.tmp`;
|
|
293
366
|
try {
|
|
294
367
|
await writeFile(temporaryPath, `${JSON.stringify(store, null, 2)}
|
|
295
368
|
`, { mode: 384 });
|
|
@@ -299,36 +372,70 @@ async function writeDeviceConfigStore(configPath, store) {
|
|
|
299
372
|
await unlink2(temporaryPath).catch(() => void 0);
|
|
300
373
|
}
|
|
301
374
|
}
|
|
375
|
+
function findStoredDeviceConfig(store, legacyProfiles, launchConfig, serverUrl, paths) {
|
|
376
|
+
const aliases = [
|
|
377
|
+
...launchConfig?.spaceId?.trim() ? [`${getServerOriginKey(serverUrl)}--space--${launchConfig.spaceId.trim()}`] : [],
|
|
378
|
+
...getConnectorToken(launchConfig) ? [getConnectorProfileKey(serverUrl, getConnectorToken(launchConfig))] : []
|
|
379
|
+
];
|
|
380
|
+
const identityKey = aliases.map((alias) => store.aliases[alias]).find(Boolean);
|
|
381
|
+
let storedConfig = identityKey ? store.identities[identityKey] : void 0;
|
|
382
|
+
if (!storedConfig && getConnectorToken(launchConfig)) {
|
|
383
|
+
const legacyKey = getConnectorProfileKey(serverUrl, getConnectorToken(launchConfig));
|
|
384
|
+
storedConfig = legacyProfiles[legacyKey];
|
|
385
|
+
}
|
|
386
|
+
if (!isDeviceConfig(storedConfig)) return null;
|
|
387
|
+
return normalizeStoredWorkdirRoot(storedConfig, paths).config;
|
|
388
|
+
}
|
|
389
|
+
function putCanonicalDeviceConfig(store, config) {
|
|
390
|
+
if (!config.spaceId?.trim()) {
|
|
391
|
+
throw new Error("Cannot persist Connector identity before the server resolves spaceId");
|
|
392
|
+
}
|
|
393
|
+
const spaceId = config.spaceId.trim();
|
|
394
|
+
const identityKey = getConnectorIdentityKey(config.deviceId, config.serverUrl, spaceId);
|
|
395
|
+
store.identities[identityKey] = { ...config, spaceId };
|
|
396
|
+
store.aliases[`${getServerOriginKey(config.serverUrl)}--space--${spaceId}`] = identityKey;
|
|
397
|
+
if (config.token?.trim()) {
|
|
398
|
+
store.aliases[getConnectorProfileKey(config.serverUrl, config.token)] = identityKey;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
302
401
|
async function readDeviceConfig(launchConfig, configRoot) {
|
|
303
402
|
const serverUrl = getServerUrl(launchConfig);
|
|
304
403
|
const paths = getConnectorPaths(serverUrl, configRoot);
|
|
305
|
-
const launchToken = getConnectorToken(launchConfig);
|
|
306
404
|
await mkdir(paths.configDir, { recursive: true });
|
|
307
405
|
return withExclusiveFileLock(paths.configPath, async () => {
|
|
308
|
-
const { store,
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
406
|
+
const { store, legacyProfiles } = await loadDeviceConfigStore(paths.configPath);
|
|
407
|
+
return findStoredDeviceConfig(store, legacyProfiles, launchConfig, serverUrl, paths);
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
async function listDeviceConfigs(configRoot) {
|
|
411
|
+
const paths = getConnectorPaths(DEFAULT_SERVER_URL, configRoot);
|
|
412
|
+
await mkdir(paths.configDir, { recursive: true });
|
|
413
|
+
return withExclusiveFileLock(paths.configPath, async () => {
|
|
414
|
+
const { legacyProfiles, store } = await loadDeviceConfigStore(paths.configPath);
|
|
415
|
+
const identities = [...Object.values(store.identities), ...Object.values(legacyProfiles)].filter(isCanonicalDeviceConfig).map(
|
|
416
|
+
(config) => normalizeStoredWorkdirRoot(config, getConnectorPaths(config.serverUrl, configRoot)).config
|
|
417
|
+
);
|
|
418
|
+
const unique = /* @__PURE__ */ new Map();
|
|
419
|
+
for (const config of identities) {
|
|
420
|
+
unique.set(`${normalizeServerOrigin(config.serverUrl)}
|
|
421
|
+
${config.connectorId}`, config);
|
|
321
422
|
}
|
|
322
|
-
return
|
|
423
|
+
return [...unique.values()].sort(
|
|
424
|
+
(left, right) => left.createdAt.localeCompare(right.createdAt) || left.connectorId.localeCompare(right.connectorId)
|
|
425
|
+
);
|
|
323
426
|
});
|
|
324
427
|
}
|
|
325
428
|
async function writeDeviceConfig(config, configRoot) {
|
|
429
|
+
if (!config.spaceId?.trim()) {
|
|
430
|
+
throw new Error("Cannot persist Connector identity before the server resolves spaceId");
|
|
431
|
+
}
|
|
432
|
+
const spaceId = config.spaceId.trim();
|
|
326
433
|
const paths = getConnectorPaths(config.serverUrl, configRoot);
|
|
327
434
|
await mkdir(config.workdirRoot, { recursive: true });
|
|
328
435
|
await mkdir(paths.configDir, { recursive: true });
|
|
329
436
|
await withExclusiveFileLock(paths.configPath, async () => {
|
|
330
437
|
const { store } = await loadDeviceConfigStore(paths.configPath);
|
|
331
|
-
store
|
|
438
|
+
putCanonicalDeviceConfig(store, { ...config, spaceId });
|
|
332
439
|
await writeDeviceConfigStore(paths.configPath, store);
|
|
333
440
|
});
|
|
334
441
|
}
|
|
@@ -336,13 +443,12 @@ async function getOrCreateSystemDeviceId(configRoot, resolver = resolveSystemDev
|
|
|
336
443
|
const paths = getConnectorPaths(DEFAULT_SERVER_URL, configRoot);
|
|
337
444
|
await mkdir(paths.configDir, { recursive: true });
|
|
338
445
|
return withExclusiveFileLock(paths.configPath, async () => {
|
|
339
|
-
const { store,
|
|
446
|
+
const { store, legacyProfiles } = await loadDeviceConfigStore(paths.configPath);
|
|
340
447
|
if (store.systemDeviceId) {
|
|
341
|
-
if (needsWrite) await writeDeviceConfigStore(paths.configPath, store);
|
|
342
448
|
return store.systemDeviceId;
|
|
343
449
|
}
|
|
344
|
-
const existingDeviceId = Object.values(store.
|
|
345
|
-
store.systemDeviceId = resolver() || existingDeviceId || `dvc_${
|
|
450
|
+
const existingDeviceId = Object.values(store.identities).find(isDeviceConfig)?.deviceId ?? Object.values(legacyProfiles).find(isDeviceConfig)?.deviceId;
|
|
451
|
+
store.systemDeviceId = resolver() || existingDeviceId || `dvc_${randomUUID2()}`;
|
|
346
452
|
await writeDeviceConfigStore(paths.configPath, store);
|
|
347
453
|
return store.systemDeviceId;
|
|
348
454
|
});
|
|
@@ -354,33 +460,28 @@ async function createDeviceConfig(launchConfig, configRoot, resolver) {
|
|
|
354
460
|
async function ensureDeviceConfig(launchConfig, configRoot, resolver = resolveSystemDeviceId) {
|
|
355
461
|
const serverUrl = getServerUrl(launchConfig);
|
|
356
462
|
const paths = getConnectorPaths(serverUrl, configRoot);
|
|
357
|
-
const token = getConnectorToken(launchConfig);
|
|
358
|
-
const profileKey = getConnectorProfileKey(serverUrl, token);
|
|
359
463
|
await mkdir(paths.configDir, { recursive: true });
|
|
360
464
|
const config = await withExclusiveFileLock(paths.configPath, async () => {
|
|
361
|
-
const { store,
|
|
362
|
-
const
|
|
363
|
-
if (
|
|
364
|
-
const
|
|
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 {
|
|
465
|
+
const { store, legacyProfiles } = await loadDeviceConfigStore(paths.configPath);
|
|
466
|
+
const existing = findStoredDeviceConfig(store, legacyProfiles, launchConfig, serverUrl, paths);
|
|
467
|
+
if (existing) {
|
|
468
|
+
const resolved = {
|
|
372
469
|
...existing,
|
|
373
470
|
serverUrl,
|
|
374
471
|
workdirRoot: launchConfig?.workdirRoot || process.env.BUDA_CONNECTOR_WORKDIR || existing.workdirRoot || getWorkdirRoot(launchConfig),
|
|
375
|
-
...
|
|
472
|
+
...getConnectorToken(launchConfig) ? { token: getConnectorToken(launchConfig) } : {}
|
|
376
473
|
};
|
|
474
|
+
if (isCanonicalDeviceConfig(resolved)) {
|
|
475
|
+
putCanonicalDeviceConfig(store, resolved);
|
|
476
|
+
await writeDeviceConfigStore(paths.configPath, store);
|
|
477
|
+
}
|
|
478
|
+
return resolved;
|
|
377
479
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
store.profiles[profileKey] = created;
|
|
480
|
+
const existingDeviceId = store.systemDeviceId ?? Object.values(store.identities).find(isDeviceConfig)?.deviceId ?? Object.values(legacyProfiles).find(isDeviceConfig)?.deviceId;
|
|
481
|
+
const deviceId = existingDeviceId || resolver() || `dvc_${randomUUID2()}`;
|
|
482
|
+
const created = buildDeviceConfig(launchConfig, deviceId);
|
|
483
|
+
store.systemDeviceId = deviceId;
|
|
484
|
+
if (isCanonicalDeviceConfig(created)) putCanonicalDeviceConfig(store, created);
|
|
384
485
|
await writeDeviceConfigStore(paths.configPath, store);
|
|
385
486
|
return created;
|
|
386
487
|
});
|
|
@@ -393,6 +494,7 @@ function parseLaunchConfig(raw) {
|
|
|
393
494
|
const bunnyDaemonPort = typeof parsed.bunnyDaemonPort === "number" && Number.isInteger(parsed.bunnyDaemonPort) ? parsed.bunnyDaemonPort : void 0;
|
|
394
495
|
return {
|
|
395
496
|
serverUrl: typeof parsed.serverUrl === "string" ? parsed.serverUrl : void 0,
|
|
497
|
+
spaceId: typeof parsed.spaceId === "string" ? parsed.spaceId : void 0,
|
|
396
498
|
workdirRoot: typeof parsed.workdirRoot === "string" ? parsed.workdirRoot : void 0,
|
|
397
499
|
oauthToken: typeof parsed.oauthToken === "string" ? parsed.oauthToken : void 0,
|
|
398
500
|
token: typeof parsed.token === "string" ? parsed.token : void 0,
|
|
@@ -401,29 +503,1064 @@ function parseLaunchConfig(raw) {
|
|
|
401
503
|
};
|
|
402
504
|
}
|
|
403
505
|
|
|
404
|
-
// src/
|
|
405
|
-
import {
|
|
506
|
+
// src/console-dashboard.ts
|
|
507
|
+
import { closeSync, openSync } from "fs";
|
|
508
|
+
import { ReadStream, WriteStream } from "tty";
|
|
509
|
+
import { format, stripVTControlCharacters } from "util";
|
|
406
510
|
|
|
407
|
-
// src/
|
|
408
|
-
import {
|
|
409
|
-
import {
|
|
511
|
+
// src/daemon-control.ts
|
|
512
|
+
import { randomBytes as randomBytes2, randomUUID as randomUUID3 } from "crypto";
|
|
513
|
+
import { chmod as chmod2, mkdir as mkdir2, readdir, readFile as readFile3, rename as rename2, unlink as unlink3, writeFile as writeFile2 } from "fs/promises";
|
|
514
|
+
import { createConnection, createServer } from "net";
|
|
515
|
+
import path2 from "path";
|
|
516
|
+
|
|
517
|
+
// package.json
|
|
518
|
+
var package_default = {
|
|
519
|
+
name: "@buda-ai/connector",
|
|
520
|
+
version: "0.1.8",
|
|
521
|
+
private: false,
|
|
522
|
+
type: "module",
|
|
523
|
+
publishConfig: {
|
|
524
|
+
access: "public"
|
|
525
|
+
},
|
|
526
|
+
files: [
|
|
527
|
+
"dist",
|
|
528
|
+
"agent-templates"
|
|
529
|
+
],
|
|
530
|
+
bin: {
|
|
531
|
+
"buda-connector": "./dist/cli.js",
|
|
532
|
+
connector: "./dist/cli.js"
|
|
533
|
+
},
|
|
534
|
+
scripts: {
|
|
535
|
+
dev: "tsx src/cli.ts dev",
|
|
536
|
+
daemon: "tsx src/cli.ts daemon",
|
|
537
|
+
new: "tsx src/cli.ts new",
|
|
538
|
+
start: "node dist/cli.js daemon",
|
|
539
|
+
build: "tsup",
|
|
540
|
+
test: "vitest run",
|
|
541
|
+
typecheck: "tsc --noEmit",
|
|
542
|
+
lint: "biome format . --write && biome check .",
|
|
543
|
+
"lint:err": "biome format . --write && biome check . --write --diagnostic-level=error"
|
|
544
|
+
},
|
|
545
|
+
dependencies: {
|
|
546
|
+
"@bunny-agent/daemon": "0.9.51",
|
|
547
|
+
"@clack/prompts": "^1.0.0",
|
|
548
|
+
"playwright-core": "^1.52.0",
|
|
549
|
+
ws: "^8.21.0"
|
|
550
|
+
},
|
|
551
|
+
optionalDependencies: {
|
|
552
|
+
"node-pty": "^1.1.0"
|
|
553
|
+
},
|
|
554
|
+
devDependencies: {
|
|
555
|
+
"@types/node": "^24.13.2",
|
|
556
|
+
"@types/ws": "^8.18.1",
|
|
557
|
+
relaylib: "workspace:*",
|
|
558
|
+
tsup: "^8.5.0",
|
|
559
|
+
tsx: "^4.20.5",
|
|
560
|
+
typescript: "^5.9.3",
|
|
561
|
+
vitest: "^2.1.8"
|
|
562
|
+
},
|
|
563
|
+
engines: {
|
|
564
|
+
node: ">=24.18.0"
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
// src/daemon-control.ts
|
|
569
|
+
var CONNECTOR_CONTROL_PROTOCOL_VERSION = 1;
|
|
570
|
+
var CONNECTOR_RUNTIME_VERSION = package_default.version;
|
|
571
|
+
var DEFAULT_CONTROL_REQUEST_TIMEOUT_MS = 3e3;
|
|
572
|
+
var getDaemonControlMetadataPath = (config, configRoot) => `${getConnectorDaemonLockTarget(config.serverUrl, config.connectorId, configRoot)}.control.json`;
|
|
573
|
+
var writeMetadata = async (metadataPath, metadata) => {
|
|
574
|
+
await mkdir2(path2.dirname(metadataPath), { recursive: true, mode: 448 });
|
|
575
|
+
const temporaryPath = `${metadataPath}.${process.pid}.${randomUUID3()}.tmp`;
|
|
576
|
+
try {
|
|
577
|
+
await writeFile2(temporaryPath, `${JSON.stringify(metadata)}
|
|
578
|
+
`, { mode: 384 });
|
|
579
|
+
await rename2(temporaryPath, metadataPath);
|
|
580
|
+
await chmod2(metadataPath, 384);
|
|
581
|
+
} finally {
|
|
582
|
+
await unlink3(temporaryPath).catch(() => void 0);
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
var publicStatus = (metadata) => ({
|
|
586
|
+
connectorId: metadata.connectorId,
|
|
587
|
+
idle: metadata.idle,
|
|
588
|
+
instanceId: metadata.instanceId,
|
|
589
|
+
...metadata.lastHeartbeatAt ? { lastHeartbeatAt: metadata.lastHeartbeatAt } : {},
|
|
590
|
+
mode: metadata.mode,
|
|
591
|
+
pid: metadata.pid,
|
|
592
|
+
protocolVersion: metadata.protocolVersion,
|
|
593
|
+
relayConnected: metadata.relayConnected,
|
|
594
|
+
serverUrl: metadata.serverUrl,
|
|
595
|
+
...metadata.spaceId ? { spaceId: metadata.spaceId } : {},
|
|
596
|
+
startedAt: metadata.startedAt,
|
|
597
|
+
version: metadata.version
|
|
598
|
+
});
|
|
599
|
+
var parseRequest = (raw) => {
|
|
600
|
+
try {
|
|
601
|
+
const value = JSON.parse(raw);
|
|
602
|
+
if (typeof value.secret !== "string" || !["hold", "persist", "release", "status", "stop", "stop-if-idle"].includes(
|
|
603
|
+
value.command ?? ""
|
|
604
|
+
)) {
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
return value;
|
|
608
|
+
} catch {
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
async function startDaemonControlServer(options) {
|
|
613
|
+
const metadataPath = getDaemonControlMetadataPath(options.config, options.configRoot);
|
|
614
|
+
let stopping = false;
|
|
615
|
+
let desktopHolds = 0;
|
|
616
|
+
let metadata;
|
|
617
|
+
let startupGrace;
|
|
618
|
+
const sockets = /* @__PURE__ */ new Set();
|
|
619
|
+
const requestStop = () => {
|
|
620
|
+
if (stopping) return;
|
|
621
|
+
stopping = true;
|
|
622
|
+
void Promise.resolve(options.onStop());
|
|
623
|
+
};
|
|
624
|
+
const server = createServer((socket) => {
|
|
625
|
+
sockets.add(socket);
|
|
626
|
+
socket.setEncoding("utf8");
|
|
627
|
+
let buffer = "";
|
|
628
|
+
let holding = false;
|
|
629
|
+
socket.on("data", (chunk) => {
|
|
630
|
+
buffer += chunk;
|
|
631
|
+
const newline = buffer.indexOf("\n");
|
|
632
|
+
if (newline < 0) return;
|
|
633
|
+
const request = parseRequest(buffer.slice(0, newline));
|
|
634
|
+
buffer = buffer.slice(newline + 1);
|
|
635
|
+
if (!request || request.secret !== metadata.secret) {
|
|
636
|
+
socket.end(
|
|
637
|
+
`${JSON.stringify({ ok: false, error: "unauthorized" })}
|
|
638
|
+
`
|
|
639
|
+
);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
metadata.idle = options.isIdle?.() ?? metadata.idle;
|
|
643
|
+
metadata.lastHeartbeatAt = options.getLastHeartbeatAt?.() ?? metadata.lastHeartbeatAt;
|
|
644
|
+
metadata.relayConnected = options.isRelayConnected?.() ?? metadata.relayConnected;
|
|
645
|
+
if (request.command === "persist") {
|
|
646
|
+
if (startupGrace) clearTimeout(startupGrace);
|
|
647
|
+
metadata.mode = "persistent";
|
|
648
|
+
void writeMetadata(metadataPath, metadata);
|
|
649
|
+
} else if (request.command === "hold") {
|
|
650
|
+
if (startupGrace) clearTimeout(startupGrace);
|
|
651
|
+
holding = true;
|
|
652
|
+
desktopHolds += 1;
|
|
653
|
+
} else if (request.command === "release") {
|
|
654
|
+
if (metadata.mode === "transient" && desktopHolds === 0) requestStop();
|
|
655
|
+
} else if (request.command === "stop") {
|
|
656
|
+
requestStop();
|
|
657
|
+
} else if (request.command === "stop-if-idle" && metadata.idle) {
|
|
658
|
+
requestStop();
|
|
659
|
+
}
|
|
660
|
+
socket.write(
|
|
661
|
+
`${JSON.stringify({ ok: true, status: publicStatus(metadata) })}
|
|
662
|
+
`
|
|
663
|
+
);
|
|
664
|
+
if (request.command !== "hold") socket.end();
|
|
665
|
+
});
|
|
666
|
+
socket.on("close", () => {
|
|
667
|
+
sockets.delete(socket);
|
|
668
|
+
if (!holding) return;
|
|
669
|
+
desktopHolds = Math.max(0, desktopHolds - 1);
|
|
670
|
+
if (metadata.mode === "transient" && desktopHolds === 0) requestStop();
|
|
671
|
+
});
|
|
672
|
+
});
|
|
673
|
+
await new Promise((resolve, reject) => {
|
|
674
|
+
server.once("error", reject);
|
|
675
|
+
server.listen(0, "127.0.0.1", () => {
|
|
676
|
+
server.off("error", reject);
|
|
677
|
+
resolve();
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
const address = server.address();
|
|
681
|
+
if (!address || typeof address === "string")
|
|
682
|
+
throw new Error("Failed to bind daemon control port");
|
|
683
|
+
metadata = {
|
|
684
|
+
connectorId: options.config.connectorId,
|
|
685
|
+
idle: options.isIdle?.() ?? true,
|
|
686
|
+
instanceId: randomUUID3(),
|
|
687
|
+
mode: options.mode,
|
|
688
|
+
pid: process.pid,
|
|
689
|
+
port: address.port,
|
|
690
|
+
protocolVersion: CONNECTOR_CONTROL_PROTOCOL_VERSION,
|
|
691
|
+
relayConnected: options.isRelayConnected?.() ?? false,
|
|
692
|
+
secret: randomBytes2(32).toString("base64url"),
|
|
693
|
+
serverUrl: options.config.serverUrl,
|
|
694
|
+
...options.config.spaceId ? { spaceId: options.config.spaceId } : {},
|
|
695
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
696
|
+
version: options.version ?? CONNECTOR_RUNTIME_VERSION
|
|
697
|
+
};
|
|
698
|
+
await writeMetadata(metadataPath, metadata);
|
|
699
|
+
if (metadata.mode === "transient") {
|
|
700
|
+
startupGrace = setTimeout(requestStop, options.transientGraceMs ?? 3e4);
|
|
701
|
+
}
|
|
702
|
+
return {
|
|
703
|
+
close: async () => {
|
|
704
|
+
if (startupGrace) clearTimeout(startupGrace);
|
|
705
|
+
for (const socket of sockets) socket.destroy();
|
|
706
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
707
|
+
const activeInstanceId = await readFile3(metadataPath, "utf8").then((raw) => JSON.parse(raw).instanceId).catch(() => void 0);
|
|
708
|
+
if (activeInstanceId === metadata.instanceId) {
|
|
709
|
+
await unlink3(metadataPath).catch(() => void 0);
|
|
710
|
+
}
|
|
711
|
+
},
|
|
712
|
+
setIdle: (idle) => {
|
|
713
|
+
metadata.idle = idle;
|
|
714
|
+
},
|
|
715
|
+
status: () => publicStatus(metadata)
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
var parseMetadata = (raw, expectedConnectorId) => {
|
|
719
|
+
const metadata = JSON.parse(raw);
|
|
720
|
+
if (metadata.protocolVersion !== CONNECTOR_CONTROL_PROTOCOL_VERSION || typeof metadata.port !== "number" || typeof metadata.secret !== "string" || typeof metadata.connectorId !== "string" || typeof metadata.serverUrl !== "string" || expectedConnectorId && metadata.connectorId !== expectedConnectorId) {
|
|
721
|
+
throw new Error("Invalid Connector daemon control metadata");
|
|
722
|
+
}
|
|
723
|
+
return metadata;
|
|
724
|
+
};
|
|
725
|
+
var readMetadata = async (config, configRoot) => {
|
|
726
|
+
const metadataPath = getDaemonControlMetadataPath(config, configRoot);
|
|
727
|
+
return parseMetadata(await readFile3(metadataPath, "utf8"), config.connectorId);
|
|
728
|
+
};
|
|
729
|
+
async function listPersistedDaemonStatuses(configRoot) {
|
|
730
|
+
const daemonRoot = path2.join(
|
|
731
|
+
getConnectorPaths(DEFAULT_SERVER_URL, configRoot).configDir,
|
|
732
|
+
"daemons"
|
|
733
|
+
);
|
|
734
|
+
const serverDirectories = await readdir(daemonRoot, { withFileTypes: true }).catch(() => []);
|
|
735
|
+
const statuses = await Promise.all(
|
|
736
|
+
serverDirectories.filter((entry) => entry.isDirectory()).map(async (serverEntry) => {
|
|
737
|
+
const serverDirectory = path2.join(daemonRoot, serverEntry.name);
|
|
738
|
+
const connectorEntries = await readdir(serverDirectory, { withFileTypes: true }).catch(
|
|
739
|
+
() => []
|
|
740
|
+
);
|
|
741
|
+
return Promise.all(
|
|
742
|
+
connectorEntries.filter((entry) => entry.isDirectory()).map(
|
|
743
|
+
(entry) => readFile3(
|
|
744
|
+
path2.join(serverDirectory, entry.name, "connector-daemon.control.json"),
|
|
745
|
+
"utf8"
|
|
746
|
+
).then((raw) => publicStatus(parseMetadata(raw))).catch(() => null)
|
|
747
|
+
)
|
|
748
|
+
);
|
|
749
|
+
})
|
|
750
|
+
);
|
|
751
|
+
const unique = /* @__PURE__ */ new Map();
|
|
752
|
+
for (const status of statuses.flat()) {
|
|
753
|
+
if (!status) continue;
|
|
754
|
+
unique.set(`${status.serverUrl}
|
|
755
|
+
${status.connectorId}`, status);
|
|
756
|
+
}
|
|
757
|
+
return [...unique.values()];
|
|
758
|
+
}
|
|
759
|
+
var connectControl = async (metadata, command, timeoutMs = DEFAULT_CONTROL_REQUEST_TIMEOUT_MS) => new Promise((resolve, reject) => {
|
|
760
|
+
const socket = createConnection({ host: "127.0.0.1", port: metadata.port });
|
|
761
|
+
let buffer = "";
|
|
762
|
+
let settled = false;
|
|
763
|
+
const cleanup = () => {
|
|
764
|
+
socket.setTimeout(0);
|
|
765
|
+
socket.off("data", onData);
|
|
766
|
+
socket.off("error", fail);
|
|
767
|
+
socket.off("timeout", onTimeout);
|
|
768
|
+
};
|
|
769
|
+
const fail = (error) => {
|
|
770
|
+
if (settled) return;
|
|
771
|
+
settled = true;
|
|
772
|
+
cleanup();
|
|
773
|
+
socket.destroy();
|
|
774
|
+
reject(error);
|
|
775
|
+
};
|
|
776
|
+
const onTimeout = () => {
|
|
777
|
+
fail(new Error(`Connector daemon control timed out after ${timeoutMs}ms`));
|
|
778
|
+
};
|
|
779
|
+
const onData = (chunk) => {
|
|
780
|
+
buffer += chunk;
|
|
781
|
+
const newline = buffer.indexOf("\n");
|
|
782
|
+
if (newline < 0) return;
|
|
783
|
+
try {
|
|
784
|
+
const response = JSON.parse(buffer.slice(0, newline));
|
|
785
|
+
if (!response.ok) return fail(new Error(response.error ?? "Daemon control request failed"));
|
|
786
|
+
settled = true;
|
|
787
|
+
cleanup();
|
|
788
|
+
resolve({ response, socket });
|
|
789
|
+
} catch (error) {
|
|
790
|
+
fail(error instanceof Error ? error : new Error(String(error)));
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
socket.once("error", fail);
|
|
794
|
+
socket.once("timeout", onTimeout);
|
|
795
|
+
socket.setTimeout(timeoutMs);
|
|
796
|
+
socket.once("connect", () => {
|
|
797
|
+
socket.write(
|
|
798
|
+
`${JSON.stringify({ command, secret: metadata.secret })}
|
|
799
|
+
`
|
|
800
|
+
);
|
|
801
|
+
});
|
|
802
|
+
socket.setEncoding("utf8");
|
|
803
|
+
socket.on("data", onData);
|
|
804
|
+
});
|
|
805
|
+
async function readPersistedDaemonStatus(config, configRoot) {
|
|
806
|
+
return publicStatus(await readMetadata(config, configRoot));
|
|
807
|
+
}
|
|
808
|
+
async function requestDaemonControl(config, command, configRoot, options = {}) {
|
|
809
|
+
const metadata = await readMetadata(config, configRoot);
|
|
810
|
+
const { response, socket } = await connectControl(metadata, command, options.timeoutMs);
|
|
811
|
+
socket.end();
|
|
812
|
+
if (!response.status) throw new Error("Daemon control response did not include status");
|
|
813
|
+
return response.status;
|
|
814
|
+
}
|
|
815
|
+
async function holdDaemonControl(config, configRoot) {
|
|
816
|
+
const metadata = await readMetadata(config, configRoot);
|
|
817
|
+
const { response, socket } = await connectControl(metadata, "hold");
|
|
818
|
+
if (!response.status) {
|
|
819
|
+
socket.destroy();
|
|
820
|
+
throw new Error("Daemon hold response did not include status");
|
|
821
|
+
}
|
|
822
|
+
const closed = new Promise((resolve) => socket.once("close", () => resolve()));
|
|
823
|
+
return { close: () => socket.end(), closed, status: response.status };
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// src/console-dashboard.ts
|
|
827
|
+
var MAX_LINES_PER_PANE = 500;
|
|
828
|
+
var MIN_DASHBOARD_WIDTH = 60;
|
|
829
|
+
var MIN_DASHBOARD_HEIGHT = 18;
|
|
830
|
+
var DASHBOARD_TITLE = `Buda Connector v${CONNECTOR_RUNTIME_VERSION}`;
|
|
831
|
+
var colors = {
|
|
832
|
+
cyan: "\x1B[36m",
|
|
833
|
+
dim: "\x1B[2m",
|
|
834
|
+
red: "\x1B[31m",
|
|
835
|
+
reset: "\x1B[0m",
|
|
836
|
+
reverse: "\x1B[7m",
|
|
837
|
+
yellow: "\x1B[33m"
|
|
838
|
+
};
|
|
839
|
+
var openControllingTerminal = () => {
|
|
840
|
+
const inputPath = process.platform === "win32" ? "CONIN$" : "/dev/tty";
|
|
841
|
+
const outputPath = process.platform === "win32" ? "CONOUT$" : "/dev/tty";
|
|
842
|
+
let inputFd;
|
|
843
|
+
let outputFd;
|
|
844
|
+
try {
|
|
845
|
+
inputFd = openSync(inputPath, "r");
|
|
846
|
+
outputFd = openSync(outputPath, "w");
|
|
847
|
+
const input = new ReadStream(inputFd);
|
|
848
|
+
const output = new WriteStream(outputFd);
|
|
849
|
+
return {
|
|
850
|
+
input,
|
|
851
|
+
output,
|
|
852
|
+
close: () => {
|
|
853
|
+
input.destroy();
|
|
854
|
+
output.destroy();
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
} catch {
|
|
858
|
+
if (inputFd !== void 0) closeSync(inputFd);
|
|
859
|
+
if (outputFd !== void 0) closeSync(outputFd);
|
|
860
|
+
return void 0;
|
|
861
|
+
}
|
|
862
|
+
};
|
|
863
|
+
var resolveDashboardStreams = () => {
|
|
864
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
865
|
+
return { input: process.stdin, output: process.stdout };
|
|
866
|
+
}
|
|
867
|
+
if (process.stdin.isTTY && process.stderr.isTTY) {
|
|
868
|
+
return { input: process.stdin, output: process.stderr };
|
|
869
|
+
}
|
|
870
|
+
return openControllingTerminal();
|
|
871
|
+
};
|
|
872
|
+
var sanitize = (value) => Array.from(stripVTControlCharacters(value).replaceAll("\r", "")).filter((character) => {
|
|
873
|
+
const code = character.charCodeAt(0);
|
|
874
|
+
return code === 9 || code >= 32;
|
|
875
|
+
}).join("");
|
|
876
|
+
var truncate = (value, width) => {
|
|
877
|
+
if (width <= 0) return "";
|
|
878
|
+
if (value.length <= width) return value.padEnd(width);
|
|
879
|
+
if (width === 1) return value.slice(0, 1);
|
|
880
|
+
return `${value.slice(0, width - 1)}~`;
|
|
881
|
+
};
|
|
882
|
+
var formatDuration = (durationMs) => {
|
|
883
|
+
const totalSeconds = Math.max(0, Math.floor(durationMs / 1e3));
|
|
884
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
885
|
+
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
886
|
+
const seconds = totalSeconds % 60;
|
|
887
|
+
return [hours, minutes, seconds].map((part) => String(part).padStart(2, "0")).join(":");
|
|
888
|
+
};
|
|
889
|
+
var createDashboardState = (startedAt = Date.now()) => ({
|
|
890
|
+
activeSection: "activity",
|
|
891
|
+
activePane: "activity",
|
|
892
|
+
activity: [],
|
|
893
|
+
agentDebug: [],
|
|
894
|
+
connectorLogs: {},
|
|
895
|
+
connectors: [],
|
|
896
|
+
heartbeat: "Waiting",
|
|
897
|
+
relay: "Starting",
|
|
898
|
+
agent: "Starting",
|
|
899
|
+
server: "Unknown",
|
|
900
|
+
startedAt,
|
|
901
|
+
scroll: { activity: 0, agentDebug: 0 }
|
|
902
|
+
});
|
|
903
|
+
var classifyDashboardLine = (rawLine) => {
|
|
904
|
+
const line = sanitize(rawLine).trimEnd();
|
|
905
|
+
const prefix = "[buda-connector] ";
|
|
906
|
+
if (line.startsWith(prefix)) {
|
|
907
|
+
const message = line.slice(prefix.length);
|
|
908
|
+
const routineHealthCheck = /^relay request GET \/healthz\b/i.test(message);
|
|
909
|
+
return { pane: routineHealthCheck ? "agentDebug" : "activity", message };
|
|
910
|
+
}
|
|
911
|
+
return { pane: "agentDebug", message: line };
|
|
912
|
+
};
|
|
913
|
+
var classifyDashboardLevel = (fallback, formattedMessage, rawLine) => {
|
|
914
|
+
const normalizedLine = sanitize(rawLine).toLowerCase();
|
|
915
|
+
if (normalizedLine.includes("[error]") || normalizedLine.includes("[fatal]")) return "error";
|
|
916
|
+
if (normalizedLine.includes("[warning]") || normalizedLine.includes("[warn]")) return "warn";
|
|
917
|
+
const benignBunnyDiagnostic = [
|
|
918
|
+
"[bunny-agent:pi] getappendsystemprompt:",
|
|
919
|
+
"[bunny-agent:pi] loadskills",
|
|
920
|
+
"[bunny-agent:pi] resume:",
|
|
921
|
+
"[bunny-agent:pi] runner:"
|
|
922
|
+
].some((prefix) => formattedMessage.toLowerCase().startsWith(prefix));
|
|
923
|
+
return benignBunnyDiagnostic ? "info" : fallback;
|
|
924
|
+
};
|
|
925
|
+
var updateStatusFromActivity = (state, message) => {
|
|
926
|
+
const normalized = message.toLowerCase();
|
|
927
|
+
if (normalized.startsWith("server ")) return { ...state, server: message.slice(7) };
|
|
928
|
+
if (normalized.startsWith("relay connected")) return { ...state, relay: "Connected" };
|
|
929
|
+
if (normalized.startsWith("relay reconnecting")) return { ...state, relay: "Retrying" };
|
|
930
|
+
if (normalized.startsWith("relay access revoked"))
|
|
931
|
+
return { ...state, relay: "Waiting for access" };
|
|
932
|
+
if (normalized.startsWith("relay disconnected") || normalized.startsWith("relay error") || normalized.startsWith("relay ping failed")) {
|
|
933
|
+
return { ...state, relay: "Disconnected" };
|
|
934
|
+
}
|
|
935
|
+
if (normalized.startsWith("bunny daemon listening") || normalized.startsWith("bunny daemon http")) {
|
|
936
|
+
return { ...state, agent: "Ready" };
|
|
937
|
+
}
|
|
938
|
+
if (normalized.startsWith("bunny daemon stopped")) return { ...state, agent: "Stopped" };
|
|
939
|
+
if (normalized.startsWith("heartbeat failed")) return { ...state, heartbeat: "Failed" };
|
|
940
|
+
return state;
|
|
941
|
+
};
|
|
942
|
+
var appendDashboardLine = (state, pane, level, message, time = (/* @__PURE__ */ new Date()).toLocaleTimeString("en-GB", { hour12: false })) => {
|
|
943
|
+
const current = state[pane];
|
|
944
|
+
const last = current.at(-1);
|
|
945
|
+
const nextLines = last?.message === message && last.level === level ? [...current.slice(0, -1), { ...last, count: last.count + 1, time }] : [...current, { count: 1, level, message, time }];
|
|
946
|
+
const trimmed = nextLines.slice(-MAX_LINES_PER_PANE);
|
|
947
|
+
const scroll = state.scroll[pane] > 0 ? { ...state.scroll, [pane]: Math.min(trimmed.length - 1, state.scroll[pane] + 1) } : state.scroll;
|
|
948
|
+
const next = { ...state, [pane]: trimmed, scroll };
|
|
949
|
+
return pane === "activity" ? updateStatusFromActivity(next, message) : next;
|
|
950
|
+
};
|
|
951
|
+
var scrollDashboardPane = (state, pane, delta) => {
|
|
952
|
+
const maxScroll = Math.max(0, state[pane].length - 1);
|
|
953
|
+
const nextScroll = delta === "follow" ? 0 : delta === "oldest" ? maxScroll : Math.max(0, Math.min(maxScroll, state.scroll[pane] + delta));
|
|
954
|
+
return { ...state, scroll: { ...state.scroll, [pane]: nextScroll } };
|
|
955
|
+
};
|
|
956
|
+
var selectedConnector = (state) => state.connectors.find((connector) => connector.connectorId === state.selectedConnectorId);
|
|
957
|
+
var applySelectedConnector = (state, resetScroll = true) => {
|
|
958
|
+
const connector = selectedConnector(state);
|
|
959
|
+
const logs = connector ? state.connectorLogs[connector.connectorId] : void 0;
|
|
960
|
+
return {
|
|
961
|
+
...state,
|
|
962
|
+
activity: logs?.activity ?? [],
|
|
963
|
+
agent: connector?.agent ?? "Stopped",
|
|
964
|
+
agentDebug: logs?.agentDebug ?? [],
|
|
965
|
+
heartbeat: connector?.heartbeat ?? "Stopped",
|
|
966
|
+
relay: connector?.relay ?? "Disconnected",
|
|
967
|
+
server: connector?.serverUrl ?? "Unknown",
|
|
968
|
+
scroll: resetScroll ? { activity: 0, agentDebug: 0 } : state.scroll
|
|
969
|
+
};
|
|
970
|
+
};
|
|
971
|
+
var updateDashboardConnectors = (state, connectors, preferredConnectorId) => {
|
|
972
|
+
const selectedStillExists = connectors.some(
|
|
973
|
+
(connector) => connector.connectorId === state.selectedConnectorId
|
|
974
|
+
);
|
|
975
|
+
const selectedConnectorId = selectedStillExists ? state.selectedConnectorId : connectors.find((connector) => connector.connectorId === preferredConnectorId)?.connectorId ?? connectors[0]?.connectorId;
|
|
976
|
+
return applySelectedConnector(
|
|
977
|
+
{
|
|
978
|
+
...state,
|
|
979
|
+
activeSection: connectors.length > 0 && state.connectors.length === 0 ? "connectors" : state.activeSection,
|
|
980
|
+
connectors,
|
|
981
|
+
selectedConnectorId
|
|
982
|
+
},
|
|
983
|
+
selectedConnectorId !== state.selectedConnectorId
|
|
984
|
+
);
|
|
985
|
+
};
|
|
986
|
+
var selectConnectorByOffset = (state, offset) => {
|
|
987
|
+
if (state.connectors.length === 0) return state;
|
|
988
|
+
const currentIndex = Math.max(
|
|
989
|
+
0,
|
|
990
|
+
state.connectors.findIndex((connector) => connector.connectorId === state.selectedConnectorId)
|
|
991
|
+
);
|
|
992
|
+
const nextIndex = Math.max(0, Math.min(state.connectors.length - 1, currentIndex + offset));
|
|
993
|
+
return applySelectedConnector({
|
|
994
|
+
...state,
|
|
995
|
+
selectedConnectorId: state.connectors[nextIndex]?.connectorId
|
|
996
|
+
});
|
|
997
|
+
};
|
|
998
|
+
var promptOptionCount = (prompt) => prompt.kind === "exit-choice" ? 3 : 2;
|
|
999
|
+
var isUpKey = (key) => ["\x1B[A", "\x1BOA", "k", "K"].includes(key);
|
|
1000
|
+
var isDownKey = (key) => ["\x1B[B", "\x1BOB", "j", "J"].includes(key);
|
|
1001
|
+
var getConnectorSectionHeight = (state, height) => state.connectors.length ? Math.min(
|
|
1002
|
+
Math.max(4, state.connectors.length + 2),
|
|
1003
|
+
Math.max(4, Math.min(Math.floor(height / 3), height - 13))
|
|
1004
|
+
) : 0;
|
|
1005
|
+
var parseDashboardMouseWheel = (input, state, size) => {
|
|
1006
|
+
const event = input.split("\x1B[<").at(-1);
|
|
1007
|
+
const match = event?.match(/^(64|65);\d+;(\d+)[Mm]/);
|
|
1008
|
+
if (!match) return void 0;
|
|
1009
|
+
const row = Number(match[2]);
|
|
1010
|
+
const connectorHeight = getConnectorSectionHeight(state, size.height);
|
|
1011
|
+
const availablePaneRows = size.height - 7 - connectorHeight;
|
|
1012
|
+
const activityHeight = Math.floor(availablePaneRows / 2);
|
|
1013
|
+
const activityStartRow = 7 + connectorHeight;
|
|
1014
|
+
const debugStartRow = activityStartRow + activityHeight;
|
|
1015
|
+
const pane = row >= activityStartRow && row < debugStartRow ? "activity" : row >= debugStartRow && row < size.height ? "agentDebug" : void 0;
|
|
1016
|
+
if (!pane) return void 0;
|
|
1017
|
+
return { delta: match[1] === "64" ? 3 : -3, pane };
|
|
1018
|
+
};
|
|
1019
|
+
var reduceDashboardInput = (state, key, managerEnabled) => {
|
|
1020
|
+
if (state.prompt) {
|
|
1021
|
+
if (key === "\x1B") return { state: { ...state, prompt: void 0 } };
|
|
1022
|
+
if (isUpKey(key) || isDownKey(key)) {
|
|
1023
|
+
const delta = isUpKey(key) ? -1 : 1;
|
|
1024
|
+
const optionCount = promptOptionCount(state.prompt);
|
|
1025
|
+
const option = (state.prompt.option + delta + optionCount) % optionCount;
|
|
1026
|
+
return { state: { ...state, prompt: { ...state.prompt, option } } };
|
|
1027
|
+
}
|
|
1028
|
+
if (key !== "\r" && key !== "\n") return { state };
|
|
1029
|
+
if (state.prompt.kind === "stop-selected") {
|
|
1030
|
+
const connector = selectedConnector(state);
|
|
1031
|
+
return state.prompt.option === 1 && connector ? {
|
|
1032
|
+
command: {
|
|
1033
|
+
connectorId: connector.connectorId,
|
|
1034
|
+
serverUrl: connector.serverUrl,
|
|
1035
|
+
type: "stop-selected"
|
|
1036
|
+
},
|
|
1037
|
+
state: { ...state, prompt: void 0 }
|
|
1038
|
+
} : { state: { ...state, prompt: void 0 } };
|
|
1039
|
+
}
|
|
1040
|
+
if (state.prompt.kind === "stop-all-confirm") {
|
|
1041
|
+
return state.prompt.option === 1 ? { command: { type: "stop-all" }, state: { ...state, prompt: void 0 } } : { state: { ...state, prompt: void 0 } };
|
|
1042
|
+
}
|
|
1043
|
+
if (state.prompt.option === 0) {
|
|
1044
|
+
return { state: { ...state, prompt: { kind: "stop-all-confirm", option: 0 } } };
|
|
1045
|
+
}
|
|
1046
|
+
if (state.prompt.option === 1) {
|
|
1047
|
+
return { command: { type: "exit-dashboard" }, state: { ...state, prompt: void 0 } };
|
|
1048
|
+
}
|
|
1049
|
+
return { state: { ...state, prompt: void 0 } };
|
|
1050
|
+
}
|
|
1051
|
+
if (managerEnabled && key === "") {
|
|
1052
|
+
return { state: { ...state, prompt: { kind: "exit-choice", option: 1 } } };
|
|
1053
|
+
}
|
|
1054
|
+
if (managerEnabled && key === "\x1B") {
|
|
1055
|
+
return { command: { type: "exit-dashboard" }, state };
|
|
1056
|
+
}
|
|
1057
|
+
if (managerEnabled && (key === "s" || key === "S") && ["Online", "Running"].includes(selectedConnector(state)?.status ?? "")) {
|
|
1058
|
+
return { state: { ...state, prompt: { kind: "stop-selected", option: 0 } } };
|
|
1059
|
+
}
|
|
1060
|
+
if (key === " ") {
|
|
1061
|
+
const sections = managerEnabled ? ["connectors", "activity", "agentDebug"] : ["activity", "agentDebug"];
|
|
1062
|
+
const index = sections.indexOf(state.activeSection);
|
|
1063
|
+
const activeSection = sections[(index + 1) % sections.length] ?? sections[0];
|
|
1064
|
+
return {
|
|
1065
|
+
state: {
|
|
1066
|
+
...state,
|
|
1067
|
+
activePane: activeSection === "connectors" ? state.activePane : activeSection,
|
|
1068
|
+
activeSection
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
if (state.activeSection === "connectors" && (isUpKey(key) || isDownKey(key))) {
|
|
1073
|
+
return { state: selectConnectorByOffset(state, isUpKey(key) ? -1 : 1) };
|
|
1074
|
+
}
|
|
1075
|
+
const pane = state.activePane;
|
|
1076
|
+
if (key === "f" || key === "F" || key === "\x1B[F") {
|
|
1077
|
+
return { state: scrollDashboardPane(state, pane, "follow") };
|
|
1078
|
+
}
|
|
1079
|
+
if (key === "\x1B[H") return { state: scrollDashboardPane(state, pane, "oldest") };
|
|
1080
|
+
if (isUpKey(key)) return { state: scrollDashboardPane(state, pane, 1) };
|
|
1081
|
+
if (isDownKey(key)) return { state: scrollDashboardPane(state, pane, -1) };
|
|
1082
|
+
if (key === "\x1B[5~") return { state: scrollDashboardPane(state, pane, 8) };
|
|
1083
|
+
if (key === "\x1B[6~") return { state: scrollDashboardPane(state, pane, -8) };
|
|
1084
|
+
return { state };
|
|
1085
|
+
};
|
|
1086
|
+
var renderStatus = (state, width, now) => {
|
|
1087
|
+
const innerWidth = width - 2;
|
|
1088
|
+
const status = (value) => value;
|
|
1089
|
+
const connector = selectedConnector(state);
|
|
1090
|
+
return [
|
|
1091
|
+
`+${"-".repeat(innerWidth)}+`,
|
|
1092
|
+
`|${colors.cyan}${truncate(` ${DASHBOARD_TITLE}`, innerWidth)}${colors.reset}|`,
|
|
1093
|
+
`|${truncate(` Connector ${connector?.label ?? connector?.connectorId ?? "None"} Space ${connector?.spaceId ?? "Unknown"}`, innerWidth)}|`,
|
|
1094
|
+
`|${truncate(` Relay ${status(state.relay)} Agent ${status(state.agent)} Heartbeat ${status(state.heartbeat)}`, innerWidth)}|`,
|
|
1095
|
+
`|${truncate(` Server ${state.server} Uptime ${formatDuration(now - state.startedAt)}`, innerWidth)}|`,
|
|
1096
|
+
`+${"-".repeat(innerWidth)}+`
|
|
1097
|
+
];
|
|
1098
|
+
};
|
|
1099
|
+
var renderConnectors = (state, height, width) => {
|
|
1100
|
+
const innerWidth = width - 2;
|
|
1101
|
+
const contentHeight = Math.max(1, height - 2);
|
|
1102
|
+
const selectedIndex = Math.max(
|
|
1103
|
+
0,
|
|
1104
|
+
state.connectors.findIndex((connector) => connector.connectorId === state.selectedConnectorId)
|
|
1105
|
+
);
|
|
1106
|
+
const start = Math.max(
|
|
1107
|
+
0,
|
|
1108
|
+
Math.min(
|
|
1109
|
+
selectedIndex - Math.floor(contentHeight / 2),
|
|
1110
|
+
state.connectors.length - contentHeight
|
|
1111
|
+
)
|
|
1112
|
+
);
|
|
1113
|
+
const visible = state.connectors.slice(start, start + contentHeight);
|
|
1114
|
+
const range = state.connectors.length ? `${start + 1}-${start + visible.length}/${state.connectors.length}` : "0/0";
|
|
1115
|
+
const title = ` Connectors ${state.activeSection === "connectors" ? "[active]" : ""}`;
|
|
1116
|
+
const headerFill = Math.max(1, innerWidth - title.length - range.length);
|
|
1117
|
+
const output = [
|
|
1118
|
+
`${state.activeSection === "connectors" ? colors.reverse : ""}+${title}${"-".repeat(headerFill)}${range}+${colors.reset}`
|
|
1119
|
+
];
|
|
1120
|
+
for (let index = 0; index < contentHeight; index += 1) {
|
|
1121
|
+
const connector = visible[index];
|
|
1122
|
+
if (!connector) {
|
|
1123
|
+
output.push(`|${" ".repeat(innerWidth)}|`);
|
|
1124
|
+
continue;
|
|
1125
|
+
}
|
|
1126
|
+
const marker = connector.connectorId === state.selectedConnectorId ? ">" : " ";
|
|
1127
|
+
const statusText = connector.status.padEnd(8);
|
|
1128
|
+
const availableFieldWidth = Math.max(24, innerWidth - 18);
|
|
1129
|
+
const connectorWidth = Math.max(10, Math.floor(availableFieldWidth * 0.4));
|
|
1130
|
+
const spaceWidth = Math.max(8, Math.floor(availableFieldWidth * 0.25));
|
|
1131
|
+
const serverWidth = Math.max(8, availableFieldWidth - connectorWidth - spaceWidth);
|
|
1132
|
+
const content = `${marker} ${statusText} ${truncate(connector.label ?? connector.connectorId, connectorWidth)} | ${truncate(connector.spaceId, spaceWidth)} | ${truncate(connector.serverUrl, serverWidth)}`;
|
|
1133
|
+
const color = connector.status === "Stopped" ? colors.dim : connector.status === "Online" ? colors.cyan : connector.status === "Connecting" || connector.status === "Stopping" ? colors.yellow : connector.status === "Error" || connector.status === "Unresponsive" ? colors.red : "";
|
|
1134
|
+
output.push(`|${color}${truncate(content, innerWidth)}${colors.reset}|`);
|
|
1135
|
+
}
|
|
1136
|
+
output.push(`+${"-".repeat(innerWidth)}+`);
|
|
1137
|
+
return output;
|
|
1138
|
+
};
|
|
1139
|
+
var renderPane = (title, lines, height, width, active, scroll) => {
|
|
1140
|
+
const innerWidth = width - 2;
|
|
1141
|
+
const contentHeight = Math.max(1, height - 2);
|
|
1142
|
+
const end = Math.max(0, lines.length - scroll);
|
|
1143
|
+
const visible = lines.slice(Math.max(0, end - contentHeight), end);
|
|
1144
|
+
const headerSuffix = scroll === 0 ? "following" : `${scroll} lines behind`;
|
|
1145
|
+
const titleText = ` ${title} ${active ? "[active]" : ""}`;
|
|
1146
|
+
const headerFill = Math.max(1, innerWidth - titleText.length - headerSuffix.length);
|
|
1147
|
+
const output = [
|
|
1148
|
+
`${active ? colors.reverse : ""}+${titleText}${"-".repeat(headerFill)}${headerSuffix}+${colors.reset}`
|
|
1149
|
+
];
|
|
1150
|
+
for (let index = 0; index < contentHeight; index += 1) {
|
|
1151
|
+
const line = visible[index];
|
|
1152
|
+
if (!line) {
|
|
1153
|
+
output.push(`|${" ".repeat(innerWidth)}|`);
|
|
1154
|
+
continue;
|
|
1155
|
+
}
|
|
1156
|
+
const levelColor = line.level === "error" ? colors.red : line.level === "warn" ? colors.yellow : colors.dim;
|
|
1157
|
+
const count = line.count > 1 ? ` x${line.count}` : "";
|
|
1158
|
+
const content = `${line.time} ${line.level.toUpperCase().padEnd(5)} ${line.message}${count}`;
|
|
1159
|
+
output.push(`|${levelColor}${truncate(content, innerWidth)}${colors.reset}|`);
|
|
1160
|
+
}
|
|
1161
|
+
output.push(`+${"-".repeat(innerWidth)}+`);
|
|
1162
|
+
return output;
|
|
1163
|
+
};
|
|
1164
|
+
var renderFooter = (state, width) => {
|
|
1165
|
+
const managerKeys = state.connectors.length ? "Up/Down select | S stop | Esc exit | " : "";
|
|
1166
|
+
return `${colors.dim}${truncate(` ${managerKeys}Tab switch pane | F follow`, width)}${colors.reset}`;
|
|
1167
|
+
};
|
|
1168
|
+
var renderPrompt = (state, size) => {
|
|
1169
|
+
const width = Math.max(10, size.width);
|
|
1170
|
+
const height = Math.max(4, size.height);
|
|
1171
|
+
const connector = selectedConnector(state);
|
|
1172
|
+
const prompt = state.prompt;
|
|
1173
|
+
if (!prompt) return "";
|
|
1174
|
+
const content = prompt.kind === "exit-choice" ? {
|
|
1175
|
+
options: ["Stop all local Connectors and exit", "Exit Dashboard only", "Cancel"],
|
|
1176
|
+
title: "Exit Buda Connector Dashboard?",
|
|
1177
|
+
warning: "Dashboard exit does not stop background Connector processes."
|
|
1178
|
+
} : prompt.kind === "stop-all-confirm" ? {
|
|
1179
|
+
options: ["Cancel", "Stop all local Connectors and exit"],
|
|
1180
|
+
title: "Stop every local Connector?",
|
|
1181
|
+
warning: "Running agent tasks will be interrupted."
|
|
1182
|
+
} : {
|
|
1183
|
+
options: ["Cancel", "Stop selected Connector"],
|
|
1184
|
+
title: `Stop ${connector?.connectorId ?? "selected Connector"}?`,
|
|
1185
|
+
warning: "The Dashboard will remain open."
|
|
1186
|
+
};
|
|
1187
|
+
const lines = [
|
|
1188
|
+
content.title,
|
|
1189
|
+
"",
|
|
1190
|
+
content.warning,
|
|
1191
|
+
"",
|
|
1192
|
+
...content.options.map((option, index) => `${index === prompt.option ? ">" : " "} ${option}`),
|
|
1193
|
+
"",
|
|
1194
|
+
"Up/Down choose | Enter confirm | Esc cancel"
|
|
1195
|
+
];
|
|
1196
|
+
const visible = lines.slice(0, height);
|
|
1197
|
+
while (visible.length < height) visible.push("");
|
|
1198
|
+
return visible.map((line) => truncate(line, width)).join("\n");
|
|
1199
|
+
};
|
|
1200
|
+
var renderDashboard = (state, size, now = Date.now()) => {
|
|
1201
|
+
const width = Math.max(10, size.width);
|
|
1202
|
+
const height = Math.max(4, size.height);
|
|
1203
|
+
if (width < MIN_DASHBOARD_WIDTH || height < MIN_DASHBOARD_HEIGHT) {
|
|
1204
|
+
return [
|
|
1205
|
+
truncate(DASHBOARD_TITLE, width),
|
|
1206
|
+
truncate("Terminal too small for dashboard. Resize or restart with --plain.", width),
|
|
1207
|
+
truncate(`Relay ${state.relay} | Agent ${state.agent}`, width),
|
|
1208
|
+
truncate(`Heartbeat ${state.heartbeat}`, width)
|
|
1209
|
+
].slice(0, height).join("\n");
|
|
1210
|
+
}
|
|
1211
|
+
if (state.prompt) return renderPrompt(state, size);
|
|
1212
|
+
const connectorHeight = getConnectorSectionHeight(state, height);
|
|
1213
|
+
const availablePaneRows = height - 7 - connectorHeight;
|
|
1214
|
+
const activityHeight = Math.floor(availablePaneRows / 2);
|
|
1215
|
+
const debugHeight = availablePaneRows - activityHeight;
|
|
1216
|
+
return [
|
|
1217
|
+
...renderStatus(state, width, now),
|
|
1218
|
+
...connectorHeight ? renderConnectors(state, connectorHeight, width) : [],
|
|
1219
|
+
...renderPane(
|
|
1220
|
+
"Activity",
|
|
1221
|
+
state.activity,
|
|
1222
|
+
activityHeight,
|
|
1223
|
+
width,
|
|
1224
|
+
state.activeSection === "activity",
|
|
1225
|
+
state.scroll.activity
|
|
1226
|
+
),
|
|
1227
|
+
...renderPane(
|
|
1228
|
+
"Agent Debug",
|
|
1229
|
+
state.agentDebug,
|
|
1230
|
+
debugHeight,
|
|
1231
|
+
width,
|
|
1232
|
+
state.activeSection === "agentDebug",
|
|
1233
|
+
state.scroll.agentDebug
|
|
1234
|
+
),
|
|
1235
|
+
renderFooter(state, width)
|
|
1236
|
+
].join("\n");
|
|
1237
|
+
};
|
|
1238
|
+
var ConnectorConsoleDashboard = class {
|
|
1239
|
+
constructor(streams, manager) {
|
|
1240
|
+
this.streams = streams;
|
|
1241
|
+
this.manager = manager;
|
|
1242
|
+
}
|
|
1243
|
+
state = createDashboardState();
|
|
1244
|
+
stopped = false;
|
|
1245
|
+
renderQueued = false;
|
|
1246
|
+
originalConsole = {
|
|
1247
|
+
error: console.error,
|
|
1248
|
+
log: console.log,
|
|
1249
|
+
warn: console.warn
|
|
1250
|
+
};
|
|
1251
|
+
start() {
|
|
1252
|
+
console.log = (...args) => this.capture("info", args);
|
|
1253
|
+
console.warn = (...args) => this.capture("warn", args);
|
|
1254
|
+
console.error = (...args) => this.capture("error", args);
|
|
1255
|
+
this.streams.output.write("\x1B[?1049h\x1B[?25l\x1B[?1000h\x1B[?1006h");
|
|
1256
|
+
if (this.streams.input.isTTY) this.streams.input.setRawMode(true);
|
|
1257
|
+
this.streams.input.resume();
|
|
1258
|
+
this.streams.input.on("data", this.handleInput);
|
|
1259
|
+
this.streams.output.on("resize", this.scheduleRender);
|
|
1260
|
+
process.once("exit", this.stop);
|
|
1261
|
+
this.scheduleRender();
|
|
1262
|
+
}
|
|
1263
|
+
updateStatus(update) {
|
|
1264
|
+
this.state = { ...this.state, ...update };
|
|
1265
|
+
this.scheduleRender();
|
|
1266
|
+
}
|
|
1267
|
+
updateConnectors(connectors, preferredConnectorId) {
|
|
1268
|
+
this.state = updateDashboardConnectors(this.state, connectors, preferredConnectorId);
|
|
1269
|
+
this.scheduleRender();
|
|
1270
|
+
}
|
|
1271
|
+
appendConnectorLog(connectorId, level, raw) {
|
|
1272
|
+
const current = this.state.connectorLogs[connectorId] ?? { activity: [], agentDebug: [] };
|
|
1273
|
+
let connectorState = { ...this.state, ...current };
|
|
1274
|
+
const formatted = raw;
|
|
1275
|
+
for (const rawLine of formatted.split("\n")) {
|
|
1276
|
+
const { pane, message } = classifyDashboardLine(rawLine);
|
|
1277
|
+
if (!message) continue;
|
|
1278
|
+
connectorState = appendDashboardLine(
|
|
1279
|
+
connectorState,
|
|
1280
|
+
pane,
|
|
1281
|
+
classifyDashboardLevel(level, formatted, rawLine),
|
|
1282
|
+
message
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
const connectorLogs = {
|
|
1286
|
+
...this.state.connectorLogs,
|
|
1287
|
+
[connectorId]: {
|
|
1288
|
+
activity: connectorState.activity,
|
|
1289
|
+
agentDebug: connectorState.agentDebug
|
|
1290
|
+
}
|
|
1291
|
+
};
|
|
1292
|
+
this.state = {
|
|
1293
|
+
...this.state,
|
|
1294
|
+
connectorLogs,
|
|
1295
|
+
...connectorId === this.state.selectedConnectorId ? { activity: connectorState.activity, agentDebug: connectorState.agentDebug } : {}
|
|
1296
|
+
};
|
|
1297
|
+
this.scheduleRender();
|
|
1298
|
+
}
|
|
1299
|
+
stop = () => {
|
|
1300
|
+
if (this.stopped) return;
|
|
1301
|
+
this.stopped = true;
|
|
1302
|
+
console.log = this.originalConsole.log;
|
|
1303
|
+
console.warn = this.originalConsole.warn;
|
|
1304
|
+
console.error = this.originalConsole.error;
|
|
1305
|
+
this.streams.input.off("data", this.handleInput);
|
|
1306
|
+
this.streams.output.off("resize", this.scheduleRender);
|
|
1307
|
+
process.off("exit", this.stop);
|
|
1308
|
+
if (this.streams.input.isTTY) this.streams.input.setRawMode(false);
|
|
1309
|
+
this.streams.input.pause();
|
|
1310
|
+
this.streams.output.write("\x1B[?1006l\x1B[?1000l\x1B[?25h\x1B[?1049l");
|
|
1311
|
+
this.streams.close?.();
|
|
1312
|
+
if (activeDashboard === this) activeDashboard = void 0;
|
|
1313
|
+
};
|
|
1314
|
+
capture(level, args) {
|
|
1315
|
+
const formatted = format(...args);
|
|
1316
|
+
for (const rawLine of formatted.split("\n")) {
|
|
1317
|
+
const { pane, message } = classifyDashboardLine(rawLine);
|
|
1318
|
+
if (!message) continue;
|
|
1319
|
+
this.state = appendDashboardLine(
|
|
1320
|
+
this.state,
|
|
1321
|
+
pane,
|
|
1322
|
+
classifyDashboardLevel(level, formatted, rawLine),
|
|
1323
|
+
message
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
this.scheduleRender();
|
|
1327
|
+
}
|
|
1328
|
+
handleInput = (chunk) => {
|
|
1329
|
+
const key = chunk.toString();
|
|
1330
|
+
if (key === "" && !this.manager) {
|
|
1331
|
+
this.stop();
|
|
1332
|
+
process.kill(process.pid, "SIGINT");
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
const mouseWheel = parseDashboardMouseWheel(key, this.state, {
|
|
1336
|
+
height: this.streams.output.rows || 24,
|
|
1337
|
+
width: this.streams.output.columns || 80
|
|
1338
|
+
});
|
|
1339
|
+
if (mouseWheel) {
|
|
1340
|
+
this.state = scrollDashboardPane(
|
|
1341
|
+
{
|
|
1342
|
+
...this.state,
|
|
1343
|
+
activePane: mouseWheel.pane,
|
|
1344
|
+
activeSection: mouseWheel.pane
|
|
1345
|
+
},
|
|
1346
|
+
mouseWheel.pane,
|
|
1347
|
+
mouseWheel.delta
|
|
1348
|
+
);
|
|
1349
|
+
this.scheduleRender();
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
const result = reduceDashboardInput(this.state, key, Boolean(this.manager));
|
|
1353
|
+
this.state = result.state;
|
|
1354
|
+
this.scheduleRender();
|
|
1355
|
+
if (result.command) void this.runCommand(result.command);
|
|
1356
|
+
};
|
|
1357
|
+
async runCommand(command) {
|
|
1358
|
+
if (!this.manager) return;
|
|
1359
|
+
if (command.type === "exit-dashboard") {
|
|
1360
|
+
this.stop();
|
|
1361
|
+
this.manager.onExit();
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
try {
|
|
1365
|
+
if (command.type === "stop-selected") {
|
|
1366
|
+
this.state = {
|
|
1367
|
+
...this.state,
|
|
1368
|
+
connectors: this.state.connectors.map(
|
|
1369
|
+
(connector) => connector.connectorId === command.connectorId ? { ...connector, status: "Stopping" } : connector
|
|
1370
|
+
)
|
|
1371
|
+
};
|
|
1372
|
+
this.scheduleRender();
|
|
1373
|
+
await this.manager.onStopConnector(command.connectorId, command.serverUrl);
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
await this.manager.onStopAll();
|
|
1377
|
+
this.stop();
|
|
1378
|
+
this.manager.onExit();
|
|
1379
|
+
} catch (error) {
|
|
1380
|
+
this.capture("error", [error instanceof Error ? error.message : String(error)]);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
scheduleRender = () => {
|
|
1384
|
+
if (this.stopped || this.renderQueued) return;
|
|
1385
|
+
this.renderQueued = true;
|
|
1386
|
+
setImmediate(() => {
|
|
1387
|
+
this.renderQueued = false;
|
|
1388
|
+
if (this.stopped) return;
|
|
1389
|
+
const width = this.streams.output.columns || 80;
|
|
1390
|
+
const height = this.streams.output.rows || 24;
|
|
1391
|
+
this.streams.output.write(`\x1B[H\x1B[2J${renderDashboard(this.state, { width, height })}`);
|
|
1392
|
+
});
|
|
1393
|
+
};
|
|
1394
|
+
};
|
|
1395
|
+
var activeDashboard;
|
|
1396
|
+
var startConnectorConsoleDashboard = (options = {}) => {
|
|
1397
|
+
if (options.plain) return void 0;
|
|
1398
|
+
const streams = options.streams ?? resolveDashboardStreams();
|
|
1399
|
+
if (!streams?.input.isTTY || !streams.output.isTTY) return void 0;
|
|
1400
|
+
if (activeDashboard) return activeDashboard;
|
|
1401
|
+
activeDashboard = new ConnectorConsoleDashboard(streams, options.manager);
|
|
1402
|
+
activeDashboard.start();
|
|
1403
|
+
return activeDashboard;
|
|
1404
|
+
};
|
|
1405
|
+
var updateConnectorDashboardStatus = (update) => {
|
|
1406
|
+
activeDashboard?.updateStatus(update);
|
|
1407
|
+
};
|
|
1408
|
+
var updateConnectorDashboardConnectors = (connectors, preferredConnectorId) => {
|
|
1409
|
+
activeDashboard?.updateConnectors(connectors, preferredConnectorId);
|
|
1410
|
+
};
|
|
1411
|
+
var appendConnectorDashboardLog = (connectorId, level, raw) => {
|
|
1412
|
+
activeDashboard?.appendConnectorLog(connectorId, level, raw);
|
|
1413
|
+
};
|
|
1414
|
+
|
|
1415
|
+
// src/daemon-client.ts
|
|
1416
|
+
import { spawn } from "child_process";
|
|
1417
|
+
import { mkdir as mkdir6, readFile as readFile6 } from "fs/promises";
|
|
1418
|
+
import path7 from "path";
|
|
1419
|
+
|
|
1420
|
+
// src/cloud-client.ts
|
|
1421
|
+
import { createHash as createHash4 } from "crypto";
|
|
1422
|
+
import { platform as platform3, release } from "os";
|
|
1423
|
+
|
|
1424
|
+
// src/browser.ts
|
|
1425
|
+
import { accessSync, constants, statSync } from "fs";
|
|
1426
|
+
import { mkdir as mkdir4 } from "fs/promises";
|
|
1427
|
+
import { platform as getPlatform, homedir as homedir2 } from "os";
|
|
1428
|
+
import path4 from "path";
|
|
410
1429
|
|
|
411
1430
|
// src/logger.ts
|
|
412
|
-
import { appendFile, mkdir as
|
|
1431
|
+
import { appendFile, mkdir as mkdir3, readdir as readdir2, rename as rename3, stat as stat2, unlink as unlink4 } from "fs/promises";
|
|
1432
|
+
import path3 from "path";
|
|
1433
|
+
import { format as format2 } from "util";
|
|
1434
|
+
var CONNECTOR_LOG_MAX_BYTES = 2 * 1024 * 1024;
|
|
1435
|
+
var CONNECTOR_LOG_BACKUP_COUNT = 2;
|
|
1436
|
+
var CONNECTOR_LOG_RETENTION_DAYS = 7;
|
|
413
1437
|
var pendingLogs = [];
|
|
1438
|
+
var fileWriteQueue = Promise.resolve();
|
|
1439
|
+
var lastPrunedDate = "";
|
|
1440
|
+
var activeLogContext;
|
|
1441
|
+
var setConnectorLogContext = (context) => {
|
|
1442
|
+
activeLogContext = { ...activeLogContext, ...context };
|
|
1443
|
+
};
|
|
1444
|
+
var ignoreMissingFile = (error) => {
|
|
1445
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
throw error;
|
|
1449
|
+
};
|
|
1450
|
+
var formatConnectorLogDate = (date) => {
|
|
1451
|
+
const year = date.getFullYear();
|
|
1452
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
1453
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
1454
|
+
return `${year}-${month}-${day}`;
|
|
1455
|
+
};
|
|
1456
|
+
var getDatedConnectorLogPath = (baseLogPath, date = /* @__PURE__ */ new Date()) => {
|
|
1457
|
+
const parsed = path3.parse(baseLogPath);
|
|
1458
|
+
return path3.join(parsed.dir, `${parsed.name}-${formatConnectorLogDate(date)}${parsed.ext}`);
|
|
1459
|
+
};
|
|
1460
|
+
async function pruneConnectorLogs(baseLogPath, now = /* @__PURE__ */ new Date(), retentionDays = CONNECTOR_LOG_RETENTION_DAYS) {
|
|
1461
|
+
const parsed = path3.parse(baseLogPath);
|
|
1462
|
+
const prefix = `${parsed.name}-`;
|
|
1463
|
+
const cutoff = new Date(now);
|
|
1464
|
+
cutoff.setHours(0, 0, 0, 0);
|
|
1465
|
+
cutoff.setDate(cutoff.getDate() - Math.max(0, retentionDays - 1));
|
|
1466
|
+
const cutoffDate = formatConnectorLogDate(cutoff);
|
|
1467
|
+
const files = await readdir2(parsed.dir).catch(() => []);
|
|
1468
|
+
const expired = files.filter((fileName) => {
|
|
1469
|
+
if (!fileName.startsWith(prefix)) return false;
|
|
1470
|
+
const date = fileName.slice(prefix.length, prefix.length + 10);
|
|
1471
|
+
const suffix = fileName.slice(prefix.length + 10);
|
|
1472
|
+
const isDatedLog = /^\d{4}-\d{2}-\d{2}$/.test(date);
|
|
1473
|
+
const segment = suffix.startsWith(`${parsed.ext}.`) ? suffix.slice(parsed.ext.length + 1) : "";
|
|
1474
|
+
const isLogSegment = suffix === parsed.ext || /^\d+$/.test(segment);
|
|
1475
|
+
return isDatedLog && isLogSegment && date < cutoffDate;
|
|
1476
|
+
});
|
|
1477
|
+
await Promise.all(expired.map((fileName) => unlink4(path3.join(parsed.dir, fileName))));
|
|
1478
|
+
return expired.length;
|
|
1479
|
+
}
|
|
1480
|
+
async function rotateConnectorLogIfNeeded(logPath, maxBytes = CONNECTOR_LOG_MAX_BYTES, backupCount = CONNECTOR_LOG_BACKUP_COUNT) {
|
|
1481
|
+
const currentSize = await stat2(logPath).then((value) => value.size).catch((error) => {
|
|
1482
|
+
ignoreMissingFile(error);
|
|
1483
|
+
return 0;
|
|
1484
|
+
});
|
|
1485
|
+
if (currentSize < maxBytes) return false;
|
|
1486
|
+
await unlink4(`${logPath}.${backupCount}`).catch(ignoreMissingFile);
|
|
1487
|
+
for (let index = backupCount - 1; index >= 1; index -= 1) {
|
|
1488
|
+
await rename3(`${logPath}.${index}`, `${logPath}.${index + 1}`).catch(ignoreMissingFile);
|
|
1489
|
+
}
|
|
1490
|
+
await rename3(logPath, `${logPath}.1`).catch(ignoreMissingFile);
|
|
1491
|
+
return true;
|
|
1492
|
+
}
|
|
1493
|
+
var appendConnectorLog = (baseLogPath, line, now = /* @__PURE__ */ new Date()) => {
|
|
1494
|
+
const date = formatConnectorLogDate(now);
|
|
1495
|
+
const logPath = getDatedConnectorLogPath(baseLogPath, now);
|
|
1496
|
+
const write = async () => {
|
|
1497
|
+
if (lastPrunedDate !== date) {
|
|
1498
|
+
await pruneConnectorLogs(baseLogPath, now).catch(() => void 0);
|
|
1499
|
+
lastPrunedDate = date;
|
|
1500
|
+
}
|
|
1501
|
+
await rotateConnectorLogIfNeeded(logPath);
|
|
1502
|
+
await appendFile(logPath, line);
|
|
1503
|
+
};
|
|
1504
|
+
const queuedWrite = fileWriteQueue.then(write, write);
|
|
1505
|
+
fileWriteQueue = queuedWrite.catch(() => void 0);
|
|
1506
|
+
return queuedWrite;
|
|
1507
|
+
};
|
|
1508
|
+
var persistLocalEntry = async (entry) => {
|
|
1509
|
+
const paths = getConnectorPaths(
|
|
1510
|
+
activeLogContext?.serverUrl ?? getServerUrl(),
|
|
1511
|
+
activeLogContext?.configRoot
|
|
1512
|
+
);
|
|
1513
|
+
await mkdir3(path3.dirname(paths.logPath), { recursive: true }).catch(() => void 0);
|
|
1514
|
+
await appendConnectorLog(paths.logPath, `${JSON.stringify(entry)}
|
|
1515
|
+
`).catch(() => void 0);
|
|
1516
|
+
};
|
|
1517
|
+
var installConnectorConsoleCapture = () => {
|
|
1518
|
+
const writes = [];
|
|
1519
|
+
const original = {
|
|
1520
|
+
error: console.error,
|
|
1521
|
+
log: console.log,
|
|
1522
|
+
warn: console.warn
|
|
1523
|
+
};
|
|
1524
|
+
const capture = (level, output) => (...args) => {
|
|
1525
|
+
const message = format2(...args);
|
|
1526
|
+
if (!message.startsWith("[buda-connector]")) {
|
|
1527
|
+
writes.push(
|
|
1528
|
+
persistLocalEntry({
|
|
1529
|
+
level,
|
|
1530
|
+
message,
|
|
1531
|
+
metadata: {
|
|
1532
|
+
source: "daemon-console",
|
|
1533
|
+
...activeLogContext?.connectorId ? { connectorId: activeLogContext.connectorId } : {}
|
|
1534
|
+
},
|
|
1535
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1536
|
+
})
|
|
1537
|
+
);
|
|
1538
|
+
}
|
|
1539
|
+
output(...args);
|
|
1540
|
+
};
|
|
1541
|
+
console.log = capture("info", original.log);
|
|
1542
|
+
console.warn = capture("warn", original.warn);
|
|
1543
|
+
console.error = capture("error", original.error);
|
|
1544
|
+
return async () => {
|
|
1545
|
+
console.log = original.log;
|
|
1546
|
+
console.warn = original.warn;
|
|
1547
|
+
console.error = original.error;
|
|
1548
|
+
await Promise.all(writes);
|
|
1549
|
+
};
|
|
1550
|
+
};
|
|
414
1551
|
async function writeLog(level, message, metadata) {
|
|
415
1552
|
const entry = {
|
|
416
1553
|
level,
|
|
417
1554
|
message,
|
|
418
|
-
metadata
|
|
1555
|
+
metadata: {
|
|
1556
|
+
...metadata ?? {},
|
|
1557
|
+
...activeLogContext?.connectorId ? { connectorId: activeLogContext.connectorId } : {}
|
|
1558
|
+
},
|
|
419
1559
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
420
1560
|
};
|
|
421
1561
|
pendingLogs.push(entry);
|
|
422
1562
|
if (pendingLogs.length > 100) pendingLogs.splice(0, pendingLogs.length - 100);
|
|
423
|
-
|
|
424
|
-
await mkdir2(paths.configDir, { recursive: true }).catch(() => void 0);
|
|
425
|
-
await appendFile(paths.logPath, `${JSON.stringify(entry)}
|
|
426
|
-
`).catch(() => void 0);
|
|
1563
|
+
await persistLocalEntry(entry);
|
|
427
1564
|
const line = `[buda-connector] ${message}`;
|
|
428
1565
|
if (level === "error") console.error(line);
|
|
429
1566
|
else if (level === "warn") console.warn(line);
|
|
@@ -434,58 +1571,9 @@ function takePendingLogs() {
|
|
|
434
1571
|
}
|
|
435
1572
|
function restorePendingLogs(logs) {
|
|
436
1573
|
pendingLogs.unshift(...logs);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// src/bunny-daemon.ts
|
|
440
|
-
async function startEmbeddedBunnyDaemon(config, launchConfig) {
|
|
441
|
-
const host = getBunnyDaemonHost(launchConfig);
|
|
442
|
-
const port = getBunnyDaemonPort(launchConfig);
|
|
443
|
-
const root = config.workdirRoot;
|
|
444
|
-
await mkdir3(root, { recursive: true });
|
|
445
|
-
const server = createDaemon({ host, port, root });
|
|
446
|
-
await new Promise((resolve, reject) => {
|
|
447
|
-
const onError = (error) => {
|
|
448
|
-
server.off("listening", onListening);
|
|
449
|
-
reject(error);
|
|
450
|
-
};
|
|
451
|
-
const onListening = () => {
|
|
452
|
-
server.off("error", onError);
|
|
453
|
-
resolve();
|
|
454
|
-
};
|
|
455
|
-
server.once("error", onError);
|
|
456
|
-
server.once("listening", onListening);
|
|
457
|
-
server.listen(port, host);
|
|
458
|
-
});
|
|
459
|
-
const address = server.address();
|
|
460
|
-
const actualPort = typeof address === "object" && address ? address.port : port;
|
|
461
|
-
const baseUrl = `http://${host}:${actualPort}`;
|
|
462
|
-
await writeLog("info", `bunny daemon listening ${baseUrl}`, {
|
|
463
|
-
root
|
|
464
|
-
});
|
|
465
|
-
return {
|
|
466
|
-
server,
|
|
467
|
-
host,
|
|
468
|
-
port: actualPort,
|
|
469
|
-
baseUrl,
|
|
470
|
-
root
|
|
471
|
-
};
|
|
472
|
-
}
|
|
473
|
-
async function stopEmbeddedBunnyDaemon(daemon) {
|
|
474
|
-
await new Promise((resolve) => {
|
|
475
|
-
daemon.server.close(() => resolve());
|
|
476
|
-
});
|
|
477
|
-
await writeLog("info", `bunny daemon stopped ${daemon.baseUrl}`);
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// src/cloud-client.ts
|
|
481
|
-
import { createHash as createHash4 } from "crypto";
|
|
482
|
-
import { platform as platform3, release } from "os";
|
|
1574
|
+
}
|
|
483
1575
|
|
|
484
1576
|
// 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
1577
|
var DEFAULT_VIEWPORT = { width: 1280, height: 800 };
|
|
490
1578
|
var MAX_SCREENCAST_DIMENSION = 2560;
|
|
491
1579
|
var SESSION_ID_PATTERN = /^[A-Za-z0-9_.:-]{1,320}$/;
|
|
@@ -791,15 +1879,15 @@ var resolveScreenshotPath = (workdirRoot, filePath) => {
|
|
|
791
1879
|
if (!workdirRoot) throw new Error("Connector workdir is unavailable for screenshots.");
|
|
792
1880
|
const raw = typeof filePath === "string" ? filePath.trim() : "";
|
|
793
1881
|
if (!raw) throw new Error("file_path is required");
|
|
794
|
-
const agentRoot =
|
|
1882
|
+
const agentRoot = path4.resolve(workdirRoot, "agent");
|
|
795
1883
|
const relativeInput = raw === "/agent" ? "" : raw.startsWith("/agent/") ? raw.slice(7) : raw;
|
|
796
|
-
const absolutePath =
|
|
797
|
-
if (absolutePath === agentRoot || !absolutePath.startsWith(`${agentRoot}${
|
|
1884
|
+
const absolutePath = path4.resolve(agentRoot, relativeInput);
|
|
1885
|
+
if (absolutePath === agentRoot || !absolutePath.startsWith(`${agentRoot}${path4.sep}`)) {
|
|
798
1886
|
throw new Error("file_path must stay under the connector agent drive");
|
|
799
1887
|
}
|
|
800
1888
|
return {
|
|
801
1889
|
absolutePath,
|
|
802
|
-
relativePath:
|
|
1890
|
+
relativePath: path4.relative(agentRoot, absolutePath).split(path4.sep).join("/")
|
|
803
1891
|
};
|
|
804
1892
|
};
|
|
805
1893
|
async function handleBrowserRequest(requestPath, method, body, context = {}) {
|
|
@@ -915,7 +2003,7 @@ async function handleBrowserRequest(requestPath, method, body, context = {}) {
|
|
|
915
2003
|
const input = body ?? {};
|
|
916
2004
|
const s = await ensureSession(sessionId);
|
|
917
2005
|
const screenshotPath = resolveScreenshotPath(context.workdirRoot, input.file_path);
|
|
918
|
-
await mkdir4(
|
|
2006
|
+
await mkdir4(path4.dirname(screenshotPath.absolutePath), { recursive: true });
|
|
919
2007
|
await s.page.screenshot({
|
|
920
2008
|
path: screenshotPath.absolutePath,
|
|
921
2009
|
fullPage: input.fullPage !== false
|
|
@@ -998,30 +2086,61 @@ async function handleBrowserRequest(requestPath, method, body, context = {}) {
|
|
|
998
2086
|
|
|
999
2087
|
// src/bundled.ts
|
|
1000
2088
|
import { createHash as createHash3 } from "crypto";
|
|
1001
|
-
import {
|
|
1002
|
-
|
|
1003
|
-
|
|
2089
|
+
import {
|
|
2090
|
+
copyFileSync,
|
|
2091
|
+
existsSync,
|
|
2092
|
+
mkdirSync,
|
|
2093
|
+
readdirSync,
|
|
2094
|
+
renameSync,
|
|
2095
|
+
rmSync,
|
|
2096
|
+
statSync as statSync2
|
|
2097
|
+
} from "fs";
|
|
2098
|
+
import { readdir as readdir3, readFile as readFile4 } from "fs/promises";
|
|
2099
|
+
import path5 from "path";
|
|
1004
2100
|
import { fileURLToPath } from "url";
|
|
1005
|
-
var PKG_ROOT =
|
|
1006
|
-
var BUNDLED_DIR =
|
|
1007
|
-
var SYSTEM_SKILLS_DIR =
|
|
1008
|
-
var SCRIPTS_DIR =
|
|
1009
|
-
var SHARED_DIR =
|
|
1010
|
-
var MONOREPO_SRC =
|
|
1011
|
-
function syncTemplatesFromMonorepo() {
|
|
2101
|
+
var PKG_ROOT = path5.resolve(fileURLToPath(import.meta.url), "../..");
|
|
2102
|
+
var BUNDLED_DIR = path5.join(PKG_ROOT, "agent-templates");
|
|
2103
|
+
var SYSTEM_SKILLS_DIR = path5.join(BUNDLED_DIR, "system-skills");
|
|
2104
|
+
var SCRIPTS_DIR = path5.join(BUNDLED_DIR, "scripts");
|
|
2105
|
+
var SHARED_DIR = path5.join(BUNDLED_DIR, "shared");
|
|
2106
|
+
var MONOREPO_SRC = path5.resolve(PKG_ROOT, "../buda/agent-templates");
|
|
2107
|
+
function syncTemplatesFromMonorepo(options = {}) {
|
|
1012
2108
|
if (!existsSync(MONOREPO_SRC)) {
|
|
1013
2109
|
throw new Error(`Cannot sync agent-templates: source missing at ${MONOREPO_SRC}`);
|
|
1014
2110
|
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
})
|
|
2111
|
+
mkdirSync(BUNDLED_DIR, { recursive: true });
|
|
2112
|
+
syncDirectory(MONOREPO_SRC, BUNDLED_DIR, Boolean(options.prune));
|
|
2113
|
+
}
|
|
2114
|
+
function syncDirectory(sourceDirectory, destinationDirectory, prune) {
|
|
2115
|
+
const sourceEntries = /* @__PURE__ */ new Set();
|
|
2116
|
+
for (const entry of readdirSync(sourceDirectory, { withFileTypes: true })) {
|
|
2117
|
+
if (entry.name === "extensions") continue;
|
|
2118
|
+
sourceEntries.add(entry.name);
|
|
2119
|
+
const source = path5.join(sourceDirectory, entry.name);
|
|
2120
|
+
const destination = path5.join(destinationDirectory, entry.name);
|
|
2121
|
+
if (statSync2(source).isDirectory()) {
|
|
2122
|
+
mkdirSync(destination, { recursive: true });
|
|
2123
|
+
syncDirectory(source, destination, prune);
|
|
2124
|
+
continue;
|
|
2125
|
+
}
|
|
2126
|
+
const temporary = `${destination}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
2127
|
+
copyFileSync(source, temporary);
|
|
2128
|
+
try {
|
|
2129
|
+
renameSync(temporary, destination);
|
|
2130
|
+
} catch {
|
|
2131
|
+
copyFileSync(temporary, destination);
|
|
2132
|
+
rmSync(temporary, { force: true });
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
if (!prune) return;
|
|
2136
|
+
for (const entry of readdirSync(destinationDirectory, { withFileTypes: true })) {
|
|
2137
|
+
if (sourceEntries.has(entry.name)) continue;
|
|
2138
|
+
rmSync(path5.join(destinationDirectory, entry.name), { recursive: true, force: true });
|
|
2139
|
+
}
|
|
1021
2140
|
}
|
|
1022
2141
|
ensureBundledDir();
|
|
1023
2142
|
function ensureBundledDir() {
|
|
1024
|
-
const missingRequiredTemplate = !existsSync(
|
|
2143
|
+
const missingRequiredTemplate = !existsSync(path5.join(SHARED_DIR, "BOOTSTRAP.md"));
|
|
1025
2144
|
if (existsSync(BUNDLED_DIR) && !missingRequiredTemplate) return;
|
|
1026
2145
|
if (!existsSync(MONOREPO_SRC)) {
|
|
1027
2146
|
console.warn(
|
|
@@ -1035,7 +2154,7 @@ function ensureBundledDir() {
|
|
|
1035
2154
|
var cached = null;
|
|
1036
2155
|
async function readBundledManifest() {
|
|
1037
2156
|
if (cached) return cached;
|
|
1038
|
-
const systemSkillNames = (await
|
|
2157
|
+
const systemSkillNames = (await readdir3(SYSTEM_SKILLS_DIR, { withFileTypes: true }).catch(() => [])).filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
1039
2158
|
const version = await hashDir(BUNDLED_DIR);
|
|
1040
2159
|
cached = {
|
|
1041
2160
|
version,
|
|
@@ -1050,19 +2169,19 @@ async function readBundledManifest() {
|
|
|
1050
2169
|
async function hashDir(dir) {
|
|
1051
2170
|
const hash = createHash3("sha256");
|
|
1052
2171
|
await walk(dir, async (relPath, abs) => {
|
|
1053
|
-
hash.update(`${relPath}:`).update(await
|
|
2172
|
+
hash.update(`${relPath}:`).update(await readFile4(abs));
|
|
1054
2173
|
});
|
|
1055
2174
|
return hash.digest("hex");
|
|
1056
2175
|
}
|
|
1057
2176
|
async function walk(dir, visit, base = dir) {
|
|
1058
|
-
const entries = await
|
|
2177
|
+
const entries = await readdir3(dir, { withFileTypes: true }).catch(() => []);
|
|
1059
2178
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
1060
2179
|
for (const entry of entries) {
|
|
1061
|
-
const abs =
|
|
2180
|
+
const abs = path5.join(dir, entry.name);
|
|
1062
2181
|
if (entry.isDirectory()) {
|
|
1063
2182
|
await walk(abs, visit, base);
|
|
1064
2183
|
} else if (entry.isFile()) {
|
|
1065
|
-
await visit(
|
|
2184
|
+
await visit(path5.relative(base, abs), abs);
|
|
1066
2185
|
}
|
|
1067
2186
|
}
|
|
1068
2187
|
}
|
|
@@ -1080,6 +2199,30 @@ var BASE_CONNECTOR_CAPABILITIES = [
|
|
|
1080
2199
|
"shell",
|
|
1081
2200
|
"git"
|
|
1082
2201
|
];
|
|
2202
|
+
async function resolveConnectorScope(deviceId, launchConfig) {
|
|
2203
|
+
const serverUrl = normalizeServerOrigin(getServerUrl(launchConfig));
|
|
2204
|
+
const headers = { "Content-Type": "application/json" };
|
|
2205
|
+
const oauthToken = launchConfig?.oauthToken?.trim();
|
|
2206
|
+
const token = getConnectorToken(launchConfig);
|
|
2207
|
+
if (oauthToken) headers.Authorization = `Bearer ${oauthToken}`;
|
|
2208
|
+
if (token) headers["X-Buda-Connector-Token"] = token;
|
|
2209
|
+
const response = await fetch(`${serverUrl}/api/connectors/scope`, {
|
|
2210
|
+
method: "POST",
|
|
2211
|
+
headers,
|
|
2212
|
+
body: JSON.stringify({ deviceId })
|
|
2213
|
+
});
|
|
2214
|
+
if (!response.ok) {
|
|
2215
|
+
throw new Error(`/api/connectors/scope failed: ${response.status} ${await response.text()}`);
|
|
2216
|
+
}
|
|
2217
|
+
const result = await response.json();
|
|
2218
|
+
if (typeof result.spaceId !== "string" || !result.spaceId.trim()) {
|
|
2219
|
+
throw new Error("Connector scope response did not include spaceId");
|
|
2220
|
+
}
|
|
2221
|
+
return {
|
|
2222
|
+
...typeof result.connectorId === "string" ? { connectorId: result.connectorId } : {},
|
|
2223
|
+
spaceId: result.spaceId.trim()
|
|
2224
|
+
};
|
|
2225
|
+
}
|
|
1083
2226
|
var browserAvailabilityWarningLogged = false;
|
|
1084
2227
|
var getBrowserAvailabilityWarning = (browserAvailability) => browserAvailability.available ? null : `${browserAvailability.error} Browser automation is disabled, but shell, filesystem, Git, and coding capabilities remain available.`;
|
|
1085
2228
|
var getAdvertisedCapabilities = (browserAvailability = resolveBrowserAvailability(), deviceToken) => [
|
|
@@ -1101,7 +2244,7 @@ async function buildPayload(config, extra) {
|
|
|
1101
2244
|
deviceName: config.deviceName,
|
|
1102
2245
|
hostLabel: config.hostLabel,
|
|
1103
2246
|
platform: `${platform3()} ${release()}`,
|
|
1104
|
-
version:
|
|
2247
|
+
version: CONNECTOR_RUNTIME_VERSION,
|
|
1105
2248
|
pid: process.pid,
|
|
1106
2249
|
workdirRoot: config.workdirRoot,
|
|
1107
2250
|
baseUrl: extra?.bunnyDaemon?.baseUrl,
|
|
@@ -1136,6 +2279,8 @@ function buildAuthHeaders(config, launchConfig) {
|
|
|
1136
2279
|
"X-Buda-Connector-Id": config.connectorId
|
|
1137
2280
|
};
|
|
1138
2281
|
const token = config.token?.trim() || getConnectorToken(launchConfig);
|
|
2282
|
+
const oauthToken = launchConfig?.oauthToken?.trim();
|
|
2283
|
+
if (oauthToken) headers.Authorization = `Bearer ${oauthToken}`;
|
|
1139
2284
|
if (token) headers["X-Buda-Connector-Token"] = token;
|
|
1140
2285
|
if (config.deviceToken) headers["X-Buda-Connector-Device-Token"] = config.deviceToken;
|
|
1141
2286
|
return headers;
|
|
@@ -1149,108 +2294,1043 @@ async function postJson(serverUrl, pathname, body, config, launchConfig) {
|
|
|
1149
2294
|
},
|
|
1150
2295
|
body: JSON.stringify(body)
|
|
1151
2296
|
});
|
|
1152
|
-
if (!response.ok) {
|
|
1153
|
-
const text2 = await response.text().catch(() => "");
|
|
1154
|
-
let errorCode;
|
|
1155
|
-
let errorMessage3 = text2;
|
|
2297
|
+
if (!response.ok) {
|
|
2298
|
+
const text2 = await response.text().catch(() => "");
|
|
2299
|
+
let errorCode;
|
|
2300
|
+
let errorMessage3 = text2;
|
|
2301
|
+
try {
|
|
2302
|
+
const payload = JSON.parse(text2);
|
|
2303
|
+
errorCode = typeof payload.code === "string" ? payload.code : void 0;
|
|
2304
|
+
errorMessage3 = typeof payload.error === "string" ? payload.error : text2;
|
|
2305
|
+
} catch {
|
|
2306
|
+
}
|
|
2307
|
+
if (errorCode === "device_revoked") {
|
|
2308
|
+
throw new Error(
|
|
2309
|
+
`This device is prohibited/revoked and cannot reconnect. Contact support. (${errorMessage3})`
|
|
2310
|
+
);
|
|
2311
|
+
}
|
|
2312
|
+
throw new Error(`${pathname} failed: ${response.status} ${errorMessage3}`);
|
|
2313
|
+
}
|
|
2314
|
+
return await response.json();
|
|
2315
|
+
}
|
|
2316
|
+
function formatConnectorOwner(connector) {
|
|
2317
|
+
return connector.userEmail ?? connector.userId ?? "anonymous";
|
|
2318
|
+
}
|
|
2319
|
+
async function warnIfStaleBundle(result) {
|
|
2320
|
+
if (!result.bundle?.staleClient) return;
|
|
2321
|
+
await writeLog(
|
|
2322
|
+
"warn",
|
|
2323
|
+
`connector bundle out of date \u2014 server expects ${result.bundle.expectedVersion ?? "newer"}; run \`pnpm up @buda-ai/connector\``
|
|
2324
|
+
);
|
|
2325
|
+
}
|
|
2326
|
+
async function registerConnector(config, launchConfig, bunnyDaemon, options) {
|
|
2327
|
+
const result = await postJson(
|
|
2328
|
+
config.serverUrl,
|
|
2329
|
+
"/api/connectors/register",
|
|
2330
|
+
await buildPayload(config, {
|
|
2331
|
+
bunnyDaemon,
|
|
2332
|
+
...options?.recoverMissingProfile ? { recovery: "missing_local_profile" } : {}
|
|
2333
|
+
}),
|
|
2334
|
+
config,
|
|
2335
|
+
launchConfig
|
|
2336
|
+
);
|
|
2337
|
+
await writeLog(
|
|
2338
|
+
"info",
|
|
2339
|
+
`registered connector=${result.connector.connectorId} status=${result.connector.status} owner=${formatConnectorOwner(result.connector)}`
|
|
2340
|
+
);
|
|
2341
|
+
await warnIfStaleBundle(result);
|
|
2342
|
+
return {
|
|
2343
|
+
connectorId: result.connector.connectorId,
|
|
2344
|
+
spaceId: result.connector.spaceId ?? result.spaceId,
|
|
2345
|
+
relayProtocol: result.relayProtocol
|
|
2346
|
+
};
|
|
2347
|
+
}
|
|
2348
|
+
async function heartbeatConnector(config, launchConfig, bunnyDaemon) {
|
|
2349
|
+
const logs = takePendingLogs();
|
|
2350
|
+
const result = await postJson(
|
|
2351
|
+
config.serverUrl,
|
|
2352
|
+
"/api/connectors/heartbeat",
|
|
2353
|
+
await buildPayload(config, {
|
|
2354
|
+
bunnyDaemon,
|
|
2355
|
+
logs
|
|
2356
|
+
}),
|
|
2357
|
+
config,
|
|
2358
|
+
launchConfig
|
|
2359
|
+
).catch((error) => {
|
|
2360
|
+
restorePendingLogs(logs);
|
|
2361
|
+
throw error;
|
|
2362
|
+
});
|
|
2363
|
+
return { relayProtocol: result.relayProtocol };
|
|
2364
|
+
}
|
|
2365
|
+
async function unregisterConnector(config, launchConfig) {
|
|
2366
|
+
await postJson(
|
|
2367
|
+
config.serverUrl,
|
|
2368
|
+
"/api/connectors/unregister",
|
|
2369
|
+
{
|
|
2370
|
+
connectorId: config.connectorId
|
|
2371
|
+
},
|
|
2372
|
+
config,
|
|
2373
|
+
launchConfig
|
|
2374
|
+
).catch((error) => {
|
|
2375
|
+
void writeLog("warn", `unregister failed: ${error instanceof Error ? error.message : error}`);
|
|
2376
|
+
});
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
// src/runtime-install.ts
|
|
2380
|
+
import { execFile } from "child_process";
|
|
2381
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
2382
|
+
import { access, cp, mkdir as mkdir5, readFile as readFile5, rename as rename4, rm, writeFile as writeFile3 } from "fs/promises";
|
|
2383
|
+
import { homedir as homedir3 } from "os";
|
|
2384
|
+
import path6 from "path";
|
|
2385
|
+
import { promisify } from "util";
|
|
2386
|
+
var execFileAsync = promisify(execFile);
|
|
2387
|
+
var RUNTIME_INSTALL_TIMEOUT_MS = 5 * 6e4;
|
|
2388
|
+
var exists = async (filePath) => access(filePath).then(() => true).catch(() => false);
|
|
2389
|
+
var runtimeRoot = () => process.env.BUDA_CONNECTOR_RUNTIME_ROOT || path6.join(homedir3(), ".buda", "runtime");
|
|
2390
|
+
var runtimePlatformKey = `${process.platform}-${process.arch}-node${process.versions.modules}`;
|
|
2391
|
+
var versionDirectory = () => path6.join(runtimeRoot(), runtimePlatformKey, CONNECTOR_RUNTIME_VERSION);
|
|
2392
|
+
var manifestPath = () => path6.join(versionDirectory(), "runtime.json");
|
|
2393
|
+
var readRuntimeManifest = async () => {
|
|
2394
|
+
try {
|
|
2395
|
+
const parsed = JSON.parse(await readFile5(manifestPath(), "utf8"));
|
|
2396
|
+
if (parsed.version !== CONNECTOR_RUNTIME_VERSION || !await exists(parsed.entrypoint) || !await exists(parsed.nodeExecutable)) {
|
|
2397
|
+
return null;
|
|
2398
|
+
}
|
|
2399
|
+
return parsed;
|
|
2400
|
+
} catch {
|
|
2401
|
+
return null;
|
|
2402
|
+
}
|
|
2403
|
+
};
|
|
2404
|
+
var writeManifest = async (directory, manifest) => {
|
|
2405
|
+
await writeFile3(path6.join(directory, "runtime.json"), `${JSON.stringify(manifest, null, 2)}
|
|
2406
|
+
`, {
|
|
2407
|
+
mode: 384
|
|
2408
|
+
});
|
|
2409
|
+
};
|
|
2410
|
+
var validateRuntime = async (runtime) => {
|
|
2411
|
+
const { stdout } = await execFileAsync(
|
|
2412
|
+
runtime.nodeExecutable,
|
|
2413
|
+
[...runtime.nodeArgs ?? [], runtime.entrypoint, "--help"],
|
|
2414
|
+
{ timeout: 15e3 }
|
|
2415
|
+
);
|
|
2416
|
+
if (!stdout.includes("Help: buda-connector")) {
|
|
2417
|
+
throw new Error("Staged Connector runtime failed its CLI validation");
|
|
2418
|
+
}
|
|
2419
|
+
};
|
|
2420
|
+
var installFromNpm = async (directory) => {
|
|
2421
|
+
await mkdir5(directory, { recursive: true, mode: 448 });
|
|
2422
|
+
await execFileAsync(
|
|
2423
|
+
process.platform === "win32" ? "npm.cmd" : "npm",
|
|
2424
|
+
[
|
|
2425
|
+
"install",
|
|
2426
|
+
"--ignore-scripts=false",
|
|
2427
|
+
"--no-audit",
|
|
2428
|
+
"--no-fund",
|
|
2429
|
+
"--omit=dev",
|
|
2430
|
+
"--prefix",
|
|
2431
|
+
directory,
|
|
2432
|
+
`@buda-ai/connector@${CONNECTOR_RUNTIME_VERSION}`
|
|
2433
|
+
],
|
|
2434
|
+
{ env: process.env, timeout: RUNTIME_INSTALL_TIMEOUT_MS }
|
|
2435
|
+
);
|
|
2436
|
+
const entrypoint = path6.join(
|
|
2437
|
+
directory,
|
|
2438
|
+
"node_modules",
|
|
2439
|
+
"@buda-ai",
|
|
2440
|
+
"connector",
|
|
2441
|
+
"dist",
|
|
2442
|
+
"cli.js"
|
|
2443
|
+
);
|
|
2444
|
+
const nodeExecutable = path6.join(directory, "node", path6.basename(process.execPath));
|
|
2445
|
+
await mkdir5(path6.dirname(nodeExecutable), { recursive: true });
|
|
2446
|
+
await cp(process.execPath, nodeExecutable, { force: true });
|
|
2447
|
+
if (!await exists(entrypoint)) throw new Error("Installed Connector runtime is incomplete");
|
|
2448
|
+
return { entrypoint, nodeExecutable, version: CONNECTOR_RUNTIME_VERSION };
|
|
2449
|
+
};
|
|
2450
|
+
var installFromBundledRuntime = async (directory, sourceRoot) => {
|
|
2451
|
+
const sourceNode = process.env.BUDA_CONNECTOR_RUNTIME_NODE || process.execPath;
|
|
2452
|
+
const sourceEntrypoint = process.env.BUDA_CONNECTOR_RUNTIME_ENTRYPOINT || process.argv[1];
|
|
2453
|
+
if (!sourceEntrypoint) throw new Error("Bundled Connector runtime entrypoint is missing");
|
|
2454
|
+
await cp(sourceRoot, directory, { recursive: true, force: true });
|
|
2455
|
+
const relativeEntrypoint = path6.relative(sourceRoot, sourceEntrypoint);
|
|
2456
|
+
if (relativeEntrypoint.startsWith("..") || path6.isAbsolute(relativeEntrypoint)) {
|
|
2457
|
+
throw new Error("Bundled Connector entrypoint must be inside its runtime root");
|
|
2458
|
+
}
|
|
2459
|
+
const relativeNode = path6.relative(sourceRoot, sourceNode);
|
|
2460
|
+
const entrypoint = path6.join(directory, relativeEntrypoint);
|
|
2461
|
+
const nodeExecutable = relativeNode.startsWith("..") ? path6.join(directory, "node", path6.basename(sourceNode)) : path6.join(directory, relativeNode);
|
|
2462
|
+
if (relativeNode.startsWith("..")) {
|
|
2463
|
+
await mkdir5(path6.dirname(nodeExecutable), { recursive: true });
|
|
2464
|
+
await cp(sourceNode, nodeExecutable, { force: true });
|
|
2465
|
+
}
|
|
2466
|
+
if (!await exists(entrypoint))
|
|
2467
|
+
throw new Error("Copied Connector runtime entrypoint is missing");
|
|
2468
|
+
return { entrypoint, nodeExecutable, version: CONNECTOR_RUNTIME_VERSION };
|
|
2469
|
+
};
|
|
2470
|
+
async function ensureStableConnectorRuntime() {
|
|
2471
|
+
if (process.env.BUDA_CONNECTOR_SKIP_RUNTIME_STAGE === "1" || process.argv[1]?.endsWith(".ts")) {
|
|
2472
|
+
const entrypoint = process.argv[1];
|
|
2473
|
+
if (!entrypoint) throw new Error("Connector CLI entrypoint is missing");
|
|
2474
|
+
return {
|
|
2475
|
+
entrypoint,
|
|
2476
|
+
...entrypoint.endsWith(".ts") ? { nodeArgs: [...process.execArgv] } : {},
|
|
2477
|
+
nodeExecutable: process.execPath,
|
|
2478
|
+
version: CONNECTOR_RUNTIME_VERSION
|
|
2479
|
+
};
|
|
2480
|
+
}
|
|
2481
|
+
const existing = await readRuntimeManifest();
|
|
2482
|
+
if (existing) return existing;
|
|
2483
|
+
const platformRoot = path6.join(runtimeRoot(), runtimePlatformKey);
|
|
2484
|
+
await mkdir5(platformRoot, { recursive: true, mode: 448 });
|
|
2485
|
+
return withExclusiveFileLock(
|
|
2486
|
+
path6.join(platformRoot, `install-${CONNECTOR_RUNTIME_VERSION}`),
|
|
2487
|
+
async () => {
|
|
2488
|
+
const installed = await readRuntimeManifest();
|
|
2489
|
+
if (installed) return installed;
|
|
2490
|
+
const temporary = path6.join(
|
|
2491
|
+
platformRoot,
|
|
2492
|
+
`.${CONNECTOR_RUNTIME_VERSION}.${randomUUID4()}.tmp`
|
|
2493
|
+
);
|
|
2494
|
+
await rm(temporary, { recursive: true, force: true });
|
|
2495
|
+
try {
|
|
2496
|
+
const bundledSource = process.env.BUDA_CONNECTOR_BUNDLED_RUNTIME_ROOT;
|
|
2497
|
+
const manifest = bundledSource ? await installFromBundledRuntime(temporary, bundledSource) : await installFromNpm(temporary);
|
|
2498
|
+
await validateRuntime(manifest);
|
|
2499
|
+
const finalDirectory = versionDirectory();
|
|
2500
|
+
const rebased = {
|
|
2501
|
+
...manifest,
|
|
2502
|
+
entrypoint: path6.join(finalDirectory, path6.relative(temporary, manifest.entrypoint)),
|
|
2503
|
+
nodeExecutable: path6.join(
|
|
2504
|
+
finalDirectory,
|
|
2505
|
+
path6.relative(temporary, manifest.nodeExecutable)
|
|
2506
|
+
)
|
|
2507
|
+
};
|
|
2508
|
+
await writeManifest(temporary, rebased);
|
|
2509
|
+
await rm(finalDirectory, { recursive: true, force: true });
|
|
2510
|
+
await rename4(temporary, finalDirectory);
|
|
2511
|
+
return rebased;
|
|
2512
|
+
} catch (error) {
|
|
2513
|
+
await rm(temporary, { recursive: true, force: true });
|
|
2514
|
+
throw error;
|
|
2515
|
+
}
|
|
2516
|
+
},
|
|
2517
|
+
{ timeoutMs: RUNTIME_INSTALL_TIMEOUT_MS }
|
|
2518
|
+
);
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
// src/daemon-client.ts
|
|
2522
|
+
var START_TIMEOUT_MS = 45e3;
|
|
2523
|
+
var BOOTSTRAP_TIMEOUT_MS = 5 * 6e4;
|
|
2524
|
+
var POLL_INTERVAL_MS = 250;
|
|
2525
|
+
var wait2 = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
2526
|
+
var getConfigRoot = () => process.env.BUDA_CONNECTOR_CONFIG_ROOT;
|
|
2527
|
+
var readLiveStatus = async (config, configRoot = getConfigRoot(), timeoutMs) => requestDaemonControl(config, "status", configRoot, { timeoutMs }).catch(() => null);
|
|
2528
|
+
var isProcessAlive2 = (pid) => {
|
|
2529
|
+
try {
|
|
2530
|
+
process.kill(pid, 0);
|
|
2531
|
+
return true;
|
|
2532
|
+
} catch (error) {
|
|
2533
|
+
return !(typeof error === "object" && error !== null && "code" in error && error.code === "ESRCH");
|
|
2534
|
+
}
|
|
2535
|
+
};
|
|
2536
|
+
var getRuntimeCommandArgs = (runtime, mode) => [
|
|
2537
|
+
...runtime.nodeArgs ?? [],
|
|
2538
|
+
runtime.entrypoint,
|
|
2539
|
+
"daemon",
|
|
2540
|
+
"--internal-daemon",
|
|
2541
|
+
"--daemon-mode",
|
|
2542
|
+
mode,
|
|
2543
|
+
"--plain"
|
|
2544
|
+
];
|
|
2545
|
+
var compareVersions = (left, right) => {
|
|
2546
|
+
const leftParts = left.split(/[.-]/).map((part) => Number.parseInt(part, 10) || 0);
|
|
2547
|
+
const rightParts = right.split(/[.-]/).map((part) => Number.parseInt(part, 10) || 0);
|
|
2548
|
+
for (let index = 0; index < Math.max(leftParts.length, rightParts.length); index += 1) {
|
|
2549
|
+
const difference = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
|
|
2550
|
+
if (difference !== 0) return difference;
|
|
2551
|
+
}
|
|
2552
|
+
return 0;
|
|
2553
|
+
};
|
|
2554
|
+
var shouldReplaceDaemonVersion = (runningVersion, candidateVersion) => compareVersions(candidateVersion, runningVersion) > 0;
|
|
2555
|
+
var waitForDaemon = async (launchConfig, deadline) => {
|
|
2556
|
+
let lastConfig = null;
|
|
2557
|
+
while (Date.now() < deadline) {
|
|
2558
|
+
lastConfig = await readDeviceConfig(launchConfig, getConfigRoot());
|
|
2559
|
+
if (lastConfig) {
|
|
2560
|
+
const status = await readLiveStatus(lastConfig);
|
|
2561
|
+
if (status) return { config: lastConfig, status };
|
|
2562
|
+
}
|
|
2563
|
+
await wait2(POLL_INTERVAL_MS);
|
|
2564
|
+
}
|
|
2565
|
+
throw new Error(
|
|
2566
|
+
lastConfig ? "Connector daemon did not expose its control channel in time" : "Connector registration did not create a canonical local identity in time"
|
|
2567
|
+
);
|
|
2568
|
+
};
|
|
2569
|
+
var spawnInternalDaemon = async (launchConfig, mode, preparedRuntime) => {
|
|
2570
|
+
const runtime = preparedRuntime ?? await ensureStableConnectorRuntime();
|
|
2571
|
+
const child = spawn(runtime.nodeExecutable, getRuntimeCommandArgs(runtime, mode), {
|
|
2572
|
+
detached: true,
|
|
2573
|
+
env: {
|
|
2574
|
+
...process.env,
|
|
2575
|
+
BUDA_CONNECTOR_INTERNAL_LAUNCH_CONFIG: JSON.stringify(launchConfig ?? {})
|
|
2576
|
+
},
|
|
2577
|
+
stdio: "ignore",
|
|
2578
|
+
windowsHide: true
|
|
2579
|
+
});
|
|
2580
|
+
child.unref();
|
|
2581
|
+
};
|
|
2582
|
+
var stopOlderIdleDaemon = async (config, status) => {
|
|
2583
|
+
if (!shouldReplaceDaemonVersion(status.version, CONNECTOR_RUNTIME_VERSION)) return false;
|
|
2584
|
+
if (!status.idle) return false;
|
|
2585
|
+
const stopStatus = await requestDaemonControl(config, "stop-if-idle", getConfigRoot());
|
|
2586
|
+
if (!stopStatus.idle) return false;
|
|
2587
|
+
const deadline = Date.now() + 1e4;
|
|
2588
|
+
while (Date.now() < deadline) {
|
|
2589
|
+
if (!await readLiveStatus(config)) return true;
|
|
2590
|
+
await wait2(POLL_INTERVAL_MS);
|
|
2591
|
+
}
|
|
2592
|
+
throw new Error("Older Connector daemon did not stop cleanly; existing identity was preserved");
|
|
2593
|
+
};
|
|
2594
|
+
async function ensureConnectorDaemon(launchConfig, mode) {
|
|
2595
|
+
const deviceId = await getOrCreateSystemDeviceId(getConfigRoot());
|
|
2596
|
+
const scope = await resolveConnectorScope(deviceId, launchConfig);
|
|
2597
|
+
launchConfig = { ...launchConfig, spaceId: scope.spaceId };
|
|
2598
|
+
const bootstrapTarget = getConnectorBootstrapLockTarget(launchConfig, getConfigRoot());
|
|
2599
|
+
await mkdir6(path7.dirname(bootstrapTarget), { recursive: true, mode: 448 });
|
|
2600
|
+
const bootstrapLock = await acquireExclusiveFileLock(bootstrapTarget, {
|
|
2601
|
+
timeoutMs: BOOTSTRAP_TIMEOUT_MS
|
|
2602
|
+
});
|
|
2603
|
+
try {
|
|
2604
|
+
const existingConfig = await readDeviceConfig(launchConfig, getConfigRoot());
|
|
2605
|
+
if (existingConfig) {
|
|
2606
|
+
const existingStatus = await readLiveStatus(existingConfig);
|
|
2607
|
+
if (existingStatus) {
|
|
2608
|
+
const preparedRuntime = !shouldReplaceDaemonVersion(existingStatus.version, CONNECTOR_RUNTIME_VERSION) || !existingStatus.idle ? void 0 : await ensureStableConnectorRuntime();
|
|
2609
|
+
const replaced = await stopOlderIdleDaemon(existingConfig, existingStatus);
|
|
2610
|
+
if (!replaced) {
|
|
2611
|
+
const status = mode === "persistent" ? await requestDaemonControl(existingConfig, "persist", getConfigRoot()) : existingStatus;
|
|
2612
|
+
return { config: existingConfig, status };
|
|
2613
|
+
}
|
|
2614
|
+
await spawnInternalDaemon(launchConfig, mode, preparedRuntime);
|
|
2615
|
+
return await waitForDaemon(launchConfig, Date.now() + START_TIMEOUT_MS);
|
|
2616
|
+
}
|
|
2617
|
+
const persistedStatus = await readPersistedDaemonStatus(
|
|
2618
|
+
existingConfig,
|
|
2619
|
+
getConfigRoot()
|
|
2620
|
+
).catch(() => null);
|
|
2621
|
+
if (persistedStatus && isProcessAlive2(persistedStatus.pid)) {
|
|
2622
|
+
throw new Error(
|
|
2623
|
+
`Connector daemon PID ${persistedStatus.pid} is running but its local control channel is not responding. Stop that process and retry.`
|
|
2624
|
+
);
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
await spawnInternalDaemon(launchConfig, mode);
|
|
2628
|
+
return await waitForDaemon(launchConfig, Date.now() + START_TIMEOUT_MS);
|
|
2629
|
+
} finally {
|
|
2630
|
+
await bootstrapLock.release();
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
var streamDaemonLogs = (options) => {
|
|
2634
|
+
const logPath = getConnectorPaths(options.serverUrl, getConfigRoot()).logPath;
|
|
2635
|
+
let activePath = "";
|
|
2636
|
+
let seenLines = 0;
|
|
2637
|
+
const poll = async () => {
|
|
2638
|
+
const datedPath = getDatedConnectorLogPath(logPath);
|
|
2639
|
+
const raw = await readFile6(datedPath, "utf8").catch(() => "");
|
|
2640
|
+
const lines = raw.split("\n").filter(Boolean);
|
|
2641
|
+
if (datedPath !== activePath || lines.length < seenLines) {
|
|
2642
|
+
activePath = datedPath;
|
|
2643
|
+
seenLines = 0;
|
|
2644
|
+
}
|
|
2645
|
+
for (const line of lines.slice(seenLines)) {
|
|
2646
|
+
try {
|
|
2647
|
+
const entry = JSON.parse(line);
|
|
2648
|
+
const connectorId = entry.metadata?.connectorId;
|
|
2649
|
+
if (options.dashboardActive && typeof connectorId === "string") {
|
|
2650
|
+
const output2 = entry.metadata?.source === "daemon-console" ? entry.message : `[buda-connector] ${entry.message}`;
|
|
2651
|
+
appendConnectorDashboardLog(connectorId, entry.level, output2);
|
|
2652
|
+
continue;
|
|
2653
|
+
}
|
|
2654
|
+
if (connectorId !== options.connectorId) continue;
|
|
2655
|
+
const output = entry.metadata?.source === "daemon-console" ? entry.message : `[buda-connector] ${entry.message}`;
|
|
2656
|
+
if (entry.level === "error") console.error(output);
|
|
2657
|
+
else if (entry.level === "warn") console.warn(output);
|
|
2658
|
+
else console.log(output);
|
|
2659
|
+
} catch {
|
|
2660
|
+
if (options.plain) console.log(line);
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
seenLines = lines.length;
|
|
2664
|
+
};
|
|
2665
|
+
void poll();
|
|
2666
|
+
const timer = setInterval(() => void poll(), 500);
|
|
2667
|
+
return () => clearInterval(timer);
|
|
2668
|
+
};
|
|
2669
|
+
var toDashboardConnector = (config, status) => ({
|
|
2670
|
+
agent: status ? status.idle ? "Ready" : "Busy" : "Stopped",
|
|
2671
|
+
connectorId: config.connectorId,
|
|
2672
|
+
heartbeat: status?.lastHeartbeatAt ? `OK ${new Date(status.lastHeartbeatAt).toLocaleTimeString("en-GB", { hour12: false })}` : status ? "Waiting" : "Stopped",
|
|
2673
|
+
relay: status?.relayConnected ? "Connected" : "Disconnected",
|
|
2674
|
+
serverUrl: config.serverUrl,
|
|
2675
|
+
spaceId: config.spaceId ?? "Unknown",
|
|
2676
|
+
status: status ? status.relayConnected ? "Online" : "Running" : "Stopped"
|
|
2677
|
+
});
|
|
2678
|
+
async function readLocalConnectorDashboardRows(configRoot = getConfigRoot()) {
|
|
2679
|
+
const configs = await listDeviceConfigs(configRoot);
|
|
2680
|
+
const persistedStatuses = await listPersistedDaemonStatuses(configRoot);
|
|
2681
|
+
const persistedByIdentity = new Map(
|
|
2682
|
+
persistedStatuses.map((status) => [
|
|
2683
|
+
`${normalizeServerOrigin(status.serverUrl)}
|
|
2684
|
+
${status.connectorId}`,
|
|
2685
|
+
status
|
|
2686
|
+
])
|
|
2687
|
+
);
|
|
2688
|
+
const identities = /* @__PURE__ */ new Map();
|
|
2689
|
+
for (const config of configs) {
|
|
2690
|
+
identities.set(`${normalizeServerOrigin(config.serverUrl)}
|
|
2691
|
+
${config.connectorId}`, {
|
|
2692
|
+
identity: config,
|
|
2693
|
+
stored: true
|
|
2694
|
+
});
|
|
2695
|
+
}
|
|
2696
|
+
for (const status of persistedStatuses) {
|
|
2697
|
+
const key = `${normalizeServerOrigin(status.serverUrl)}
|
|
2698
|
+
${status.connectorId}`;
|
|
2699
|
+
if (!identities.has(key)) {
|
|
2700
|
+
identities.set(key, { identity: status, stored: false });
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
const rows = await Promise.all(
|
|
2704
|
+
[...identities.entries()].map(async ([key, { identity, stored }]) => {
|
|
2705
|
+
const status = await readLiveStatus(identity, configRoot, 1500);
|
|
2706
|
+
if (status) return toDashboardConnector(identity, status);
|
|
2707
|
+
const persistedStatus = persistedByIdentity.get(key) ?? await readPersistedDaemonStatus(identity, configRoot).catch(() => null);
|
|
2708
|
+
const row = toDashboardConnector(identity, null);
|
|
2709
|
+
if (!stored && (!persistedStatus || !isProcessAlive2(persistedStatus.pid))) return null;
|
|
2710
|
+
return persistedStatus && isProcessAlive2(persistedStatus.pid) ? { ...row, status: "Unresponsive" } : row;
|
|
2711
|
+
})
|
|
2712
|
+
);
|
|
2713
|
+
return rows.filter((row) => row !== null);
|
|
2714
|
+
}
|
|
2715
|
+
async function stopLocalConnectorDaemon(connectorId, serverUrl, configRoot = getConfigRoot()) {
|
|
2716
|
+
const configs = await listDeviceConfigs(configRoot);
|
|
2717
|
+
const persistedStatuses = await listPersistedDaemonStatuses(configRoot);
|
|
2718
|
+
const serverOrigin = normalizeServerOrigin(serverUrl);
|
|
2719
|
+
const config = [...configs, ...persistedStatuses].find(
|
|
2720
|
+
(candidate) => candidate.connectorId === connectorId && normalizeServerOrigin(candidate.serverUrl) === serverOrigin
|
|
2721
|
+
);
|
|
2722
|
+
if (!config) throw new Error(`Local Connector ${connectorId} was not found`);
|
|
2723
|
+
const status = await readLiveStatus(config, configRoot);
|
|
2724
|
+
if (!status) return;
|
|
2725
|
+
await requestDaemonControl(config, "stop", configRoot);
|
|
2726
|
+
}
|
|
2727
|
+
async function stopAllLocalConnectorDaemons(configRoot = getConfigRoot()) {
|
|
2728
|
+
const configs = await listDeviceConfigs(configRoot);
|
|
2729
|
+
const persistedStatuses = await listPersistedDaemonStatuses(configRoot);
|
|
2730
|
+
const identities = /* @__PURE__ */ new Map();
|
|
2731
|
+
for (const config of [...configs, ...persistedStatuses]) {
|
|
2732
|
+
identities.set(`${normalizeServerOrigin(config.serverUrl)}
|
|
2733
|
+
${config.connectorId}`, config);
|
|
2734
|
+
}
|
|
2735
|
+
const results = await Promise.allSettled(
|
|
2736
|
+
[...identities.values()].map(async (config) => {
|
|
2737
|
+
const status = await readLiveStatus(config, configRoot);
|
|
2738
|
+
if (status) await requestDaemonControl(config, "stop", configRoot);
|
|
2739
|
+
})
|
|
2740
|
+
);
|
|
2741
|
+
const failures = results.filter((result) => result.status === "rejected");
|
|
2742
|
+
if (failures.length) {
|
|
2743
|
+
throw new Error(`Failed to stop ${failures.length} local Connector daemon(s)`);
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
var waitForConnectorViewerExit = async (dashboardClosed) => {
|
|
2747
|
+
let finishSignal = () => void 0;
|
|
2748
|
+
const signal = new Promise((resolve) => {
|
|
2749
|
+
finishSignal = resolve;
|
|
2750
|
+
});
|
|
2751
|
+
process.once("SIGINT", finishSignal);
|
|
2752
|
+
process.once("SIGTERM", finishSignal);
|
|
2753
|
+
await (dashboardClosed ? Promise.race([dashboardClosed, signal]) : signal);
|
|
2754
|
+
process.off("SIGINT", finishSignal);
|
|
2755
|
+
process.off("SIGTERM", finishSignal);
|
|
2756
|
+
};
|
|
2757
|
+
async function runConnectorDashboard(launchConfig, options = {}) {
|
|
2758
|
+
let finishDashboard = () => void 0;
|
|
2759
|
+
const dashboardClosed = new Promise((resolve) => {
|
|
2760
|
+
finishDashboard = resolve;
|
|
2761
|
+
});
|
|
2762
|
+
const dashboard = startConnectorConsoleDashboard({
|
|
2763
|
+
manager: {
|
|
2764
|
+
onExit: finishDashboard,
|
|
2765
|
+
onStopAll: () => stopAllLocalConnectorDaemons(),
|
|
2766
|
+
onStopConnector: (connectorId, serverUrl) => stopLocalConnectorDaemon(connectorId, serverUrl)
|
|
2767
|
+
},
|
|
2768
|
+
plain: options.plain
|
|
2769
|
+
});
|
|
2770
|
+
if (!dashboard) {
|
|
2771
|
+
const { config } = await ensureConnectorDaemon(launchConfig, "persistent");
|
|
2772
|
+
const stopLogs2 = streamDaemonLogs({
|
|
2773
|
+
connectorId: config.connectorId,
|
|
2774
|
+
dashboardActive: false,
|
|
2775
|
+
plain: Boolean(options.plain),
|
|
2776
|
+
serverUrl: config.serverUrl
|
|
2777
|
+
});
|
|
2778
|
+
if (!process.stdin.isTTY) {
|
|
2779
|
+
stopLogs2();
|
|
2780
|
+
console.log(`Buda Connector ${config.connectorId} is running in the background.`);
|
|
2781
|
+
return;
|
|
2782
|
+
}
|
|
2783
|
+
await waitForConnectorViewerExit();
|
|
2784
|
+
stopLogs2();
|
|
2785
|
+
return;
|
|
2786
|
+
}
|
|
2787
|
+
const pendingConnectorId = `pending:${normalizeServerOrigin(getServerUrl(launchConfig))}`;
|
|
2788
|
+
let pendingConnector = {
|
|
2789
|
+
agent: "Starting",
|
|
2790
|
+
connectorId: pendingConnectorId,
|
|
2791
|
+
heartbeat: "Waiting",
|
|
2792
|
+
label: "New Connector",
|
|
2793
|
+
relay: "Connecting",
|
|
2794
|
+
serverUrl: getServerUrl(launchConfig),
|
|
2795
|
+
spaceId: "Resolving Space",
|
|
2796
|
+
status: "Connecting"
|
|
2797
|
+
};
|
|
2798
|
+
let preferredConnectorId = pendingConnectorId;
|
|
2799
|
+
let bootstrapSettled = false;
|
|
2800
|
+
const stopLogs = streamDaemonLogs({
|
|
2801
|
+
dashboardActive: true,
|
|
2802
|
+
plain: false,
|
|
2803
|
+
serverUrl: getServerUrl(launchConfig)
|
|
2804
|
+
});
|
|
2805
|
+
let refreshing = false;
|
|
2806
|
+
const refresh = async () => {
|
|
2807
|
+
if (refreshing) return;
|
|
2808
|
+
refreshing = true;
|
|
2809
|
+
try {
|
|
2810
|
+
const connectors = await readLocalConnectorDashboardRows();
|
|
2811
|
+
updateConnectorDashboardConnectors(
|
|
2812
|
+
pendingConnector ? [...connectors, pendingConnector] : connectors,
|
|
2813
|
+
preferredConnectorId
|
|
2814
|
+
);
|
|
2815
|
+
} finally {
|
|
2816
|
+
refreshing = false;
|
|
2817
|
+
}
|
|
2818
|
+
};
|
|
2819
|
+
updateConnectorDashboardConnectors([pendingConnector], pendingConnectorId);
|
|
2820
|
+
void readDeviceConfig(launchConfig, getConfigRoot()).then((config) => {
|
|
2821
|
+
if (!config) return;
|
|
2822
|
+
pendingConnector = void 0;
|
|
2823
|
+
preferredConnectorId = config.connectorId;
|
|
2824
|
+
}).catch(() => void 0).finally(() => refresh());
|
|
2825
|
+
const timer = setInterval(() => void refresh(), 1e3);
|
|
2826
|
+
void ensureConnectorDaemon(launchConfig, "persistent").then(async ({ config }) => {
|
|
2827
|
+
pendingConnector = void 0;
|
|
2828
|
+
preferredConnectorId = config.connectorId;
|
|
2829
|
+
await refresh();
|
|
2830
|
+
}).catch(async (error) => {
|
|
2831
|
+
const config = await readDeviceConfig(launchConfig, getConfigRoot()).catch(() => null);
|
|
2832
|
+
const message = `connector startup failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
2833
|
+
if (config) {
|
|
2834
|
+
pendingConnector = void 0;
|
|
2835
|
+
preferredConnectorId = config.connectorId;
|
|
2836
|
+
appendConnectorDashboardLog(config.connectorId, "error", `[buda-connector] ${message}`);
|
|
2837
|
+
await refresh();
|
|
2838
|
+
} else {
|
|
2839
|
+
pendingConnector = {
|
|
2840
|
+
...pendingConnector,
|
|
2841
|
+
agent: "Error",
|
|
2842
|
+
heartbeat: "Failed",
|
|
2843
|
+
relay: "Failed",
|
|
2844
|
+
status: "Error"
|
|
2845
|
+
};
|
|
2846
|
+
appendConnectorDashboardLog(pendingConnectorId, "error", `[buda-connector] ${message}`);
|
|
2847
|
+
await refresh();
|
|
2848
|
+
}
|
|
2849
|
+
}).finally(() => {
|
|
2850
|
+
bootstrapSettled = true;
|
|
2851
|
+
});
|
|
2852
|
+
await waitForConnectorViewerExit(dashboardClosed);
|
|
2853
|
+
clearInterval(timer);
|
|
2854
|
+
stopLogs();
|
|
2855
|
+
dashboard.stop();
|
|
2856
|
+
if (!bootstrapSettled) process.exit(0);
|
|
2857
|
+
}
|
|
2858
|
+
async function runDesktopConnectorController(launchConfig) {
|
|
2859
|
+
const { config } = await ensureConnectorDaemon(launchConfig, "transient");
|
|
2860
|
+
if (!config.spaceId?.trim()) {
|
|
2861
|
+
throw new Error("Connector scope did not resolve a canonical spaceId");
|
|
2862
|
+
}
|
|
2863
|
+
const lease = await holdDaemonControl(config, getConfigRoot());
|
|
2864
|
+
process.stdout.write(
|
|
2865
|
+
`${JSON.stringify({
|
|
2866
|
+
type: "buda-connector-ready",
|
|
2867
|
+
connectorId: lease.status.connectorId,
|
|
2868
|
+
pid: lease.status.pid,
|
|
2869
|
+
spaceId: lease.status.spaceId
|
|
2870
|
+
})}
|
|
2871
|
+
`
|
|
2872
|
+
);
|
|
2873
|
+
const stopped = new Promise((resolve) => {
|
|
2874
|
+
process.once("SIGINT", resolve);
|
|
2875
|
+
process.once("SIGTERM", resolve);
|
|
2876
|
+
process.stdin.once("end", resolve);
|
|
2877
|
+
process.stdin.once("close", resolve);
|
|
2878
|
+
if (process.stdin.readableEnded || process.stdin.destroyed) resolve();
|
|
2879
|
+
});
|
|
2880
|
+
await Promise.race([lease.closed, stopped]);
|
|
2881
|
+
lease.close();
|
|
2882
|
+
}
|
|
2883
|
+
async function releaseDesktopConnector(launchConfig) {
|
|
2884
|
+
if (!launchConfig?.spaceId?.trim()) return;
|
|
2885
|
+
const config = await readDeviceConfig(launchConfig, getConfigRoot());
|
|
2886
|
+
if (!config) return;
|
|
2887
|
+
await requestDaemonControl(config, "release", getConfigRoot()).catch(() => void 0);
|
|
2888
|
+
}
|
|
2889
|
+
var getInternalLaunchConfig = () => {
|
|
2890
|
+
const raw = process.env.BUDA_CONNECTOR_INTERNAL_LAUNCH_CONFIG;
|
|
2891
|
+
if (!raw) return void 0;
|
|
2892
|
+
return JSON.parse(raw);
|
|
2893
|
+
};
|
|
2894
|
+
var getDaemonModeFlag = () => {
|
|
2895
|
+
const index = process.argv.indexOf("--daemon-mode");
|
|
2896
|
+
return process.argv[index + 1] === "transient" ? "transient" : "persistent";
|
|
2897
|
+
};
|
|
2898
|
+
|
|
2899
|
+
// src/onboard.ts
|
|
2900
|
+
import { cancel, intro, isCancel, log, note, outro, spinner, text } from "@clack/prompts";
|
|
2901
|
+
|
|
2902
|
+
// src/bunny-daemon.ts
|
|
2903
|
+
import { mkdir as mkdir7 } from "fs/promises";
|
|
2904
|
+
import { createDaemon } from "@bunny-agent/daemon";
|
|
2905
|
+
|
|
2906
|
+
// src/terminal.ts
|
|
2907
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
2908
|
+
import { chmod as chmod3, stat as stat3 } from "fs/promises";
|
|
2909
|
+
import { createRequire } from "module";
|
|
2910
|
+
import path8 from "path";
|
|
2911
|
+
import WebSocket, { WebSocketServer } from "ws";
|
|
2912
|
+
var TERMINAL_IDLE_TTL_MS = 10 * 6e4;
|
|
2913
|
+
var TERMINAL_HANDSHAKE_TIMEOUT_MS = 1e4;
|
|
2914
|
+
var LOCAL_TERMINAL_GRANT_TTL_MS = 3e4;
|
|
2915
|
+
var LOCAL_TERMINAL_CLOSE_SPAWN_FAILED = 4401;
|
|
2916
|
+
var MAX_BUFFER_CHUNKS = 1e3;
|
|
2917
|
+
var MAX_TERMINAL_FRAME_BYTES = 1024 * 1024;
|
|
2918
|
+
var require2 = createRequire(import.meta.url);
|
|
2919
|
+
var ensureNodePtySpawnHelperExecutable = async (options = {}) => {
|
|
2920
|
+
const platform4 = options.platform ?? process.platform;
|
|
2921
|
+
if (platform4 !== "darwin") return;
|
|
2922
|
+
const arch = options.arch ?? process.arch;
|
|
2923
|
+
const entryPath = options.entryPath ?? require2.resolve("node-pty");
|
|
2924
|
+
const packageRoot = path8.dirname(path8.dirname(entryPath));
|
|
2925
|
+
const helperPath = path8.join(packageRoot, "prebuilds", `${platform4}-${arch}`, "spawn-helper");
|
|
2926
|
+
const helper = await stat3(helperPath);
|
|
2927
|
+
if ((helper.mode & 64) === 0) await chmod3(helperPath, helper.mode | 64);
|
|
2928
|
+
};
|
|
2929
|
+
var loadNodePty = async () => {
|
|
2930
|
+
await ensureNodePtySpawnHelperExecutable();
|
|
2931
|
+
return import("node-pty");
|
|
2932
|
+
};
|
|
2933
|
+
var normalizeTerminalCloseCode = (code, fallback = 1011) => code >= 1e3 && code <= 1014 && ![1004, 1005, 1006].includes(code) || code >= 3e3 && code <= 4999 ? code : fallback;
|
|
2934
|
+
var truncateTerminalCloseReason = (reason, maxBytes = 120) => {
|
|
2935
|
+
let result = reason;
|
|
2936
|
+
while (Buffer.byteLength(result, "utf8") > maxBytes) result = result.slice(0, -1);
|
|
2937
|
+
return result;
|
|
2938
|
+
};
|
|
2939
|
+
var sessions2 = /* @__PURE__ */ new Map();
|
|
2940
|
+
var localGrants = /* @__PURE__ */ new Map();
|
|
2941
|
+
var installedServers = /* @__PURE__ */ new WeakSet();
|
|
2942
|
+
var isSafeId = (value) => typeof value === "string" && /^[A-Za-z0-9_.:-]{1,160}$/.test(value);
|
|
2943
|
+
var parseDimension = (value, fallback, min, max) => typeof value === "number" && Number.isSafeInteger(value) ? Math.max(min, Math.min(max, value)) : fallback;
|
|
2944
|
+
var parseConnectorTerminalRequest = (request) => {
|
|
2945
|
+
if (request.method !== "CONNECT" || request.path !== "/api/terminal") {
|
|
2946
|
+
throw new Error("Invalid connector terminal request.");
|
|
2947
|
+
}
|
|
2948
|
+
if (!request.body || typeof request.body !== "object" || Array.isArray(request.body)) {
|
|
2949
|
+
throw new Error("Connector terminal request body is invalid.");
|
|
2950
|
+
}
|
|
2951
|
+
const body = request.body;
|
|
2952
|
+
if (!isSafeId(body.sessionId) || !isSafeId(body.tabId)) {
|
|
2953
|
+
throw new Error("Connector terminal session identity is invalid.");
|
|
2954
|
+
}
|
|
2955
|
+
if (typeof body.token !== "string" || body.token.length < 32 || body.token.length > 2048) {
|
|
2956
|
+
throw new Error("Connector terminal token is invalid.");
|
|
2957
|
+
}
|
|
2958
|
+
return {
|
|
2959
|
+
sessionId: body.sessionId,
|
|
2960
|
+
tabId: body.tabId,
|
|
2961
|
+
token: body.token,
|
|
2962
|
+
cols: parseDimension(body.cols, 80, 10, 500),
|
|
2963
|
+
rows: parseDimension(body.rows, 24, 5, 200)
|
|
2964
|
+
};
|
|
2965
|
+
};
|
|
2966
|
+
var clearIdleTimer = (session) => {
|
|
2967
|
+
if (!session.idleTimer) return;
|
|
2968
|
+
clearTimeout(session.idleTimer);
|
|
2969
|
+
session.idleTimer = null;
|
|
2970
|
+
};
|
|
2971
|
+
var scheduleIdleCleanup = (sessionKey, session) => {
|
|
2972
|
+
clearIdleTimer(session);
|
|
2973
|
+
session.idleTimer = setTimeout(() => {
|
|
2974
|
+
if (session.clients.size > 0 || sessions2.get(sessionKey) !== session) return;
|
|
2975
|
+
sessions2.delete(sessionKey);
|
|
2976
|
+
try {
|
|
2977
|
+
session.pty.kill();
|
|
2978
|
+
} catch {
|
|
2979
|
+
}
|
|
2980
|
+
}, TERMINAL_IDLE_TTL_MS);
|
|
2981
|
+
session.idleTimer.unref();
|
|
2982
|
+
};
|
|
2983
|
+
var sendJson = (socket, value) => {
|
|
2984
|
+
if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify(value));
|
|
2985
|
+
};
|
|
2986
|
+
var LocalTerminalUnavailableError = class extends Error {
|
|
2987
|
+
};
|
|
2988
|
+
var serializeErrorMessage = (error) => error instanceof Error ? error.message : "Unknown error";
|
|
2989
|
+
var createTerminalSession = async (grant, nodePtyLoader) => {
|
|
2990
|
+
let nodePty;
|
|
2991
|
+
try {
|
|
2992
|
+
nodePty = await nodePtyLoader();
|
|
2993
|
+
} catch (error) {
|
|
2994
|
+
void writeLog(
|
|
2995
|
+
"error",
|
|
2996
|
+
`local terminal could not load node-pty: ${serializeErrorMessage(error)}`
|
|
2997
|
+
);
|
|
2998
|
+
throw new LocalTerminalUnavailableError(
|
|
2999
|
+
"Local terminal is unavailable because node-pty could not be loaded. The connector remains available for other capabilities."
|
|
3000
|
+
);
|
|
3001
|
+
}
|
|
3002
|
+
const shell = process.platform === "win32" ? "powershell.exe" : process.env.SHELL || "/bin/bash";
|
|
3003
|
+
const shellArgs = process.platform === "win32" ? [] : ["-l"];
|
|
3004
|
+
let pty;
|
|
3005
|
+
try {
|
|
3006
|
+
pty = nodePty.spawn(shell, shellArgs, {
|
|
3007
|
+
name: "xterm-256color",
|
|
3008
|
+
cols: grant.cols,
|
|
3009
|
+
rows: grant.rows,
|
|
3010
|
+
cwd: path8.resolve(grant.cwd),
|
|
3011
|
+
env: { ...process.env, TERM: "xterm-256color" }
|
|
3012
|
+
});
|
|
3013
|
+
} catch (error) {
|
|
3014
|
+
const message = serializeErrorMessage(error);
|
|
3015
|
+
void writeLog(
|
|
3016
|
+
"error",
|
|
3017
|
+
`local terminal failed to spawn shell=${shell} cwd=${grant.cwd}: ${message}`
|
|
3018
|
+
);
|
|
3019
|
+
throw new LocalTerminalUnavailableError(
|
|
3020
|
+
"Local terminal is unavailable because node-pty could not start a shell. The connector remains available for other capabilities."
|
|
3021
|
+
);
|
|
3022
|
+
}
|
|
3023
|
+
const session = {
|
|
3024
|
+
pty,
|
|
3025
|
+
clients: /* @__PURE__ */ new Set(),
|
|
3026
|
+
buffer: [],
|
|
3027
|
+
cols: grant.cols,
|
|
3028
|
+
rows: grant.rows,
|
|
3029
|
+
idleTimer: null
|
|
3030
|
+
};
|
|
3031
|
+
sessions2.set(grant.sessionKey, session);
|
|
3032
|
+
pty.onData((data) => {
|
|
3033
|
+
session.buffer.push(data);
|
|
3034
|
+
if (session.buffer.length > MAX_BUFFER_CHUNKS) session.buffer.shift();
|
|
3035
|
+
for (const socket of session.clients) {
|
|
3036
|
+
if (socket.readyState === WebSocket.OPEN) socket.send(data);
|
|
3037
|
+
}
|
|
3038
|
+
});
|
|
3039
|
+
pty.onExit(({ exitCode }) => {
|
|
3040
|
+
if (sessions2.get(grant.sessionKey) === session) sessions2.delete(grant.sessionKey);
|
|
3041
|
+
clearIdleTimer(session);
|
|
3042
|
+
for (const socket of session.clients) {
|
|
3043
|
+
sendJson(socket, { type: "status", message: `exited (code ${exitCode})` });
|
|
3044
|
+
socket.close(1e3, "Terminal exited");
|
|
3045
|
+
}
|
|
3046
|
+
session.clients.clear();
|
|
3047
|
+
});
|
|
3048
|
+
return session;
|
|
3049
|
+
};
|
|
3050
|
+
var attachLocalTerminal = async (socket, grant, nodePtyLoader) => {
|
|
3051
|
+
const pendingMessages = [];
|
|
3052
|
+
let session;
|
|
3053
|
+
let started = false;
|
|
3054
|
+
const handleMessage = (data) => {
|
|
3055
|
+
if (!session) {
|
|
3056
|
+
pendingMessages.push(data);
|
|
3057
|
+
return;
|
|
3058
|
+
}
|
|
1156
3059
|
try {
|
|
1157
|
-
const
|
|
1158
|
-
|
|
1159
|
-
|
|
3060
|
+
const message = JSON.parse(data);
|
|
3061
|
+
if (message.type === "bridge-start") {
|
|
3062
|
+
if (!started) {
|
|
3063
|
+
started = true;
|
|
3064
|
+
sendJson(socket, { type: "status", message: "connected" });
|
|
3065
|
+
for (const chunk of session.buffer) socket.send(chunk);
|
|
3066
|
+
}
|
|
3067
|
+
return;
|
|
3068
|
+
}
|
|
3069
|
+
if (message.type === "input" && typeof message.data === "string") {
|
|
3070
|
+
session.pty.write(message.data);
|
|
3071
|
+
return;
|
|
3072
|
+
}
|
|
3073
|
+
if (message.type === "resize") {
|
|
3074
|
+
const cols = parseDimension(message.cols, session.cols, 10, 500);
|
|
3075
|
+
const rows = parseDimension(message.rows, session.rows, 5, 200);
|
|
3076
|
+
if (cols !== session.cols || rows !== session.rows) {
|
|
3077
|
+
session.cols = cols;
|
|
3078
|
+
session.rows = rows;
|
|
3079
|
+
session.pty.resize(cols, rows);
|
|
3080
|
+
}
|
|
3081
|
+
return;
|
|
3082
|
+
}
|
|
1160
3083
|
} catch {
|
|
1161
3084
|
}
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
3085
|
+
session.pty.write(data);
|
|
3086
|
+
};
|
|
3087
|
+
socket.on("message", (raw) => handleMessage(raw.toString()));
|
|
3088
|
+
session = sessions2.get(grant.sessionKey);
|
|
3089
|
+
if (!session) session = await createTerminalSession(grant, nodePtyLoader);
|
|
3090
|
+
else clearIdleTimer(session);
|
|
3091
|
+
session.clients.add(socket);
|
|
3092
|
+
for (const data of pendingMessages.splice(0)) handleMessage(data);
|
|
3093
|
+
const detach = () => {
|
|
3094
|
+
if (!session) return;
|
|
3095
|
+
session.clients.delete(socket);
|
|
3096
|
+
if (session.clients.size === 0) scheduleIdleCleanup(grant.sessionKey, session);
|
|
3097
|
+
};
|
|
3098
|
+
socket.once("close", detach);
|
|
3099
|
+
socket.once("error", detach);
|
|
3100
|
+
};
|
|
3101
|
+
var installConnectorTerminalEndpoint = (server, options = {}) => {
|
|
3102
|
+
if (installedServers.has(server)) return;
|
|
3103
|
+
installedServers.add(server);
|
|
3104
|
+
const terminalServer = new WebSocketServer({ noServer: true });
|
|
3105
|
+
server.on("upgrade", (request, socket, head) => {
|
|
3106
|
+
const url = new URL(request.url ?? "/", "http://connector.local");
|
|
3107
|
+
if (url.pathname !== "/api/terminal") return;
|
|
3108
|
+
const tokenHeader = request.headers["x-buda-local-terminal-token"];
|
|
3109
|
+
const token = Array.isArray(tokenHeader) ? tokenHeader[0] : tokenHeader;
|
|
3110
|
+
const grant = token ? localGrants.get(token) : void 0;
|
|
3111
|
+
if (!token || !grant || grant.expiresAt <= Date.now()) {
|
|
3112
|
+
if (token) localGrants.delete(token);
|
|
3113
|
+
socket.destroy();
|
|
3114
|
+
return;
|
|
3115
|
+
}
|
|
3116
|
+
localGrants.delete(token);
|
|
3117
|
+
terminalServer.handleUpgrade(request, socket, head, (client) => {
|
|
3118
|
+
void attachLocalTerminal(client, grant, options.nodePtyLoader ?? loadNodePty).catch(
|
|
3119
|
+
(error) => {
|
|
3120
|
+
const message = error instanceof LocalTerminalUnavailableError ? error.message : "Local terminal is unavailable. The connector remains available for other capabilities.";
|
|
3121
|
+
sendJson(client, { type: "error", fatal: true, message });
|
|
3122
|
+
client.close(LOCAL_TERMINAL_CLOSE_SPAWN_FAILED, "Local terminal unavailable");
|
|
3123
|
+
}
|
|
1165
3124
|
);
|
|
3125
|
+
});
|
|
3126
|
+
});
|
|
3127
|
+
server.once("close", () => {
|
|
3128
|
+
for (const client of terminalServer.clients) client.terminate();
|
|
3129
|
+
terminalServer.close();
|
|
3130
|
+
localGrants.clear();
|
|
3131
|
+
for (const session of sessions2.values()) {
|
|
3132
|
+
clearIdleTimer(session);
|
|
3133
|
+
try {
|
|
3134
|
+
session.pty.kill();
|
|
3135
|
+
} catch {
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
sessions2.clear();
|
|
3139
|
+
});
|
|
3140
|
+
};
|
|
3141
|
+
var registerLocalTerminal = (input) => {
|
|
3142
|
+
if (!isSafeId(input.workspaceKey) || !isSafeId(input.tabId)) {
|
|
3143
|
+
throw new Error("Invalid local terminal workspace session.");
|
|
3144
|
+
}
|
|
3145
|
+
const token = randomBytes3(32).toString("base64url");
|
|
3146
|
+
localGrants.set(token, {
|
|
3147
|
+
sessionKey: `${input.workspaceKey}:${input.tabId}`,
|
|
3148
|
+
workspaceKey: input.workspaceKey,
|
|
3149
|
+
cwd: input.cwd,
|
|
3150
|
+
cols: input.cols,
|
|
3151
|
+
rows: input.rows,
|
|
3152
|
+
expiresAt: Date.now() + LOCAL_TERMINAL_GRANT_TTL_MS
|
|
3153
|
+
});
|
|
3154
|
+
const cleanupTimer = setTimeout(() => localGrants.delete(token), LOCAL_TERMINAL_GRANT_TTL_MS);
|
|
3155
|
+
cleanupTimer.unref();
|
|
3156
|
+
return token;
|
|
3157
|
+
};
|
|
3158
|
+
var toLocalTerminalUrl = (baseUrl) => {
|
|
3159
|
+
const url = new URL("/api/terminal", baseUrl);
|
|
3160
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
3161
|
+
return url.toString();
|
|
3162
|
+
};
|
|
3163
|
+
var connectSocket = async (socket) => new Promise((resolve, reject) => {
|
|
3164
|
+
const onOpen = () => {
|
|
3165
|
+
socket.removeListener("error", onError);
|
|
3166
|
+
resolve();
|
|
3167
|
+
};
|
|
3168
|
+
const onError = (error) => {
|
|
3169
|
+
socket.removeListener("open", onOpen);
|
|
3170
|
+
reject(error);
|
|
3171
|
+
};
|
|
3172
|
+
socket.once("open", onOpen);
|
|
3173
|
+
socket.once("error", onError);
|
|
3174
|
+
});
|
|
3175
|
+
var proxyConnectorTerminalTunnel = async (input) => {
|
|
3176
|
+
const cloud = new WebSocket(
|
|
3177
|
+
getConnectorTerminalUrl(input.serverUrl, input.connectorId, input.sessionId),
|
|
3178
|
+
{
|
|
3179
|
+
headers: { "X-Buda-Terminal-Token": input.cloudToken },
|
|
3180
|
+
handshakeTimeout: TERMINAL_HANDSHAKE_TIMEOUT_MS
|
|
1166
3181
|
}
|
|
1167
|
-
throw new Error(`${pathname} failed: ${response.status} ${errorMessage3}`);
|
|
1168
|
-
}
|
|
1169
|
-
return await response.json();
|
|
1170
|
-
}
|
|
1171
|
-
function formatConnectorOwner(connector) {
|
|
1172
|
-
return connector.userEmail ?? connector.userId ?? "anonymous";
|
|
1173
|
-
}
|
|
1174
|
-
async function warnIfStaleBundle(result) {
|
|
1175
|
-
if (!result.bundle?.staleClient) return;
|
|
1176
|
-
await writeLog(
|
|
1177
|
-
"warn",
|
|
1178
|
-
`connector bundle out of date \u2014 server expects ${result.bundle.expectedVersion ?? "newer"}; run \`pnpm up @buda-ai/connector\``
|
|
1179
|
-
);
|
|
1180
|
-
}
|
|
1181
|
-
async function registerConnector(config, launchConfig, bunnyDaemon, options) {
|
|
1182
|
-
const result = await postJson(
|
|
1183
|
-
config.serverUrl,
|
|
1184
|
-
"/api/connectors/register",
|
|
1185
|
-
await buildPayload(config, {
|
|
1186
|
-
bunnyDaemon,
|
|
1187
|
-
...options?.recoverMissingProfile ? { recovery: "missing_local_profile" } : {}
|
|
1188
|
-
}),
|
|
1189
|
-
config,
|
|
1190
|
-
launchConfig
|
|
1191
3182
|
);
|
|
3183
|
+
const local = new WebSocket(toLocalTerminalUrl(input.localBaseUrl), {
|
|
3184
|
+
headers: { "X-Buda-Local-Terminal-Token": input.localToken },
|
|
3185
|
+
handshakeTimeout: TERMINAL_HANDSHAKE_TIMEOUT_MS
|
|
3186
|
+
});
|
|
3187
|
+
try {
|
|
3188
|
+
await Promise.all([connectSocket(cloud), connectSocket(local)]);
|
|
3189
|
+
} catch (error) {
|
|
3190
|
+
cloud.terminate();
|
|
3191
|
+
local.terminate();
|
|
3192
|
+
throw error;
|
|
3193
|
+
}
|
|
3194
|
+
let bridgeAccepted = false;
|
|
3195
|
+
let activityEnded = false;
|
|
3196
|
+
const endActivity = () => {
|
|
3197
|
+
if (activityEnded) return;
|
|
3198
|
+
activityEnded = true;
|
|
3199
|
+
input.onActivityEnd?.();
|
|
3200
|
+
};
|
|
3201
|
+
let pendingBytes = 0;
|
|
3202
|
+
const pendingCloudFrames = [];
|
|
3203
|
+
const pendingLocalFrames = [];
|
|
3204
|
+
const bufferFrame = (frames, data, isBinary) => {
|
|
3205
|
+
const bytes = Array.isArray(data) ? data.reduce((total, chunk) => total + chunk.byteLength, 0) : data.byteLength;
|
|
3206
|
+
if (pendingBytes + bytes > MAX_TERMINAL_FRAME_BYTES) {
|
|
3207
|
+
cloud.close(1009, "Terminal handshake buffer too large");
|
|
3208
|
+
local.close(1009, "Terminal handshake buffer too large");
|
|
3209
|
+
return false;
|
|
3210
|
+
}
|
|
3211
|
+
pendingBytes += bytes;
|
|
3212
|
+
frames.push([data, isBinary]);
|
|
3213
|
+
return true;
|
|
3214
|
+
};
|
|
3215
|
+
cloud.on("message", (data, isBinary) => {
|
|
3216
|
+
if (!bridgeAccepted) {
|
|
3217
|
+
if (!isBinary && data.toString() === '{"type":"bridge-accepted"}') {
|
|
3218
|
+
bridgeAccepted = true;
|
|
3219
|
+
local.send(JSON.stringify({ type: "bridge-start" }));
|
|
3220
|
+
for (const [frame, binary] of pendingCloudFrames) {
|
|
3221
|
+
if (local.readyState === WebSocket.OPEN) local.send(frame, { binary });
|
|
3222
|
+
}
|
|
3223
|
+
for (const [frame, binary] of pendingLocalFrames) {
|
|
3224
|
+
if (cloud.readyState === WebSocket.OPEN) cloud.send(frame, { binary });
|
|
3225
|
+
}
|
|
3226
|
+
pendingCloudFrames.length = 0;
|
|
3227
|
+
pendingLocalFrames.length = 0;
|
|
3228
|
+
pendingBytes = 0;
|
|
3229
|
+
return;
|
|
3230
|
+
}
|
|
3231
|
+
bufferFrame(pendingCloudFrames, data, isBinary);
|
|
3232
|
+
return;
|
|
3233
|
+
}
|
|
3234
|
+
if (local.readyState === WebSocket.OPEN) local.send(data, { binary: isBinary });
|
|
3235
|
+
});
|
|
3236
|
+
local.on("message", (data, isBinary) => {
|
|
3237
|
+
if (!bridgeAccepted) {
|
|
3238
|
+
bufferFrame(pendingLocalFrames, data, isBinary);
|
|
3239
|
+
return;
|
|
3240
|
+
}
|
|
3241
|
+
if (cloud.readyState === WebSocket.OPEN) cloud.send(data, { binary: isBinary });
|
|
3242
|
+
});
|
|
3243
|
+
cloud.once("close", (code, reason) => {
|
|
3244
|
+
endActivity();
|
|
3245
|
+
if (!bridgeAccepted) {
|
|
3246
|
+
void writeLog(
|
|
3247
|
+
"warn",
|
|
3248
|
+
`terminal cloud tunnel closed before readiness session=${input.sessionId} code=${code} reason=${truncateTerminalCloseReason(reason.toString())}`
|
|
3249
|
+
);
|
|
3250
|
+
}
|
|
3251
|
+
if (local.readyState === WebSocket.OPEN) {
|
|
3252
|
+
local.close(normalizeTerminalCloseCode(code), truncateTerminalCloseReason(reason.toString()));
|
|
3253
|
+
}
|
|
3254
|
+
});
|
|
3255
|
+
local.once("close", (code, reason) => {
|
|
3256
|
+
endActivity();
|
|
3257
|
+
if (cloud.readyState === WebSocket.OPEN) {
|
|
3258
|
+
cloud.close(normalizeTerminalCloseCode(code), truncateTerminalCloseReason(reason.toString()));
|
|
3259
|
+
}
|
|
3260
|
+
});
|
|
3261
|
+
cloud.once("error", () => local.terminate());
|
|
3262
|
+
local.once("error", () => cloud.terminate());
|
|
3263
|
+
input.onActivityStart?.();
|
|
3264
|
+
cloud.send(JSON.stringify({ type: "bridge-ready" }));
|
|
1192
3265
|
await writeLog(
|
|
1193
3266
|
"info",
|
|
1194
|
-
`
|
|
3267
|
+
`terminal proxy connected session=${input.sessionId} workspace=${input.workspaceKey}`
|
|
1195
3268
|
);
|
|
1196
|
-
|
|
3269
|
+
};
|
|
3270
|
+
|
|
3271
|
+
// src/bunny-daemon.ts
|
|
3272
|
+
async function startEmbeddedBunnyDaemon(config, launchConfig) {
|
|
3273
|
+
const host = getBunnyDaemonHost(launchConfig);
|
|
3274
|
+
const port = getBunnyDaemonPort(launchConfig);
|
|
3275
|
+
const root = config.workdirRoot;
|
|
3276
|
+
await mkdir7(root, { recursive: true });
|
|
3277
|
+
const server = createDaemon({ host, port, root });
|
|
3278
|
+
installConnectorTerminalEndpoint(server);
|
|
3279
|
+
await new Promise((resolve, reject) => {
|
|
3280
|
+
const onError = (error) => {
|
|
3281
|
+
server.off("listening", onListening);
|
|
3282
|
+
reject(error);
|
|
3283
|
+
};
|
|
3284
|
+
const onListening = () => {
|
|
3285
|
+
server.off("error", onError);
|
|
3286
|
+
resolve();
|
|
3287
|
+
};
|
|
3288
|
+
server.once("error", onError);
|
|
3289
|
+
server.once("listening", onListening);
|
|
3290
|
+
server.listen(port, host);
|
|
3291
|
+
});
|
|
3292
|
+
const address = server.address();
|
|
3293
|
+
const actualPort = typeof address === "object" && address ? address.port : port;
|
|
3294
|
+
const baseUrl = `http://${host}:${actualPort}`;
|
|
3295
|
+
await writeLog("info", `bunny daemon listening ${baseUrl}`, {
|
|
3296
|
+
root
|
|
3297
|
+
});
|
|
1197
3298
|
return {
|
|
1198
|
-
|
|
1199
|
-
|
|
3299
|
+
server,
|
|
3300
|
+
host,
|
|
3301
|
+
port: actualPort,
|
|
3302
|
+
baseUrl,
|
|
3303
|
+
root
|
|
1200
3304
|
};
|
|
1201
3305
|
}
|
|
1202
|
-
async function
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
config.serverUrl,
|
|
1206
|
-
"/api/connectors/heartbeat",
|
|
1207
|
-
await buildPayload(config, {
|
|
1208
|
-
bunnyDaemon,
|
|
1209
|
-
logs
|
|
1210
|
-
}),
|
|
1211
|
-
config,
|
|
1212
|
-
launchConfig
|
|
1213
|
-
).catch((error) => {
|
|
1214
|
-
restorePendingLogs(logs);
|
|
1215
|
-
throw error;
|
|
1216
|
-
});
|
|
1217
|
-
return { relayProtocol: result.relayProtocol };
|
|
1218
|
-
}
|
|
1219
|
-
async function unregisterConnector(config, launchConfig) {
|
|
1220
|
-
await postJson(
|
|
1221
|
-
config.serverUrl,
|
|
1222
|
-
"/api/connectors/unregister",
|
|
1223
|
-
{
|
|
1224
|
-
connectorId: config.connectorId
|
|
1225
|
-
},
|
|
1226
|
-
config,
|
|
1227
|
-
launchConfig
|
|
1228
|
-
).catch((error) => {
|
|
1229
|
-
void writeLog("warn", `unregister failed: ${error instanceof Error ? error.message : error}`);
|
|
3306
|
+
async function stopEmbeddedBunnyDaemon(daemon) {
|
|
3307
|
+
await new Promise((resolve) => {
|
|
3308
|
+
daemon.server.close(() => resolve());
|
|
1230
3309
|
});
|
|
3310
|
+
await writeLog("info", `bunny daemon stopped ${daemon.baseUrl}`);
|
|
1231
3311
|
}
|
|
1232
3312
|
|
|
1233
3313
|
// src/legacy-workspace-migration.ts
|
|
1234
|
-
import { createHash as createHash5, randomUUID as
|
|
3314
|
+
import { createHash as createHash5, randomUUID as randomUUID5 } from "crypto";
|
|
1235
3315
|
import { constants as constants2, createReadStream } from "fs";
|
|
1236
3316
|
import {
|
|
1237
|
-
chmod as
|
|
3317
|
+
chmod as chmod4,
|
|
1238
3318
|
copyFile,
|
|
1239
3319
|
lstat,
|
|
1240
|
-
mkdir as
|
|
3320
|
+
mkdir as mkdir8,
|
|
1241
3321
|
open as open2,
|
|
1242
|
-
readdir as
|
|
1243
|
-
readFile as
|
|
3322
|
+
readdir as readdir4,
|
|
3323
|
+
readFile as readFile7,
|
|
1244
3324
|
readlink,
|
|
1245
3325
|
realpath,
|
|
1246
|
-
rename as
|
|
1247
|
-
rm,
|
|
3326
|
+
rename as rename5,
|
|
3327
|
+
rm as rm2,
|
|
1248
3328
|
symlink,
|
|
1249
|
-
unlink as
|
|
3329
|
+
unlink as unlink5
|
|
1250
3330
|
} from "fs/promises";
|
|
1251
|
-
import
|
|
3331
|
+
import path9 from "path";
|
|
1252
3332
|
var MIGRATION_VERSION = 1;
|
|
1253
|
-
var MIGRATION_DIRECTORY =
|
|
3333
|
+
var MIGRATION_DIRECTORY = path9.join(".buda", "migrations", "legacy-workspace-v1");
|
|
1254
3334
|
var JOURNAL_FILENAME = "journal.json";
|
|
1255
3335
|
var IMMUTABLE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
1256
3336
|
var LEGACY_TEMPLATE_FILES = [
|
|
@@ -1286,7 +3366,7 @@ var LegacyWorkspaceMigrationError = class extends Error {
|
|
|
1286
3366
|
recoverable = true;
|
|
1287
3367
|
};
|
|
1288
3368
|
var migrateLegacyWorkspaceRoot = async (storageRoot, options = {}) => {
|
|
1289
|
-
await
|
|
3369
|
+
await mkdir8(storageRoot, { recursive: true, mode: 448 });
|
|
1290
3370
|
const rootInfo = await lstat(storageRoot);
|
|
1291
3371
|
if (rootInfo.isSymbolicLink() || !rootInfo.isDirectory()) {
|
|
1292
3372
|
throw new LegacyWorkspaceMigrationError(
|
|
@@ -1295,9 +3375,9 @@ var migrateLegacyWorkspaceRoot = async (storageRoot, options = {}) => {
|
|
|
1295
3375
|
);
|
|
1296
3376
|
}
|
|
1297
3377
|
const canonicalRoot = await realpath(storageRoot);
|
|
1298
|
-
const migrationDirectory =
|
|
3378
|
+
const migrationDirectory = path9.join(canonicalRoot, MIGRATION_DIRECTORY);
|
|
1299
3379
|
await ensurePrivateDirectoryWithin(canonicalRoot, migrationDirectory);
|
|
1300
|
-
const journalPath =
|
|
3380
|
+
const journalPath = path9.join(migrationDirectory, JOURNAL_FILENAME);
|
|
1301
3381
|
let journal = await readJournal(journalPath);
|
|
1302
3382
|
if (journal) validateJournalPaths(journal, canonicalRoot, migrationDirectory);
|
|
1303
3383
|
if (journal?.status === "complete") {
|
|
@@ -1330,8 +3410,8 @@ var migrateLegacyWorkspaceRoot = async (storageRoot, options = {}) => {
|
|
|
1330
3410
|
};
|
|
1331
3411
|
var migrateItem = async (storageRoot, journalPath, journal, item, options) => {
|
|
1332
3412
|
if (item.phase === "committed") return;
|
|
1333
|
-
if (await
|
|
1334
|
-
if (!await
|
|
3413
|
+
if (await exists2(item.target)) {
|
|
3414
|
+
if (!await exists2(item.staging) && ["verified", "copied", "copying"].includes(item.phase)) {
|
|
1335
3415
|
await verifyTree(item.source, item.target);
|
|
1336
3416
|
item.phase = "committed";
|
|
1337
3417
|
await writeJournal(journalPath, journal);
|
|
@@ -1346,8 +3426,8 @@ var migrateItem = async (storageRoot, journalPath, journal, item, options) => {
|
|
|
1346
3426
|
item.phase = "copying";
|
|
1347
3427
|
await writeJournal(journalPath, journal);
|
|
1348
3428
|
await checkpoint(options, "before-copy", item.id);
|
|
1349
|
-
await
|
|
1350
|
-
await ensurePrivateDirectoryWithin(storageRoot,
|
|
3429
|
+
await rm2(item.staging, { recursive: true, force: true });
|
|
3430
|
+
await ensurePrivateDirectoryWithin(storageRoot, path9.dirname(item.staging));
|
|
1351
3431
|
await copyTree(item.source, item.staging, () => checkpoint(options, "during-copy", item.id));
|
|
1352
3432
|
await checkpoint(options, "after-copy", item.id);
|
|
1353
3433
|
item.phase = "copied";
|
|
@@ -1358,7 +3438,7 @@ var migrateItem = async (storageRoot, journalPath, journal, item, options) => {
|
|
|
1358
3438
|
try {
|
|
1359
3439
|
await verifyTree(item.source, item.staging);
|
|
1360
3440
|
} catch (error) {
|
|
1361
|
-
await
|
|
3441
|
+
await rm2(item.staging, { recursive: true, force: true });
|
|
1362
3442
|
item.phase = "copying";
|
|
1363
3443
|
await writeJournal(journalPath, journal);
|
|
1364
3444
|
throw error;
|
|
@@ -1369,9 +3449,9 @@ var migrateItem = async (storageRoot, journalPath, journal, item, options) => {
|
|
|
1369
3449
|
}
|
|
1370
3450
|
if (item.phase === "verified") {
|
|
1371
3451
|
await checkpoint(options, "before-rename", item.id);
|
|
1372
|
-
await ensurePrivateDirectoryWithin(storageRoot,
|
|
1373
|
-
await
|
|
1374
|
-
await
|
|
3452
|
+
await ensurePrivateDirectoryWithin(storageRoot, path9.dirname(item.target));
|
|
3453
|
+
await rename5(item.staging, item.target);
|
|
3454
|
+
await syncDirectory2(path9.dirname(item.target));
|
|
1375
3455
|
await checkpoint(options, "after-rename", item.id);
|
|
1376
3456
|
item.phase = "committed";
|
|
1377
3457
|
await writeJournal(journalPath, journal);
|
|
@@ -1381,14 +3461,14 @@ var resumeRollback = async (journalPath, journal, options) => {
|
|
|
1381
3461
|
for (const item of [...journal.items].reverse()) {
|
|
1382
3462
|
if (item.phase === "rolled-back") continue;
|
|
1383
3463
|
await checkpoint(options, "before-rollback", item.id);
|
|
1384
|
-
if (!await
|
|
3464
|
+
if (!await exists2(item.source)) {
|
|
1385
3465
|
throw new LegacyWorkspaceMigrationError(
|
|
1386
3466
|
"MIGRATION_VERIFY_FAILED",
|
|
1387
3467
|
`Cannot roll back ${item.id}: the preserved legacy source is missing`
|
|
1388
3468
|
);
|
|
1389
3469
|
}
|
|
1390
|
-
await
|
|
1391
|
-
await
|
|
3470
|
+
await rm2(item.staging, { recursive: true, force: true });
|
|
3471
|
+
await rm2(item.target, { recursive: true, force: true });
|
|
1392
3472
|
await checkpoint(options, "after-rollback", item.id);
|
|
1393
3473
|
item.phase = "rolled-back";
|
|
1394
3474
|
await writeJournal(journalPath, journal);
|
|
@@ -1402,12 +3482,12 @@ var createJournal = async (storageRoot, migrationDirectory, owner) => {
|
|
|
1402
3482
|
const ownership = owner ? "explicit-owner" : "shared-legacy";
|
|
1403
3483
|
const label = owner ? `Legacy Connector root assigned to ${owner.spaceId}/${owner.agentId}` : "Shared legacy Connector root (ownership unassigned)";
|
|
1404
3484
|
const sources = await inventoryLegacySources(storageRoot);
|
|
1405
|
-
const stagingRoot =
|
|
3485
|
+
const stagingRoot = path9.join(migrationDirectory, "staging", randomUUID5());
|
|
1406
3486
|
const items = sources.map(({ id, source }) => ({
|
|
1407
3487
|
id,
|
|
1408
3488
|
source,
|
|
1409
3489
|
target: deriveTarget(storageRoot, id, owner),
|
|
1410
|
-
staging:
|
|
3490
|
+
staging: path9.join(stagingRoot, id),
|
|
1411
3491
|
phase: "pending"
|
|
1412
3492
|
}));
|
|
1413
3493
|
assertDistinctTargets(items);
|
|
@@ -1424,29 +3504,29 @@ var createJournal = async (storageRoot, migrationDirectory, owner) => {
|
|
|
1424
3504
|
};
|
|
1425
3505
|
};
|
|
1426
3506
|
var inventoryLegacySources = async (storageRoot) => {
|
|
1427
|
-
const rootEntries = await
|
|
3507
|
+
const rootEntries = await readdir4(storageRoot, { withFileTypes: true });
|
|
1428
3508
|
const templateNames = new Set(LEGACY_TEMPLATE_FILES);
|
|
1429
3509
|
for (const entry of rootEntries) {
|
|
1430
3510
|
if (entry.name.endsWith(".md")) templateNames.add(entry.name);
|
|
1431
3511
|
}
|
|
1432
3512
|
const candidates = [
|
|
1433
|
-
{ id: "agent", source:
|
|
1434
|
-
{ id: "space", source:
|
|
1435
|
-
{ id: "logs", source:
|
|
1436
|
-
{ id: "connector-log", source:
|
|
1437
|
-
{ id: "runtime-config", source:
|
|
1438
|
-
{ id: "runtime-config-hidden", source:
|
|
1439
|
-
{ id: "runtime-config-file", source:
|
|
1440
|
-
{ id: "runtime-config-hidden-file", source:
|
|
1441
|
-
{ id: "bunny-runtime", source:
|
|
3513
|
+
{ id: "agent", source: path9.join(storageRoot, "agent") },
|
|
3514
|
+
{ id: "space", source: path9.join(storageRoot, "space") },
|
|
3515
|
+
{ id: "logs", source: path9.join(storageRoot, "logs") },
|
|
3516
|
+
{ id: "connector-log", source: path9.join(storageRoot, "connector.log") },
|
|
3517
|
+
{ id: "runtime-config", source: path9.join(storageRoot, "runtime-config") },
|
|
3518
|
+
{ id: "runtime-config-hidden", source: path9.join(storageRoot, ".runtime-config") },
|
|
3519
|
+
{ id: "runtime-config-file", source: path9.join(storageRoot, "runtime-config.json") },
|
|
3520
|
+
{ id: "runtime-config-hidden-file", source: path9.join(storageRoot, ".runtime-config.json") },
|
|
3521
|
+
{ id: "bunny-runtime", source: path9.join(storageRoot, ".bunny") },
|
|
1442
3522
|
...[...templateNames].sort().map((name) => ({
|
|
1443
3523
|
id: `template-${name}`,
|
|
1444
|
-
source:
|
|
3524
|
+
source: path9.join(storageRoot, name)
|
|
1445
3525
|
}))
|
|
1446
3526
|
];
|
|
1447
3527
|
const found = [];
|
|
1448
3528
|
for (const candidate of candidates) {
|
|
1449
|
-
if (!await
|
|
3529
|
+
if (!await exists2(candidate.source)) continue;
|
|
1450
3530
|
const info = await lstat(candidate.source);
|
|
1451
3531
|
if (info.isSymbolicLink()) {
|
|
1452
3532
|
throw new LegacyWorkspaceMigrationError(
|
|
@@ -1454,36 +3534,36 @@ var inventoryLegacySources = async (storageRoot) => {
|
|
|
1454
3534
|
`Legacy migration source ${candidate.id} must not be a symbolic link`
|
|
1455
3535
|
);
|
|
1456
3536
|
}
|
|
1457
|
-
if (info.isDirectory() && (await
|
|
3537
|
+
if (info.isDirectory() && (await readdir4(candidate.source)).length === 0) continue;
|
|
1458
3538
|
found.push(candidate);
|
|
1459
3539
|
}
|
|
1460
3540
|
return found;
|
|
1461
3541
|
};
|
|
1462
3542
|
var deriveTarget = (storageRoot, id, owner) => {
|
|
1463
|
-
if (!owner) return
|
|
1464
|
-
const spaceRoot =
|
|
1465
|
-
const agentRoot =
|
|
1466
|
-
if (id === "agent") return
|
|
1467
|
-
if (id === "space") return
|
|
3543
|
+
if (!owner) return path9.join(storageRoot, ".buda", "legacy", "shared", id);
|
|
3544
|
+
const spaceRoot = path9.join(storageRoot, ".buda", "spaces", owner.spaceId);
|
|
3545
|
+
const agentRoot = path9.join(spaceRoot, "agents", owner.agentId);
|
|
3546
|
+
if (id === "agent") return path9.join(agentRoot, "workspace", "legacy-connector-root");
|
|
3547
|
+
if (id === "space") return path9.join(spaceRoot, "shared", "legacy-connector-root");
|
|
1468
3548
|
if (id.startsWith("template-")) {
|
|
1469
|
-
return
|
|
3549
|
+
return path9.join(spaceRoot, "shared", "legacy-templates", id.slice("template-".length));
|
|
1470
3550
|
}
|
|
1471
|
-
return
|
|
3551
|
+
return path9.join(agentRoot, "state", "legacy-connector-root", id);
|
|
1472
3552
|
};
|
|
1473
3553
|
var copyTree = async (source, destination, onEntryCopied) => {
|
|
1474
3554
|
const info = await lstat(source);
|
|
1475
3555
|
if (info.isDirectory()) {
|
|
1476
|
-
await
|
|
1477
|
-
const entries = (await
|
|
3556
|
+
await mkdir8(destination, { mode: info.mode & 4095 });
|
|
3557
|
+
const entries = (await readdir4(source)).sort();
|
|
1478
3558
|
for (const entry of entries) {
|
|
1479
|
-
await copyTree(
|
|
3559
|
+
await copyTree(path9.join(source, entry), path9.join(destination, entry), onEntryCopied);
|
|
1480
3560
|
}
|
|
1481
|
-
await
|
|
3561
|
+
await chmod4(destination, info.mode & 4095);
|
|
1482
3562
|
return;
|
|
1483
3563
|
}
|
|
1484
3564
|
if (info.isFile()) {
|
|
1485
3565
|
await copyFile(source, destination, constants2.COPYFILE_EXCL);
|
|
1486
|
-
await
|
|
3566
|
+
await chmod4(destination, info.mode & 4095);
|
|
1487
3567
|
await onEntryCopied();
|
|
1488
3568
|
return;
|
|
1489
3569
|
}
|
|
@@ -1511,8 +3591,8 @@ var verifyTree = async (source, destination) => {
|
|
|
1511
3591
|
}
|
|
1512
3592
|
if (sourceInfo.isDirectory()) {
|
|
1513
3593
|
const [sourceEntries, destinationEntries] = await Promise.all([
|
|
1514
|
-
|
|
1515
|
-
|
|
3594
|
+
readdir4(source),
|
|
3595
|
+
readdir4(destination)
|
|
1516
3596
|
]);
|
|
1517
3597
|
sourceEntries.sort();
|
|
1518
3598
|
destinationEntries.sort();
|
|
@@ -1520,7 +3600,7 @@ var verifyTree = async (source, destination) => {
|
|
|
1520
3600
|
throw verifyFailure("directory entries differ");
|
|
1521
3601
|
}
|
|
1522
3602
|
for (const entry of sourceEntries) {
|
|
1523
|
-
await verifyTree(
|
|
3603
|
+
await verifyTree(path9.join(source, entry), path9.join(destination, entry));
|
|
1524
3604
|
}
|
|
1525
3605
|
return;
|
|
1526
3606
|
}
|
|
@@ -1550,7 +3630,7 @@ var hashFile = async (filePath) => {
|
|
|
1550
3630
|
var readJournal = async (journalPath) => {
|
|
1551
3631
|
let raw;
|
|
1552
3632
|
try {
|
|
1553
|
-
raw = await
|
|
3633
|
+
raw = await readFile7(journalPath, "utf8");
|
|
1554
3634
|
} catch (error) {
|
|
1555
3635
|
if (isMissing(error)) return void 0;
|
|
1556
3636
|
throw error;
|
|
@@ -1562,13 +3642,13 @@ var readJournal = async (journalPath) => {
|
|
|
1562
3642
|
throw corrupt("Legacy migration journal contains invalid JSON");
|
|
1563
3643
|
}
|
|
1564
3644
|
if (!isJournal(value)) throw corrupt("Legacy migration journal is malformed");
|
|
1565
|
-
await
|
|
3645
|
+
await chmod4(journalPath, 384);
|
|
1566
3646
|
return value;
|
|
1567
3647
|
};
|
|
1568
3648
|
var writeJournal = async (journalPath, journal) => {
|
|
1569
3649
|
journal.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1570
|
-
await ensurePrivateDirectory(
|
|
1571
|
-
const temporaryPath = `${journalPath}.${process.pid}.${
|
|
3650
|
+
await ensurePrivateDirectory(path9.dirname(journalPath));
|
|
3651
|
+
const temporaryPath = `${journalPath}.${process.pid}.${randomUUID5()}.tmp`;
|
|
1572
3652
|
const handle = await open2(
|
|
1573
3653
|
temporaryPath,
|
|
1574
3654
|
constants2.O_CREAT | constants2.O_EXCL | constants2.O_WRONLY,
|
|
@@ -1582,14 +3662,14 @@ var writeJournal = async (journalPath, journal) => {
|
|
|
1582
3662
|
await handle.close();
|
|
1583
3663
|
}
|
|
1584
3664
|
try {
|
|
1585
|
-
await
|
|
1586
|
-
await
|
|
1587
|
-
await
|
|
3665
|
+
await rename5(temporaryPath, journalPath);
|
|
3666
|
+
await chmod4(journalPath, 384);
|
|
3667
|
+
await syncDirectory2(path9.dirname(journalPath));
|
|
1588
3668
|
} finally {
|
|
1589
|
-
await
|
|
3669
|
+
await unlink5(temporaryPath).catch(() => void 0);
|
|
1590
3670
|
}
|
|
1591
3671
|
};
|
|
1592
|
-
var
|
|
3672
|
+
var syncDirectory2 = async (directory) => {
|
|
1593
3673
|
const handle = await open2(directory, constants2.O_RDONLY);
|
|
1594
3674
|
try {
|
|
1595
3675
|
await handle.sync();
|
|
@@ -1598,7 +3678,7 @@ var syncDirectory = async (directory) => {
|
|
|
1598
3678
|
}
|
|
1599
3679
|
};
|
|
1600
3680
|
var ensurePrivateDirectory = async (directory) => {
|
|
1601
|
-
await
|
|
3681
|
+
await mkdir8(directory, { mode: 448 }).catch((error) => {
|
|
1602
3682
|
if (!isAlreadyExists2(error)) throw error;
|
|
1603
3683
|
});
|
|
1604
3684
|
const info = await lstat(directory);
|
|
@@ -1608,16 +3688,16 @@ var ensurePrivateDirectory = async (directory) => {
|
|
|
1608
3688
|
"Legacy migration control path must be a real directory"
|
|
1609
3689
|
);
|
|
1610
3690
|
}
|
|
1611
|
-
await
|
|
3691
|
+
await chmod4(directory, 448);
|
|
1612
3692
|
};
|
|
1613
3693
|
var ensurePrivateDirectoryWithin = async (storageRoot, directory) => {
|
|
1614
3694
|
if (directory !== storageRoot && !isPathInside(storageRoot, directory)) {
|
|
1615
3695
|
throw corrupt("Legacy migration control path escapes protected storage");
|
|
1616
3696
|
}
|
|
1617
|
-
const relative =
|
|
3697
|
+
const relative = path9.relative(storageRoot, directory);
|
|
1618
3698
|
let current = storageRoot;
|
|
1619
|
-
for (const segment of relative.split(
|
|
1620
|
-
current =
|
|
3699
|
+
for (const segment of relative.split(path9.sep).filter(Boolean)) {
|
|
3700
|
+
current = path9.join(current, segment);
|
|
1621
3701
|
await ensurePrivateDirectory(current);
|
|
1622
3702
|
}
|
|
1623
3703
|
};
|
|
@@ -1642,7 +3722,7 @@ var validateJournalPaths = (journal, storageRoot, migrationDirectory) => {
|
|
|
1642
3722
|
const stagingPaths = /* @__PURE__ */ new Set();
|
|
1643
3723
|
for (const item of journal.items) {
|
|
1644
3724
|
const sourceRelative = sourceRelativePath(item.id);
|
|
1645
|
-
if (!sourceRelative || item.source !==
|
|
3725
|
+
if (!sourceRelative || item.source !== path9.join(storageRoot, sourceRelative)) {
|
|
1646
3726
|
throw corrupt("Legacy migration journal contains an unexpected source path");
|
|
1647
3727
|
}
|
|
1648
3728
|
if (item.target !== deriveTarget(storageRoot, item.id, journal.owner)) {
|
|
@@ -1651,8 +3731,8 @@ var validateJournalPaths = (journal, storageRoot, migrationDirectory) => {
|
|
|
1651
3731
|
if (!isPathInside(migrationDirectory, item.staging)) {
|
|
1652
3732
|
throw corrupt("Legacy migration journal staging path escapes the migration directory");
|
|
1653
3733
|
}
|
|
1654
|
-
const stagingParent =
|
|
1655
|
-
if (
|
|
3734
|
+
const stagingParent = path9.dirname(item.staging);
|
|
3735
|
+
if (path9.dirname(stagingParent) !== path9.join(migrationDirectory, "staging") || item.staging !== path9.join(stagingParent, item.id)) {
|
|
1656
3736
|
throw corrupt("Legacy migration journal staging path is not canonical");
|
|
1657
3737
|
}
|
|
1658
3738
|
stagingParents.add(stagingParent);
|
|
@@ -1682,7 +3762,7 @@ var sourceRelativePath = (id) => {
|
|
|
1682
3762
|
if (known[id]) return known[id];
|
|
1683
3763
|
if (!id.startsWith("template-")) return null;
|
|
1684
3764
|
const name = id.slice("template-".length);
|
|
1685
|
-
return name.endsWith(".md") &&
|
|
3765
|
+
return name.endsWith(".md") && path9.basename(name) === name ? name : null;
|
|
1686
3766
|
};
|
|
1687
3767
|
var validateJournalOwnership = (journal) => {
|
|
1688
3768
|
if (journal.ownership === "explicit-owner") {
|
|
@@ -1745,7 +3825,7 @@ var isJournal = (value) => {
|
|
|
1745
3825
|
(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)
|
|
1746
3826
|
);
|
|
1747
3827
|
};
|
|
1748
|
-
var
|
|
3828
|
+
var exists2 = async (filePath) => {
|
|
1749
3829
|
try {
|
|
1750
3830
|
await lstat(filePath);
|
|
1751
3831
|
return true;
|
|
@@ -1755,8 +3835,8 @@ var exists = async (filePath) => {
|
|
|
1755
3835
|
}
|
|
1756
3836
|
};
|
|
1757
3837
|
var isPathInside = (parent, child) => {
|
|
1758
|
-
const relative =
|
|
1759
|
-
return relative !== "" && relative !== ".." && !relative.startsWith(`..${
|
|
3838
|
+
const relative = path9.relative(parent, child);
|
|
3839
|
+
return relative !== "" && relative !== ".." && !relative.startsWith(`..${path9.sep}`) && !path9.isAbsolute(relative);
|
|
1760
3840
|
};
|
|
1761
3841
|
var isMissing = (error) => error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
1762
3842
|
var isAlreadyExists2 = (error) => error instanceof Error && "code" in error && error.code === "EEXIST";
|
|
@@ -1768,9 +3848,9 @@ var corrupt = (message) => new LegacyWorkspaceMigrationError("MIGRATION_CORRUPT"
|
|
|
1768
3848
|
var errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
1769
3849
|
|
|
1770
3850
|
// src/relay.ts
|
|
1771
|
-
import
|
|
3851
|
+
import path11 from "path";
|
|
1772
3852
|
import { createNextHandler } from "@bunny-agent/daemon/nextjs";
|
|
1773
|
-
import
|
|
3853
|
+
import WebSocket2 from "ws";
|
|
1774
3854
|
|
|
1775
3855
|
// src/execution-identity.ts
|
|
1776
3856
|
import {
|
|
@@ -1940,15 +4020,15 @@ import { constants as constants3 } from "fs";
|
|
|
1940
4020
|
import {
|
|
1941
4021
|
copyFile as copyFile2,
|
|
1942
4022
|
lstat as lstat2,
|
|
1943
|
-
mkdir as
|
|
1944
|
-
readdir as
|
|
1945
|
-
readFile as
|
|
4023
|
+
mkdir as mkdir9,
|
|
4024
|
+
readdir as readdir5,
|
|
4025
|
+
readFile as readFile8,
|
|
1946
4026
|
realpath as realpath2,
|
|
1947
|
-
rm as
|
|
4027
|
+
rm as rm3,
|
|
1948
4028
|
symlink as symlink2,
|
|
1949
|
-
writeFile as
|
|
4029
|
+
writeFile as writeFile4
|
|
1950
4030
|
} from "fs/promises";
|
|
1951
|
-
import
|
|
4031
|
+
import path10 from "path";
|
|
1952
4032
|
var MOUNT_DIRECTORY = "mount";
|
|
1953
4033
|
var ARCHIVE_MARKER = "archived.json";
|
|
1954
4034
|
var AGENT_CONTEXT_TEMPLATE_FILES = [
|
|
@@ -1960,8 +4040,8 @@ var AGENT_CONTEXT_TEMPLATE_FILES = [
|
|
|
1960
4040
|
"BOOTSTRAP.md",
|
|
1961
4041
|
"MEMORY.md"
|
|
1962
4042
|
];
|
|
1963
|
-
var deriveSystemVolume = (descriptor) =>
|
|
1964
|
-
|
|
4043
|
+
var deriveSystemVolume = (descriptor) => path10.join(
|
|
4044
|
+
path10.dirname(path10.dirname(path10.dirname(path10.dirname(descriptor.sharedVolume)))),
|
|
1965
4045
|
"system"
|
|
1966
4046
|
);
|
|
1967
4047
|
var WorkspaceVolumeLifecycle = class {
|
|
@@ -1973,14 +4053,14 @@ var WorkspaceVolumeLifecycle = class {
|
|
|
1973
4053
|
async mount() {
|
|
1974
4054
|
await this.ensureVolumes();
|
|
1975
4055
|
await this.bootstrapAgentTemplates((await readBundledManifest()).sharedDir);
|
|
1976
|
-
const mountRoot =
|
|
1977
|
-
await
|
|
4056
|
+
const mountRoot = path10.join(this.descriptor.stateVolume, MOUNT_DIRECTORY);
|
|
4057
|
+
await mkdir9(mountRoot, { recursive: true, mode: 448 });
|
|
1978
4058
|
const mounts = {
|
|
1979
4059
|
mountRoot,
|
|
1980
|
-
agent:
|
|
1981
|
-
space:
|
|
1982
|
-
system:
|
|
1983
|
-
temporary:
|
|
4060
|
+
agent: path10.join(mountRoot, "agent"),
|
|
4061
|
+
space: path10.join(mountRoot, "space"),
|
|
4062
|
+
system: path10.join(mountRoot, "system"),
|
|
4063
|
+
temporary: path10.join(mountRoot, "tmp")
|
|
1984
4064
|
};
|
|
1985
4065
|
await Promise.all([
|
|
1986
4066
|
ensureVolumeLink(this.descriptor.privateVolume, mounts.agent),
|
|
@@ -1988,69 +4068,69 @@ var WorkspaceVolumeLifecycle = class {
|
|
|
1988
4068
|
ensureVolumeLink(this.systemVolume, mounts.system),
|
|
1989
4069
|
ensureVolumeLink(this.descriptor.temporaryVolume, mounts.temporary)
|
|
1990
4070
|
]);
|
|
1991
|
-
const daemonVolumes =
|
|
1992
|
-
await
|
|
4071
|
+
const daemonVolumes = path10.join(mountRoot, "volumes");
|
|
4072
|
+
await mkdir9(daemonVolumes, { recursive: true, mode: 448 });
|
|
1993
4073
|
await Promise.all([
|
|
1994
|
-
ensureVolumeLink(this.descriptor.privateVolume,
|
|
1995
|
-
ensureVolumeLink(this.descriptor.sharedVolume,
|
|
1996
|
-
ensureVolumeLink(this.systemVolume,
|
|
1997
|
-
ensureVolumeLink(this.descriptor.temporaryVolume,
|
|
4074
|
+
ensureVolumeLink(this.descriptor.privateVolume, path10.join(daemonVolumes, "agent")),
|
|
4075
|
+
ensureVolumeLink(this.descriptor.sharedVolume, path10.join(daemonVolumes, "space")),
|
|
4076
|
+
ensureVolumeLink(this.systemVolume, path10.join(daemonVolumes, "system")),
|
|
4077
|
+
ensureVolumeLink(this.descriptor.temporaryVolume, path10.join(daemonVolumes, "tmp"))
|
|
1998
4078
|
]);
|
|
1999
4079
|
return mounts;
|
|
2000
4080
|
}
|
|
2001
4081
|
async unmount() {
|
|
2002
|
-
await
|
|
4082
|
+
await rm3(path10.join(this.descriptor.stateVolume, MOUNT_DIRECTORY), {
|
|
2003
4083
|
recursive: true,
|
|
2004
4084
|
force: true
|
|
2005
4085
|
});
|
|
2006
4086
|
}
|
|
2007
4087
|
async archiveAgent() {
|
|
2008
4088
|
await this.unmount();
|
|
2009
|
-
await
|
|
2010
|
-
await
|
|
2011
|
-
|
|
4089
|
+
await mkdir9(this.descriptor.stateVolume, { recursive: true, mode: 448 });
|
|
4090
|
+
await writeFile4(
|
|
4091
|
+
path10.join(this.descriptor.stateVolume, ARCHIVE_MARKER),
|
|
2012
4092
|
`${JSON.stringify({ archivedAt: (/* @__PURE__ */ new Date()).toISOString() })}
|
|
2013
4093
|
`,
|
|
2014
4094
|
{ encoding: "utf8", mode: 384 }
|
|
2015
4095
|
);
|
|
2016
4096
|
}
|
|
2017
4097
|
async restoreAgent() {
|
|
2018
|
-
await
|
|
4098
|
+
await rm3(path10.join(this.descriptor.stateVolume, ARCHIVE_MARKER), { force: true });
|
|
2019
4099
|
return this.mount();
|
|
2020
4100
|
}
|
|
2021
4101
|
async deleteAgent(options) {
|
|
2022
4102
|
requireConfirmation(options);
|
|
2023
4103
|
await this.unmount();
|
|
2024
|
-
const agentRoot =
|
|
2025
|
-
await
|
|
4104
|
+
const agentRoot = path10.dirname(this.descriptor.privateVolume);
|
|
4105
|
+
await rm3(agentRoot, { recursive: true, force: true });
|
|
2026
4106
|
}
|
|
2027
4107
|
async archiveSpace() {
|
|
2028
|
-
await
|
|
2029
|
-
await
|
|
2030
|
-
|
|
4108
|
+
await mkdir9(this.descriptor.sharedVolume, { recursive: true, mode: 448 });
|
|
4109
|
+
await writeFile4(
|
|
4110
|
+
path10.join(this.descriptor.sharedVolume, ARCHIVE_MARKER),
|
|
2031
4111
|
`${JSON.stringify({ archivedAt: (/* @__PURE__ */ new Date()).toISOString() })}
|
|
2032
4112
|
`,
|
|
2033
4113
|
{ encoding: "utf8", mode: 384 }
|
|
2034
4114
|
);
|
|
2035
4115
|
}
|
|
2036
4116
|
async restoreSpace() {
|
|
2037
|
-
await
|
|
4117
|
+
await rm3(path10.join(this.descriptor.sharedVolume, ARCHIVE_MARKER), { force: true });
|
|
2038
4118
|
}
|
|
2039
4119
|
async deleteSpace(options) {
|
|
2040
4120
|
requireConfirmation(options);
|
|
2041
4121
|
await this.unmount();
|
|
2042
|
-
await
|
|
4122
|
+
await rm3(this.descriptor.sharedVolume, { recursive: true, force: true });
|
|
2043
4123
|
}
|
|
2044
4124
|
async bootstrapAgentTemplates(templateDirectory) {
|
|
2045
|
-
await
|
|
4125
|
+
await mkdir9(this.descriptor.privateVolume, { recursive: true, mode: 448 });
|
|
2046
4126
|
const bootstrapped = [];
|
|
2047
4127
|
const skipped = [];
|
|
2048
4128
|
const entries = new Set(
|
|
2049
|
-
(await
|
|
4129
|
+
(await readdir5(templateDirectory, { withFileTypes: true }).catch(() => [])).filter((entry) => entry.isFile()).map((entry) => entry.name)
|
|
2050
4130
|
);
|
|
2051
4131
|
for (const filename of AGENT_CONTEXT_TEMPLATE_FILES) {
|
|
2052
4132
|
if (!entries.has(filename)) continue;
|
|
2053
|
-
const destination =
|
|
4133
|
+
const destination = path10.join(this.descriptor.privateVolume, filename);
|
|
2054
4134
|
try {
|
|
2055
4135
|
await lstat2(destination);
|
|
2056
4136
|
skipped.push(filename);
|
|
@@ -2058,7 +4138,7 @@ var WorkspaceVolumeLifecycle = class {
|
|
|
2058
4138
|
if (!isMissingFile(error)) throw error;
|
|
2059
4139
|
try {
|
|
2060
4140
|
await copyFile2(
|
|
2061
|
-
|
|
4141
|
+
path10.join(templateDirectory, filename),
|
|
2062
4142
|
destination,
|
|
2063
4143
|
constants3.COPYFILE_EXCL
|
|
2064
4144
|
);
|
|
@@ -2079,7 +4159,7 @@ var WorkspaceVolumeLifecycle = class {
|
|
|
2079
4159
|
this.descriptor.temporaryVolume,
|
|
2080
4160
|
this.systemVolume
|
|
2081
4161
|
]) {
|
|
2082
|
-
await
|
|
4162
|
+
await mkdir9(volume, { recursive: true, mode: 448 });
|
|
2083
4163
|
const info = await lstat2(volume);
|
|
2084
4164
|
if (info.isSymbolicLink() || !info.isDirectory() || await realpath2(volume) !== volume) {
|
|
2085
4165
|
throw new Error("Workspace volume must be a canonical real directory");
|
|
@@ -2121,6 +4201,20 @@ var RELAY_RECONNECT_JITTER = 0.2;
|
|
|
2121
4201
|
var RELAY_FAST_RECONNECT_MS = 500;
|
|
2122
4202
|
var CONNECTOR_SOCKET_REPLACED_CLOSE_CODE = 4001;
|
|
2123
4203
|
var CONNECTOR_SOCKET_REVOKED_CLOSE_CODE = 4003;
|
|
4204
|
+
var SAFE_TRACE_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
|
|
4205
|
+
var getBudaCodingRunSessionId = (body) => {
|
|
4206
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return void 0;
|
|
4207
|
+
const record = body;
|
|
4208
|
+
for (const key of ["systemEnv", "env"]) {
|
|
4209
|
+
const env = record[key];
|
|
4210
|
+
if (!env || typeof env !== "object" || Array.isArray(env)) continue;
|
|
4211
|
+
const value = env.BUDA_SESSION_ID;
|
|
4212
|
+
if (typeof value !== "string") continue;
|
|
4213
|
+
const normalized = value.trim();
|
|
4214
|
+
if (SAFE_TRACE_ID_PATTERN.test(normalized)) return normalized;
|
|
4215
|
+
}
|
|
4216
|
+
return void 0;
|
|
4217
|
+
};
|
|
2124
4218
|
var shouldReconnectConnectorRelay = (closeCode) => closeCode !== CONNECTOR_SOCKET_REPLACED_CLOSE_CODE && closeCode !== CONNECTOR_SOCKET_REVOKED_CLOSE_CODE;
|
|
2125
4219
|
var getConnectorRelayReconnectBaseDelay = (hadOpenConnection, backoffMs) => hadOpenConnection ? RELAY_FAST_RECONNECT_MS : backoffMs;
|
|
2126
4220
|
var formatConnectorRelayError = (error, depth = 0) => {
|
|
@@ -2149,6 +4243,8 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
|
|
|
2149
4243
|
let reconnectDelayMs = RELAY_RECONNECT_INTERVAL_MS;
|
|
2150
4244
|
let hadOpenConnection = false;
|
|
2151
4245
|
let activeRelayProtocol = relayProtocol;
|
|
4246
|
+
let currentInflight = /* @__PURE__ */ new Map();
|
|
4247
|
+
let activeTerminalTunnels = 0;
|
|
2152
4248
|
const replayGuard = new ConnectorExecutionReplayGuard();
|
|
2153
4249
|
const clearTimers = () => {
|
|
2154
4250
|
if (pingTimer) {
|
|
@@ -2196,12 +4292,13 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
|
|
|
2196
4292
|
if (token) headers["X-Buda-Connector-Token"] = token;
|
|
2197
4293
|
headers["X-Buda-Connector-Id"] = config.connectorId;
|
|
2198
4294
|
if (config.deviceToken) headers["X-Buda-Connector-Device-Token"] = config.deviceToken;
|
|
2199
|
-
const ws = new
|
|
4295
|
+
const ws = new WebSocket2(relayUrl, {
|
|
2200
4296
|
headers: Object.keys(headers).length > 0 ? headers : void 0,
|
|
2201
4297
|
handshakeTimeout: RELAY_HANDSHAKE_TIMEOUT_MS
|
|
2202
4298
|
});
|
|
2203
4299
|
socket = ws;
|
|
2204
4300
|
const inflight = /* @__PURE__ */ new Map();
|
|
4301
|
+
currentInflight = inflight;
|
|
2205
4302
|
let detached = false;
|
|
2206
4303
|
const detach = () => {
|
|
2207
4304
|
if (detached) return;
|
|
@@ -2215,7 +4312,7 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
|
|
|
2215
4312
|
hadOpenConnection = true;
|
|
2216
4313
|
reconnectDelayMs = RELAY_RECONNECT_INTERVAL_MS;
|
|
2217
4314
|
pingTimer = setInterval(() => {
|
|
2218
|
-
if (ws.readyState !==
|
|
4315
|
+
if (ws.readyState !== WebSocket2.OPEN) return;
|
|
2219
4316
|
try {
|
|
2220
4317
|
ws.ping();
|
|
2221
4318
|
} catch (error) {
|
|
@@ -2245,7 +4342,17 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
|
|
|
2245
4342
|
config.connectorId,
|
|
2246
4343
|
activeRelayProtocol,
|
|
2247
4344
|
replayGuard,
|
|
2248
|
-
workspaceRegistry
|
|
4345
|
+
workspaceRegistry,
|
|
4346
|
+
config.serverUrl,
|
|
4347
|
+
bunnyDaemon.baseUrl,
|
|
4348
|
+
{
|
|
4349
|
+
onTerminalOpen: () => {
|
|
4350
|
+
activeTerminalTunnels += 1;
|
|
4351
|
+
},
|
|
4352
|
+
onTerminalClose: () => {
|
|
4353
|
+
activeTerminalTunnels = Math.max(0, activeTerminalTunnels - 1);
|
|
4354
|
+
}
|
|
4355
|
+
}
|
|
2249
4356
|
);
|
|
2250
4357
|
});
|
|
2251
4358
|
ws.on("close", (code) => {
|
|
@@ -2277,6 +4384,12 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
|
|
|
2277
4384
|
scheduleReconnect("connect-throw");
|
|
2278
4385
|
}
|
|
2279
4386
|
return {
|
|
4387
|
+
isIdle() {
|
|
4388
|
+
return currentInflight.size === 0 && activeTerminalTunnels === 0;
|
|
4389
|
+
},
|
|
4390
|
+
isConnected() {
|
|
4391
|
+
return socket?.readyState === WebSocket2.OPEN && !suspendedForAccessRevocation;
|
|
4392
|
+
},
|
|
2280
4393
|
updateRelayProtocol(nextRelayProtocol) {
|
|
2281
4394
|
const previousKeyId = activeRelayProtocol?.keyId;
|
|
2282
4395
|
activeRelayProtocol = nextRelayProtocol;
|
|
@@ -2308,8 +4421,8 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
|
|
|
2308
4421
|
}
|
|
2309
4422
|
};
|
|
2310
4423
|
}
|
|
2311
|
-
async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedConnectorId, relayProtocol, replayGuard, workspaceRegistry) {
|
|
2312
|
-
if (!socket || socket.readyState !==
|
|
4424
|
+
async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedConnectorId, relayProtocol, replayGuard, workspaceRegistry, serverUrl, localDaemonBaseUrl, activity) {
|
|
4425
|
+
if (!socket || socket.readyState !== WebSocket2.OPEN) return;
|
|
2313
4426
|
let message;
|
|
2314
4427
|
try {
|
|
2315
4428
|
message = JSON.parse(raw);
|
|
@@ -2326,6 +4439,54 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
|
|
|
2326
4439
|
return;
|
|
2327
4440
|
}
|
|
2328
4441
|
if (message.type === "event") {
|
|
4442
|
+
if (message.event.channel === "terminal-open") {
|
|
4443
|
+
try {
|
|
4444
|
+
if (!workspaceRegistry || !serverUrl || !localDaemonBaseUrl) {
|
|
4445
|
+
throw new ConnectorRelayProtocolError("PROTOCOL_UNSUPPORTED");
|
|
4446
|
+
}
|
|
4447
|
+
const identity = verifyConnectorExecutionIdentity({
|
|
4448
|
+
protocolVersion: message.event.payload.protocolVersion,
|
|
4449
|
+
executionIdentity: message.event.payload.executionIdentity,
|
|
4450
|
+
request: message.event.payload.request,
|
|
4451
|
+
protocol: relayProtocol,
|
|
4452
|
+
expectedConnectorId,
|
|
4453
|
+
replayGuard
|
|
4454
|
+
});
|
|
4455
|
+
const terminal = parseConnectorTerminalRequest(message.event.payload.request);
|
|
4456
|
+
if (terminal.sessionId !== message.event.sessionId) {
|
|
4457
|
+
throw new ConnectorRelayProtocolError("IDENTITY_INVALID");
|
|
4458
|
+
}
|
|
4459
|
+
const descriptor = await workspaceRegistry.resolve(identity.spaceId, identity.agentId);
|
|
4460
|
+
if (descriptor.spaceId !== identity.spaceId || descriptor.agentId !== identity.agentId) {
|
|
4461
|
+
throw new ConnectorRelayProtocolError("WORKSPACE_MISMATCH");
|
|
4462
|
+
}
|
|
4463
|
+
const mounts = await new WorkspaceVolumeLifecycle(descriptor).mount();
|
|
4464
|
+
const localToken = registerLocalTerminal({
|
|
4465
|
+
workspaceKey: descriptor.workspaceKey,
|
|
4466
|
+
tabId: terminal.tabId,
|
|
4467
|
+
cwd: mounts.agent,
|
|
4468
|
+
cols: terminal.cols,
|
|
4469
|
+
rows: terminal.rows
|
|
4470
|
+
});
|
|
4471
|
+
await proxyConnectorTerminalTunnel({
|
|
4472
|
+
serverUrl,
|
|
4473
|
+
connectorId: expectedConnectorId,
|
|
4474
|
+
sessionId: terminal.sessionId,
|
|
4475
|
+
cloudToken: terminal.token,
|
|
4476
|
+
localBaseUrl: localDaemonBaseUrl,
|
|
4477
|
+
localToken,
|
|
4478
|
+
workspaceKey: descriptor.workspaceKey,
|
|
4479
|
+
onActivityStart: activity?.onTerminalOpen,
|
|
4480
|
+
onActivityEnd: activity?.onTerminalClose
|
|
4481
|
+
});
|
|
4482
|
+
} catch (error) {
|
|
4483
|
+
await writeLog(
|
|
4484
|
+
"warn",
|
|
4485
|
+
`terminal open rejected: ${error instanceof Error ? error.message : String(error)}`
|
|
4486
|
+
);
|
|
4487
|
+
}
|
|
4488
|
+
return;
|
|
4489
|
+
}
|
|
2329
4490
|
await writeLog("warn", "relay rejected unsigned browser input event");
|
|
2330
4491
|
return;
|
|
2331
4492
|
}
|
|
@@ -2351,6 +4512,15 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
|
|
|
2351
4512
|
}
|
|
2352
4513
|
const controller = new AbortController();
|
|
2353
4514
|
inflight.set(message.requestId, controller);
|
|
4515
|
+
const codingRunStartedAt = message.request.path === "/api/coding/run" ? Date.now() : void 0;
|
|
4516
|
+
const codingRunTrace = codingRunStartedAt !== void 0 && executionIdentity ? {
|
|
4517
|
+
requestId: message.requestId,
|
|
4518
|
+
connectorId: executionIdentity.connectorId,
|
|
4519
|
+
agentId: executionIdentity.agentId,
|
|
4520
|
+
runId: executionIdentity.runId,
|
|
4521
|
+
budaSessionId: getBudaCodingRunSessionId(message.request.body) ?? null
|
|
4522
|
+
} : void 0;
|
|
4523
|
+
let codingRunStatus = null;
|
|
2354
4524
|
try {
|
|
2355
4525
|
const requestPath = message.request.path;
|
|
2356
4526
|
const isBrowserRequest = requestPath.startsWith("/browser/") || requestPath === "/browser";
|
|
@@ -2411,6 +4581,14 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
|
|
|
2411
4581
|
signal: controller.signal
|
|
2412
4582
|
});
|
|
2413
4583
|
}
|
|
4584
|
+
if (codingRunTrace && codingRunStartedAt !== void 0) {
|
|
4585
|
+
codingRunStatus = response.status;
|
|
4586
|
+
await writeLog("info", "relay request coding run started", {
|
|
4587
|
+
...codingRunTrace,
|
|
4588
|
+
status: codingRunStatus,
|
|
4589
|
+
durationMs: Date.now() - codingRunStartedAt
|
|
4590
|
+
});
|
|
4591
|
+
}
|
|
2414
4592
|
sendRelayMessage(socket, {
|
|
2415
4593
|
type: "start",
|
|
2416
4594
|
requestId: message.requestId,
|
|
@@ -2453,7 +4631,21 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
|
|
|
2453
4631
|
type: "end",
|
|
2454
4632
|
requestId: message.requestId
|
|
2455
4633
|
});
|
|
4634
|
+
if (codingRunTrace && codingRunStartedAt !== void 0) {
|
|
4635
|
+
await writeLog("info", "relay request coding run completed", {
|
|
4636
|
+
...codingRunTrace,
|
|
4637
|
+
status: codingRunStatus,
|
|
4638
|
+
durationMs: Date.now() - codingRunStartedAt
|
|
4639
|
+
});
|
|
4640
|
+
}
|
|
2456
4641
|
} catch (error) {
|
|
4642
|
+
if (codingRunTrace && codingRunStartedAt !== void 0) {
|
|
4643
|
+
await writeLog("error", "relay request coding run error", {
|
|
4644
|
+
...codingRunTrace,
|
|
4645
|
+
status: codingRunStatus,
|
|
4646
|
+
durationMs: Date.now() - codingRunStartedAt
|
|
4647
|
+
});
|
|
4648
|
+
}
|
|
2457
4649
|
sendRelayError(socket, message.requestId, error);
|
|
2458
4650
|
} finally {
|
|
2459
4651
|
inflight.delete(message.requestId);
|
|
@@ -2478,12 +4670,12 @@ var normalizeWorkspaceRequest = (request, mounts) => {
|
|
|
2478
4670
|
}
|
|
2479
4671
|
const body = request.body;
|
|
2480
4672
|
const cwd = typeof body.cwd === "string" ? body.cwd : "/agent";
|
|
2481
|
-
const normalized =
|
|
4673
|
+
const normalized = path11.posix.normalize(cwd);
|
|
2482
4674
|
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)] : [];
|
|
2483
4675
|
if (!mount || relative === void 0) {
|
|
2484
4676
|
throw new ConnectorRelayProtocolError("WORKSPACE_MISMATCH");
|
|
2485
4677
|
}
|
|
2486
|
-
return { ...request, body: { ...body, cwd:
|
|
4678
|
+
return { ...request, body: { ...body, cwd: path11.join(mount, relative) } };
|
|
2487
4679
|
};
|
|
2488
4680
|
function sendRelayError(socket, requestId, error) {
|
|
2489
4681
|
sendRelayMessage(socket, {
|
|
@@ -2494,7 +4686,7 @@ function sendRelayError(socket, requestId, error) {
|
|
|
2494
4686
|
});
|
|
2495
4687
|
}
|
|
2496
4688
|
function sendRelayMessage(socket, message) {
|
|
2497
|
-
if (socket.readyState !==
|
|
4689
|
+
if (socket.readyState !== WebSocket2.OPEN) return;
|
|
2498
4690
|
socket.send(JSON.stringify(message));
|
|
2499
4691
|
}
|
|
2500
4692
|
function headersToRecord(headers) {
|
|
@@ -2514,14 +4706,14 @@ function getHeaderValue(headers, name) {
|
|
|
2514
4706
|
}
|
|
2515
4707
|
|
|
2516
4708
|
// src/workdir-layout.ts
|
|
2517
|
-
import { lstat as lstat3, mkdir as
|
|
2518
|
-
import
|
|
4709
|
+
import { lstat as lstat3, mkdir as mkdir10, symlink as symlink3, unlink as unlink6 } from "fs/promises";
|
|
4710
|
+
import path12 from "path";
|
|
2519
4711
|
async function provisionWorkdirLayout(workdirRoot) {
|
|
2520
4712
|
const manifest = await readBundledManifest();
|
|
2521
|
-
await
|
|
2522
|
-
await
|
|
2523
|
-
const systemSkillsLink =
|
|
2524
|
-
const systemScriptsLink =
|
|
4713
|
+
await mkdir10(workdirRoot, { recursive: true });
|
|
4714
|
+
await mkdir10(path12.join(workdirRoot, "system", ".agents"), { recursive: true });
|
|
4715
|
+
const systemSkillsLink = path12.join(workdirRoot, "system", ".agents", "skills");
|
|
4716
|
+
const systemScriptsLink = path12.join(workdirRoot, "system", "scripts");
|
|
2525
4717
|
await ensureSymlink(manifest.systemSkillsDir, systemSkillsLink);
|
|
2526
4718
|
await ensureSymlink(manifest.scriptsDir, systemScriptsLink);
|
|
2527
4719
|
await writeLog("info", "workdir layout provisioned", {
|
|
@@ -2541,7 +4733,7 @@ async function ensureSymlink(target, linkPath) {
|
|
|
2541
4733
|
try {
|
|
2542
4734
|
const existing = await lstat3(linkPath);
|
|
2543
4735
|
if (existing.isSymbolicLink()) {
|
|
2544
|
-
await
|
|
4736
|
+
await unlink6(linkPath);
|
|
2545
4737
|
} else if (existing.isDirectory()) {
|
|
2546
4738
|
await writeLog(
|
|
2547
4739
|
"warn",
|
|
@@ -2549,28 +4741,28 @@ async function ensureSymlink(target, linkPath) {
|
|
|
2549
4741
|
);
|
|
2550
4742
|
return;
|
|
2551
4743
|
} else {
|
|
2552
|
-
await
|
|
4744
|
+
await unlink6(linkPath);
|
|
2553
4745
|
}
|
|
2554
4746
|
} catch {
|
|
2555
4747
|
}
|
|
2556
|
-
await
|
|
4748
|
+
await mkdir10(path12.dirname(linkPath), { recursive: true });
|
|
2557
4749
|
await symlink3(target, linkPath, "dir");
|
|
2558
4750
|
}
|
|
2559
4751
|
|
|
2560
4752
|
// src/workspace-registry.ts
|
|
2561
4753
|
import { constants as constants4 } from "fs";
|
|
2562
4754
|
import {
|
|
2563
|
-
chmod as
|
|
4755
|
+
chmod as chmod5,
|
|
2564
4756
|
lstat as lstat4,
|
|
2565
|
-
mkdir as
|
|
4757
|
+
mkdir as mkdir11,
|
|
2566
4758
|
open as open3,
|
|
2567
|
-
readFile as
|
|
4759
|
+
readFile as readFile9,
|
|
2568
4760
|
realpath as realpath3,
|
|
2569
|
-
rename as
|
|
4761
|
+
rename as rename6,
|
|
2570
4762
|
statfs,
|
|
2571
|
-
unlink as
|
|
4763
|
+
unlink as unlink7
|
|
2572
4764
|
} from "fs/promises";
|
|
2573
|
-
import
|
|
4765
|
+
import path13 from "path";
|
|
2574
4766
|
var REGISTRY_VERSION = 1;
|
|
2575
4767
|
var REGISTRY_DIRECTORY = ".buda";
|
|
2576
4768
|
var REGISTRY_FILENAME = "workspace-registry.json";
|
|
@@ -2591,16 +4783,16 @@ var LocalWorkspaceRegistry = class _LocalWorkspaceRegistry {
|
|
|
2591
4783
|
this.document = document;
|
|
2592
4784
|
}
|
|
2593
4785
|
static async open(storageRoot) {
|
|
2594
|
-
await
|
|
4786
|
+
await mkdir11(storageRoot, { recursive: true, mode: 448 });
|
|
2595
4787
|
await rejectUnsafeFilesystem(storageRoot);
|
|
2596
4788
|
const canonicalRoot = await canonicalDirectory(storageRoot, "storage root");
|
|
2597
|
-
const registryDirectory =
|
|
4789
|
+
const registryDirectory = path13.join(canonicalRoot, REGISTRY_DIRECTORY);
|
|
2598
4790
|
await ensurePrivateDirectory2(registryDirectory);
|
|
2599
|
-
const registryPath =
|
|
4791
|
+
const registryPath = path13.join(registryDirectory, REGISTRY_FILENAME);
|
|
2600
4792
|
const document = await withExclusiveFileLock(registryPath, async () => {
|
|
2601
4793
|
let current;
|
|
2602
4794
|
try {
|
|
2603
|
-
current = parseRegistry(await
|
|
4795
|
+
current = parseRegistry(await readFile9(registryPath, "utf8"));
|
|
2604
4796
|
} catch (error) {
|
|
2605
4797
|
if (isMissingFile2(error)) {
|
|
2606
4798
|
current = { version: REGISTRY_VERSION, storageRoot: canonicalRoot, workspaces: {} };
|
|
@@ -2614,11 +4806,11 @@ var LocalWorkspaceRegistry = class _LocalWorkspaceRegistry {
|
|
|
2614
4806
|
if (current.storageRoot !== canonicalRoot) {
|
|
2615
4807
|
throw corrupt2("Registry storage root does not match the selected storage root");
|
|
2616
4808
|
}
|
|
2617
|
-
await
|
|
4809
|
+
await chmod5(registryPath, 384);
|
|
2618
4810
|
await reconcileDocument(current, canonicalRoot);
|
|
2619
4811
|
return current;
|
|
2620
4812
|
});
|
|
2621
|
-
await
|
|
4813
|
+
await chmod5(registryPath, 384);
|
|
2622
4814
|
return new _LocalWorkspaceRegistry(registryPath, canonicalRoot, document);
|
|
2623
4815
|
}
|
|
2624
4816
|
async resolve(spaceId, agentId) {
|
|
@@ -2665,23 +4857,23 @@ var LocalWorkspaceRegistry = class _LocalWorkspaceRegistry {
|
|
|
2665
4857
|
};
|
|
2666
4858
|
var readRegistryDocument = async (registryPath) => {
|
|
2667
4859
|
try {
|
|
2668
|
-
return parseRegistry(await
|
|
4860
|
+
return parseRegistry(await readFile9(registryPath, "utf8"));
|
|
2669
4861
|
} catch (error) {
|
|
2670
4862
|
if (error instanceof WorkspaceRegistryError) throw error;
|
|
2671
4863
|
throw corrupt2(`Cannot read local workspace registry: ${errorMessage2(error)}`);
|
|
2672
4864
|
}
|
|
2673
4865
|
};
|
|
2674
4866
|
var deriveWorkspace = (storageRoot, spaceId, agentId) => {
|
|
2675
|
-
const spaceRoot =
|
|
2676
|
-
const agentRoot =
|
|
4867
|
+
const spaceRoot = path13.join(storageRoot, REGISTRY_DIRECTORY, "spaces", spaceId);
|
|
4868
|
+
const agentRoot = path13.join(spaceRoot, "agents", agentId);
|
|
2677
4869
|
return {
|
|
2678
4870
|
workspaceKey: `${spaceId}:${agentId}`,
|
|
2679
4871
|
spaceId,
|
|
2680
4872
|
agentId,
|
|
2681
|
-
privateVolume:
|
|
2682
|
-
sharedVolume:
|
|
2683
|
-
stateVolume:
|
|
2684
|
-
temporaryVolume:
|
|
4873
|
+
privateVolume: path13.join(agentRoot, "workspace"),
|
|
4874
|
+
sharedVolume: path13.join(spaceRoot, "shared"),
|
|
4875
|
+
stateVolume: path13.join(agentRoot, "state"),
|
|
4876
|
+
temporaryVolume: path13.join(agentRoot, "tmp")
|
|
2685
4877
|
};
|
|
2686
4878
|
};
|
|
2687
4879
|
var parseRegistry = (raw) => {
|
|
@@ -2763,12 +4955,12 @@ var assertNoCollision = (workspaces, candidate) => {
|
|
|
2763
4955
|
}
|
|
2764
4956
|
};
|
|
2765
4957
|
var ensurePrivateDirectory2 = async (directory) => {
|
|
2766
|
-
await
|
|
4958
|
+
await mkdir11(directory, { recursive: true, mode: 448 });
|
|
2767
4959
|
const info = await lstat4(directory);
|
|
2768
4960
|
if (info.isSymbolicLink() || !info.isDirectory()) {
|
|
2769
4961
|
throw corrupt2("Workspace registry path is not a real directory");
|
|
2770
4962
|
}
|
|
2771
|
-
await
|
|
4963
|
+
await chmod5(directory, 448);
|
|
2772
4964
|
};
|
|
2773
4965
|
var canonicalDirectory = async (directory, label) => {
|
|
2774
4966
|
const info = await lstat4(directory);
|
|
@@ -2802,10 +4994,10 @@ var writeRegistryAtomically = async (registryPath, document) => {
|
|
|
2802
4994
|
await handle.close();
|
|
2803
4995
|
}
|
|
2804
4996
|
try {
|
|
2805
|
-
await
|
|
2806
|
-
await
|
|
4997
|
+
await rename6(temporaryPath, registryPath);
|
|
4998
|
+
await chmod5(registryPath, 384);
|
|
2807
4999
|
} catch (error) {
|
|
2808
|
-
await
|
|
5000
|
+
await unlink7(temporaryPath).catch(() => void 0);
|
|
2809
5001
|
throw error;
|
|
2810
5002
|
}
|
|
2811
5003
|
};
|
|
@@ -2828,8 +5020,8 @@ var assertContained = (root, candidate) => {
|
|
|
2828
5020
|
if (!isPathInside2(root, candidate)) throw corrupt2("Workspace path escapes the storage root");
|
|
2829
5021
|
};
|
|
2830
5022
|
var isPathInside2 = (parent, child) => {
|
|
2831
|
-
const relative =
|
|
2832
|
-
return relative !== "" && relative !== ".." && !relative.startsWith(`..${
|
|
5023
|
+
const relative = path13.relative(parent, child);
|
|
5024
|
+
return relative !== "" && relative !== ".." && !relative.startsWith(`..${path13.sep}`) && !path13.isAbsolute(relative);
|
|
2833
5025
|
};
|
|
2834
5026
|
var toDescriptor = (workspace) => ({
|
|
2835
5027
|
workspaceKey: workspace.workspaceKey,
|
|
@@ -2859,7 +5051,7 @@ var corrupt2 = (message) => new WorkspaceRegistryError("REGISTRY_CORRUPT", messa
|
|
|
2859
5051
|
var collision = (message) => new WorkspaceRegistryError("WORKSPACE_COLLISION", message);
|
|
2860
5052
|
|
|
2861
5053
|
// src/onboard.ts
|
|
2862
|
-
async function runOnboard(launchConfig) {
|
|
5054
|
+
async function runOnboard(launchConfig, options = {}) {
|
|
2863
5055
|
intro("Welcome to Buda Connector");
|
|
2864
5056
|
const existing = await readDeviceConfig(launchConfig);
|
|
2865
5057
|
let relayProtocol;
|
|
@@ -2905,10 +5097,12 @@ async function runOnboard(launchConfig) {
|
|
|
2905
5097
|
await provisionWorkdirLayout(config2.workdirRoot);
|
|
2906
5098
|
try {
|
|
2907
5099
|
const registration = await registerConnector(config2, launchConfig, void 0, {
|
|
2908
|
-
recoverMissingProfile:
|
|
5100
|
+
recoverMissingProfile: false
|
|
2909
5101
|
});
|
|
2910
5102
|
config2.connectorId = registration.connectorId;
|
|
5103
|
+
config2.spaceId = registration.spaceId ?? config2.spaceId;
|
|
2911
5104
|
relayProtocol = registration.relayProtocol;
|
|
5105
|
+
if (!config2.spaceId) throw new Error("Registration did not resolve a Space");
|
|
2912
5106
|
await writeDeviceConfig(config2);
|
|
2913
5107
|
} catch (error) {
|
|
2914
5108
|
s2.stop("Registration failed");
|
|
@@ -2952,23 +5146,33 @@ async function runOnboard(launchConfig) {
|
|
|
2952
5146
|
launchConfig
|
|
2953
5147
|
);
|
|
2954
5148
|
s.stop("Daemon running");
|
|
2955
|
-
await writeLog("info", `server ${config.serverUrl}`);
|
|
2956
|
-
await writeLog("info", `config ${paths.configPath}`);
|
|
2957
|
-
await writeLog("info", `workdir root ${config.workdirRoot}`);
|
|
2958
|
-
await writeLog("info", `bunny daemon ${bunnyDaemon.baseUrl}`);
|
|
2959
|
-
await writeLog("info", `log file ${paths.logPath}`);
|
|
2960
5149
|
note(
|
|
2961
5150
|
[
|
|
2962
5151
|
`Server: ${config.serverUrl}`,
|
|
2963
5152
|
`Bunny daemon: ${bunnyDaemon.baseUrl}`,
|
|
2964
5153
|
`Relay: connected`,
|
|
2965
|
-
`Logs: ${paths.logPath}`
|
|
5154
|
+
`Logs: ${getDatedConnectorLogPath(paths.logPath)}`
|
|
2966
5155
|
].join("\n"),
|
|
2967
5156
|
"Status"
|
|
2968
5157
|
);
|
|
2969
5158
|
outro("Buda Connector is running. Press Ctrl+C to stop.");
|
|
5159
|
+
const dashboard = startConnectorConsoleDashboard({ plain: options.plain });
|
|
5160
|
+
updateConnectorDashboardStatus({
|
|
5161
|
+
agent: "Ready",
|
|
5162
|
+
relay: "Connected",
|
|
5163
|
+
server: config.serverUrl
|
|
5164
|
+
});
|
|
5165
|
+
await writeLog("info", `server ${config.serverUrl}`);
|
|
5166
|
+
await writeLog("info", `config ${paths.configPath}`);
|
|
5167
|
+
await writeLog("info", `workdir root ${config.workdirRoot}`);
|
|
5168
|
+
await writeLog("info", `bunny daemon ${bunnyDaemon.baseUrl}`);
|
|
5169
|
+
await writeLog("info", `log file ${getDatedConnectorLogPath(paths.logPath)}`);
|
|
2970
5170
|
const interval = setInterval(() => {
|
|
2971
|
-
heartbeatConnector(config, launchConfig, bunnyDaemon).
|
|
5171
|
+
heartbeatConnector(config, launchConfig, bunnyDaemon).then(() => {
|
|
5172
|
+
updateConnectorDashboardStatus({
|
|
5173
|
+
heartbeat: `OK ${(/* @__PURE__ */ new Date()).toLocaleTimeString("en-GB", { hour12: false })}`
|
|
5174
|
+
});
|
|
5175
|
+
}).catch((error) => {
|
|
2972
5176
|
void writeLog(
|
|
2973
5177
|
"warn",
|
|
2974
5178
|
`heartbeat failed: ${error instanceof Error ? error.message : String(error)}`
|
|
@@ -2976,6 +5180,7 @@ async function runOnboard(launchConfig) {
|
|
|
2976
5180
|
});
|
|
2977
5181
|
}, HEARTBEAT_INTERVAL_MS);
|
|
2978
5182
|
const shutdown = async () => {
|
|
5183
|
+
dashboard?.stop();
|
|
2979
5184
|
clearInterval(interval);
|
|
2980
5185
|
relay.stop();
|
|
2981
5186
|
await unregisterConnector(config, launchConfig);
|
|
@@ -2987,8 +5192,31 @@ async function runOnboard(launchConfig) {
|
|
|
2987
5192
|
}
|
|
2988
5193
|
|
|
2989
5194
|
// src/runtime.ts
|
|
2990
|
-
import { mkdir as
|
|
2991
|
-
import
|
|
5195
|
+
import { mkdir as mkdir12 } from "fs/promises";
|
|
5196
|
+
import path14 from "path";
|
|
5197
|
+
var RECOVERED_DAEMON_STOP_TIMEOUT_MS = 1e4;
|
|
5198
|
+
var RECOVERED_DAEMON_STOP_POLL_MS = 100;
|
|
5199
|
+
var wait3 = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
5200
|
+
async function stopRecoveredIdleDaemon(config, configRoot) {
|
|
5201
|
+
const stopStatus = await requestDaemonControl(config, "stop-if-idle", configRoot).catch(
|
|
5202
|
+
() => null
|
|
5203
|
+
);
|
|
5204
|
+
if (!stopStatus) return;
|
|
5205
|
+
if (!stopStatus.idle) {
|
|
5206
|
+
throw new Error(
|
|
5207
|
+
`The recovered Connector ${config.connectorId} is still running an Agent task and cannot be replaced yet.`
|
|
5208
|
+
);
|
|
5209
|
+
}
|
|
5210
|
+
const deadline = Date.now() + RECOVERED_DAEMON_STOP_TIMEOUT_MS;
|
|
5211
|
+
while (Date.now() < deadline) {
|
|
5212
|
+
const status = await requestDaemonControl(config, "status", configRoot, {
|
|
5213
|
+
timeoutMs: 500
|
|
5214
|
+
}).catch(() => null);
|
|
5215
|
+
if (!status) return;
|
|
5216
|
+
await wait3(RECOVERED_DAEMON_STOP_POLL_MS);
|
|
5217
|
+
}
|
|
5218
|
+
throw new Error(`Recovered Connector ${config.connectorId} did not stop cleanly.`);
|
|
5219
|
+
}
|
|
2992
5220
|
async function runNewConnector(launchConfig) {
|
|
2993
5221
|
const existing = await readDeviceConfig(launchConfig);
|
|
2994
5222
|
const config = await ensureDeviceConfig(launchConfig);
|
|
@@ -2997,27 +5225,41 @@ async function runNewConnector(launchConfig) {
|
|
|
2997
5225
|
await provisionWorkdirLayout(config.workdirRoot);
|
|
2998
5226
|
await LocalWorkspaceRegistry.open(config.workdirRoot);
|
|
2999
5227
|
const registration = await registerConnector(config, launchConfig, void 0, {
|
|
3000
|
-
recoverMissingProfile:
|
|
5228
|
+
recoverMissingProfile: shouldRecoverMissingProfile(Boolean(existing), launchConfig)
|
|
3001
5229
|
});
|
|
3002
5230
|
config.connectorId = registration.connectorId;
|
|
5231
|
+
config.spaceId = registration.spaceId ?? config.spaceId;
|
|
3003
5232
|
await writeDeviceConfig(config);
|
|
3004
5233
|
await writeLog("info", `wrote ${paths.configPath}`);
|
|
3005
5234
|
await writeLog("info", `workdir root ${config.workdirRoot}`);
|
|
3006
|
-
await writeLog("info", `log file ${paths.logPath}`);
|
|
5235
|
+
await writeLog("info", `log file ${getDatedConnectorLogPath(paths.logPath)}`);
|
|
3007
5236
|
}
|
|
3008
|
-
async function runConnectorDaemon(launchConfig) {
|
|
3009
|
-
const
|
|
3010
|
-
const
|
|
3011
|
-
const
|
|
3012
|
-
|
|
3013
|
-
|
|
5237
|
+
async function runConnectorDaemon(launchConfig, options = {}) {
|
|
5238
|
+
const configRoot = options.configRoot ?? process.env.BUDA_CONNECTOR_CONFIG_ROOT;
|
|
5239
|
+
const existing = await readDeviceConfig(launchConfig, configRoot);
|
|
5240
|
+
const config = await ensureDeviceConfig(launchConfig, configRoot);
|
|
5241
|
+
setConnectorLogContext({
|
|
5242
|
+
configRoot,
|
|
5243
|
+
connectorId: config.connectorId,
|
|
5244
|
+
serverUrl: config.serverUrl
|
|
5245
|
+
});
|
|
5246
|
+
const paths = getConnectorPaths(config.serverUrl, configRoot);
|
|
5247
|
+
const daemonLockTarget = getConnectorDaemonLockTarget(
|
|
5248
|
+
config.serverUrl,
|
|
5249
|
+
config.connectorId,
|
|
5250
|
+
configRoot
|
|
5251
|
+
);
|
|
5252
|
+
await mkdir12(path14.dirname(daemonLockTarget), { recursive: true, mode: 448 });
|
|
3014
5253
|
let daemonLock = await acquireExclusiveFileLock(daemonLockTarget, { timeoutMs: 0 }).catch(() => {
|
|
3015
5254
|
throw new Error(
|
|
3016
5255
|
`Another Buda Connector daemon is already running for this profile on ${config.serverUrl}. Stop it before starting another instance.`
|
|
3017
5256
|
);
|
|
3018
5257
|
});
|
|
5258
|
+
const restoreConsole = options.captureConsole ? installConnectorConsoleCapture() : void 0;
|
|
3019
5259
|
let bunnyDaemon;
|
|
3020
5260
|
let relayProtocol;
|
|
5261
|
+
let control;
|
|
5262
|
+
let lastHeartbeatAt;
|
|
3021
5263
|
try {
|
|
3022
5264
|
await migrateLegacyWorkspaceRoot(config.workdirRoot);
|
|
3023
5265
|
await provisionWorkdirLayout(config.workdirRoot);
|
|
@@ -3025,15 +5267,22 @@ async function runConnectorDaemon(launchConfig) {
|
|
|
3025
5267
|
bunnyDaemon = await startEmbeddedBunnyDaemon(config, launchConfig);
|
|
3026
5268
|
const activeBunnyDaemon = bunnyDaemon;
|
|
3027
5269
|
const registration = await registerConnector(config, launchConfig, activeBunnyDaemon, {
|
|
3028
|
-
recoverMissingProfile:
|
|
5270
|
+
recoverMissingProfile: shouldRecoverMissingProfile(Boolean(existing), launchConfig)
|
|
3029
5271
|
});
|
|
3030
5272
|
const connectorIdChanged = config.connectorId !== registration.connectorId;
|
|
3031
5273
|
if (connectorIdChanged) {
|
|
5274
|
+
const registeredConfig = {
|
|
5275
|
+
...config,
|
|
5276
|
+
connectorId: registration.connectorId,
|
|
5277
|
+
spaceId: registration.spaceId ?? config.spaceId
|
|
5278
|
+
};
|
|
5279
|
+
await stopRecoveredIdleDaemon(registeredConfig, configRoot);
|
|
3032
5280
|
const registeredLockTarget = getConnectorDaemonLockTarget(
|
|
3033
5281
|
config.serverUrl,
|
|
3034
|
-
registration.connectorId
|
|
5282
|
+
registration.connectorId,
|
|
5283
|
+
configRoot
|
|
3035
5284
|
);
|
|
3036
|
-
await
|
|
5285
|
+
await mkdir12(path14.dirname(registeredLockTarget), { recursive: true, mode: 448 });
|
|
3037
5286
|
const registeredLock = await acquireExclusiveFileLock(registeredLockTarget, {
|
|
3038
5287
|
timeoutMs: 0
|
|
3039
5288
|
}).catch(() => {
|
|
@@ -3045,8 +5294,15 @@ async function runConnectorDaemon(launchConfig) {
|
|
|
3045
5294
|
daemonLock = registeredLock;
|
|
3046
5295
|
}
|
|
3047
5296
|
config.connectorId = registration.connectorId;
|
|
5297
|
+
setConnectorLogContext({ connectorId: config.connectorId });
|
|
5298
|
+
config.spaceId = registration.spaceId ?? config.spaceId;
|
|
3048
5299
|
relayProtocol = registration.relayProtocol;
|
|
3049
|
-
if (!
|
|
5300
|
+
if (!config.spaceId) {
|
|
5301
|
+
throw new Error("Connector registration did not return a canonical spaceId");
|
|
5302
|
+
}
|
|
5303
|
+
if (!existing || connectorIdChanged || existing.spaceId !== config.spaceId) {
|
|
5304
|
+
await writeDeviceConfig(config, configRoot);
|
|
5305
|
+
}
|
|
3050
5306
|
const relay = startConnectorRelay(
|
|
3051
5307
|
config,
|
|
3052
5308
|
activeBunnyDaemon,
|
|
@@ -3057,12 +5313,16 @@ async function runConnectorDaemon(launchConfig) {
|
|
|
3057
5313
|
await writeLog("info", `server ${config.serverUrl}`);
|
|
3058
5314
|
await writeLog("info", `config ${paths.configPath}`);
|
|
3059
5315
|
await writeLog("info", `workdir root ${config.workdirRoot}`);
|
|
3060
|
-
await writeLog("info", `log file ${paths.logPath}`);
|
|
5316
|
+
await writeLog("info", `log file ${getDatedConnectorLogPath(paths.logPath)}`);
|
|
3061
5317
|
await writeLog("info", `bunny daemon ${activeBunnyDaemon.baseUrl}`);
|
|
3062
5318
|
const interval = setInterval(() => {
|
|
3063
5319
|
heartbeatConnector(config, launchConfig, activeBunnyDaemon).then(({ relayProtocol: nextRelayProtocol }) => {
|
|
5320
|
+
lastHeartbeatAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3064
5321
|
relay.updateRelayProtocol(nextRelayProtocol);
|
|
3065
5322
|
relay.resumeAfterAccessRestore();
|
|
5323
|
+
updateConnectorDashboardStatus({
|
|
5324
|
+
heartbeat: `OK ${(/* @__PURE__ */ new Date()).toLocaleTimeString("en-GB", { hour12: false })}`
|
|
5325
|
+
});
|
|
3066
5326
|
}).catch((error) => {
|
|
3067
5327
|
void writeLog(
|
|
3068
5328
|
"warn",
|
|
@@ -3081,23 +5341,37 @@ async function runConnectorDaemon(launchConfig) {
|
|
|
3081
5341
|
await unregisterConnector(config, launchConfig);
|
|
3082
5342
|
await stopEmbeddedBunnyDaemon(activeBunnyDaemon);
|
|
3083
5343
|
} finally {
|
|
5344
|
+
await control?.close();
|
|
3084
5345
|
await daemonLock.release();
|
|
3085
|
-
|
|
5346
|
+
await restoreConsole?.();
|
|
5347
|
+
if (options.exitOnShutdown !== false) process.exit(0);
|
|
3086
5348
|
}
|
|
3087
5349
|
};
|
|
5350
|
+
control = await startDaemonControlServer({
|
|
5351
|
+
config,
|
|
5352
|
+
configRoot,
|
|
5353
|
+
getLastHeartbeatAt: () => lastHeartbeatAt,
|
|
5354
|
+
isIdle: () => relay.isIdle(),
|
|
5355
|
+
isRelayConnected: () => relay.isConnected(),
|
|
5356
|
+
mode: options.mode ?? "persistent",
|
|
5357
|
+
onStop: shutdown,
|
|
5358
|
+
version: options.version
|
|
5359
|
+
});
|
|
3088
5360
|
process.once("SIGINT", () => void shutdown());
|
|
3089
5361
|
process.once("SIGTERM", () => void shutdown());
|
|
3090
5362
|
} catch (error) {
|
|
3091
5363
|
if (bunnyDaemon) await stopEmbeddedBunnyDaemon(bunnyDaemon);
|
|
3092
5364
|
await daemonLock.release();
|
|
5365
|
+
await restoreConsole?.();
|
|
3093
5366
|
throw error;
|
|
3094
5367
|
}
|
|
3095
5368
|
}
|
|
5369
|
+
var shouldRecoverMissingProfile = (hasExistingProfile, launchConfig) => !hasExistingProfile && Boolean(getConnectorToken(launchConfig)) && !launchConfig?.oauthToken?.trim();
|
|
3096
5370
|
|
|
3097
5371
|
// src/cli.ts
|
|
3098
5372
|
function printHelp() {
|
|
3099
5373
|
console.log(
|
|
3100
|
-
"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"
|
|
5374
|
+
"Help: buda-connector [onboard | new | dev | daemon] [--token <token>] [--server-url <url>] [--workdir <path>] [--plain] [--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\n\nOptions:\n --plain Disable the interactive dashboard and print ordinary logs"
|
|
3101
5375
|
);
|
|
3102
5376
|
}
|
|
3103
5377
|
function readFlagValue(name) {
|
|
@@ -3110,6 +5384,7 @@ function readCliLaunchConfig() {
|
|
|
3110
5384
|
const bunnyDaemonPort = readFlagValue("--bunny-daemon-port");
|
|
3111
5385
|
return {
|
|
3112
5386
|
serverUrl: readFlagValue("--server-url"),
|
|
5387
|
+
spaceId: readFlagValue("--space-id"),
|
|
3113
5388
|
workdirRoot: readFlagValue("--workdir"),
|
|
3114
5389
|
oauthToken: readFlagValue("--oauth-token"),
|
|
3115
5390
|
token: readFlagValue("--token"),
|
|
@@ -3157,25 +5432,49 @@ async function main() {
|
|
|
3157
5432
|
const isTTY = process.stdin.isTTY && process.stdout.isTTY;
|
|
3158
5433
|
const defaultCommand = isTTY ? "onboard" : "daemon";
|
|
3159
5434
|
const command = !rawCommand || rawCommand.startsWith("-") ? defaultCommand : rawCommand;
|
|
5435
|
+
const configRoot = readFlagValue("--config-root");
|
|
5436
|
+
if (configRoot) process.env.BUDA_CONNECTOR_CONFIG_ROOT = configRoot;
|
|
3160
5437
|
process.on("unhandledRejection", (reason) => {
|
|
3161
5438
|
console.error("[buda-connector] unhandledRejection:", reason);
|
|
3162
5439
|
});
|
|
3163
5440
|
process.on("uncaughtException", (error) => {
|
|
3164
5441
|
console.error("[buda-connector] uncaughtException:", error);
|
|
3165
5442
|
});
|
|
3166
|
-
const launchConfig = await readLaunchConfig();
|
|
5443
|
+
const launchConfig = process.argv.includes("--internal-daemon") ? getInternalLaunchConfig() : await readLaunchConfig();
|
|
5444
|
+
const plain = process.argv.includes("--plain");
|
|
3167
5445
|
if (command === "new") {
|
|
3168
5446
|
await runNewConnector(launchConfig);
|
|
3169
5447
|
return;
|
|
3170
5448
|
}
|
|
3171
5449
|
if (command === "onboard") {
|
|
3172
|
-
await runOnboard(launchConfig);
|
|
5450
|
+
await runOnboard(launchConfig, { plain });
|
|
3173
5451
|
return;
|
|
3174
5452
|
}
|
|
3175
|
-
if (command === "dev" ||
|
|
5453
|
+
if (command === "dev" || process.argv.includes("--foreground")) {
|
|
5454
|
+
startConnectorConsoleDashboard({ plain });
|
|
3176
5455
|
await runConnectorDaemon(launchConfig);
|
|
3177
5456
|
return;
|
|
3178
5457
|
}
|
|
5458
|
+
if (command === "daemon" && process.argv.includes("--internal-daemon")) {
|
|
5459
|
+
await runConnectorDaemon(launchConfig, {
|
|
5460
|
+
captureConsole: true,
|
|
5461
|
+
mode: getDaemonModeFlag(),
|
|
5462
|
+
version: CONNECTOR_RUNTIME_VERSION
|
|
5463
|
+
});
|
|
5464
|
+
return;
|
|
5465
|
+
}
|
|
5466
|
+
if (command === "daemon" && process.argv.includes("--desktop-release")) {
|
|
5467
|
+
await releaseDesktopConnector(launchConfig);
|
|
5468
|
+
return;
|
|
5469
|
+
}
|
|
5470
|
+
if (command === "daemon" && process.argv.includes("--desktop")) {
|
|
5471
|
+
await runDesktopConnectorController(launchConfig);
|
|
5472
|
+
return;
|
|
5473
|
+
}
|
|
5474
|
+
if (command === "daemon") {
|
|
5475
|
+
await runConnectorDashboard(launchConfig, { plain });
|
|
5476
|
+
return;
|
|
5477
|
+
}
|
|
3179
5478
|
printHelp();
|
|
3180
5479
|
process.exit(1);
|
|
3181
5480
|
}
|