@anybrowser/cli 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +78 -2
- package/dist/allowedSites.js +164 -0
- package/dist/allowedSites.js.map +1 -0
- package/dist/blockLedger.js +111 -0
- package/dist/blockLedger.js.map +1 -0
- package/dist/browser.js +1096 -0
- package/dist/browser.js.map +1 -0
- package/dist/browserDetect.js +53 -0
- package/dist/browserDetect.js.map +1 -0
- package/dist/cli.js +366 -0
- package/dist/cli.js.map +1 -0
- package/dist/connect.js +195 -0
- package/dist/connect.js.map +1 -0
- package/dist/daemon.js +475 -0
- package/dist/daemon.js.map +1 -0
- package/dist/hello.js +62 -0
- package/dist/hello.js.map +1 -0
- package/dist/idle.js +84 -0
- package/dist/idle.js.map +1 -0
- package/dist/install.js +63 -0
- package/dist/install.js.map +1 -0
- package/dist/paths.js +36 -0
- package/dist/paths.js.map +1 -0
- package/dist/profile.js +273 -0
- package/dist/profile.js.map +1 -0
- package/dist/proxy.js +40 -0
- package/dist/proxy.js.map +1 -0
- package/dist/siteProxy.js +354 -0
- package/dist/siteProxy.js.map +1 -0
- package/dist/transport.js +85 -0
- package/dist/transport.js.map +1 -0
- package/dist/version.js +12 -0
- package/dist/version.js.map +1 -0
- package/package.json +50 -4
- package/skills/anybrowser/SKILL.md +42 -0
package/dist/connect.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { chmodSync, closeSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeSync, } from "node:fs";
|
|
3
|
+
import { createConnection } from "node:net";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { assertSocketPathFits } from "./paths.js";
|
|
6
|
+
const DEFAULT_SPAWN_WAIT_MS = 10000;
|
|
7
|
+
/**
|
|
8
|
+
* How long `ensureDaemon` waits to connect to (or spawn and connect to) the
|
|
9
|
+
* daemon before giving up, configurable for tests. Mirrors
|
|
10
|
+
* `ANYBROWSER_STOP_WAIT_MS` in cli.ts. Invalid values (non-numeric, zero,
|
|
11
|
+
* negative) fall back to the default.
|
|
12
|
+
*/
|
|
13
|
+
function spawnWaitMs(env = process.env) {
|
|
14
|
+
const raw = env["ANYBROWSER_SPAWN_WAIT_MS"];
|
|
15
|
+
if (raw === undefined || raw.trim() === "") {
|
|
16
|
+
return DEFAULT_SPAWN_WAIT_MS;
|
|
17
|
+
}
|
|
18
|
+
const parsed = Number(raw);
|
|
19
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_SPAWN_WAIT_MS;
|
|
20
|
+
}
|
|
21
|
+
/** Lock is considered stale once it is this old, regardless of its contents. */
|
|
22
|
+
const LOCK_MAX_AGE_MS = 15000;
|
|
23
|
+
/** Lock is considered stale if it's empty/unreadable and at least this old. */
|
|
24
|
+
const LOCK_EMPTY_GRACE_MS = 2000;
|
|
25
|
+
function delay(ms) {
|
|
26
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
27
|
+
}
|
|
28
|
+
function pollIntervalMs() {
|
|
29
|
+
return 50 + Math.floor(Math.random() * 50);
|
|
30
|
+
}
|
|
31
|
+
/** Tries once to connect to the daemon socket. Resolves null instead of throwing on any failure. */
|
|
32
|
+
export function connectOnce(socketPath) {
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
const socket = createConnection(socketPath);
|
|
35
|
+
const onConnect = () => {
|
|
36
|
+
cleanup();
|
|
37
|
+
resolve(socket);
|
|
38
|
+
};
|
|
39
|
+
const onError = () => {
|
|
40
|
+
cleanup();
|
|
41
|
+
socket.destroy();
|
|
42
|
+
resolve(null);
|
|
43
|
+
};
|
|
44
|
+
const cleanup = () => {
|
|
45
|
+
socket.off("connect", onConnect);
|
|
46
|
+
socket.off("error", onError);
|
|
47
|
+
};
|
|
48
|
+
socket.once("connect", onConnect);
|
|
49
|
+
socket.once("error", onError);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function tryCreateLockFile(lockPath) {
|
|
53
|
+
let fd;
|
|
54
|
+
try {
|
|
55
|
+
fd = openSync(lockPath, "wx");
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (error.code === "EEXIST") {
|
|
59
|
+
return "busy";
|
|
60
|
+
}
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
writeSync(fd, String(process.pid));
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
closeSync(fd);
|
|
68
|
+
}
|
|
69
|
+
return "acquired";
|
|
70
|
+
}
|
|
71
|
+
function removeLockFile(lockPath) {
|
|
72
|
+
try {
|
|
73
|
+
unlinkSync(lockPath);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// best effort: nothing useful to do if it's already gone or unremovable
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Whether the spawn lock at `lockPath` looks abandoned and safe to remove. */
|
|
80
|
+
function isLockStale(lockPath) {
|
|
81
|
+
let stat;
|
|
82
|
+
try {
|
|
83
|
+
stat = statSync(lockPath);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return false; // gone already; nothing to clean up
|
|
87
|
+
}
|
|
88
|
+
const ageMs = Date.now() - stat.mtimeMs;
|
|
89
|
+
if (ageMs > LOCK_MAX_AGE_MS) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
let content;
|
|
93
|
+
try {
|
|
94
|
+
content = readFileSync(lockPath, "utf8");
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return ageMs > LOCK_EMPTY_GRACE_MS;
|
|
98
|
+
}
|
|
99
|
+
const trimmed = content.trim();
|
|
100
|
+
if (trimmed.length === 0) {
|
|
101
|
+
return ageMs > LOCK_EMPTY_GRACE_MS;
|
|
102
|
+
}
|
|
103
|
+
const pid = Number(trimmed);
|
|
104
|
+
if (!Number.isInteger(pid)) {
|
|
105
|
+
return ageMs > LOCK_EMPTY_GRACE_MS;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
process.kill(pid, 0);
|
|
109
|
+
return false; // still alive
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
return error.code === "ESRCH";
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async function spawnDaemonProcess(paths) {
|
|
116
|
+
const cliPath = fileURLToPath(new URL("./cli.js", import.meta.url));
|
|
117
|
+
const logFd = openSync(paths.log, "a");
|
|
118
|
+
try {
|
|
119
|
+
const child = spawn(process.execPath, [cliPath, "daemon"], {
|
|
120
|
+
detached: true,
|
|
121
|
+
stdio: ["ignore", logFd, logFd],
|
|
122
|
+
});
|
|
123
|
+
child.unref();
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
closeSync(logFd);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function displayLogPath(paths, env = process.env) {
|
|
130
|
+
if (env["ANYBROWSER_HOME"]?.trim()) {
|
|
131
|
+
return paths.log;
|
|
132
|
+
}
|
|
133
|
+
return "~/.anybrowser/daemon.log";
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Returns a socket connected to the daemon, spawning it first if nothing is
|
|
137
|
+
* listening yet. Safe to call concurrently from many processes: exactly one
|
|
138
|
+
* spawns the daemon (via an O_EXCL lock file), the rest wait for it.
|
|
139
|
+
*/
|
|
140
|
+
export async function ensureDaemon(paths, deadlineMs = spawnWaitMs()) {
|
|
141
|
+
assertSocketPathFits(paths.socket);
|
|
142
|
+
// First run: the state directory (e.g. ~/.anybrowser) may not exist yet.
|
|
143
|
+
// Create it before anything below needs to open the lock or log file
|
|
144
|
+
// inside it.
|
|
145
|
+
mkdirSync(paths.home, { recursive: true, mode: 0o700 });
|
|
146
|
+
chmodSync(paths.home, 0o700);
|
|
147
|
+
const deadline = Date.now() + deadlineMs;
|
|
148
|
+
for (;;) {
|
|
149
|
+
const connected = await connectOnce(paths.socket);
|
|
150
|
+
if (connected) {
|
|
151
|
+
return connected;
|
|
152
|
+
}
|
|
153
|
+
if (Date.now() >= deadline) {
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
const lockAttempt = tryCreateLockFile(paths.lock);
|
|
157
|
+
if (lockAttempt === "acquired") {
|
|
158
|
+
try {
|
|
159
|
+
await spawnDaemonProcess(paths);
|
|
160
|
+
while (Date.now() < deadline) {
|
|
161
|
+
const socket = await connectOnce(paths.socket);
|
|
162
|
+
if (socket) {
|
|
163
|
+
return socket;
|
|
164
|
+
}
|
|
165
|
+
await delay(pollIntervalMs());
|
|
166
|
+
}
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
removeLockFile(paths.lock);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Someone else holds the lock: wait for them, but reclaim a stale lock.
|
|
174
|
+
let staleDetected = false;
|
|
175
|
+
while (Date.now() < deadline) {
|
|
176
|
+
const socket = await connectOnce(paths.socket);
|
|
177
|
+
if (socket) {
|
|
178
|
+
return socket;
|
|
179
|
+
}
|
|
180
|
+
if (isLockStale(paths.lock)) {
|
|
181
|
+
removeLockFile(paths.lock);
|
|
182
|
+
staleDetected = true;
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
await delay(pollIntervalMs());
|
|
186
|
+
}
|
|
187
|
+
if (!staleDetected) {
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
// loop back around and try to acquire the lock ourselves
|
|
191
|
+
}
|
|
192
|
+
throw new Error(`anyb: could not connect to or start the daemon within ${deadlineMs}ms. ` +
|
|
193
|
+
`Check the log at ${displayLogPath(paths)}`);
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=connect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connect.js","sourceRoot":"","sources":["../src/connect.ts"],"names":[],"mappings":"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"}
|
package/dist/daemon.js
ADDED
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { createConnection, createServer } from "node:net";
|
|
3
|
+
import { Server as McpServer } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
|
+
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { assertSocketPathFits } from "./paths.js";
|
|
6
|
+
import { formatErrorLine, formatStatusLine, formatStopAckLine, parseHelloLine, } from "./hello.js";
|
|
7
|
+
import { readVersion } from "./version.js";
|
|
8
|
+
import { SocketTransport } from "./transport.js";
|
|
9
|
+
import { BrowserContextRouter, BrowserSession, ProfileHeadlessRunningError, ProfileUrlBlockedError, } from "./browser.js";
|
|
10
|
+
import { addProfileWithDetection, allowedOriginsProblem, listProfiles, listProfilesWithBadList, profileExists, readProfile, } from "./profile.js";
|
|
11
|
+
import { resolveIdleMs } from "./idle.js";
|
|
12
|
+
const DAEMON_STATUS_TOOL = {
|
|
13
|
+
name: "daemon_status",
|
|
14
|
+
description: "Report this daemon's version, pid, uptime, and connected MCP session count.",
|
|
15
|
+
inputSchema: {
|
|
16
|
+
type: "object",
|
|
17
|
+
properties: {},
|
|
18
|
+
additionalProperties: false,
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
const PROFILE_LIST_TOOL = {
|
|
22
|
+
name: "profile_list",
|
|
23
|
+
description: "List every anybrowser profile: id, browser, headless, allowedOrigins (the sites the profile is restricted to, or null for no restriction), and whether it is currently running.",
|
|
24
|
+
inputSchema: {
|
|
25
|
+
type: "object",
|
|
26
|
+
properties: {},
|
|
27
|
+
additionalProperties: false,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
const PROFILE_CREATE_TOOL = {
|
|
31
|
+
name: "profile_create",
|
|
32
|
+
description: "Create a new anybrowser profile. `browser` is one of chrome, chromium, brave, edge, arc, vivaldi, opera. Returns the new profile's id.",
|
|
33
|
+
inputSchema: {
|
|
34
|
+
type: "object",
|
|
35
|
+
properties: {
|
|
36
|
+
name: {
|
|
37
|
+
type: "string",
|
|
38
|
+
description: "A lowercase slug, e.g. 'work'. Composed into the id as '<name>-in-<browser>'.",
|
|
39
|
+
},
|
|
40
|
+
browser: {
|
|
41
|
+
type: "string",
|
|
42
|
+
description: "chrome | chromium | brave | edge | arc | vivaldi | opera",
|
|
43
|
+
},
|
|
44
|
+
headless: {
|
|
45
|
+
type: "boolean",
|
|
46
|
+
description: "Defaults to false.",
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
required: ["name", "browser"],
|
|
50
|
+
additionalProperties: false,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
const PROFILE_STATUS_TOOL = {
|
|
54
|
+
name: "profile_status",
|
|
55
|
+
description: "Report a profile's running state, tab count (when running), headless flag, browser, and allowedOrigins (the sites the profile is restricted to, or null for no restriction).",
|
|
56
|
+
inputSchema: {
|
|
57
|
+
type: "object",
|
|
58
|
+
properties: {
|
|
59
|
+
profile: { type: "string", description: "The profile id." },
|
|
60
|
+
},
|
|
61
|
+
required: ["profile"],
|
|
62
|
+
additionalProperties: false,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
const PROFILE_LOGIN_TOOL = {
|
|
66
|
+
name: "profile_login",
|
|
67
|
+
description: "Open `url` in a new, foregrounded tab of `profile`'s browser (launching it headed if not already running), so a human can log in. Returns immediately; ask the user to log in, then verify with a snapshot.",
|
|
68
|
+
inputSchema: {
|
|
69
|
+
type: "object",
|
|
70
|
+
properties: {
|
|
71
|
+
profile: { type: "string", description: "The profile id." },
|
|
72
|
+
url: { type: "string", description: "The URL to open for login." },
|
|
73
|
+
},
|
|
74
|
+
required: ["profile", "url"],
|
|
75
|
+
additionalProperties: false,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
const DAEMON_TOOLS = [
|
|
79
|
+
DAEMON_STATUS_TOOL,
|
|
80
|
+
PROFILE_LIST_TOOL,
|
|
81
|
+
PROFILE_CREATE_TOOL,
|
|
82
|
+
PROFILE_STATUS_TOOL,
|
|
83
|
+
PROFILE_LOGIN_TOOL,
|
|
84
|
+
];
|
|
85
|
+
/** Bytes we'll buffer looking for the hello line's newline before giving up on a connection. */
|
|
86
|
+
const MAX_HELLO_LINE_BYTES = 4096;
|
|
87
|
+
function log(event) {
|
|
88
|
+
process.stderr.write(`[anyb daemon] ${new Date().toISOString()} ${event}\n`);
|
|
89
|
+
}
|
|
90
|
+
function buildStatus(state) {
|
|
91
|
+
return {
|
|
92
|
+
version: state.version,
|
|
93
|
+
pid: process.pid,
|
|
94
|
+
uptimeSeconds: (Date.now() - state.startedAt) / 1000,
|
|
95
|
+
sessions: state.sessions.size,
|
|
96
|
+
profiles: state.browserRouter.runningProfileIds(),
|
|
97
|
+
idleMs: state.idleMs,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Clears any pending idle-exit timer, e.g. because a session just connected
|
|
102
|
+
* or the daemon is shutting down for another reason.
|
|
103
|
+
*/
|
|
104
|
+
function clearIdleExitTimer(state) {
|
|
105
|
+
if (state.idleExitTimer !== undefined) {
|
|
106
|
+
clearTimeout(state.idleExitTimer);
|
|
107
|
+
state.idleExitTimer = undefined;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Re-arms the "exit for idleness" timer whenever sessions.size is (or just
|
|
112
|
+
* became) zero. A session connecting cancels it (see startMcpSession); this
|
|
113
|
+
* is called again on every session close so the countdown restarts from that
|
|
114
|
+
* moment, not from whenever the daemon happened to start.
|
|
115
|
+
*
|
|
116
|
+
* Idling is disabled entirely when `idleMs` is null (ANYBROWSER_IDLE_MS=off).
|
|
117
|
+
*/
|
|
118
|
+
function armIdleExitTimer(state) {
|
|
119
|
+
clearIdleExitTimer(state);
|
|
120
|
+
if (state.idleMs === null || state.sessions.size > 0 || state.shuttingDown) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
state.idleExitTimer = setTimeout(() => {
|
|
124
|
+
// Re-check at fire time: a session may have connected in the interim.
|
|
125
|
+
if (state.sessions.size === 0 && !state.shuttingDown) {
|
|
126
|
+
log(`idle exit: no sessions connected for ${state.idleMs}ms`);
|
|
127
|
+
void shutdown(state, 0);
|
|
128
|
+
}
|
|
129
|
+
}, state.idleMs);
|
|
130
|
+
}
|
|
131
|
+
/** Connects to `socketPath` just to see whether anything answers. Never throws. */
|
|
132
|
+
function probeSocket(socketPath) {
|
|
133
|
+
return new Promise((resolve) => {
|
|
134
|
+
const socket = createConnection(socketPath);
|
|
135
|
+
const finish = (alive) => {
|
|
136
|
+
socket.removeAllListeners();
|
|
137
|
+
socket.destroy();
|
|
138
|
+
resolve(alive);
|
|
139
|
+
};
|
|
140
|
+
socket.once("connect", () => finish(true));
|
|
141
|
+
socket.once("error", () => finish(false));
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* If daemon.sock exists and something is listening on it, another daemon
|
|
146
|
+
* already owns this state dir: exit quietly. If it exists but nothing
|
|
147
|
+
* answers, it's a stale leftover: remove it and let the caller bind fresh.
|
|
148
|
+
*/
|
|
149
|
+
async function claimSocketOrExit(paths) {
|
|
150
|
+
if (!existsSync(paths.socket)) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const alive = await probeSocket(paths.socket);
|
|
154
|
+
if (alive) {
|
|
155
|
+
log("another daemon is already live on this socket; exiting");
|
|
156
|
+
process.exit(0);
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
unlinkSync(paths.socket);
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
if (error.code !== "ENOENT") {
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function startMcpSession(socket, initialData, state, clientVersion) {
|
|
168
|
+
state.sessions.add(socket);
|
|
169
|
+
clearIdleExitTimer(state);
|
|
170
|
+
log(`session open (client version ${clientVersion}, sessions=${state.sessions.size})`);
|
|
171
|
+
const browserSession = new BrowserSession(state.browserRouter);
|
|
172
|
+
socket.on("close", () => {
|
|
173
|
+
state.sessions.delete(socket);
|
|
174
|
+
log(`session close (sessions=${state.sessions.size})`);
|
|
175
|
+
armIdleExitTimer(state);
|
|
176
|
+
// Its tabs go with it; the browser stays (the daemon owns its lifetime).
|
|
177
|
+
browserSession.dispose().catch((error) => {
|
|
178
|
+
log(`session tab cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
// No 'error' listener here: attachHelloHandler already installed the one
|
|
182
|
+
// permanent listener for this socket's whole life (see its comment), so
|
|
183
|
+
// adding a second one here would log every socket error twice.
|
|
184
|
+
try {
|
|
185
|
+
const transport = new SocketTransport(socket, initialData);
|
|
186
|
+
const server = new McpServer({ name: "anybrowser", version: state.version }, { capabilities: { tools: {} } });
|
|
187
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
188
|
+
const browserTools = await browserSession.listTools();
|
|
189
|
+
return { tools: [...DAEMON_TOOLS, ...browserTools] };
|
|
190
|
+
});
|
|
191
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
192
|
+
if (request.params.name === "daemon_status") {
|
|
193
|
+
return {
|
|
194
|
+
content: [{ type: "text", text: JSON.stringify(buildStatus(state)) }],
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
if (request.params.name === "profile_list") {
|
|
198
|
+
const profiles = listProfiles(state.paths).map((profile) => ({
|
|
199
|
+
id: profile.id,
|
|
200
|
+
browser: profile.browser,
|
|
201
|
+
headless: profile.headless,
|
|
202
|
+
allowedOrigins: profile.allowedOrigins,
|
|
203
|
+
running: state.browserRouter.runningProfileIds().includes(profile.id),
|
|
204
|
+
}));
|
|
205
|
+
const broken = listProfilesWithBadList(state.paths).map(({ id, problem }) => ({ id, error: problem }));
|
|
206
|
+
return {
|
|
207
|
+
content: [
|
|
208
|
+
{
|
|
209
|
+
type: "text",
|
|
210
|
+
text: JSON.stringify([...profiles, ...broken]),
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
if (request.params.name === "profile_create") {
|
|
216
|
+
const args = (request.params.arguments ?? {});
|
|
217
|
+
if (args.allowedOrigins !== undefined) {
|
|
218
|
+
// The allowed-sites list is a human guard rail, like `headless`
|
|
219
|
+
// (decision 7). Failing loudly beats ignoring it: an agent that
|
|
220
|
+
// thought it had pinned a profile would act as if it were safe.
|
|
221
|
+
return {
|
|
222
|
+
isError: true,
|
|
223
|
+
content: [
|
|
224
|
+
{
|
|
225
|
+
type: "text",
|
|
226
|
+
text: "profile_create cannot set an allowed-sites list; only the user can, with 'anyb profile add <name> <browser> --allow <list>' or 'anyb profile set <id> allowedOrigins=<list>'",
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
if (typeof args.name !== "string" || typeof args.browser !== "string") {
|
|
232
|
+
return {
|
|
233
|
+
isError: true,
|
|
234
|
+
content: [
|
|
235
|
+
{
|
|
236
|
+
type: "text",
|
|
237
|
+
text: "profile_create requires string 'name' and 'browser'",
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
const outcome = addProfileWithDetection(state.paths, {
|
|
243
|
+
name: args.name,
|
|
244
|
+
browser: args.browser,
|
|
245
|
+
headless: args.headless === true,
|
|
246
|
+
});
|
|
247
|
+
if (!outcome.ok) {
|
|
248
|
+
return {
|
|
249
|
+
isError: true,
|
|
250
|
+
content: [{ type: "text", text: outcome.message }],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
return { content: [{ type: "text", text: outcome.id }] };
|
|
254
|
+
}
|
|
255
|
+
if (request.params.name === "profile_status") {
|
|
256
|
+
const args = (request.params.arguments ?? {});
|
|
257
|
+
if (typeof args.profile !== "string" || args.profile.length === 0) {
|
|
258
|
+
throw new McpError(ErrorCode.InvalidParams, "profile_status requires a string 'profile' argument");
|
|
259
|
+
}
|
|
260
|
+
if (!profileExists(state.paths, args.profile)) {
|
|
261
|
+
throw new McpError(ErrorCode.InvalidParams, `unknown profile '${args.profile}'`);
|
|
262
|
+
}
|
|
263
|
+
const problem = allowedOriginsProblem(state.paths, args.profile);
|
|
264
|
+
if (problem) {
|
|
265
|
+
return {
|
|
266
|
+
isError: true,
|
|
267
|
+
content: [
|
|
268
|
+
{
|
|
269
|
+
type: "text",
|
|
270
|
+
text: `Profile '${args.profile}': ${problem}. The last list read stays in force while the daemon runs. Ask the user to fix profile.json outside the agent.`,
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
const stored = readProfile(state.paths, args.profile);
|
|
276
|
+
const runningContext = state.browserRouter.runningContext(args.profile);
|
|
277
|
+
const idleForMs = state.browserRouter.idleForMs(args.profile);
|
|
278
|
+
const status = {
|
|
279
|
+
running: runningContext !== undefined,
|
|
280
|
+
headless: stored.headless,
|
|
281
|
+
browser: stored.browser,
|
|
282
|
+
allowedOrigins: stored.allowedOrigins,
|
|
283
|
+
...(runningContext
|
|
284
|
+
? {
|
|
285
|
+
tabCount: runningContext.pages().length,
|
|
286
|
+
...(idleForMs !== undefined ? { idleForMs } : {}),
|
|
287
|
+
}
|
|
288
|
+
: {}),
|
|
289
|
+
};
|
|
290
|
+
return { content: [{ type: "text", text: JSON.stringify(status) }] };
|
|
291
|
+
}
|
|
292
|
+
if (request.params.name === "profile_login") {
|
|
293
|
+
const args = (request.params.arguments ?? {});
|
|
294
|
+
if (typeof args.profile !== "string" ||
|
|
295
|
+
args.profile.length === 0 ||
|
|
296
|
+
typeof args.url !== "string" ||
|
|
297
|
+
args.url.length === 0) {
|
|
298
|
+
throw new McpError(ErrorCode.InvalidParams, "profile_login requires string 'profile' and 'url' arguments");
|
|
299
|
+
}
|
|
300
|
+
if (!profileExists(state.paths, args.profile)) {
|
|
301
|
+
throw new McpError(ErrorCode.InvalidParams, `unknown profile '${args.profile}'`);
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
await state.browserRouter.loginLaunch(args.profile, args.url);
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
if (error instanceof ProfileHeadlessRunningError ||
|
|
308
|
+
error instanceof ProfileUrlBlockedError) {
|
|
309
|
+
return {
|
|
310
|
+
isError: true,
|
|
311
|
+
content: [{ type: "text", text: error.message }],
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
throw error;
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
content: [
|
|
318
|
+
{
|
|
319
|
+
type: "text",
|
|
320
|
+
text: `Opened ${args.url} in a new tab for profile '${args.profile}'. Ask the user to log in, then verify with a snapshot.`,
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
return browserSession.callTool(request.params.name, (request.params.arguments ?? {}));
|
|
326
|
+
});
|
|
327
|
+
server.connect(transport).catch((error) => {
|
|
328
|
+
log(`session error: ${error instanceof Error ? error.message : String(error)}`);
|
|
329
|
+
socket.destroy();
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
log(`session error: ${error instanceof Error ? error.message : String(error)}`);
|
|
334
|
+
socket.destroy();
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function handleHello(socket, line, rest, state) {
|
|
338
|
+
const parsed = parseHelloLine(line);
|
|
339
|
+
if (!parsed.ok) {
|
|
340
|
+
log(`bad hello: ${parsed.error}`);
|
|
341
|
+
socket.end(formatErrorLine(parsed.error));
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
switch (parsed.role) {
|
|
345
|
+
case "status":
|
|
346
|
+
socket.end(formatStatusLine(buildStatus(state)));
|
|
347
|
+
return;
|
|
348
|
+
case "stop":
|
|
349
|
+
socket.write(formatStopAckLine(), () => {
|
|
350
|
+
socket.end(() => {
|
|
351
|
+
void shutdown(state, 0);
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
return;
|
|
355
|
+
case "mcp":
|
|
356
|
+
startMcpSession(socket, rest, state, parsed.version);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function attachHelloHandler(socket, state) {
|
|
361
|
+
let buffered = Buffer.alloc(0);
|
|
362
|
+
// Every accepted socket must have an 'error' listener for its whole life:
|
|
363
|
+
// Node treats an unhandled 'error' event as fatal to the process. A reply
|
|
364
|
+
// write can fail (e.g. EPIPE) after the hello-parsing phase is done, once
|
|
365
|
+
// control has moved on to handleHello/startMcpSession, so this listener is
|
|
366
|
+
// never removed - unlike the hello-parsing 'data'/'close' listeners below,
|
|
367
|
+
// which are only needed until the hello line is read.
|
|
368
|
+
socket.on("error", (error) => {
|
|
369
|
+
log(`session error: ${error.message}`);
|
|
370
|
+
});
|
|
371
|
+
const cleanup = () => {
|
|
372
|
+
socket.off("data", onData);
|
|
373
|
+
socket.off("close", onClose);
|
|
374
|
+
};
|
|
375
|
+
const onClose = () => cleanup();
|
|
376
|
+
const onData = (chunk) => {
|
|
377
|
+
buffered = buffered.length === 0 ? chunk : Buffer.concat([buffered, chunk]);
|
|
378
|
+
const newlineIndex = buffered.indexOf(0x0a);
|
|
379
|
+
if (newlineIndex === -1) {
|
|
380
|
+
if (buffered.length > MAX_HELLO_LINE_BYTES) {
|
|
381
|
+
log("bad hello: first line too long");
|
|
382
|
+
socket.destroy();
|
|
383
|
+
}
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
cleanup();
|
|
387
|
+
const line = buffered
|
|
388
|
+
.subarray(0, newlineIndex)
|
|
389
|
+
.toString("utf8")
|
|
390
|
+
.replace(/\r$/, "");
|
|
391
|
+
const rest = buffered.subarray(newlineIndex + 1);
|
|
392
|
+
handleHello(socket, line, rest, state);
|
|
393
|
+
};
|
|
394
|
+
socket.on("data", onData);
|
|
395
|
+
socket.on("close", onClose);
|
|
396
|
+
}
|
|
397
|
+
async function shutdown(state, exitCode) {
|
|
398
|
+
if (state.shuttingDown) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
state.shuttingDown = true;
|
|
402
|
+
clearIdleExitTimer(state);
|
|
403
|
+
log("stop: shutting down");
|
|
404
|
+
state.server.close();
|
|
405
|
+
for (const session of state.sessions) {
|
|
406
|
+
session.destroy();
|
|
407
|
+
}
|
|
408
|
+
state.sessions.clear();
|
|
409
|
+
// Close every open browser context (flushing its cookie store etc. to
|
|
410
|
+
// disk) BEFORE removing the socket file: a caller of `anyb stop` waits for
|
|
411
|
+
// the socket to disappear as its signal that shutdown is complete (see
|
|
412
|
+
// cli.ts's runStopCommand), so the socket must not vanish until a script
|
|
413
|
+
// that immediately inspects the profile directory afterwards sees a
|
|
414
|
+
// flushed cookie store.
|
|
415
|
+
await state.browserRouter.closeAll();
|
|
416
|
+
try {
|
|
417
|
+
unlinkSync(state.paths.socket);
|
|
418
|
+
}
|
|
419
|
+
catch (error) {
|
|
420
|
+
if (error.code !== "ENOENT") {
|
|
421
|
+
log(`stop: failed to remove socket: ${error.message}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
log("stop: done");
|
|
425
|
+
process.exit(exitCode);
|
|
426
|
+
}
|
|
427
|
+
/** Runs the daemon in the foreground. Resolves once it is listening; keeps running until shutdown. */
|
|
428
|
+
export async function runDaemon(paths) {
|
|
429
|
+
// Playwright's own switch for NOT forcing loopback through a browser's proxy.
|
|
430
|
+
// The allowed-sites proxy must see local servers too, and the check upstream
|
|
431
|
+
// is plain truthiness (even "0" disables forcing), so only deleting it is safe.
|
|
432
|
+
delete process.env["PLAYWRIGHT_DISABLE_FORCED_CHROMIUM_PROXIED_LOOPBACK"];
|
|
433
|
+
assertSocketPathFits(paths.socket);
|
|
434
|
+
log(`start pid=${process.pid} version=${readVersion()}`);
|
|
435
|
+
mkdirSync(paths.home, { recursive: true, mode: 0o700 });
|
|
436
|
+
chmodSync(paths.home, 0o700);
|
|
437
|
+
await claimSocketOrExit(paths);
|
|
438
|
+
const version = readVersion();
|
|
439
|
+
const server = createServer();
|
|
440
|
+
const idleMs = resolveIdleMs(paths);
|
|
441
|
+
const state = {
|
|
442
|
+
paths,
|
|
443
|
+
version,
|
|
444
|
+
startedAt: Date.now(),
|
|
445
|
+
sessions: new Set(),
|
|
446
|
+
server,
|
|
447
|
+
shuttingDown: false,
|
|
448
|
+
browserRouter: new BrowserContextRouter(paths, log, idleMs),
|
|
449
|
+
idleMs,
|
|
450
|
+
idleExitTimer: undefined,
|
|
451
|
+
};
|
|
452
|
+
log(`idle: idleMs=${idleMs === null ? "off" : idleMs}`);
|
|
453
|
+
// No session has connected yet, so the countdown to an idle exit starts
|
|
454
|
+
// from daemon startup itself (see armIdleExitTimer's doc comment).
|
|
455
|
+
armIdleExitTimer(state);
|
|
456
|
+
server.on("connection", (socket) => {
|
|
457
|
+
attachHelloHandler(socket, state);
|
|
458
|
+
});
|
|
459
|
+
server.on("error", (error) => {
|
|
460
|
+
log(`server error: ${error.message}`);
|
|
461
|
+
});
|
|
462
|
+
await new Promise((resolve, reject) => {
|
|
463
|
+
const onError = (error) => reject(error);
|
|
464
|
+
server.once("error", onError);
|
|
465
|
+
server.listen(paths.socket, () => {
|
|
466
|
+
server.off("error", onError);
|
|
467
|
+
resolve();
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
chmodSync(paths.socket, 0o600);
|
|
471
|
+
log(`listening on ${paths.socket}`);
|
|
472
|
+
process.on("SIGTERM", () => void shutdown(state, 0));
|
|
473
|
+
process.on("SIGINT", () => void shutdown(state, 0));
|
|
474
|
+
}
|
|
475
|
+
//# sourceMappingURL=daemon.js.map
|