negotium 0.1.41 → 0.1.42
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/dist/agent-helpers.js +171 -141
- package/dist/agent-helpers.js.map +6 -6
- package/dist/browser-runtime.js +149 -101
- package/dist/browser-runtime.js.map +7 -7
- package/dist/hosted-agent.js +2 -2
- package/dist/hosted-agent.js.map +2 -2
- package/dist/main.js +237 -207
- package/dist/main.js.map +6 -6
- package/dist/mcp-factories.js +201 -171
- package/dist/mcp-factories.js.map +6 -6
- package/dist/runtime/scripts/mcp-patchright-http.mjs +3 -0
- package/dist/runtime/src/platform/playwright/browser-processes.ts +34 -72
- package/dist/runtime/src/platform/playwright/manager-utils.ts +24 -0
- package/dist/runtime/src/platform/playwright/manager.ts +135 -58
- package/dist/runtime/src/version.ts +1 -1
- package/dist/types/packages/core/src/platform/playwright/browser-processes.d.ts +2 -2
- package/dist/types/packages/core/src/platform/playwright/manager-utils.d.ts +6 -0
- package/dist/types/packages/core/src/platform/playwright/manager.d.ts +6 -1
- package/dist/types/packages/core/src/version.d.ts +1 -1
- package/package.json +1 -1
package/dist/agent-helpers.js
CHANGED
|
@@ -2571,7 +2571,7 @@ var init_claude_provider = __esm(async () => {
|
|
|
2571
2571
|
});
|
|
2572
2572
|
|
|
2573
2573
|
// ../../packages/core/src/version.ts
|
|
2574
|
-
var NEGOTIUM_VERSION = "0.1.
|
|
2574
|
+
var NEGOTIUM_VERSION = "0.1.42";
|
|
2575
2575
|
|
|
2576
2576
|
// ../../packages/core/src/agents/codex-native-multi-agent.ts
|
|
2577
2577
|
import { spawn as spawn3 } from "child_process";
|
|
@@ -9628,7 +9628,15 @@ var init_self_schedules = __esm(async () => {
|
|
|
9628
9628
|
});
|
|
9629
9629
|
|
|
9630
9630
|
// ../../packages/core/src/platform/playwright/manager-utils.ts
|
|
9631
|
-
|
|
9631
|
+
function isLiveOwnedChildProcess(current2, expected) {
|
|
9632
|
+
return current2?.process === expected && expected.exitCode === null && expected.signalCode === null && !expected.killed;
|
|
9633
|
+
}
|
|
9634
|
+
function matchesSpawnedBrowserHealth(health, expectedSpawnNonce) {
|
|
9635
|
+
if (!health || typeof health !== "object")
|
|
9636
|
+
return false;
|
|
9637
|
+
const candidate = health;
|
|
9638
|
+
return candidate.ok === true && candidate.name === "negotium-browser-gateway" && candidate.spawnNonce === expectedSpawnNonce;
|
|
9639
|
+
}
|
|
9632
9640
|
function selectIdleEvictionKey(candidates, pinnedKeys, busyKeys, now, maxIdleMs) {
|
|
9633
9641
|
const pinned = new Set(pinnedKeys);
|
|
9634
9642
|
const busy = new Set(busyKeys);
|
|
@@ -9644,22 +9652,10 @@ function selectIdleEvictionKey(candidates, pinnedKeys, busyKeys, now, maxIdleMs)
|
|
|
9644
9652
|
}
|
|
9645
9653
|
return oldest?.key ?? null;
|
|
9646
9654
|
}
|
|
9647
|
-
function selectReusablePort(minPort, maxPort, reservedPorts, isOccupied) {
|
|
9648
|
-
for (let port = minPort;port <= maxPort; port++) {
|
|
9649
|
-
if (reservedPorts.has(port) || isOccupied(port))
|
|
9650
|
-
continue;
|
|
9651
|
-
return port;
|
|
9652
|
-
}
|
|
9653
|
-
return null;
|
|
9654
|
-
}
|
|
9655
9655
|
function extractUserDataDirArg(cmdline) {
|
|
9656
9656
|
const match = cmdline.match(/--user-data-dir(?:\s+|=)(\S+)/);
|
|
9657
9657
|
return match ? match[1] : null;
|
|
9658
9658
|
}
|
|
9659
|
-
function browserProcessMatchesExpectedProfile(cmdline, expectedUserDataDir) {
|
|
9660
|
-
const actualUserDataDir = extractUserDataDirArg(cmdline);
|
|
9661
|
-
return actualUserDataDir !== null && resolve9(actualUserDataDir) === resolve9(expectedUserDataDir);
|
|
9662
|
-
}
|
|
9663
9659
|
function waitForChildProcessExit(proc, timeoutMs) {
|
|
9664
9660
|
if (proc.exitCode !== null || proc.signalCode !== null)
|
|
9665
9661
|
return Promise.resolve(true);
|
|
@@ -9683,73 +9679,47 @@ function waitForChildProcessExit(proc, timeoutMs) {
|
|
|
9683
9679
|
finish(true);
|
|
9684
9680
|
});
|
|
9685
9681
|
}
|
|
9686
|
-
function waitForChildProcessSpawnError(proc) {
|
|
9687
|
-
return new Promise((_resolve, reject) => {
|
|
9688
|
-
proc.once("error", reject);
|
|
9689
|
-
});
|
|
9690
|
-
}
|
|
9691
9682
|
var init_manager_utils = () => {};
|
|
9692
9683
|
|
|
9693
9684
|
// ../../packages/core/src/platform/playwright/browser-processes.ts
|
|
9694
9685
|
import { execFileSync as execFileSync6 } from "child_process";
|
|
9695
9686
|
import { readdirSync as readdirSync3, unlinkSync as unlinkSync10 } from "fs";
|
|
9696
|
-
import {
|
|
9687
|
+
import { createServer } from "net";
|
|
9688
|
+
import { resolve as resolve9, sep } from "path";
|
|
9697
9689
|
function isPortInUse(port) {
|
|
9698
|
-
|
|
9699
|
-
|
|
9700
|
-
|
|
9701
|
-
|
|
9702
|
-
|
|
9703
|
-
|
|
9690
|
+
return new Promise((resolveProbe) => {
|
|
9691
|
+
const server = createServer();
|
|
9692
|
+
let settled = false;
|
|
9693
|
+
const finish = (occupied) => {
|
|
9694
|
+
if (settled)
|
|
9695
|
+
return;
|
|
9696
|
+
settled = true;
|
|
9697
|
+
server.removeAllListeners();
|
|
9698
|
+
resolveProbe(occupied);
|
|
9699
|
+
};
|
|
9700
|
+
server.unref();
|
|
9701
|
+
server.once("error", () => finish(true));
|
|
9702
|
+
server.listen({ host: "127.0.0.1", port, exclusive: true }, () => {
|
|
9703
|
+
server.close((error) => finish(error !== undefined));
|
|
9704
|
+
});
|
|
9705
|
+
});
|
|
9704
9706
|
}
|
|
9705
|
-
async function
|
|
9706
|
-
|
|
9707
|
-
|
|
9708
|
-
|
|
9709
|
-
|
|
9710
|
-
|
|
9711
|
-
|
|
9712
|
-
|
|
9713
|
-
const cmdline = execFileSync6("ps", ["-p", pid, "-o", "command="], { stdio: "pipe" }).toString().trim();
|
|
9714
|
-
if (!cmdline.includes("mcp-patchright-http.mjs")) {
|
|
9715
|
-
logger.warn({ port, pid, cmdline: cmdline.slice(0, 80) }, "Port occupied by non-browser-MCP process, skipping");
|
|
9716
|
-
continue;
|
|
9717
|
-
}
|
|
9718
|
-
if (expectedUserDataDir) {
|
|
9719
|
-
const otherDataDir = extractUserDataDirArg(cmdline);
|
|
9720
|
-
if (!browserProcessMatchesExpectedProfile(cmdline, expectedUserDataDir)) {
|
|
9721
|
-
logger.warn({
|
|
9722
|
-
port,
|
|
9723
|
-
pid,
|
|
9724
|
-
otherDataDir,
|
|
9725
|
-
expectedUserDataDir
|
|
9726
|
-
}, "Port occupied by another topic's playwright-mcp, skipping");
|
|
9727
|
-
continue;
|
|
9728
|
-
}
|
|
9729
|
-
}
|
|
9730
|
-
const pidNum = parseInt(pid, 10);
|
|
9731
|
-
if (!Number.isNaN(pidNum)) {
|
|
9732
|
-
killProcessTreeChildren(pidNum);
|
|
9733
|
-
process.kill(pidNum, "SIGKILL");
|
|
9734
|
-
}
|
|
9735
|
-
logger.info({ pid, port }, "Killed zombie mcp-patchright");
|
|
9736
|
-
} catch (e) {
|
|
9737
|
-
logger.warn({ err: e, port }, "Failed to inspect process occupying port");
|
|
9738
|
-
}
|
|
9707
|
+
async function reserveAvailableLoopbackPort(minPort, maxPort, reservedPorts, probeOccupied = isPortInUse) {
|
|
9708
|
+
for (let port = minPort;port <= maxPort; port++) {
|
|
9709
|
+
if (reservedPorts.has(port))
|
|
9710
|
+
continue;
|
|
9711
|
+
reservedPorts.add(port);
|
|
9712
|
+
if (await probeOccupied(port)) {
|
|
9713
|
+
reservedPorts.delete(port);
|
|
9714
|
+
continue;
|
|
9739
9715
|
}
|
|
9740
|
-
|
|
9741
|
-
logger.warn({ err: e, port }, "Failed to check processes on port");
|
|
9742
|
-
}
|
|
9743
|
-
const start = Date.now();
|
|
9744
|
-
while (Date.now() - start < 3000) {
|
|
9745
|
-
if (!isPortInUse(port))
|
|
9746
|
-
return;
|
|
9747
|
-
await delay(200);
|
|
9716
|
+
return port;
|
|
9748
9717
|
}
|
|
9718
|
+
return null;
|
|
9749
9719
|
}
|
|
9750
9720
|
function killBrowserProcsForUserDataDir(userDataDir) {
|
|
9751
|
-
const target =
|
|
9752
|
-
const profileRoot =
|
|
9721
|
+
const target = resolve9(userDataDir);
|
|
9722
|
+
const profileRoot = resolve9(BROWSER_PROFILES_DIR);
|
|
9753
9723
|
if (target !== profileRoot && !target.startsWith(`${profileRoot}${sep}`))
|
|
9754
9724
|
return;
|
|
9755
9725
|
let pids;
|
|
@@ -9770,7 +9740,7 @@ function killBrowserProcsForUserDataDir(userDataDir) {
|
|
|
9770
9740
|
stdio: "pipe"
|
|
9771
9741
|
}).toString().trim();
|
|
9772
9742
|
const argDir = extractUserDataDirArg(cmdline);
|
|
9773
|
-
if (!argDir ||
|
|
9743
|
+
if (!argDir || resolve9(argDir) !== target)
|
|
9774
9744
|
continue;
|
|
9775
9745
|
killProcessTreeChildren(pidNum);
|
|
9776
9746
|
process.kill(pidNum, "SIGKILL");
|
|
@@ -9781,13 +9751,13 @@ function killBrowserProcsForUserDataDir(userDataDir) {
|
|
|
9781
9751
|
}
|
|
9782
9752
|
}
|
|
9783
9753
|
function selectOrphanBrowserPids(procs, liveUserDataDirs, profileRoot, selfPid) {
|
|
9784
|
-
const root =
|
|
9785
|
-
const live = new Set([...liveUserDataDirs].map((d) =>
|
|
9754
|
+
const root = resolve9(profileRoot);
|
|
9755
|
+
const live = new Set([...liveUserDataDirs].map((d) => resolve9(d)));
|
|
9786
9756
|
const out = [];
|
|
9787
9757
|
for (const { pid, userDataDir } of procs) {
|
|
9788
9758
|
if (pid === selfPid || !userDataDir)
|
|
9789
9759
|
continue;
|
|
9790
|
-
const dir =
|
|
9760
|
+
const dir = resolve9(userDataDir);
|
|
9791
9761
|
if (dir !== root && !dir.startsWith(`${root}${sep}`))
|
|
9792
9762
|
continue;
|
|
9793
9763
|
if (live.has(dir))
|
|
@@ -9803,7 +9773,7 @@ function reapOrphanBrowsers(liveUserDataDirs) {
|
|
|
9803
9773
|
const daemonLease = getRuntimeProcessLease("node-daemon", Date.now(), Number.POSITIVE_INFINITY);
|
|
9804
9774
|
if (!isBrowserJanitorOwner(daemonLease?.pid ?? null, process.pid))
|
|
9805
9775
|
return;
|
|
9806
|
-
const profileRoot =
|
|
9776
|
+
const profileRoot = resolve9(BROWSER_PROFILES_DIR);
|
|
9807
9777
|
let pids;
|
|
9808
9778
|
try {
|
|
9809
9779
|
pids = execFileSync6("pgrep", ["-f", "--", profileRoot], { stdio: "pipe" }).toString().trim();
|
|
@@ -9836,26 +9806,13 @@ function reapOrphanBrowsers(liveUserDataDirs) {
|
|
|
9836
9806
|
}
|
|
9837
9807
|
}
|
|
9838
9808
|
}
|
|
9839
|
-
async function isHealthy(port) {
|
|
9840
|
-
for (const path of ["/health", "/sse?owner=__negotium_health__"]) {
|
|
9841
|
-
try {
|
|
9842
|
-
const res = await fetch(`http://127.0.0.1:${port}${path}`, {
|
|
9843
|
-
signal: AbortSignal.timeout(2000)
|
|
9844
|
-
});
|
|
9845
|
-
await res.body?.cancel();
|
|
9846
|
-
if (res.ok)
|
|
9847
|
-
return true;
|
|
9848
|
-
} catch {}
|
|
9849
|
-
}
|
|
9850
|
-
return false;
|
|
9851
|
-
}
|
|
9852
9809
|
function cleanSingletonFiles(userDataDir) {
|
|
9853
9810
|
try {
|
|
9854
9811
|
const files = readdirSync3(userDataDir);
|
|
9855
9812
|
for (const f of files) {
|
|
9856
9813
|
if (f.startsWith("Singleton")) {
|
|
9857
9814
|
try {
|
|
9858
|
-
unlinkSync10(
|
|
9815
|
+
unlinkSync10(resolve9(userDataDir, f));
|
|
9859
9816
|
logger.info({ file: f, userDataDir }, "Removed stale Singleton file");
|
|
9860
9817
|
} catch (e) {
|
|
9861
9818
|
logger.warn({ err: e, file: f }, "Failed to remove stale Chrome Singleton file");
|
|
@@ -9895,9 +9852,9 @@ var init_browser_processes = __esm(async () => {
|
|
|
9895
9852
|
|
|
9896
9853
|
// ../../packages/core/src/platform/playwright/headed-launch.ts
|
|
9897
9854
|
import { accessSync as accessSync2, constants as constants2 } from "fs";
|
|
9898
|
-
import { delimiter, isAbsolute as isAbsolute3, resolve as
|
|
9855
|
+
import { delimiter, isAbsolute as isAbsolute3, resolve as resolve10 } from "path";
|
|
9899
9856
|
function findExecutableOnPath(command, environment = process.env) {
|
|
9900
|
-
const candidates = isAbsolute3(command) ? [command] : (environment.PATH ?? "").split(delimiter).filter(Boolean).map((directory) =>
|
|
9857
|
+
const candidates = isAbsolute3(command) ? [command] : (environment.PATH ?? "").split(delimiter).filter(Boolean).map((directory) => resolve10(directory, command));
|
|
9901
9858
|
for (const candidate of candidates) {
|
|
9902
9859
|
try {
|
|
9903
9860
|
accessSync2(candidate, constants2.X_OK);
|
|
@@ -10058,7 +10015,7 @@ import {
|
|
|
10058
10015
|
unlinkSync as unlinkSync11,
|
|
10059
10016
|
writeFileSync as writeFileSync8
|
|
10060
10017
|
} from "fs";
|
|
10061
|
-
import { dirname as dirname11, join as join18, resolve as
|
|
10018
|
+
import { dirname as dirname11, join as join18, resolve as resolve11 } from "path";
|
|
10062
10019
|
function makeInstanceKey(userId, topic) {
|
|
10063
10020
|
return resolvePlaywrightTopicBinding(userId, topic).instanceKey;
|
|
10064
10021
|
}
|
|
@@ -10097,7 +10054,7 @@ function migrateLegacyTopicProfile(ownerId, topic) {
|
|
|
10097
10054
|
const current2 = getTopicBrowserProfile(topic);
|
|
10098
10055
|
if (current2 !== "default" || !hasBrowserProfileTopic(topic))
|
|
10099
10056
|
return current2;
|
|
10100
|
-
const legacyDir =
|
|
10057
|
+
const legacyDir = resolve11(BROWSER_PROFILES_DIR, sanitizeTopicName(topic));
|
|
10101
10058
|
if (!existsSync16(legacyDir))
|
|
10102
10059
|
return current2;
|
|
10103
10060
|
const profile = legacyBrowserProfileName(topic);
|
|
@@ -10248,29 +10205,19 @@ function evictIdleInstance() {
|
|
|
10248
10205
|
}
|
|
10249
10206
|
return null;
|
|
10250
10207
|
}
|
|
10251
|
-
async function allocatePort(
|
|
10252
|
-
|
|
10253
|
-
|
|
10254
|
-
|
|
10255
|
-
|
|
10256
|
-
|
|
10257
|
-
|
|
10258
|
-
|
|
10259
|
-
if (isPortInUse(port)) {
|
|
10260
|
-
usedPorts.delete(port);
|
|
10261
|
-
continue;
|
|
10262
|
-
}
|
|
10263
|
-
}
|
|
10208
|
+
async function allocatePort() {
|
|
10209
|
+
const port = await reserveAvailableLoopbackPort(managerHost.basePort, managerHost.maxPort, usedPorts, async (candidate) => {
|
|
10210
|
+
const occupied = await isPortInUse(candidate);
|
|
10211
|
+
if (occupied)
|
|
10212
|
+
logger.warn({ port: candidate }, "Port occupied by external process, skipping");
|
|
10213
|
+
return occupied;
|
|
10214
|
+
});
|
|
10215
|
+
if (port !== null)
|
|
10264
10216
|
return port;
|
|
10265
|
-
}
|
|
10266
10217
|
const evictedPort = evictIdleInstance();
|
|
10267
10218
|
if (evictedPort !== null) {
|
|
10268
10219
|
await waitForPortRelease(evictedPort);
|
|
10269
|
-
|
|
10270
|
-
if (reusablePort !== null) {
|
|
10271
|
-
usedPorts.add(reusablePort);
|
|
10272
|
-
return reusablePort;
|
|
10273
|
-
}
|
|
10220
|
+
return allocatePort();
|
|
10274
10221
|
}
|
|
10275
10222
|
throw new Error(`No available ports for Playwright MCP (${instances.size} active instances, range ${managerHost.basePort}-${managerHost.maxPort})`);
|
|
10276
10223
|
}
|
|
@@ -10282,7 +10229,7 @@ function ownerDirectory(ownerId) {
|
|
|
10282
10229
|
return `${sanitizeTopicName(ownerId).slice(0, 24)}_${digest}`;
|
|
10283
10230
|
}
|
|
10284
10231
|
function defaultProfileDir(ownerId, profile) {
|
|
10285
|
-
return
|
|
10232
|
+
return resolve11(BROWSER_PROFILES_DIR, "profiles", ownerDirectory(ownerId), profile);
|
|
10286
10233
|
}
|
|
10287
10234
|
function resolveUserDataDir(instanceKey) {
|
|
10288
10235
|
return managerHost.resolveInstanceDataDir(instanceKey);
|
|
@@ -10308,9 +10255,53 @@ function killInstance(instanceKey, opts) {
|
|
|
10308
10255
|
deletePortFile(instanceKey);
|
|
10309
10256
|
logger.info({ instanceKey, port: inst.port, keepPort: !!opts?.keepPort }, "Killed Playwright MCP (with cleanup)");
|
|
10310
10257
|
}
|
|
10258
|
+
function captureBoundedStderr(proc) {
|
|
10259
|
+
let tail = Buffer.alloc(0);
|
|
10260
|
+
proc.stderr?.on("data", (chunk) => {
|
|
10261
|
+
const next = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
10262
|
+
tail = Buffer.concat([tail, next]);
|
|
10263
|
+
if (tail.byteLength > PLAYWRIGHT_STARTUP_STDERR_LIMIT) {
|
|
10264
|
+
tail = tail.subarray(tail.byteLength - PLAYWRIGHT_STARTUP_STDERR_LIMIT);
|
|
10265
|
+
}
|
|
10266
|
+
});
|
|
10267
|
+
return () => tail.toString("utf8").trim();
|
|
10268
|
+
}
|
|
10269
|
+
function watchChildStartup(proc, stderrTail) {
|
|
10270
|
+
let stopped = false;
|
|
10271
|
+
let rejectFailure = () => {
|
|
10272
|
+
return;
|
|
10273
|
+
};
|
|
10274
|
+
const diagnostics = () => {
|
|
10275
|
+
const stderr = stderrTail();
|
|
10276
|
+
return stderr ? `
|
|
10277
|
+
stderr (last ${PLAYWRIGHT_STARTUP_STDERR_LIMIT} bytes):
|
|
10278
|
+
${stderr}` : "";
|
|
10279
|
+
};
|
|
10280
|
+
const onError = (error) => {
|
|
10281
|
+
rejectFailure(new Error(`Playwright MCP failed to spawn: ${error.message}${diagnostics()}`, {
|
|
10282
|
+
cause: error
|
|
10283
|
+
}));
|
|
10284
|
+
};
|
|
10285
|
+
const onExit = (code, signal) => {
|
|
10286
|
+
rejectFailure(new Error(`Playwright MCP exited during startup (code=${code ?? "null"}, signal=${signal ?? "null"})${diagnostics()}`));
|
|
10287
|
+
};
|
|
10288
|
+
const failure = new Promise((_resolve, reject) => {
|
|
10289
|
+
rejectFailure = reject;
|
|
10290
|
+
proc.once("error", onError);
|
|
10291
|
+
proc.once("exit", onExit);
|
|
10292
|
+
});
|
|
10293
|
+
const stop = () => {
|
|
10294
|
+
if (stopped)
|
|
10295
|
+
return;
|
|
10296
|
+
stopped = true;
|
|
10297
|
+
proc.off("error", onError);
|
|
10298
|
+
proc.off("exit", onExit);
|
|
10299
|
+
};
|
|
10300
|
+
return { failure, stop };
|
|
10301
|
+
}
|
|
10311
10302
|
async function spawnPlaywright(instanceKey, ownerId, reservedPort, browserBin = managerHost.browserBin, allowFallback = true) {
|
|
10312
10303
|
const userDataDir = resolveUserDataDir(instanceKey);
|
|
10313
|
-
const port = reservedPort ?? await allocatePort(
|
|
10304
|
+
const port = reservedPort ?? await allocatePort();
|
|
10314
10305
|
mkdirSync10(userDataDir, { recursive: true });
|
|
10315
10306
|
const mcpArgs = [
|
|
10316
10307
|
"--port",
|
|
@@ -10323,6 +10314,7 @@ async function spawnPlaywright(instanceKey, ownerId, reservedPort, browserBin =
|
|
|
10323
10314
|
];
|
|
10324
10315
|
const proxy = managerHost.resolveProxy();
|
|
10325
10316
|
const capability = randomBytes5(32).toString("hex");
|
|
10317
|
+
const spawnNonce = randomBytes5(32).toString("hex");
|
|
10326
10318
|
const childEnv = {
|
|
10327
10319
|
...managerHost.createChildEnvironment({
|
|
10328
10320
|
instanceKey,
|
|
@@ -10332,7 +10324,8 @@ async function spawnPlaywright(instanceKey, ownerId, reservedPort, browserBin =
|
|
|
10332
10324
|
browserRsBin: managerHost.browserRsBin,
|
|
10333
10325
|
environment: process.env
|
|
10334
10326
|
}),
|
|
10335
|
-
NEGOTIUM_BROWSER_CAPABILITY: capability
|
|
10327
|
+
NEGOTIUM_BROWSER_CAPABILITY: capability,
|
|
10328
|
+
NEGOTIUM_BROWSER_SPAWN_NONCE: spawnNonce
|
|
10336
10329
|
};
|
|
10337
10330
|
if (proxy) {
|
|
10338
10331
|
logger.info({ instanceKey, proxyServer: proxy.server }, "Browser egress proxy enabled");
|
|
@@ -10349,7 +10342,7 @@ async function spawnPlaywright(instanceKey, ownerId, reservedPort, browserBin =
|
|
|
10349
10342
|
let proc;
|
|
10350
10343
|
try {
|
|
10351
10344
|
proc = spawn4(spawnSpec.command, spawnSpec.args, {
|
|
10352
|
-
stdio: "ignore",
|
|
10345
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
10353
10346
|
detached: false,
|
|
10354
10347
|
env: childEnv
|
|
10355
10348
|
});
|
|
@@ -10357,14 +10350,15 @@ async function spawnPlaywright(instanceKey, ownerId, reservedPort, browserBin =
|
|
|
10357
10350
|
releasePort(port);
|
|
10358
10351
|
throw err;
|
|
10359
10352
|
}
|
|
10360
|
-
const
|
|
10353
|
+
const stderrTail = captureBoundedStderr(proc);
|
|
10354
|
+
const startup = watchChildStartup(proc, stderrTail);
|
|
10361
10355
|
const reapCrashedBrowser = () => {
|
|
10362
10356
|
const userDataDir2 = resolveUserDataDir(instanceKey);
|
|
10363
10357
|
managerHost.cleanupBrowserProcessesForDataDir(userDataDir2);
|
|
10364
10358
|
cleanSingletonFiles(userDataDir2);
|
|
10365
10359
|
};
|
|
10366
10360
|
proc.once("error", (err) => {
|
|
10367
|
-
logger.error({ err, instanceKey }, "Playwright MCP error");
|
|
10361
|
+
logger.error({ err, instanceKey, stderr: stderrTail() || undefined }, "Playwright MCP error");
|
|
10368
10362
|
if (instances.get(instanceKey)?.process === proc) {
|
|
10369
10363
|
releasePort(port);
|
|
10370
10364
|
instances.delete(instanceKey);
|
|
@@ -10376,7 +10370,7 @@ async function spawnPlaywright(instanceKey, ownerId, reservedPort, browserBin =
|
|
|
10376
10370
|
}
|
|
10377
10371
|
});
|
|
10378
10372
|
proc.once("exit", (code) => {
|
|
10379
|
-
logger.info({ instanceKey, code }, "Playwright MCP exited");
|
|
10373
|
+
logger.info({ instanceKey, code, stderr: stderrTail() || undefined }, "Playwright MCP exited");
|
|
10380
10374
|
const wasOurs = instances.get(instanceKey)?.process === proc;
|
|
10381
10375
|
if (wasOurs) {
|
|
10382
10376
|
releasePort(port);
|
|
@@ -10394,18 +10388,42 @@ async function spawnPlaywright(instanceKey, ownerId, reservedPort, browserBin =
|
|
|
10394
10388
|
lastUsedAt: now,
|
|
10395
10389
|
capability
|
|
10396
10390
|
});
|
|
10397
|
-
|
|
10398
|
-
|
|
10399
|
-
|
|
10400
|
-
|
|
10391
|
+
let startupError;
|
|
10392
|
+
let ready = false;
|
|
10393
|
+
try {
|
|
10394
|
+
ready = await Promise.race([
|
|
10395
|
+
(async () => await waitForServer(port, spawnNonce, 1e4) && await supportsOwnerCleanup(port, capability) && await probePlaywrightMcpTransports(port, capability))(),
|
|
10396
|
+
startup.failure
|
|
10397
|
+
]);
|
|
10398
|
+
} catch (error) {
|
|
10399
|
+
startupError = error instanceof Error ? error : new Error(String(error));
|
|
10400
|
+
} finally {
|
|
10401
|
+
startup.stop();
|
|
10402
|
+
}
|
|
10403
|
+
if (ready && !isLiveOwnedChildProcess(instances.get(instanceKey), proc)) {
|
|
10404
|
+
ready = false;
|
|
10405
|
+
startupError = new Error(`Playwright MCP exited after readiness but before publication on port ${port}` + (stderrTail() ? `
|
|
10406
|
+
stderr:
|
|
10407
|
+
${stderrTail()}` : ""));
|
|
10408
|
+
}
|
|
10401
10409
|
if (!ready) {
|
|
10402
10410
|
const exitCode = proc.exitCode;
|
|
10403
10411
|
killInstance(instanceKey);
|
|
10404
10412
|
if (allowFallback && browserBin !== managerHost.fallbackBrowserBin) {
|
|
10405
|
-
logger.warn({
|
|
10413
|
+
logger.warn({
|
|
10414
|
+
err: startupError,
|
|
10415
|
+
instanceKey,
|
|
10416
|
+
browserBin,
|
|
10417
|
+
fallback: managerHost.fallbackBrowserBin,
|
|
10418
|
+
stderr: stderrTail() || undefined
|
|
10419
|
+
}, "Preferred browser MCP unavailable or lacks owner isolation; using Patchright fallback");
|
|
10406
10420
|
return spawnPlaywright(instanceKey, ownerId, undefined, managerHost.fallbackBrowserBin, false);
|
|
10407
10421
|
}
|
|
10408
|
-
|
|
10422
|
+
if (startupError)
|
|
10423
|
+
throw startupError;
|
|
10424
|
+
throw new Error(`Playwright MCP failed health check after spawn on port ${port}` + (exitCode === null ? "" : ` (exitCode=${exitCode})`) + (stderrTail() ? `
|
|
10425
|
+
stderr:
|
|
10426
|
+
${stderrTail()}` : ""));
|
|
10409
10427
|
}
|
|
10410
10428
|
writePortFile(instanceKey, port);
|
|
10411
10429
|
logger.info({ instanceKey, port, pid: proc.pid, ready, virtualDisplay: spawnSpec.virtualDisplay }, "Playwright MCP started");
|
|
@@ -10469,7 +10487,7 @@ async function ensurePlaywright(userId, topic) {
|
|
|
10469
10487
|
async function waitForPortRelease(port, timeoutMs = 3000) {
|
|
10470
10488
|
const start = Date.now();
|
|
10471
10489
|
while (Date.now() - start < timeoutMs) {
|
|
10472
|
-
if (!isPortInUse(port))
|
|
10490
|
+
if (!await isPortInUse(port))
|
|
10473
10491
|
return;
|
|
10474
10492
|
await delay(200);
|
|
10475
10493
|
}
|
|
@@ -10498,11 +10516,22 @@ async function closeBrowserOwnerTabs(ownerId, rawProfile, owner) {
|
|
|
10498
10516
|
const result = await response.json();
|
|
10499
10517
|
return typeof result.closed === "number" ? result.closed : 0;
|
|
10500
10518
|
}
|
|
10501
|
-
async function waitForServer(port, timeoutMs) {
|
|
10519
|
+
async function waitForServer(port, expectedSpawnNonce, timeoutMs) {
|
|
10502
10520
|
const start = Date.now();
|
|
10503
10521
|
while (Date.now() - start < timeoutMs) {
|
|
10504
|
-
|
|
10505
|
-
|
|
10522
|
+
try {
|
|
10523
|
+
const response = await fetch(`http://127.0.0.1:${port}/health`, {
|
|
10524
|
+
signal: AbortSignal.timeout(1000)
|
|
10525
|
+
});
|
|
10526
|
+
if (response.ok) {
|
|
10527
|
+
const health = await response.json();
|
|
10528
|
+
if (matchesSpawnedBrowserHealth(health, expectedSpawnNonce)) {
|
|
10529
|
+
return true;
|
|
10530
|
+
}
|
|
10531
|
+
} else {
|
|
10532
|
+
await response.body?.cancel();
|
|
10533
|
+
}
|
|
10534
|
+
} catch {}
|
|
10506
10535
|
await delay(300);
|
|
10507
10536
|
}
|
|
10508
10537
|
logger.warn({ port, timeoutMs }, "Playwright MCP not ready before timeout");
|
|
@@ -10576,7 +10605,7 @@ async function cloneProfileForChild(opts) {
|
|
|
10576
10605
|
cleanSingletonFiles(dstDir);
|
|
10577
10606
|
for (const f of ["DevToolsActivePort", "LOCK"]) {
|
|
10578
10607
|
try {
|
|
10579
|
-
unlinkSync11(
|
|
10608
|
+
unlinkSync11(resolve11(dstDir, f));
|
|
10580
10609
|
} catch {}
|
|
10581
10610
|
}
|
|
10582
10611
|
logger.info({ srcKey, dstKey, srcDir, dstDir }, "Cloned Playwright profile for child topic");
|
|
@@ -10589,7 +10618,7 @@ function deleteTopicProfileDir(userId, topic) {
|
|
|
10589
10618
|
logger.info({ dir, userId, topic }, "Preserved shared browser profile on topic deletion");
|
|
10590
10619
|
return { deleted: false, dir };
|
|
10591
10620
|
}
|
|
10592
|
-
var defaultManagerHost, managerHost, MAX_IDLE_MS, instances, usedPorts, spawning, pinnedInstances, playwrightFailureHandlers;
|
|
10621
|
+
var defaultManagerHost, managerHost, MAX_IDLE_MS, instances, usedPorts, spawning, pinnedInstances, playwrightFailureHandlers, PLAYWRIGHT_STARTUP_STDERR_LIMIT;
|
|
10593
10622
|
var init_manager2 = __esm(async () => {
|
|
10594
10623
|
init_config();
|
|
10595
10624
|
init_logger();
|
|
@@ -10626,6 +10655,7 @@ var init_manager2 = __esm(async () => {
|
|
|
10626
10655
|
spawning = new Map;
|
|
10627
10656
|
pinnedInstances = new Map;
|
|
10628
10657
|
playwrightFailureHandlers = new Set;
|
|
10658
|
+
PLAYWRIGHT_STARTUP_STDERR_LIMIT = 8 * 1024;
|
|
10629
10659
|
setInterval(() => {
|
|
10630
10660
|
while (evictIdleInstance() !== null) {}
|
|
10631
10661
|
try {
|
|
@@ -13188,12 +13218,12 @@ var init_errors = __esm(() => {
|
|
|
13188
13218
|
|
|
13189
13219
|
// ../../packages/core/src/runtime/event-heartbeat.ts
|
|
13190
13220
|
function nextOrHeartbeat(pending, intervalMs) {
|
|
13191
|
-
return new Promise((
|
|
13192
|
-
const timer = setTimeout(() =>
|
|
13221
|
+
return new Promise((resolve12, reject) => {
|
|
13222
|
+
const timer = setTimeout(() => resolve12({ kind: "heartbeat" }), intervalMs);
|
|
13193
13223
|
timer.unref?.();
|
|
13194
13224
|
pending.then((result) => {
|
|
13195
13225
|
clearTimeout(timer);
|
|
13196
|
-
|
|
13226
|
+
resolve12({ kind: "event", result });
|
|
13197
13227
|
}, (error) => {
|
|
13198
13228
|
clearTimeout(timer);
|
|
13199
13229
|
reject(error);
|
|
@@ -13448,7 +13478,7 @@ var init_visual_html = __esm(() => {
|
|
|
13448
13478
|
|
|
13449
13479
|
// ../../packages/core/src/runtime/visuals.ts
|
|
13450
13480
|
import { realpathSync as realpathSync4 } from "fs";
|
|
13451
|
-
import { isAbsolute as isAbsolute4, resolve as
|
|
13481
|
+
import { isAbsolute as isAbsolute4, resolve as resolve12 } from "path";
|
|
13452
13482
|
function activeVisualHtmlForPrompt(html) {
|
|
13453
13483
|
if (html.length <= ACTIVE_VISUAL_PROMPT_MAX_CHARS) {
|
|
13454
13484
|
return { html, omittedChars: 0 };
|
|
@@ -13499,8 +13529,8 @@ function topicAllowsVisualFileId(topicId, fileId) {
|
|
|
13499
13529
|
return topicHasAttachmentFileId(topicId, fileId) || topicHasVisualFileId(topicId, fileId);
|
|
13500
13530
|
}
|
|
13501
13531
|
function isPathInside(baseDir, filePath) {
|
|
13502
|
-
const base =
|
|
13503
|
-
const normalized =
|
|
13532
|
+
const base = resolve12(baseDir);
|
|
13533
|
+
const normalized = resolve12(filePath);
|
|
13504
13534
|
try {
|
|
13505
13535
|
const realBase = realpathSync4(base);
|
|
13506
13536
|
const real = realpathSync4(normalized);
|
|
@@ -13562,7 +13592,7 @@ function resolveVisualMediaInput(topicId, input) {
|
|
|
13562
13592
|
}
|
|
13563
13593
|
const rawPath = input.file_path.trim();
|
|
13564
13594
|
const cwd = workspaceCwdFor(topicId);
|
|
13565
|
-
const candidate = isAbsolute4(rawPath) ? rawPath :
|
|
13595
|
+
const candidate = isAbsolute4(rawPath) ? rawPath : resolve12(cwd, rawPath);
|
|
13566
13596
|
if (!isPathInside(cwd, candidate)) {
|
|
13567
13597
|
return { error: "file_path must be inside the topic workspace" };
|
|
13568
13598
|
}
|
|
@@ -13635,7 +13665,7 @@ var init_token_stats = __esm(async () => {
|
|
|
13635
13665
|
// ../../packages/core/src/runtime/turn-event-stream.ts
|
|
13636
13666
|
import { randomUUID as randomUUID15 } from "crypto";
|
|
13637
13667
|
import { realpathSync as realpathSync5, statSync as statSync8 } from "fs";
|
|
13638
|
-
import { isAbsolute as isAbsolute5, resolve as
|
|
13668
|
+
import { isAbsolute as isAbsolute5, resolve as resolve13 } from "path";
|
|
13639
13669
|
function sessionEventMatchesCurrentExecution(topicId, queryId, agent, model) {
|
|
13640
13670
|
if (getRoomQuery(topicId)?.queryId !== queryId)
|
|
13641
13671
|
return false;
|
|
@@ -13942,7 +13972,7 @@ async function runTurnEventStream(topicId, topicTitle, queryId, events, control,
|
|
|
13942
13972
|
case "file":
|
|
13943
13973
|
if (!silent && peerBridge) {
|
|
13944
13974
|
const cwd = workspaceCwdFor(topicId);
|
|
13945
|
-
const path = isAbsolute5(event.path) ? event.path :
|
|
13975
|
+
const path = isAbsolute5(event.path) ? event.path : resolve13(cwd, event.path);
|
|
13946
13976
|
if (!isPathInside(cwd, path)) {
|
|
13947
13977
|
logger.warn({ topicId, path }, "peer output file is outside the topic workspace");
|
|
13948
13978
|
break;
|
|
@@ -17100,4 +17130,4 @@ export {
|
|
|
17100
17130
|
DEFAULT_SELF_CONFIG_PRODUCT
|
|
17101
17131
|
};
|
|
17102
17132
|
|
|
17103
|
-
//# debugId=
|
|
17133
|
+
//# debugId=B58744AF223B7C9664756E2164756E21
|