@buda-ai/connector 0.1.31 → 0.1.33
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/agent-templates/shared/AGENTS.md +1 -1
- package/dist/cli.js +462 -96
- package/package.json +3 -2
|
@@ -47,7 +47,7 @@ Capture what matters. Decisions, context, things to remember. Skip the secrets u
|
|
|
47
47
|
|
|
48
48
|
- **Memory is limited** — if you want to remember something, you MUST write it to a file using your tools (like `bash` or `write`).
|
|
49
49
|
- "Mental notes" do not survive session restarts or context limits. Only files do.
|
|
50
|
-
- When the user gives you a **new rule, a new preference, changes your identity/role, or tells you to "remember this"** → You **MUST IMMEDIATELY** use your tools to update `SOUL.md`, `AGENTS.md`, `
|
|
50
|
+
- When the user gives you a **new rule, a new preference, changes your identity/role, or tells you to "remember this"** → You **MUST IMMEDIATELY** use your tools to update `SOUL.md`, `AGENTS.md`, `MEMORY.md` or `memory/YYYY-MM-DD.md`.
|
|
51
51
|
- DO NOT just reply "Okay, I will remember this." You must actually execute a file write operation to make it permanent.
|
|
52
52
|
- When you learn a lesson → update `AGENTS.md`, `TOOLS.md`, or the relevant skill.
|
|
53
53
|
- When you make a mistake → document it so future-you doesn't repeat it.
|
package/dist/cli.js
CHANGED
|
@@ -25,8 +25,8 @@ async function acquireExclusiveFileLock(targetPath, options = {}) {
|
|
|
25
25
|
} catch (error) {
|
|
26
26
|
if (!isAlreadyExists(error)) throw error;
|
|
27
27
|
if (await shouldRecoverLock(lockPath, staleMs)) {
|
|
28
|
-
await unlink(lockPath).catch(() =>
|
|
29
|
-
continue;
|
|
28
|
+
const recovered = await unlink(lockPath).then(() => true).catch(() => false);
|
|
29
|
+
if (recovered) continue;
|
|
30
30
|
}
|
|
31
31
|
if (Date.now() >= deadline) {
|
|
32
32
|
throw new Error(`Timed out waiting for exclusive access to ${targetPath}`);
|
|
@@ -35,17 +35,23 @@ async function acquireExclusiveFileLock(targetPath, options = {}) {
|
|
|
35
35
|
continue;
|
|
36
36
|
}
|
|
37
37
|
const ownerToken = randomUUID();
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
try {
|
|
39
|
+
await handle.writeFile(
|
|
40
|
+
`${JSON.stringify({ pid: process.pid, ownerToken, createdAt: Date.now() })}
|
|
40
41
|
`
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
);
|
|
43
|
+
await handle.sync();
|
|
44
|
+
} catch (error) {
|
|
45
|
+
await handle.close().catch(() => void 0);
|
|
46
|
+
await unlink(lockPath).catch(() => void 0);
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
await handle.close();
|
|
43
50
|
let released = false;
|
|
44
51
|
return {
|
|
45
52
|
release: async () => {
|
|
46
53
|
if (released) return;
|
|
47
54
|
released = true;
|
|
48
|
-
await handle.close();
|
|
49
55
|
const currentOwner = await readFile(lockPath, "utf8").then((raw) => JSON.parse(raw).ownerToken).catch(() => void 0);
|
|
50
56
|
if (currentOwner === ownerToken) await unlink(lockPath).catch(() => void 0);
|
|
51
57
|
}
|
|
@@ -517,7 +523,7 @@ import path2 from "path";
|
|
|
517
523
|
// package.json
|
|
518
524
|
var package_default = {
|
|
519
525
|
name: "@buda-ai/connector",
|
|
520
|
-
version: "0.1.
|
|
526
|
+
version: "0.1.33",
|
|
521
527
|
private: false,
|
|
522
528
|
type: "module",
|
|
523
529
|
publishConfig: {
|
|
@@ -535,6 +541,7 @@ var package_default = {
|
|
|
535
541
|
dev: "tsx src/cli.ts dev",
|
|
536
542
|
daemon: "tsx src/cli.ts daemon",
|
|
537
543
|
new: "tsx src/cli.ts new",
|
|
544
|
+
prepack: "tsup --silent",
|
|
538
545
|
start: "node dist/cli.js daemon",
|
|
539
546
|
build: "tsup",
|
|
540
547
|
test: "vitest run",
|
|
@@ -543,7 +550,7 @@ var package_default = {
|
|
|
543
550
|
"lint:err": "biome format . --write && biome check . --write --diagnostic-level=error"
|
|
544
551
|
},
|
|
545
552
|
dependencies: {
|
|
546
|
-
"@bunny-agent/daemon": "0.9.
|
|
553
|
+
"@bunny-agent/daemon": "0.9.58",
|
|
547
554
|
"@clack/prompts": "^1.0.0",
|
|
548
555
|
"playwright-core": "^1.52.0",
|
|
549
556
|
ws: "^8.21.0"
|
|
@@ -2208,6 +2215,13 @@ var ConnectorDeletedError = class extends Error {
|
|
|
2208
2215
|
this.name = "ConnectorDeletedError";
|
|
2209
2216
|
}
|
|
2210
2217
|
};
|
|
2218
|
+
var ConnectorUnauthorizedError = class extends Error {
|
|
2219
|
+
constructor(message) {
|
|
2220
|
+
super(message);
|
|
2221
|
+
this.name = "ConnectorUnauthorizedError";
|
|
2222
|
+
}
|
|
2223
|
+
};
|
|
2224
|
+
var UNREGISTER_TIMEOUT_MS = 3e3;
|
|
2211
2225
|
async function resolveConnectorScope(deviceId, launchConfig) {
|
|
2212
2226
|
const serverUrl = normalizeServerOrigin(getServerUrl(launchConfig));
|
|
2213
2227
|
const headers = { "Content-Type": "application/json" };
|
|
@@ -2294,14 +2308,15 @@ function buildAuthHeaders(config, launchConfig) {
|
|
|
2294
2308
|
if (config.deviceToken) headers["X-Buda-Connector-Device-Token"] = config.deviceToken;
|
|
2295
2309
|
return headers;
|
|
2296
2310
|
}
|
|
2297
|
-
async function postJson(serverUrl, pathname, body, config, launchConfig) {
|
|
2311
|
+
async function postJson(serverUrl, pathname, body, config, launchConfig, options = {}) {
|
|
2298
2312
|
const response = await fetch(`${serverUrl}${pathname}`, {
|
|
2299
2313
|
method: "POST",
|
|
2300
2314
|
headers: {
|
|
2301
2315
|
"Content-Type": "application/json",
|
|
2302
2316
|
...buildAuthHeaders(config, launchConfig)
|
|
2303
2317
|
},
|
|
2304
|
-
body: JSON.stringify(body)
|
|
2318
|
+
body: JSON.stringify(body),
|
|
2319
|
+
...options.timeoutMs ? { signal: AbortSignal.timeout(options.timeoutMs) } : {}
|
|
2305
2320
|
});
|
|
2306
2321
|
if (!response.ok) {
|
|
2307
2322
|
const text2 = await response.text().catch(() => "");
|
|
@@ -2321,6 +2336,9 @@ async function postJson(serverUrl, pathname, body, config, launchConfig) {
|
|
|
2321
2336
|
if (errorCode === "connector_deleted") {
|
|
2322
2337
|
throw new ConnectorDeletedError(errorMessage3 || "Connector no longer exists");
|
|
2323
2338
|
}
|
|
2339
|
+
if (response.status === 401) {
|
|
2340
|
+
throw new ConnectorUnauthorizedError(errorMessage3 || "Connector authentication failed");
|
|
2341
|
+
}
|
|
2324
2342
|
throw new Error(`${pathname} failed: ${response.status} ${errorMessage3}`);
|
|
2325
2343
|
}
|
|
2326
2344
|
return await response.json();
|
|
@@ -2396,7 +2414,8 @@ async function unregisterConnector(config, launchConfig) {
|
|
|
2396
2414
|
connectorId: config.connectorId
|
|
2397
2415
|
},
|
|
2398
2416
|
config,
|
|
2399
|
-
launchConfig
|
|
2417
|
+
launchConfig,
|
|
2418
|
+
{ timeoutMs: UNREGISTER_TIMEOUT_MS }
|
|
2400
2419
|
).catch((error) => {
|
|
2401
2420
|
void writeLog("warn", `unregister failed: ${error instanceof Error ? error.message : error}`);
|
|
2402
2421
|
});
|
|
@@ -2405,32 +2424,117 @@ async function unregisterConnector(config, launchConfig) {
|
|
|
2405
2424
|
// src/runtime-install.ts
|
|
2406
2425
|
import { execFile } from "child_process";
|
|
2407
2426
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
2408
|
-
import {
|
|
2427
|
+
import {
|
|
2428
|
+
access,
|
|
2429
|
+
cp,
|
|
2430
|
+
mkdir as mkdir5,
|
|
2431
|
+
readdir as readdir4,
|
|
2432
|
+
readFile as readFile5,
|
|
2433
|
+
realpath,
|
|
2434
|
+
rename as rename4,
|
|
2435
|
+
rm,
|
|
2436
|
+
writeFile as writeFile3
|
|
2437
|
+
} from "fs/promises";
|
|
2409
2438
|
import { homedir as homedir3 } from "os";
|
|
2410
2439
|
import path6 from "path";
|
|
2411
2440
|
import { promisify } from "util";
|
|
2412
2441
|
var execFileAsync = promisify(execFile);
|
|
2413
2442
|
var RUNTIME_INSTALL_TIMEOUT_MS = 5 * 6e4;
|
|
2443
|
+
var WINDOWS_RENAME_RETRY_DELAYS_MS = [50, 100, 200, 400, 500, 500, 500, 500, 500];
|
|
2414
2444
|
var MACOS_NODE_LIBRARY_PATTERN = /^libnode(?:\.\d+)*\.dylib$/;
|
|
2445
|
+
var getLoadedSharedObjects = () => {
|
|
2446
|
+
const report = process.report?.getReport();
|
|
2447
|
+
return Array.isArray(report?.sharedObjects) ? report.sharedObjects.filter((value) => typeof value === "string") : [];
|
|
2448
|
+
};
|
|
2449
|
+
var resolveBundledNodePlacement = (sourceRoot, sourceNode, pathApi = path6) => {
|
|
2450
|
+
const relativeNode = pathApi.relative(sourceRoot, sourceNode);
|
|
2451
|
+
return {
|
|
2452
|
+
copyNode: relativeNode.startsWith("..") || pathApi.isAbsolute(relativeNode),
|
|
2453
|
+
relativeNode
|
|
2454
|
+
};
|
|
2455
|
+
};
|
|
2415
2456
|
var copyNodeRuntime = async (sourceNode, destinationNode, options = {}) => {
|
|
2416
|
-
const
|
|
2457
|
+
const resolvedSourceNode = await realpath(sourceNode);
|
|
2458
|
+
const sourceDirectory = path6.dirname(resolvedSourceNode);
|
|
2417
2459
|
const destinationDirectory = path6.dirname(destinationNode);
|
|
2418
2460
|
await mkdir5(destinationDirectory, { recursive: true });
|
|
2419
|
-
await cp(
|
|
2461
|
+
await cp(resolvedSourceNode, destinationNode, { dereference: true, force: true });
|
|
2420
2462
|
if ((options.platform ?? process.platform) !== "darwin") return;
|
|
2421
|
-
const
|
|
2463
|
+
const launcherDirectory = path6.resolve(path6.dirname(sourceNode));
|
|
2464
|
+
const companionDirectories = [
|
|
2465
|
+
.../* @__PURE__ */ new Set([
|
|
2466
|
+
launcherDirectory,
|
|
2467
|
+
path6.resolve(launcherDirectory, "..", "lib"),
|
|
2468
|
+
sourceDirectory,
|
|
2469
|
+
path6.resolve(sourceDirectory, "..", "lib")
|
|
2470
|
+
])
|
|
2471
|
+
];
|
|
2472
|
+
const companions = (await Promise.all(
|
|
2473
|
+
companionDirectories.map(async (directory) => ({
|
|
2474
|
+
directory,
|
|
2475
|
+
entries: await readdir4(directory, { withFileTypes: true }).catch((error) => {
|
|
2476
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return [];
|
|
2477
|
+
throw error;
|
|
2478
|
+
})
|
|
2479
|
+
}))
|
|
2480
|
+
)).flatMap(({ directory, entries }) => entries.map((entry) => ({ directory, entry })));
|
|
2481
|
+
const companionFiles = /* @__PURE__ */ new Map();
|
|
2482
|
+
for (const library of options.loadedLibraries ?? getLoadedSharedObjects()) {
|
|
2483
|
+
const name = path6.basename(library);
|
|
2484
|
+
if (MACOS_NODE_LIBRARY_PATTERN.test(name)) companionFiles.set(name, library);
|
|
2485
|
+
}
|
|
2486
|
+
for (const { directory, entry } of companions) {
|
|
2487
|
+
if ((entry.isFile() || entry.isSymbolicLink()) && MACOS_NODE_LIBRARY_PATTERN.test(entry.name) && !companionFiles.has(entry.name)) {
|
|
2488
|
+
companionFiles.set(entry.name, path6.join(directory, entry.name));
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2422
2491
|
await Promise.all(
|
|
2423
|
-
|
|
2424
|
-
(
|
|
2425
|
-
).map(
|
|
2426
|
-
(entry) => cp(path6.join(sourceDirectory, entry.name), path6.join(destinationDirectory, entry.name), {
|
|
2492
|
+
[...companionFiles].map(
|
|
2493
|
+
([name, source]) => cp(source, path6.join(destinationDirectory, name), {
|
|
2427
2494
|
dereference: true,
|
|
2428
2495
|
force: true
|
|
2429
2496
|
})
|
|
2430
2497
|
)
|
|
2431
2498
|
);
|
|
2432
2499
|
};
|
|
2500
|
+
var isWindowsRenameContention = (error) => error instanceof Error && "code" in error && ["EACCES", "EBUSY", "EPERM"].includes(String(error.code));
|
|
2501
|
+
var renameStagedRuntime = async (source, destination, options = {}) => {
|
|
2502
|
+
const platform4 = options.platform ?? process.platform;
|
|
2503
|
+
const renamePath = options.renamePath ?? rename4;
|
|
2504
|
+
const wait4 = options.wait ?? ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)));
|
|
2505
|
+
const delays = platform4 === "win32" ? WINDOWS_RENAME_RETRY_DELAYS_MS : [];
|
|
2506
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
2507
|
+
try {
|
|
2508
|
+
await renamePath(source, destination);
|
|
2509
|
+
return;
|
|
2510
|
+
} catch (error) {
|
|
2511
|
+
const delay = delays[attempt];
|
|
2512
|
+
if (delay === void 0 || !isWindowsRenameContention(error)) throw error;
|
|
2513
|
+
await wait4(delay);
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
};
|
|
2433
2517
|
var exists = async (filePath) => access(filePath).then(() => true).catch(() => false);
|
|
2518
|
+
var resolveNpmInvocation = async (options = {}) => {
|
|
2519
|
+
const platform4 = options.platform ?? process.platform;
|
|
2520
|
+
if (platform4 !== "win32") return { argsPrefix: [], executable: "npm" };
|
|
2521
|
+
const nodeExecutable = options.nodeExecutable ?? process.execPath;
|
|
2522
|
+
const pathApi = options.pathApi ?? path6;
|
|
2523
|
+
const pathExists = options.pathExists ?? exists;
|
|
2524
|
+
const candidates = [
|
|
2525
|
+
options.npmExecPath ?? process.env.npm_execpath,
|
|
2526
|
+
pathApi.join(pathApi.dirname(nodeExecutable), "node_modules", "npm", "bin", "npm-cli.js")
|
|
2527
|
+
];
|
|
2528
|
+
for (const candidate of candidates) {
|
|
2529
|
+
if (!candidate || pathApi.basename(candidate).toLowerCase() !== "npm-cli.js") continue;
|
|
2530
|
+
if (await pathExists(candidate)) {
|
|
2531
|
+
return { argsPrefix: [candidate], executable: nodeExecutable };
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
throw new Error(
|
|
2535
|
+
`Unable to locate npm-cli.js for the stable Connector runtime installation (node=${JSON.stringify(nodeExecutable)}, npm_execpath=${JSON.stringify(options.npmExecPath ?? process.env.npm_execpath ?? "")})`
|
|
2536
|
+
);
|
|
2537
|
+
};
|
|
2434
2538
|
var runtimeRoot = () => process.env.BUDA_CONNECTOR_RUNTIME_ROOT || path6.join(homedir3(), ".buda", "runtime");
|
|
2435
2539
|
var runtimePlatformKey = `${process.platform}-${process.arch}-node${process.versions.modules}`;
|
|
2436
2540
|
var versionDirectory = () => path6.join(runtimeRoot(), runtimePlatformKey, CONNECTOR_RUNTIME_VERSION);
|
|
@@ -2464,9 +2568,11 @@ var validateRuntime = async (runtime) => {
|
|
|
2464
2568
|
};
|
|
2465
2569
|
var installFromNpm = async (directory) => {
|
|
2466
2570
|
await mkdir5(directory, { recursive: true, mode: 448 });
|
|
2571
|
+
const npm = await resolveNpmInvocation();
|
|
2467
2572
|
await execFileAsync(
|
|
2468
|
-
|
|
2573
|
+
npm.executable,
|
|
2469
2574
|
[
|
|
2575
|
+
...npm.argsPrefix,
|
|
2470
2576
|
"install",
|
|
2471
2577
|
"--ignore-scripts=false",
|
|
2472
2578
|
"--no-audit",
|
|
@@ -2500,10 +2606,10 @@ var installFromBundledRuntime = async (directory, sourceRoot) => {
|
|
|
2500
2606
|
if (relativeEntrypoint.startsWith("..") || path6.isAbsolute(relativeEntrypoint)) {
|
|
2501
2607
|
throw new Error("Bundled Connector entrypoint must be inside its runtime root");
|
|
2502
2608
|
}
|
|
2503
|
-
const relativeNode =
|
|
2609
|
+
const { copyNode, relativeNode } = resolveBundledNodePlacement(sourceRoot, sourceNode);
|
|
2504
2610
|
const entrypoint = path6.join(directory, relativeEntrypoint);
|
|
2505
|
-
const nodeExecutable =
|
|
2506
|
-
if (
|
|
2611
|
+
const nodeExecutable = copyNode ? path6.join(directory, "node", path6.basename(sourceNode)) : path6.join(directory, relativeNode);
|
|
2612
|
+
if (copyNode) {
|
|
2507
2613
|
await copyNodeRuntime(sourceNode, nodeExecutable);
|
|
2508
2614
|
}
|
|
2509
2615
|
if (!await exists(entrypoint))
|
|
@@ -2550,7 +2656,7 @@ async function ensureStableConnectorRuntime() {
|
|
|
2550
2656
|
};
|
|
2551
2657
|
await writeManifest(temporary, rebased);
|
|
2552
2658
|
await rm(finalDirectory, { recursive: true, force: true });
|
|
2553
|
-
await
|
|
2659
|
+
await renameStagedRuntime(temporary, finalDirectory);
|
|
2554
2660
|
return rebased;
|
|
2555
2661
|
} catch (error) {
|
|
2556
2662
|
await rm(temporary, { recursive: true, force: true });
|
|
@@ -2564,6 +2670,8 @@ async function ensureStableConnectorRuntime() {
|
|
|
2564
2670
|
// src/daemon-client.ts
|
|
2565
2671
|
var START_TIMEOUT_MS = 45e3;
|
|
2566
2672
|
var BOOTSTRAP_TIMEOUT_MS = 5 * 6e4;
|
|
2673
|
+
var GRACEFUL_STOP_TIMEOUT_MS = 3e3;
|
|
2674
|
+
var LOCK_ONLY_RECOVERY_MAX_AGE_MS = 5 * 6e4;
|
|
2567
2675
|
var POLL_INTERVAL_MS = 250;
|
|
2568
2676
|
var wait2 = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
2569
2677
|
var getConfigRoot = () => process.env.BUDA_CONNECTOR_CONFIG_ROOT;
|
|
@@ -2596,14 +2704,78 @@ var compareVersions = (left, right) => {
|
|
|
2596
2704
|
return 0;
|
|
2597
2705
|
};
|
|
2598
2706
|
var shouldReplaceDaemonVersion = (runningVersion, candidateVersion) => compareVersions(candidateVersion, runningVersion) > 0;
|
|
2707
|
+
var shouldDisplayConnectorLogEntry = (createdAt, startedAt) => {
|
|
2708
|
+
const timestamp = Date.parse(createdAt);
|
|
2709
|
+
return Number.isNaN(timestamp) || timestamp >= startedAt;
|
|
2710
|
+
};
|
|
2711
|
+
var getErrorCode = (error) => typeof error === "object" && error !== null && "code" in error ? String(error.code) : "UNKNOWN";
|
|
2712
|
+
var waitForChildSpawn = (child) => new Promise((resolve, reject) => {
|
|
2713
|
+
const onError = (error) => {
|
|
2714
|
+
child.off("spawn", onSpawn);
|
|
2715
|
+
reject(error);
|
|
2716
|
+
};
|
|
2717
|
+
const onSpawn = () => {
|
|
2718
|
+
child.off("error", onError);
|
|
2719
|
+
resolve();
|
|
2720
|
+
};
|
|
2721
|
+
child.once("error", onError);
|
|
2722
|
+
child.once("spawn", onSpawn);
|
|
2723
|
+
});
|
|
2724
|
+
async function spawnDetachedDaemonProcess(command, args, options, dependencies = {}) {
|
|
2725
|
+
const platform4 = dependencies.platform ?? process.platform;
|
|
2726
|
+
const spawnProcess = dependencies.spawnProcess ?? ((file, fileArgs, spawnOptions) => spawn(file, fileArgs, spawnOptions));
|
|
2727
|
+
const strategies = platform4 === "win32" ? [
|
|
2728
|
+
{ name: "detached-hidden", windowsHide: true },
|
|
2729
|
+
{ name: "detached-unhidden", windowsHide: false }
|
|
2730
|
+
] : [{ name: "detached-hidden", windowsHide: true }];
|
|
2731
|
+
const attempted = [];
|
|
2732
|
+
for (const [index, strategy] of strategies.entries()) {
|
|
2733
|
+
attempted.push(strategy.name);
|
|
2734
|
+
const child = spawnProcess(command, args, {
|
|
2735
|
+
detached: true,
|
|
2736
|
+
env: options.env,
|
|
2737
|
+
stdio: options.stdio,
|
|
2738
|
+
windowsHide: strategy.windowsHide
|
|
2739
|
+
});
|
|
2740
|
+
try {
|
|
2741
|
+
await waitForChildSpawn(child);
|
|
2742
|
+
if (!child.pid) {
|
|
2743
|
+
throw Object.assign(new Error("spawned Connector daemon did not expose a PID"), {
|
|
2744
|
+
code: "NO_PID"
|
|
2745
|
+
});
|
|
2746
|
+
}
|
|
2747
|
+
child.unref();
|
|
2748
|
+
return { pid: child.pid, strategy: strategy.name };
|
|
2749
|
+
} catch (error) {
|
|
2750
|
+
const code = getErrorCode(error);
|
|
2751
|
+
const shouldRetry = platform4 === "win32" && index === 0 && code === "EINVAL";
|
|
2752
|
+
if (shouldRetry) continue;
|
|
2753
|
+
const diagnostics = [
|
|
2754
|
+
`code=${code}`,
|
|
2755
|
+
`strategies=${attempted.join(",")}`,
|
|
2756
|
+
`platform=${platform4}`,
|
|
2757
|
+
`node=${dependencies.nodeVersion ?? process.version}`,
|
|
2758
|
+
`executable=${JSON.stringify(command)}`,
|
|
2759
|
+
`entrypoint=${JSON.stringify(args[0] ?? "")}`,
|
|
2760
|
+
`cwd=${JSON.stringify(dependencies.cwd ?? process.cwd())}`
|
|
2761
|
+
].join(" ");
|
|
2762
|
+
throw new Error(`Connector daemon process creation failed: ${diagnostics}`, { cause: error });
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
throw new Error("Connector daemon process creation failed without a spawn attempt");
|
|
2766
|
+
}
|
|
2767
|
+
var readReadyDaemon = async (launchConfig) => {
|
|
2768
|
+
const config = await readDeviceConfig(launchConfig, getConfigRoot());
|
|
2769
|
+
if (!config) return null;
|
|
2770
|
+
const status = await readLiveStatus(config);
|
|
2771
|
+
return status ? { config, status } : null;
|
|
2772
|
+
};
|
|
2599
2773
|
var waitForDaemon = async (launchConfig, deadline) => {
|
|
2600
2774
|
let lastConfig = null;
|
|
2601
2775
|
while (Date.now() < deadline) {
|
|
2776
|
+
const ready = await readReadyDaemon(launchConfig);
|
|
2777
|
+
if (ready) return ready;
|
|
2602
2778
|
lastConfig = await readDeviceConfig(launchConfig, getConfigRoot());
|
|
2603
|
-
if (lastConfig) {
|
|
2604
|
-
const status = await readLiveStatus(lastConfig);
|
|
2605
|
-
if (status) return { config: lastConfig, status };
|
|
2606
|
-
}
|
|
2607
2779
|
await wait2(POLL_INTERVAL_MS);
|
|
2608
2780
|
}
|
|
2609
2781
|
throw new Error(
|
|
@@ -2612,17 +2784,126 @@ var waitForDaemon = async (launchConfig, deadline) => {
|
|
|
2612
2784
|
};
|
|
2613
2785
|
var spawnInternalDaemon = async (launchConfig, mode, preparedRuntime, options = {}) => {
|
|
2614
2786
|
const runtime = preparedRuntime ?? await ensureStableConnectorRuntime();
|
|
2615
|
-
const
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2787
|
+
const debugDaemonStdio = process.env.BUDA_CONNECTOR_DEBUG_DAEMON_STDIO === "1";
|
|
2788
|
+
return spawnDetachedDaemonProcess(
|
|
2789
|
+
runtime.nodeExecutable,
|
|
2790
|
+
getRuntimeCommandArgs(runtime, mode, options),
|
|
2791
|
+
{
|
|
2792
|
+
env: {
|
|
2793
|
+
...process.env,
|
|
2794
|
+
BUDA_CONNECTOR_INTERNAL_LAUNCH_CONFIG: JSON.stringify(launchConfig ?? {})
|
|
2795
|
+
},
|
|
2796
|
+
stdio: debugDaemonStdio ? ["ignore", "inherit", "inherit"] : "ignore"
|
|
2797
|
+
}
|
|
2798
|
+
);
|
|
2799
|
+
};
|
|
2800
|
+
var waitForProcessExit = async (pid, timeoutMs) => {
|
|
2801
|
+
const deadline = Date.now() + timeoutMs;
|
|
2802
|
+
while (Date.now() < deadline) {
|
|
2803
|
+
if (!isProcessAlive2(pid)) return true;
|
|
2804
|
+
await wait2(50);
|
|
2805
|
+
}
|
|
2806
|
+
return !isProcessAlive2(pid);
|
|
2807
|
+
};
|
|
2808
|
+
var signalProcess = (pid, signal) => {
|
|
2809
|
+
try {
|
|
2810
|
+
process.kill(pid, signal);
|
|
2811
|
+
return true;
|
|
2812
|
+
} catch (error) {
|
|
2813
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ESRCH") {
|
|
2814
|
+
return false;
|
|
2815
|
+
}
|
|
2816
|
+
throw error;
|
|
2817
|
+
}
|
|
2625
2818
|
};
|
|
2819
|
+
var stopUnresponsiveDaemonProcess = async (pid) => {
|
|
2820
|
+
if (!isProcessAlive2(pid)) return;
|
|
2821
|
+
if (!signalProcess(pid, "SIGTERM")) return;
|
|
2822
|
+
if (await waitForProcessExit(pid, 2e3)) return;
|
|
2823
|
+
if (!signalProcess(pid, "SIGKILL")) return;
|
|
2824
|
+
if (await waitForProcessExit(pid, 3e3)) return;
|
|
2825
|
+
throw new Error(`Timed-out Connector daemon PID ${pid} could not be stopped safely`);
|
|
2826
|
+
};
|
|
2827
|
+
var waitForDaemonRelease = async (config, status, configRoot = getConfigRoot()) => {
|
|
2828
|
+
const deadline = Date.now() + GRACEFUL_STOP_TIMEOUT_MS;
|
|
2829
|
+
while (Date.now() < deadline) {
|
|
2830
|
+
if (!isProcessAlive2(status.pid)) return true;
|
|
2831
|
+
const current = await readPersistedDaemonStatus(config, configRoot).catch(() => null);
|
|
2832
|
+
if (!current || current.pid !== status.pid || current.instanceId !== status.instanceId) {
|
|
2833
|
+
return true;
|
|
2834
|
+
}
|
|
2835
|
+
await wait2(50);
|
|
2836
|
+
}
|
|
2837
|
+
return !isProcessAlive2(status.pid);
|
|
2838
|
+
};
|
|
2839
|
+
var stopLiveDaemon = async (config, status, configRoot = getConfigRoot(), command = "stop") => {
|
|
2840
|
+
const stopStatus = await requestDaemonControl(config, command, configRoot);
|
|
2841
|
+
if (command === "stop-if-idle" && !stopStatus.idle) return false;
|
|
2842
|
+
if (await waitForDaemonRelease(config, status, configRoot)) return true;
|
|
2843
|
+
const current = await readPersistedDaemonStatus(config, configRoot).catch(() => null);
|
|
2844
|
+
if (!current || current.pid !== status.pid || current.instanceId !== status.instanceId) {
|
|
2845
|
+
return true;
|
|
2846
|
+
}
|
|
2847
|
+
await stopUnresponsiveDaemonProcess(status.pid);
|
|
2848
|
+
return true;
|
|
2849
|
+
};
|
|
2850
|
+
var matchesDaemonIdentity = (config, status) => status.connectorId === config.connectorId && normalizeServerOrigin(status.serverUrl) === normalizeServerOrigin(config.serverUrl) && Number.isSafeInteger(status.pid) && status.pid > 0 && typeof status.instanceId === "string" && status.instanceId.length > 0;
|
|
2851
|
+
var readMatchingPersistedDaemonStatus = async (config, configRoot = getConfigRoot()) => {
|
|
2852
|
+
const status = await readPersistedDaemonStatus(config, configRoot).catch(() => null);
|
|
2853
|
+
return status && matchesDaemonIdentity(config, status) ? status : null;
|
|
2854
|
+
};
|
|
2855
|
+
var stopDaemonForIdentity = async (config, configRoot = getConfigRoot()) => {
|
|
2856
|
+
const liveStatus = await readLiveStatus(config, configRoot);
|
|
2857
|
+
if (liveStatus) {
|
|
2858
|
+
await stopLiveDaemon(config, liveStatus, configRoot);
|
|
2859
|
+
return;
|
|
2860
|
+
}
|
|
2861
|
+
const persistedStatus = await readMatchingPersistedDaemonStatus(config, configRoot);
|
|
2862
|
+
if (!persistedStatus || !isProcessAlive2(persistedStatus.pid)) return;
|
|
2863
|
+
const currentStatus = await readMatchingPersistedDaemonStatus(config, configRoot);
|
|
2864
|
+
if (!currentStatus || currentStatus.pid !== persistedStatus.pid || currentStatus.instanceId !== persistedStatus.instanceId || !isProcessAlive2(currentStatus.pid)) {
|
|
2865
|
+
return;
|
|
2866
|
+
}
|
|
2867
|
+
await stopUnresponsiveDaemonProcess(currentStatus.pid);
|
|
2868
|
+
};
|
|
2869
|
+
var shouldRestartUnhealthyDaemonForEnrollment = (status, explicitEnrollment) => explicitEnrollment && status.idle && status.heartbeatHealthy === false;
|
|
2870
|
+
var readDaemonLockOwnerPid = async (config) => {
|
|
2871
|
+
const lockPath = `${getConnectorDaemonLockTarget(
|
|
2872
|
+
config.serverUrl,
|
|
2873
|
+
config.connectorId,
|
|
2874
|
+
getConfigRoot()
|
|
2875
|
+
)}.lock`;
|
|
2876
|
+
const raw = await readFile6(lockPath, "utf8").catch(() => "");
|
|
2877
|
+
try {
|
|
2878
|
+
const metadata = JSON.parse(raw);
|
|
2879
|
+
const age = typeof metadata.createdAt === "number" && Number.isFinite(metadata.createdAt) ? Date.now() - metadata.createdAt : Number.POSITIVE_INFINITY;
|
|
2880
|
+
return typeof metadata.pid === "number" && Number.isSafeInteger(metadata.pid) && metadata.pid > 0 && age >= 0 && age <= LOCK_ONLY_RECOVERY_MAX_AGE_MS ? metadata.pid : null;
|
|
2881
|
+
} catch {
|
|
2882
|
+
return null;
|
|
2883
|
+
}
|
|
2884
|
+
};
|
|
2885
|
+
async function startDaemonWithRecovery(dependencies, maxAttempts = 2) {
|
|
2886
|
+
let lastError;
|
|
2887
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
2888
|
+
const spawned = await dependencies.spawnDaemon();
|
|
2889
|
+
try {
|
|
2890
|
+
return await dependencies.waitForReady();
|
|
2891
|
+
} catch (error) {
|
|
2892
|
+
lastError = error;
|
|
2893
|
+
const lateReady = await dependencies.readReady().catch(() => null);
|
|
2894
|
+
if (lateReady) return lateReady;
|
|
2895
|
+
await dependencies.stopDaemon(spawned.pid);
|
|
2896
|
+
if (attempt === maxAttempts) throw error;
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
throw lastError;
|
|
2900
|
+
}
|
|
2901
|
+
var startInternalDaemonWithRecovery = (launchConfig, mode, preparedRuntime, options = {}) => startDaemonWithRecovery({
|
|
2902
|
+
readReady: () => readReadyDaemon(launchConfig),
|
|
2903
|
+
spawnDaemon: () => spawnInternalDaemon(launchConfig, mode, preparedRuntime, options),
|
|
2904
|
+
stopDaemon: stopUnresponsiveDaemonProcess,
|
|
2905
|
+
waitForReady: () => waitForDaemon(launchConfig, Date.now() + START_TIMEOUT_MS)
|
|
2906
|
+
});
|
|
2626
2907
|
var stopOlderIdleDaemon = async (config, status) => {
|
|
2627
2908
|
if (!shouldReplaceDaemonVersion(status.version, CONNECTOR_RUNTIME_VERSION)) return false;
|
|
2628
2909
|
if (!status.idle) return false;
|
|
@@ -2649,14 +2930,19 @@ async function ensureConnectorDaemon(launchConfig, mode, options = {}) {
|
|
|
2649
2930
|
if (existingConfig) {
|
|
2650
2931
|
const existingStatus = await readLiveStatus(existingConfig);
|
|
2651
2932
|
if (existingStatus) {
|
|
2933
|
+
if (shouldRestartUnhealthyDaemonForEnrollment(
|
|
2934
|
+
existingStatus,
|
|
2935
|
+
Boolean(options.explicitEnrollment)
|
|
2936
|
+
) && await stopLiveDaemon(existingConfig, existingStatus, getConfigRoot(), "stop-if-idle")) {
|
|
2937
|
+
return await startInternalDaemonWithRecovery(launchConfig, mode, void 0, options);
|
|
2938
|
+
}
|
|
2652
2939
|
const preparedRuntime = !shouldReplaceDaemonVersion(existingStatus.version, CONNECTOR_RUNTIME_VERSION) || !existingStatus.idle ? void 0 : await ensureStableConnectorRuntime();
|
|
2653
2940
|
const replaced = await stopOlderIdleDaemon(existingConfig, existingStatus);
|
|
2654
2941
|
if (!replaced) {
|
|
2655
2942
|
const status = mode === "persistent" ? await requestDaemonControl(existingConfig, "persist", getConfigRoot()) : existingStatus;
|
|
2656
2943
|
return { config: existingConfig, status };
|
|
2657
2944
|
}
|
|
2658
|
-
await
|
|
2659
|
-
return await waitForDaemon(launchConfig, Date.now() + START_TIMEOUT_MS);
|
|
2945
|
+
return await startInternalDaemonWithRecovery(launchConfig, mode, preparedRuntime, options);
|
|
2660
2946
|
}
|
|
2661
2947
|
const persistedStatus = await readPersistedDaemonStatus(
|
|
2662
2948
|
existingConfig,
|
|
@@ -2667,9 +2953,12 @@ async function ensureConnectorDaemon(launchConfig, mode, options = {}) {
|
|
|
2667
2953
|
`Connector daemon PID ${persistedStatus.pid} is running but its local control channel is not responding. Stop that process and retry.`
|
|
2668
2954
|
);
|
|
2669
2955
|
}
|
|
2956
|
+
const lockOwnerPid = await readDaemonLockOwnerPid(existingConfig);
|
|
2957
|
+
if (lockOwnerPid && isProcessAlive2(lockOwnerPid)) {
|
|
2958
|
+
await stopUnresponsiveDaemonProcess(lockOwnerPid);
|
|
2959
|
+
}
|
|
2670
2960
|
}
|
|
2671
|
-
await
|
|
2672
|
-
return await waitForDaemon(launchConfig, Date.now() + START_TIMEOUT_MS);
|
|
2961
|
+
return await startInternalDaemonWithRecovery(launchConfig, mode, void 0, options);
|
|
2673
2962
|
} finally {
|
|
2674
2963
|
await bootstrapLock.release();
|
|
2675
2964
|
}
|
|
@@ -2689,6 +2978,7 @@ var streamDaemonLogs = (options) => {
|
|
|
2689
2978
|
for (const line of lines.slice(seenLines)) {
|
|
2690
2979
|
try {
|
|
2691
2980
|
const entry = JSON.parse(line);
|
|
2981
|
+
if (!shouldDisplayConnectorLogEntry(entry.createdAt, options.startedAt)) continue;
|
|
2692
2982
|
const connectorId = entry.metadata?.connectorId;
|
|
2693
2983
|
if (options.dashboardActive && typeof connectorId === "string") {
|
|
2694
2984
|
const output2 = entry.metadata?.source === "daemon-console" ? entry.message : `[buda-connector] ${entry.message}`;
|
|
@@ -2764,9 +3054,7 @@ async function stopLocalConnectorDaemon(connectorId, serverUrl, configRoot = get
|
|
|
2764
3054
|
(candidate) => candidate.connectorId === connectorId && normalizeServerOrigin(candidate.serverUrl) === serverOrigin
|
|
2765
3055
|
);
|
|
2766
3056
|
if (!config) throw new Error(`Local Connector ${connectorId} was not found`);
|
|
2767
|
-
|
|
2768
|
-
if (!status) return;
|
|
2769
|
-
await requestDaemonControl(config, "stop", configRoot);
|
|
3057
|
+
await stopDaemonForIdentity(config, configRoot);
|
|
2770
3058
|
}
|
|
2771
3059
|
async function stopAllLocalConnectorDaemons(configRoot = getConfigRoot()) {
|
|
2772
3060
|
const configs = await listDeviceConfigs(configRoot);
|
|
@@ -2777,10 +3065,7 @@ async function stopAllLocalConnectorDaemons(configRoot = getConfigRoot()) {
|
|
|
2777
3065
|
${config.connectorId}`, config);
|
|
2778
3066
|
}
|
|
2779
3067
|
const results = await Promise.allSettled(
|
|
2780
|
-
[...identities.values()].map(
|
|
2781
|
-
const status = await readLiveStatus(config, configRoot);
|
|
2782
|
-
if (status) await requestDaemonControl(config, "stop", configRoot);
|
|
2783
|
-
})
|
|
3068
|
+
[...identities.values()].map((config) => stopDaemonForIdentity(config, configRoot))
|
|
2784
3069
|
);
|
|
2785
3070
|
const failures = results.filter((result) => result.status === "rejected");
|
|
2786
3071
|
if (failures.length) {
|
|
@@ -2799,6 +3084,7 @@ var waitForConnectorViewerExit = async (dashboardClosed) => {
|
|
|
2799
3084
|
process.off("SIGTERM", finishSignal);
|
|
2800
3085
|
};
|
|
2801
3086
|
async function runConnectorDashboard(launchConfig, options = {}) {
|
|
3087
|
+
const startedAt = Date.now();
|
|
2802
3088
|
let finishDashboard = () => void 0;
|
|
2803
3089
|
const dashboardClosed = new Promise((resolve) => {
|
|
2804
3090
|
finishDashboard = resolve;
|
|
@@ -2819,7 +3105,8 @@ async function runConnectorDashboard(launchConfig, options = {}) {
|
|
|
2819
3105
|
connectorId: config.connectorId,
|
|
2820
3106
|
dashboardActive: false,
|
|
2821
3107
|
plain: Boolean(options.plain),
|
|
2822
|
-
serverUrl: config.serverUrl
|
|
3108
|
+
serverUrl: config.serverUrl,
|
|
3109
|
+
startedAt
|
|
2823
3110
|
});
|
|
2824
3111
|
if (!process.stdin.isTTY) {
|
|
2825
3112
|
stopLogs2();
|
|
@@ -2846,7 +3133,8 @@ async function runConnectorDashboard(launchConfig, options = {}) {
|
|
|
2846
3133
|
const stopLogs = streamDaemonLogs({
|
|
2847
3134
|
dashboardActive: true,
|
|
2848
3135
|
plain: false,
|
|
2849
|
-
serverUrl: getServerUrl(launchConfig)
|
|
3136
|
+
serverUrl: getServerUrl(launchConfig),
|
|
3137
|
+
startedAt
|
|
2850
3138
|
});
|
|
2851
3139
|
let refreshing = false;
|
|
2852
3140
|
const refresh = async () => {
|
|
@@ -3394,7 +3682,7 @@ import {
|
|
|
3394
3682
|
readdir as readdir5,
|
|
3395
3683
|
readFile as readFile7,
|
|
3396
3684
|
readlink,
|
|
3397
|
-
realpath,
|
|
3685
|
+
realpath as realpath2,
|
|
3398
3686
|
rename as rename5,
|
|
3399
3687
|
rm as rm2,
|
|
3400
3688
|
symlink,
|
|
@@ -3446,7 +3734,7 @@ var migrateLegacyWorkspaceRoot = async (storageRoot, options = {}) => {
|
|
|
3446
3734
|
"Legacy migration storage root must be a real directory"
|
|
3447
3735
|
);
|
|
3448
3736
|
}
|
|
3449
|
-
const canonicalRoot = await
|
|
3737
|
+
const canonicalRoot = await realpath2(storageRoot);
|
|
3450
3738
|
const migrationDirectory = path9.join(canonicalRoot, MIGRATION_DIRECTORY);
|
|
3451
3739
|
await ensurePrivateDirectoryWithin(canonicalRoot, migrationDirectory);
|
|
3452
3740
|
const journalPath = path9.join(migrationDirectory, JOURNAL_FILENAME);
|
|
@@ -3742,6 +4030,7 @@ var writeJournal = async (journalPath, journal) => {
|
|
|
3742
4030
|
}
|
|
3743
4031
|
};
|
|
3744
4032
|
var syncDirectory2 = async (directory) => {
|
|
4033
|
+
if (process.platform === "win32") return;
|
|
3745
4034
|
const handle = await open2(directory, constants2.O_RDONLY);
|
|
3746
4035
|
try {
|
|
3747
4036
|
await handle.sync();
|
|
@@ -4089,7 +4378,7 @@ function isNonEmptyString(value) {
|
|
|
4089
4378
|
}
|
|
4090
4379
|
|
|
4091
4380
|
// src/html-preview.ts
|
|
4092
|
-
import { readFile as readFile8, realpath as
|
|
4381
|
+
import { readFile as readFile8, realpath as realpath3, stat as stat4 } from "fs/promises";
|
|
4093
4382
|
import { createServer as createServer2 } from "http";
|
|
4094
4383
|
import path10 from "path";
|
|
4095
4384
|
var PREVIEW_HOST = "127.0.0.1";
|
|
@@ -4179,7 +4468,7 @@ var resolveWorkspaceFile = async (input) => {
|
|
|
4179
4468
|
const virtualPath = input.filePath.replaceAll("\\", "/");
|
|
4180
4469
|
if (virtualPath.includes("\0")) throw new Error("HTML preview path contains invalid data.");
|
|
4181
4470
|
const mountPath = path10.resolve(input.mounts[input.rootVolume]);
|
|
4182
|
-
const root = await
|
|
4471
|
+
const root = await realpath3(mountPath);
|
|
4183
4472
|
let relativePath = virtualPath;
|
|
4184
4473
|
let explicitVolume;
|
|
4185
4474
|
for (const volume of ["agent", "space"]) {
|
|
@@ -4209,7 +4498,7 @@ var resolveWorkspaceFile = async (input) => {
|
|
|
4209
4498
|
}
|
|
4210
4499
|
const candidate = path10.resolve(root, ...segments);
|
|
4211
4500
|
assertContained(root, candidate);
|
|
4212
|
-
const canonicalFile = await
|
|
4501
|
+
const canonicalFile = await realpath3(candidate);
|
|
4213
4502
|
assertContained(root, canonicalFile);
|
|
4214
4503
|
const fileStat = await stat4(canonicalFile);
|
|
4215
4504
|
if (!fileStat.isFile()) throw new Error("HTML preview path must identify a file.");
|
|
@@ -4271,10 +4560,10 @@ var serveStaticFile = async (folder, method, requestUrl) => {
|
|
|
4271
4560
|
try {
|
|
4272
4561
|
const candidate = path10.resolve(folder, ...segments);
|
|
4273
4562
|
assertContained(folder, candidate);
|
|
4274
|
-
let canonicalFile = await
|
|
4563
|
+
let canonicalFile = await realpath3(candidate);
|
|
4275
4564
|
assertContained(folder, canonicalFile);
|
|
4276
4565
|
if ((await stat4(canonicalFile)).isDirectory()) {
|
|
4277
|
-
canonicalFile = await
|
|
4566
|
+
canonicalFile = await realpath3(path10.join(canonicalFile, "index.html"));
|
|
4278
4567
|
assertContained(folder, canonicalFile);
|
|
4279
4568
|
}
|
|
4280
4569
|
if (!(await stat4(canonicalFile)).isFile()) throw new Error("Not a file");
|
|
@@ -4309,12 +4598,17 @@ import {
|
|
|
4309
4598
|
mkdir as mkdir9,
|
|
4310
4599
|
readdir as readdir6,
|
|
4311
4600
|
readFile as readFile9,
|
|
4312
|
-
realpath as
|
|
4601
|
+
realpath as realpath4,
|
|
4313
4602
|
rm as rm3,
|
|
4314
4603
|
symlink as symlink2,
|
|
4315
4604
|
writeFile as writeFile4
|
|
4316
4605
|
} from "fs/promises";
|
|
4317
4606
|
import path11 from "path";
|
|
4607
|
+
|
|
4608
|
+
// src/directory-link.ts
|
|
4609
|
+
var directoryLinkType = (platform4 = process.platform) => platform4 === "win32" ? "junction" : "dir";
|
|
4610
|
+
|
|
4611
|
+
// src/workspace-volume-lifecycle.ts
|
|
4318
4612
|
var MOUNT_DIRECTORY = "mount";
|
|
4319
4613
|
var ARCHIVE_MARKER = "archived.json";
|
|
4320
4614
|
var AGENT_CONTEXT_TEMPLATE_FILES = [
|
|
@@ -4447,7 +4741,7 @@ var WorkspaceVolumeLifecycle = class {
|
|
|
4447
4741
|
]) {
|
|
4448
4742
|
await mkdir9(volume, { recursive: true, mode: 448 });
|
|
4449
4743
|
const info = await lstat2(volume);
|
|
4450
|
-
if (info.isSymbolicLink() || !info.isDirectory() || await
|
|
4744
|
+
if (info.isSymbolicLink() || !info.isDirectory() || await realpath4(volume) !== volume) {
|
|
4451
4745
|
throw new Error("Workspace volume must be a canonical real directory");
|
|
4452
4746
|
}
|
|
4453
4747
|
}
|
|
@@ -4460,17 +4754,17 @@ var isMissingFile = (error) => error instanceof Error && "code" in error && erro
|
|
|
4460
4754
|
var ensureVolumeLink = async (target, linkPath) => {
|
|
4461
4755
|
try {
|
|
4462
4756
|
const info = await lstat2(linkPath);
|
|
4463
|
-
if (!info.isSymbolicLink() || await
|
|
4757
|
+
if (!info.isSymbolicLink() || await realpath4(linkPath) !== target) {
|
|
4464
4758
|
throw new Error("Workspace mount link does not match its canonical volume");
|
|
4465
4759
|
}
|
|
4466
4760
|
} catch (error) {
|
|
4467
4761
|
if (!isMissingFile(error)) throw error;
|
|
4468
4762
|
try {
|
|
4469
|
-
await symlink2(target, linkPath,
|
|
4763
|
+
await symlink2(target, linkPath, directoryLinkType());
|
|
4470
4764
|
} catch (createError) {
|
|
4471
4765
|
if (!isAlreadyExists3(createError)) throw createError;
|
|
4472
4766
|
const info = await lstat2(linkPath);
|
|
4473
|
-
if (!info.isSymbolicLink() || await
|
|
4767
|
+
if (!info.isSymbolicLink() || await realpath4(linkPath) !== target) {
|
|
4474
4768
|
throw new Error("Workspace mount link raced with an invalid target");
|
|
4475
4769
|
}
|
|
4476
4770
|
}
|
|
@@ -4481,6 +4775,7 @@ var isAlreadyExists3 = (error) => error instanceof Error && "code" in error && e
|
|
|
4481
4775
|
// src/relay.ts
|
|
4482
4776
|
var RELAY_PING_INTERVAL_MS = 2e4;
|
|
4483
4777
|
var RELAY_PONG_TIMEOUT_MS = 1e4;
|
|
4778
|
+
var RELAY_ACTIVE_REQUEST_PONG_TIMEOUT_LIMIT = 2;
|
|
4484
4779
|
var RELAY_HANDSHAKE_TIMEOUT_MS = 1e4;
|
|
4485
4780
|
var RELAY_RECONNECT_MAX_MS = 3e4;
|
|
4486
4781
|
var RELAY_RECONNECT_JITTER = 0.2;
|
|
@@ -4586,6 +4881,7 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
|
|
|
4586
4881
|
socket = ws;
|
|
4587
4882
|
const inflight = /* @__PURE__ */ new Map();
|
|
4588
4883
|
currentInflight = inflight;
|
|
4884
|
+
let consecutivePongTimeouts = 0;
|
|
4589
4885
|
let detached = false;
|
|
4590
4886
|
const detach = () => {
|
|
4591
4887
|
if (detached) return;
|
|
@@ -4598,6 +4894,7 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
|
|
|
4598
4894
|
void writeLog("info", `relay connected ${relayUrl}`);
|
|
4599
4895
|
hadOpenConnection = true;
|
|
4600
4896
|
reconnectDelayMs = RELAY_RECONNECT_INTERVAL_MS;
|
|
4897
|
+
consecutivePongTimeouts = 0;
|
|
4601
4898
|
pingTimer = setInterval(() => {
|
|
4602
4899
|
if (ws.readyState !== WebSocket2.OPEN) return;
|
|
4603
4900
|
try {
|
|
@@ -4609,18 +4906,33 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
|
|
|
4609
4906
|
}
|
|
4610
4907
|
if (pongTimer) clearTimeout(pongTimer);
|
|
4611
4908
|
pongTimer = setTimeout(() => {
|
|
4909
|
+
pongTimer = null;
|
|
4910
|
+
consecutivePongTimeouts += 1;
|
|
4911
|
+
if (inflight.size > 0 && consecutivePongTimeouts < RELAY_ACTIVE_REQUEST_PONG_TIMEOUT_LIMIT) {
|
|
4912
|
+
void writeLog(
|
|
4913
|
+
"warn",
|
|
4914
|
+
`relay pong timeout deferred while ${inflight.size} request(s) are active`
|
|
4915
|
+
);
|
|
4916
|
+
return;
|
|
4917
|
+
}
|
|
4612
4918
|
void writeLog("warn", "relay pong timeout, terminating");
|
|
4613
4919
|
ws.terminate();
|
|
4614
4920
|
}, RELAY_PONG_TIMEOUT_MS);
|
|
4615
4921
|
}, RELAY_PING_INTERVAL_MS);
|
|
4616
4922
|
});
|
|
4617
4923
|
ws.on("pong", () => {
|
|
4924
|
+
consecutivePongTimeouts = 0;
|
|
4618
4925
|
if (pongTimer) {
|
|
4619
4926
|
clearTimeout(pongTimer);
|
|
4620
4927
|
pongTimer = null;
|
|
4621
4928
|
}
|
|
4622
4929
|
});
|
|
4623
4930
|
ws.on("message", (raw) => {
|
|
4931
|
+
consecutivePongTimeouts = 0;
|
|
4932
|
+
if (pongTimer) {
|
|
4933
|
+
clearTimeout(pongTimer);
|
|
4934
|
+
pongTimer = null;
|
|
4935
|
+
}
|
|
4624
4936
|
void handleRelayMessage(
|
|
4625
4937
|
ws,
|
|
4626
4938
|
bunnyDaemon,
|
|
@@ -4796,7 +5108,7 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
|
|
|
4796
5108
|
});
|
|
4797
5109
|
executionIdentity = message.executionIdentity;
|
|
4798
5110
|
} catch (error) {
|
|
4799
|
-
sendRelayError(socket, message.requestId, error);
|
|
5111
|
+
await sendRelayError(socket, message.requestId, error).catch(() => void 0);
|
|
4800
5112
|
return;
|
|
4801
5113
|
}
|
|
4802
5114
|
}
|
|
@@ -4882,7 +5194,7 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
|
|
|
4882
5194
|
durationMs: Date.now() - codingRunStartedAt
|
|
4883
5195
|
});
|
|
4884
5196
|
}
|
|
4885
|
-
sendRelayMessage(socket, {
|
|
5197
|
+
await sendRelayMessage(socket, {
|
|
4886
5198
|
type: "start",
|
|
4887
5199
|
requestId: message.requestId,
|
|
4888
5200
|
status: response.status,
|
|
@@ -4902,7 +5214,7 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
|
|
|
4902
5214
|
while (true) {
|
|
4903
5215
|
const { value, done } = await reader.read();
|
|
4904
5216
|
if (done) break;
|
|
4905
|
-
sendRelayMessage(socket, {
|
|
5217
|
+
await sendRelayMessage(socket, {
|
|
4906
5218
|
type: "chunk",
|
|
4907
5219
|
requestId: message.requestId,
|
|
4908
5220
|
data: binaryResponse ? Buffer.from(value).toString("base64") : decoder?.decode(value, { stream: true }) ?? "",
|
|
@@ -4911,7 +5223,7 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
|
|
|
4911
5223
|
}
|
|
4912
5224
|
const tail = decoder?.decode() ?? "";
|
|
4913
5225
|
if (tail) {
|
|
4914
|
-
sendRelayMessage(socket, {
|
|
5226
|
+
await sendRelayMessage(socket, {
|
|
4915
5227
|
type: "chunk",
|
|
4916
5228
|
requestId: message.requestId,
|
|
4917
5229
|
data: tail
|
|
@@ -4922,7 +5234,7 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
|
|
|
4922
5234
|
reader.releaseLock();
|
|
4923
5235
|
}
|
|
4924
5236
|
}
|
|
4925
|
-
sendRelayMessage(socket, {
|
|
5237
|
+
await sendRelayMessage(socket, {
|
|
4926
5238
|
type: "end",
|
|
4927
5239
|
requestId: message.requestId
|
|
4928
5240
|
});
|
|
@@ -4934,14 +5246,16 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
|
|
|
4934
5246
|
});
|
|
4935
5247
|
}
|
|
4936
5248
|
} catch (error) {
|
|
5249
|
+
controller.abort();
|
|
4937
5250
|
if (codingRunTrace && codingRunStartedAt !== void 0) {
|
|
4938
5251
|
await writeLog("error", "relay request coding run error", {
|
|
4939
5252
|
...codingRunTrace,
|
|
4940
5253
|
status: codingRunStatus,
|
|
4941
|
-
durationMs: Date.now() - codingRunStartedAt
|
|
5254
|
+
durationMs: Date.now() - codingRunStartedAt,
|
|
5255
|
+
error: formatConnectorRelayError(error)
|
|
4942
5256
|
});
|
|
4943
5257
|
}
|
|
4944
|
-
sendRelayError(socket, message.requestId, error);
|
|
5258
|
+
await sendRelayError(socket, message.requestId, error).catch(() => void 0);
|
|
4945
5259
|
} finally {
|
|
4946
5260
|
inflight.delete(message.requestId);
|
|
4947
5261
|
}
|
|
@@ -4995,17 +5309,20 @@ var normalizeWorkspaceRequest = (request, mounts) => {
|
|
|
4995
5309
|
if (!mount || relative === void 0) {
|
|
4996
5310
|
throw new ConnectorRelayProtocolError("WORKSPACE_MISMATCH");
|
|
4997
5311
|
}
|
|
4998
|
-
const
|
|
5312
|
+
const systemEnv = body.systemEnv && typeof body.systemEnv === "object" && !Array.isArray(body.systemEnv) ? body.systemEnv : {};
|
|
5313
|
+
const workspaceEnv = {
|
|
5314
|
+
BUDA_AGENT_ROOT: mounts.agent,
|
|
5315
|
+
BUDA_SPACE_ROOT: mounts.space,
|
|
5316
|
+
BUDA_SYSTEM_ROOT: mounts.system
|
|
5317
|
+
};
|
|
4999
5318
|
return {
|
|
5000
5319
|
...request,
|
|
5001
5320
|
body: {
|
|
5002
5321
|
...body,
|
|
5003
5322
|
cwd: path12.join(mount, relative),
|
|
5004
|
-
|
|
5005
|
-
...
|
|
5006
|
-
|
|
5007
|
-
BUDA_SPACE_ROOT: mounts.space,
|
|
5008
|
-
BUDA_SYSTEM_ROOT: mounts.system
|
|
5323
|
+
systemEnv: {
|
|
5324
|
+
...systemEnv,
|
|
5325
|
+
...workspaceEnv
|
|
5009
5326
|
}
|
|
5010
5327
|
}
|
|
5011
5328
|
};
|
|
@@ -5037,17 +5354,24 @@ var normalizeConnectorFilesystemRequest = (request, mounts) => {
|
|
|
5037
5354
|
}
|
|
5038
5355
|
return request;
|
|
5039
5356
|
};
|
|
5040
|
-
function sendRelayError(socket, requestId, error) {
|
|
5041
|
-
sendRelayMessage(socket, {
|
|
5357
|
+
async function sendRelayError(socket, requestId, error) {
|
|
5358
|
+
await sendRelayMessage(socket, {
|
|
5042
5359
|
type: "error",
|
|
5043
5360
|
requestId,
|
|
5044
5361
|
error: error instanceof Error ? error.message : String(error),
|
|
5045
5362
|
...error instanceof ConnectorRelayProtocolError ? { code: error.code } : {}
|
|
5046
5363
|
});
|
|
5047
5364
|
}
|
|
5048
|
-
function sendRelayMessage(socket, message) {
|
|
5049
|
-
if (socket.readyState !== WebSocket2.OPEN)
|
|
5050
|
-
|
|
5365
|
+
async function sendRelayMessage(socket, message) {
|
|
5366
|
+
if (socket.readyState !== WebSocket2.OPEN) {
|
|
5367
|
+
throw new Error(`Connector relay socket closed before ${message.type} could be sent.`);
|
|
5368
|
+
}
|
|
5369
|
+
await new Promise((resolve, reject) => {
|
|
5370
|
+
socket.send(JSON.stringify(message), (error) => {
|
|
5371
|
+
if (error) reject(error);
|
|
5372
|
+
else resolve();
|
|
5373
|
+
});
|
|
5374
|
+
});
|
|
5051
5375
|
}
|
|
5052
5376
|
function headersToRecord(headers) {
|
|
5053
5377
|
const result = {};
|
|
@@ -5106,7 +5430,7 @@ async function ensureSymlink(target, linkPath) {
|
|
|
5106
5430
|
} catch {
|
|
5107
5431
|
}
|
|
5108
5432
|
await mkdir10(path13.dirname(linkPath), { recursive: true });
|
|
5109
|
-
await symlink3(target, linkPath,
|
|
5433
|
+
await symlink3(target, linkPath, directoryLinkType());
|
|
5110
5434
|
}
|
|
5111
5435
|
|
|
5112
5436
|
// src/workspace-registry.ts
|
|
@@ -5117,7 +5441,7 @@ import {
|
|
|
5117
5441
|
mkdir as mkdir11,
|
|
5118
5442
|
open as open3,
|
|
5119
5443
|
readFile as readFile10,
|
|
5120
|
-
realpath as
|
|
5444
|
+
realpath as realpath5,
|
|
5121
5445
|
rename as rename6,
|
|
5122
5446
|
statfs,
|
|
5123
5447
|
unlink as unlink7
|
|
@@ -5281,7 +5605,7 @@ var reconcileWorkspace = async (workspace, storageRoot) => {
|
|
|
5281
5605
|
for (const volume of volumes) {
|
|
5282
5606
|
assertContained2(storageRoot, volume);
|
|
5283
5607
|
await ensurePrivateDirectory2(volume);
|
|
5284
|
-
const canonical = await
|
|
5608
|
+
const canonical = await realpath5(volume);
|
|
5285
5609
|
if (canonical !== volume) throw corrupt2("Workspace path resolves through a symbolic link");
|
|
5286
5610
|
}
|
|
5287
5611
|
};
|
|
@@ -5327,7 +5651,7 @@ var canonicalDirectory = async (directory, label) => {
|
|
|
5327
5651
|
if (info.isSymbolicLink() || !info.isDirectory()) {
|
|
5328
5652
|
throw new WorkspaceRegistryError("STORAGE_UNSAFE", `${label} must be a real directory`);
|
|
5329
5653
|
}
|
|
5330
|
-
return
|
|
5654
|
+
return realpath5(directory);
|
|
5331
5655
|
};
|
|
5332
5656
|
var rejectUnsafeFilesystem = async (storageRoot) => {
|
|
5333
5657
|
const stats = await statfs(storageRoot, { bigint: true });
|
|
@@ -5563,7 +5887,30 @@ import { mkdir as mkdir12 } from "fs/promises";
|
|
|
5563
5887
|
import path15 from "path";
|
|
5564
5888
|
var RECOVERED_DAEMON_STOP_TIMEOUT_MS = 1e4;
|
|
5565
5889
|
var RECOVERED_DAEMON_STOP_POLL_MS = 100;
|
|
5890
|
+
var SHUTDOWN_FORCE_EXIT_MS = 1e4;
|
|
5891
|
+
var SHUTDOWN_STEP_TIMEOUT_MS = 2e3;
|
|
5566
5892
|
var wait3 = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
5893
|
+
var runShutdownStep = async (label, operation) => {
|
|
5894
|
+
let timeout;
|
|
5895
|
+
try {
|
|
5896
|
+
await Promise.race([
|
|
5897
|
+
Promise.resolve().then(operation),
|
|
5898
|
+
new Promise((_, reject) => {
|
|
5899
|
+
timeout = setTimeout(
|
|
5900
|
+
() => reject(new Error(`${label} timed out after ${SHUTDOWN_STEP_TIMEOUT_MS}ms`)),
|
|
5901
|
+
SHUTDOWN_STEP_TIMEOUT_MS
|
|
5902
|
+
);
|
|
5903
|
+
})
|
|
5904
|
+
]);
|
|
5905
|
+
} catch (error) {
|
|
5906
|
+
await writeLog(
|
|
5907
|
+
"warn",
|
|
5908
|
+
`${label} failed during shutdown: ${error instanceof Error ? error.message : String(error)}`
|
|
5909
|
+
).catch(() => void 0);
|
|
5910
|
+
} finally {
|
|
5911
|
+
if (timeout) clearTimeout(timeout);
|
|
5912
|
+
}
|
|
5913
|
+
};
|
|
5567
5914
|
async function stopRecoveredIdleDaemon(config, configRoot) {
|
|
5568
5915
|
const stopStatus = await requestDaemonControl(config, "stop-if-idle", configRoot).catch(
|
|
5569
5916
|
() => null
|
|
@@ -5683,18 +6030,30 @@ async function runConnectorDaemon(launchConfig, options = {}) {
|
|
|
5683
6030
|
const shutdown = async (shutdownOptions = {}) => {
|
|
5684
6031
|
if (shuttingDown) return;
|
|
5685
6032
|
shuttingDown = true;
|
|
6033
|
+
const forceExit = options.exitOnShutdown === false ? void 0 : setTimeout(() => process.exit(0), SHUTDOWN_FORCE_EXIT_MS);
|
|
5686
6034
|
if (interval) clearInterval(interval);
|
|
5687
6035
|
relay.stop();
|
|
5688
6036
|
try {
|
|
5689
|
-
await
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
6037
|
+
await Promise.all([
|
|
6038
|
+
runShutdownStep("browser session cleanup", closeAllBrowserSessions),
|
|
6039
|
+
...shutdownOptions.unregister === false ? [] : [
|
|
6040
|
+
runShutdownStep(
|
|
6041
|
+
"connector unregister",
|
|
6042
|
+
() => unregisterConnector(config, launchConfig)
|
|
6043
|
+
)
|
|
6044
|
+
],
|
|
6045
|
+
runShutdownStep(
|
|
6046
|
+
"embedded Bunny daemon shutdown",
|
|
6047
|
+
() => stopEmbeddedBunnyDaemon(activeBunnyDaemon)
|
|
6048
|
+
)
|
|
6049
|
+
]);
|
|
5694
6050
|
} finally {
|
|
5695
|
-
await
|
|
5696
|
-
|
|
5697
|
-
|
|
6051
|
+
await Promise.all([
|
|
6052
|
+
runShutdownStep("control channel shutdown", async () => control?.close()),
|
|
6053
|
+
runShutdownStep("daemon lock release", () => daemonLock.release()),
|
|
6054
|
+
runShutdownStep("console capture restore", async () => restoreConsole?.())
|
|
6055
|
+
]);
|
|
6056
|
+
if (forceExit) clearTimeout(forceExit);
|
|
5698
6057
|
if (options.exitOnShutdown !== false) process.exit(0);
|
|
5699
6058
|
}
|
|
5700
6059
|
};
|
|
@@ -5709,8 +6068,11 @@ async function runConnectorDaemon(launchConfig, options = {}) {
|
|
|
5709
6068
|
});
|
|
5710
6069
|
}).catch((error) => {
|
|
5711
6070
|
heartbeatHealthy = false;
|
|
5712
|
-
if (error instanceof ConnectorDeletedError) {
|
|
5713
|
-
void writeLog(
|
|
6071
|
+
if (error instanceof ConnectorDeletedError || error instanceof ConnectorUnauthorizedError) {
|
|
6072
|
+
void writeLog(
|
|
6073
|
+
"warn",
|
|
6074
|
+
error instanceof ConnectorDeletedError ? "connector deleted remotely; stopping daemon" : "connector authentication rejected remotely; stopping daemon"
|
|
6075
|
+
);
|
|
5714
6076
|
void shutdown({ unregister: false });
|
|
5715
6077
|
return;
|
|
5716
6078
|
}
|
|
@@ -5734,6 +6096,10 @@ async function runConnectorDaemon(launchConfig, options = {}) {
|
|
|
5734
6096
|
process.once("SIGINT", () => void shutdown());
|
|
5735
6097
|
process.once("SIGTERM", () => void shutdown());
|
|
5736
6098
|
} catch (error) {
|
|
6099
|
+
await writeLog(
|
|
6100
|
+
"error",
|
|
6101
|
+
`daemon startup failed: ${error instanceof Error ? error.stack || error.message : String(error)}`
|
|
6102
|
+
).catch(() => void 0);
|
|
5737
6103
|
if (bunnyDaemon) await stopEmbeddedBunnyDaemon(bunnyDaemon);
|
|
5738
6104
|
await daemonLock.release();
|
|
5739
6105
|
await restoreConsole?.();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buda-ai/connector",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.33",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"dev": "tsx src/cli.ts dev",
|
|
19
19
|
"daemon": "tsx src/cli.ts daemon",
|
|
20
20
|
"new": "tsx src/cli.ts new",
|
|
21
|
+
"prepack": "tsup --silent",
|
|
21
22
|
"start": "node dist/cli.js daemon",
|
|
22
23
|
"build": "tsup",
|
|
23
24
|
"test": "vitest run",
|
|
@@ -26,7 +27,7 @@
|
|
|
26
27
|
"lint:err": "biome format . --write && biome check . --write --diagnostic-level=error"
|
|
27
28
|
},
|
|
28
29
|
"dependencies": {
|
|
29
|
-
"@bunny-agent/daemon": "0.9.
|
|
30
|
+
"@bunny-agent/daemon": "0.9.58",
|
|
30
31
|
"@clack/prompts": "^1.0.0",
|
|
31
32
|
"playwright-core": "^1.52.0",
|
|
32
33
|
"ws": "^8.21.0"
|