negotium 0.1.7 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/README.md +2 -2
  2. package/dist/chunk-0ay6cjdy.js +1331 -0
  3. package/dist/chunk-0ay6cjdy.js.map +16 -0
  4. package/dist/chunk-1k7t7bnx.js +97 -0
  5. package/dist/chunk-1k7t7bnx.js.map +10 -0
  6. package/dist/chunk-5ahkew9c.js +806 -0
  7. package/dist/chunk-5ahkew9c.js.map +17 -0
  8. package/dist/chunk-6cpn7ht2.js +287 -0
  9. package/dist/chunk-6cpn7ht2.js.map +13 -0
  10. package/dist/chunk-6kctn20e.js +3529 -0
  11. package/dist/chunk-6kctn20e.js.map +20 -0
  12. package/dist/chunk-7xqj7aex.js +77 -0
  13. package/dist/chunk-7xqj7aex.js.map +11 -0
  14. package/dist/chunk-9p0ww3tk.js +41 -0
  15. package/dist/chunk-9p0ww3tk.js.map +10 -0
  16. package/dist/chunk-a92a7nrk.js +252 -0
  17. package/dist/chunk-a92a7nrk.js.map +10 -0
  18. package/dist/chunk-c500xe9z.js +33 -0
  19. package/dist/chunk-c500xe9z.js.map +10 -0
  20. package/dist/chunk-cds1qxsf.js +1644 -0
  21. package/dist/chunk-cds1qxsf.js.map +17 -0
  22. package/dist/chunk-e4407g8x.js +274 -0
  23. package/dist/chunk-e4407g8x.js.map +10 -0
  24. package/dist/chunk-e5pbggsh.js +363 -0
  25. package/dist/chunk-e5pbggsh.js.map +11 -0
  26. package/dist/chunk-edb07sjp.js +567 -0
  27. package/dist/chunk-edb07sjp.js.map +14 -0
  28. package/dist/chunk-exd0v74a.js +3012 -0
  29. package/dist/chunk-exd0v74a.js.map +32 -0
  30. package/dist/chunk-fgrme9tr.js +4398 -0
  31. package/dist/chunk-fgrme9tr.js.map +47 -0
  32. package/dist/chunk-fka9eqe1.js +581 -0
  33. package/dist/chunk-fka9eqe1.js.map +11 -0
  34. package/dist/chunk-g82jygez.js +108 -0
  35. package/dist/chunk-g82jygez.js.map +10 -0
  36. package/dist/chunk-ghs2j1fr.js +312 -0
  37. package/dist/chunk-ghs2j1fr.js.map +10 -0
  38. package/dist/chunk-gmy11efb.js +125 -0
  39. package/dist/chunk-gmy11efb.js.map +10 -0
  40. package/dist/chunk-gxma4c70.js +110 -0
  41. package/dist/chunk-gxma4c70.js.map +10 -0
  42. package/dist/chunk-hkdqhm34.js +61 -0
  43. package/dist/chunk-hkdqhm34.js.map +10 -0
  44. package/dist/chunk-hpa0cpzh.js +134 -0
  45. package/dist/chunk-hpa0cpzh.js.map +10 -0
  46. package/dist/chunk-hwy3n31w.js +21 -0
  47. package/dist/chunk-hwy3n31w.js.map +9 -0
  48. package/dist/chunk-j2behpy7.js +214 -0
  49. package/dist/chunk-j2behpy7.js.map +10 -0
  50. package/dist/chunk-jc0at0tc.js +102 -0
  51. package/dist/chunk-jc0at0tc.js.map +10 -0
  52. package/dist/chunk-jkqq5ef6.js +243 -0
  53. package/dist/chunk-jkqq5ef6.js.map +11 -0
  54. package/dist/chunk-jzkw2f01.js +51 -0
  55. package/dist/chunk-jzkw2f01.js.map +10 -0
  56. package/dist/chunk-jzz91jwh.js +70 -0
  57. package/dist/chunk-jzz91jwh.js.map +10 -0
  58. package/dist/chunk-k9z47mab.js +247 -0
  59. package/dist/chunk-k9z47mab.js.map +12 -0
  60. package/dist/chunk-n17yxpa8.js +23 -0
  61. package/dist/chunk-n17yxpa8.js.map +10 -0
  62. package/dist/chunk-naa09ags.js +2143 -0
  63. package/dist/chunk-naa09ags.js.map +15 -0
  64. package/dist/chunk-ns3ke88n.js +142 -0
  65. package/dist/chunk-ns3ke88n.js.map +10 -0
  66. package/dist/chunk-p3cr2ejg.js +1626 -0
  67. package/dist/chunk-p3cr2ejg.js.map +15 -0
  68. package/dist/chunk-q5bcpf23.js +2925 -0
  69. package/dist/chunk-q5bcpf23.js.map +20 -0
  70. package/dist/chunk-rafc55bs.js +424 -0
  71. package/dist/chunk-rafc55bs.js.map +11 -0
  72. package/dist/chunk-rbrxjvbj.js +2830 -0
  73. package/dist/chunk-rbrxjvbj.js.map +23 -0
  74. package/dist/chunk-sa874j5c.js +9 -0
  75. package/dist/chunk-sa874j5c.js.map +10 -0
  76. package/dist/chunk-sgsz2fpq.js +289 -0
  77. package/dist/chunk-sgsz2fpq.js.map +10 -0
  78. package/dist/chunk-sm6fh5c5.js +569 -0
  79. package/dist/chunk-sm6fh5c5.js.map +14 -0
  80. package/dist/chunk-t9zcg8f1.js +499 -0
  81. package/dist/chunk-t9zcg8f1.js.map +13 -0
  82. package/dist/chunk-vp2db42d.js +121 -0
  83. package/dist/chunk-vp2db42d.js.map +10 -0
  84. package/dist/chunk-w13jjf4a.js +46 -0
  85. package/dist/chunk-w13jjf4a.js.map +10 -0
  86. package/dist/chunk-x1arr7ze.js +63 -0
  87. package/dist/chunk-x1arr7ze.js.map +10 -0
  88. package/dist/chunk-xfq68rch.js +583 -0
  89. package/dist/chunk-xfq68rch.js.map +11 -0
  90. package/dist/main.js +132 -20
  91. package/dist/main.js.map +4 -4
  92. package/dist/runtime/scripts/faster-whisper-wrapper.py +87 -0
  93. package/dist/runtime/scripts/mcp-patchright-http.mjs +349 -0
  94. package/dist/runtime/src/agents/api-topic-agent-switch.ts +178 -0
  95. package/dist/runtime/src/agents/archiver.ts +396 -0
  96. package/dist/runtime/src/agents/auth-check.ts +64 -0
  97. package/dist/runtime/src/agents/claude-provider.ts +737 -0
  98. package/dist/runtime/src/agents/claude-registry.ts +115 -0
  99. package/dist/runtime/src/agents/codex-native-multi-agent.ts +177 -0
  100. package/dist/runtime/src/agents/codex-provider.ts +785 -0
  101. package/dist/runtime/src/agents/codex-registry.ts +115 -0
  102. package/dist/runtime/src/agents/codex-tree-kill.ts +365 -0
  103. package/dist/runtime/src/agents/contracts.ts +69 -0
  104. package/dist/runtime/src/agents/deep-map.ts +13 -0
  105. package/dist/runtime/src/agents/execution-host.ts +91 -0
  106. package/dist/runtime/src/agents/fixtures/claude-attachments.jsonl +2 -0
  107. package/dist/runtime/src/agents/fixtures/codex-shell.jsonl +5 -0
  108. package/dist/runtime/src/agents/fork.ts +65 -0
  109. package/dist/runtime/src/agents/hosted-agent.ts +48 -0
  110. package/dist/runtime/src/agents/idle-archiver.ts +128 -0
  111. package/dist/runtime/src/agents/index.ts +205 -0
  112. package/dist/runtime/src/agents/maestro-provider.ts +206 -0
  113. package/dist/runtime/src/agents/maestro-registry.ts +31 -0
  114. package/dist/runtime/src/agents/mcp-tools/ask-user.ts +280 -0
  115. package/dist/runtime/src/agents/mcp-tools/common.ts +21 -0
  116. package/dist/runtime/src/agents/mcp-tools/self-config.ts +220 -0
  117. package/dist/runtime/src/agents/mcp-tools/spawn-subagent.ts +525 -0
  118. package/dist/runtime/src/agents/mcp-tools/visuals.ts +88 -0
  119. package/dist/runtime/src/agents/model-catalog.ts +151 -0
  120. package/dist/runtime/src/agents/registry.ts +24 -0
  121. package/dist/runtime/src/agents/rollout/claude.ts +331 -0
  122. package/dist/runtime/src/agents/rollout/codex.ts +569 -0
  123. package/dist/runtime/src/agents/rollout/shared.ts +205 -0
  124. package/dist/runtime/src/agents/self-config-core.ts +415 -0
  125. package/dist/runtime/src/agents/task-events.ts +44 -0
  126. package/dist/runtime/src/agents/tool-format.ts +352 -0
  127. package/dist/runtime/src/agents/topic-agent-switch.ts +193 -0
  128. package/dist/runtime/src/agents/topic-cleanup.ts +231 -0
  129. package/dist/runtime/src/agents/vault-tool-policy.ts +33 -0
  130. package/dist/runtime/src/application/execute-external-user-turn.ts +23 -0
  131. package/dist/runtime/src/application/submit-user-message.ts +47 -0
  132. package/dist/runtime/src/application/switch-topic-effort.ts +48 -0
  133. package/dist/runtime/src/application/switch-topic-model.ts +79 -0
  134. package/dist/runtime/src/application/topic-service.ts +139 -0
  135. package/dist/runtime/src/application/vault-command.ts +170 -0
  136. package/dist/runtime/src/bus.ts +319 -0
  137. package/dist/runtime/src/index.ts +333 -0
  138. package/dist/runtime/src/mcp/agent-health-server.ts +309 -0
  139. package/dist/runtime/src/mcp/background-bash-server.ts +346 -0
  140. package/dist/runtime/src/mcp/canonical-bridge-config.ts +84 -0
  141. package/dist/runtime/src/mcp/canonical-proxy-server.ts +171 -0
  142. package/dist/runtime/src/mcp/dm-ipc.ts +60 -0
  143. package/dist/runtime/src/mcp/mcp-helpers.ts +40 -0
  144. package/dist/runtime/src/mcp/paddleocr-server.ts +105 -0
  145. package/dist/runtime/src/mcp/peer-bridge.ts +114 -0
  146. package/dist/runtime/src/mcp/runtime-spec.ts +134 -0
  147. package/dist/runtime/src/mcp/session-comm/bridge-ipc-config.ts +27 -0
  148. package/dist/runtime/src/mcp/session-comm/peer-forward.ts +165 -0
  149. package/dist/runtime/src/mcp/session-comm/runtime.ts +115 -0
  150. package/dist/runtime/src/mcp/session-comm/server.ts +649 -0
  151. package/dist/runtime/src/mcp/session-comm/topic-config.ts +131 -0
  152. package/dist/runtime/src/mcp/session-comm/topics.ts +229 -0
  153. package/dist/runtime/src/mcp/stdio-protect.ts +84 -0
  154. package/dist/runtime/src/mcp/system-health-server.ts +130 -0
  155. package/dist/runtime/src/mcp/task-server.ts +173 -0
  156. package/dist/runtime/src/mcp/token-stats-server.ts +98 -0
  157. package/dist/runtime/src/mcp/vault-http.ts +219 -0
  158. package/dist/runtime/src/mcp/vault-run.ts +153 -0
  159. package/dist/runtime/src/mcp/vault-server.ts +104 -0
  160. package/dist/runtime/src/mcp/wiki-server.ts +755 -0
  161. package/dist/runtime/src/media/file-events.ts +33 -0
  162. package/dist/runtime/src/media/text-extractor.ts +312 -0
  163. package/dist/runtime/src/media/video.ts +92 -0
  164. package/dist/runtime/src/outbox/file-ops.ts +210 -0
  165. package/dist/runtime/src/outbox/index.ts +8 -0
  166. package/dist/runtime/src/outbox/utils.ts +51 -0
  167. package/dist/runtime/src/platform/background-bash/manager.ts +279 -0
  168. package/dist/runtime/src/platform/config.ts +371 -0
  169. package/dist/runtime/src/platform/constants.ts +18 -0
  170. package/dist/runtime/src/platform/debug-users.ts +14 -0
  171. package/dist/runtime/src/platform/delay.ts +1 -0
  172. package/dist/runtime/src/platform/error.ts +9 -0
  173. package/dist/runtime/src/platform/file-utils.ts +16 -0
  174. package/dist/runtime/src/platform/jsonl.ts +202 -0
  175. package/dist/runtime/src/platform/lifecycle.ts +174 -0
  176. package/dist/runtime/src/platform/logger.ts +31 -0
  177. package/dist/runtime/src/platform/maestro-bootstrap-env.ts +3 -0
  178. package/dist/runtime/src/platform/mcp-config.ts +865 -0
  179. package/dist/runtime/src/platform/modules.ts +145 -0
  180. package/dist/runtime/src/platform/node-plugins.ts +46 -0
  181. package/dist/runtime/src/platform/playwright/manager.ts +1233 -0
  182. package/dist/runtime/src/prompts/agents/wiki-archiver.md +261 -0
  183. package/dist/runtime/src/prompts/builders.ts +357 -0
  184. package/dist/runtime/src/prompts/sessions/channel-system.md +48 -0
  185. package/dist/runtime/src/prompts/sessions/manager-system.md +37 -0
  186. package/dist/runtime/src/prompts/sessions/topic-system.md +47 -0
  187. package/dist/runtime/src/prompts/sessions/visual-design.md +86 -0
  188. package/dist/runtime/src/query/active-rooms.ts +532 -0
  189. package/dist/runtime/src/query/control.ts +313 -0
  190. package/dist/runtime/src/query/index.ts +24 -0
  191. package/dist/runtime/src/query/session-inbox-cleanup.ts +44 -0
  192. package/dist/runtime/src/query/session-inbox-path.ts +42 -0
  193. package/dist/runtime/src/query/state.ts +94 -0
  194. package/dist/runtime/src/query/types.ts +83 -0
  195. package/dist/runtime/src/runtime/ask-callbacks.ts +153 -0
  196. package/dist/runtime/src/runtime/attachments.ts +151 -0
  197. package/dist/runtime/src/runtime/background-sessions.ts +83 -0
  198. package/dist/runtime/src/runtime/channel-context.ts +107 -0
  199. package/dist/runtime/src/runtime/errors.ts +69 -0
  200. package/dist/runtime/src/runtime/event-heartbeat.ts +77 -0
  201. package/dist/runtime/src/runtime/file-hooks.ts +81 -0
  202. package/dist/runtime/src/runtime/footer.ts +23 -0
  203. package/dist/runtime/src/runtime/inbox.ts +946 -0
  204. package/dist/runtime/src/runtime/tasks.ts +56 -0
  205. package/dist/runtime/src/runtime/topic-config.ts +29 -0
  206. package/dist/runtime/src/runtime/turn-runner.ts +2286 -0
  207. package/dist/runtime/src/runtime/usage-alert.ts +84 -0
  208. package/dist/runtime/src/runtime/visual-html.ts +109 -0
  209. package/dist/runtime/src/runtime/visual-store.ts +430 -0
  210. package/dist/runtime/src/runtime/visuals.ts +171 -0
  211. package/dist/runtime/src/security/sanitize.ts +27 -0
  212. package/dist/runtime/src/security/sensitive-path.ts +34 -0
  213. package/dist/runtime/src/storage/activity-log.ts +152 -0
  214. package/dist/runtime/src/storage/api-messages.ts +617 -0
  215. package/dist/runtime/src/storage/api-topic-brief.ts +127 -0
  216. package/dist/runtime/src/storage/api-topic-config.ts +192 -0
  217. package/dist/runtime/src/storage/api-topics.ts +844 -0
  218. package/dist/runtime/src/storage/app-settings.ts +44 -0
  219. package/dist/runtime/src/storage/browser-profiles.ts +133 -0
  220. package/dist/runtime/src/storage/conversations.ts +268 -0
  221. package/dist/runtime/src/storage/forum/agent-settings.ts +20 -0
  222. package/dist/runtime/src/storage/forum/index.ts +4 -0
  223. package/dist/runtime/src/storage/forum/repository.ts +283 -0
  224. package/dist/runtime/src/storage/forum/schema.ts +300 -0
  225. package/dist/runtime/src/storage/forum-db.ts +23 -0
  226. package/dist/runtime/src/storage/runtime-events.ts +130 -0
  227. package/dist/runtime/src/storage/runtime-leases.ts +189 -0
  228. package/dist/runtime/src/storage/runtime-process-leases.ts +216 -0
  229. package/dist/runtime/src/storage/runtime-topic-state.ts +148 -0
  230. package/dist/runtime/src/storage/runtime-turn-requests.ts +300 -0
  231. package/dist/runtime/src/storage/self-schedules.ts +298 -0
  232. package/dist/runtime/src/storage/session-asks.ts +347 -0
  233. package/dist/runtime/src/storage/sqlite.ts +129 -0
  234. package/dist/runtime/src/storage/tasks.ts +179 -0
  235. package/dist/runtime/src/storage/token-stats.ts +144 -0
  236. package/dist/runtime/src/storage/topic-archive-state.ts +59 -0
  237. package/dist/runtime/src/storage/topic-archive.ts +74 -0
  238. package/dist/runtime/src/storage/topic-settings.ts +67 -0
  239. package/dist/runtime/src/storage/topic-transcript.ts +98 -0
  240. package/dist/runtime/src/storage/vault-crypto.ts +65 -0
  241. package/dist/runtime/src/storage/vault.ts +225 -0
  242. package/dist/runtime/src/storage/wiki-summary-names.ts +33 -0
  243. package/dist/runtime/src/storage/wiki.ts +85 -0
  244. package/dist/runtime/src/topics/create.ts +123 -0
  245. package/dist/runtime/src/topics/derive.ts +398 -0
  246. package/dist/runtime/src/topics/lifecycle.ts +311 -0
  247. package/dist/runtime/src/topics/links.ts +7 -0
  248. package/dist/runtime/src/topics/personal-general.ts +65 -0
  249. package/dist/runtime/src/topics/session.ts +381 -0
  250. package/dist/runtime/src/types/api.ts +497 -0
  251. package/dist/runtime/src/types.ts +255 -0
  252. package/dist/runtime/src/version.ts +1 -0
  253. package/dist/runtime/tsconfig.json +13 -0
  254. package/package.json +16 -3
  255. package/dist/main.d.ts +0 -3
  256. package/dist/main.d.ts.map +0 -1
@@ -0,0 +1,349 @@
1
+ #!/usr/bin/env node
2
+ import { createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
3
+ import { createRequire } from "node:module";
4
+ import { dirname, resolve } from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+ import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
7
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
8
+ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
9
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
10
+ import {
11
+ CallToolRequestSchema,
12
+ isInitializeRequest,
13
+ ListToolsRequestSchema,
14
+ } from "@modelcontextprotocol/sdk/types.js";
15
+
16
+ function parseCli(argv = process.argv.slice(2)) {
17
+ const options = { host: "127.0.0.1" };
18
+ for (let index = 0; index < argv.length; index += 1) {
19
+ const arg = argv[index];
20
+ const [flag, inlineValue] = arg.includes("=") ? arg.split(/=(.*)/s, 2) : [arg, undefined];
21
+ const nextValue = () => inlineValue ?? argv[++index];
22
+ switch (flag) {
23
+ case "--host":
24
+ options.host = nextValue();
25
+ break;
26
+ case "--port": {
27
+ const value = Number(nextValue());
28
+ if (!Number.isInteger(value) || value <= 0) throw new Error(`Invalid --port: ${value}`);
29
+ options.port = value;
30
+ break;
31
+ }
32
+ case "--user-data-dir":
33
+ options.userDataDir = nextValue();
34
+ break;
35
+ case "--headless":
36
+ options.headless = true;
37
+ break;
38
+ case "--headed":
39
+ options.headless = false;
40
+ break;
41
+ default:
42
+ throw new Error(`Unknown argument: ${arg}`);
43
+ }
44
+ }
45
+ if (!options.port) throw new Error("--port is required");
46
+ return options;
47
+ }
48
+
49
+ const require = createRequire(import.meta.url);
50
+ const packageRoot = dirname(require.resolve("mcp-patchright/package.json"));
51
+ const [{ BrowserManager }, { handleTool }, { tools }] = await Promise.all([
52
+ import(pathToFileURL(resolve(packageRoot, "dist/browser/manager.js")).href),
53
+ import(pathToFileURL(resolve(packageRoot, "dist/tools/handlers.js")).href),
54
+ import(pathToFileURL(resolve(packageRoot, "dist/tools/registry.js")).href),
55
+ ]);
56
+
57
+ function createMcpServer(defaultStartOptions, sharedManager, owner) {
58
+ const server = new Server(
59
+ { name: "mcp-patchright", version: "0.1.0" },
60
+ { capabilities: { tools: {} } },
61
+ );
62
+ const manager = sharedManager ?? new BrowserManager(defaultStartOptions);
63
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
64
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
65
+ try {
66
+ return await manager.runAsOwner(owner, () =>
67
+ handleTool(manager, request.params.name, request.params.arguments ?? {}),
68
+ );
69
+ } catch (error) {
70
+ return {
71
+ content: [
72
+ {
73
+ type: "text",
74
+ text: error instanceof Error ? `${error.name}: ${error.message}` : String(error),
75
+ },
76
+ ],
77
+ isError: true,
78
+ };
79
+ }
80
+ });
81
+ return server;
82
+ }
83
+
84
+ function requestOwner(req) {
85
+ const owner = req.header("x-browser-owner");
86
+ if (!owner || owner.length > 256) return undefined;
87
+ return owner;
88
+ }
89
+
90
+ function sseRequestOwner(req) {
91
+ const owner = requestOwner(req) ?? (typeof req.query.owner === "string" ? req.query.owner : "");
92
+ if (!owner || owner.length > 256) return undefined;
93
+ return owner;
94
+ }
95
+
96
+ const expectedCapability = process.env.NEGOTIUM_BROWSER_CAPABILITY;
97
+ if (!expectedCapability) throw new Error("NEGOTIUM_BROWSER_CAPABILITY is required");
98
+
99
+ function safeCapabilityEqual(provided, expectedValue) {
100
+ if (!provided) return false;
101
+ const actual = Buffer.from(provided);
102
+ const expected = Buffer.from(expectedValue);
103
+ return actual.length === expected.length && timingSafeEqual(actual, expected);
104
+ }
105
+
106
+ function hasCapability(req) {
107
+ const provided = req.header("x-browser-capability");
108
+ return safeCapabilityEqual(provided, expectedCapability);
109
+ }
110
+
111
+ function hasOwnerCapability(req, owner) {
112
+ const expected = createHmac("sha256", expectedCapability).update(owner).digest("hex");
113
+ return safeCapabilityEqual(req.header("x-browser-capability"), expected);
114
+ }
115
+
116
+ function requireCapability(req, res) {
117
+ if (hasCapability(req)) return true;
118
+ res.status(401).json({ ok: false, error: "invalid browser capability" });
119
+ return false;
120
+ }
121
+
122
+ function requireOwnerCapability(req, res, owner) {
123
+ const queryCapability =
124
+ req.path === "/sse" && typeof req.query.capability === "string"
125
+ ? req.query.capability
126
+ : undefined;
127
+ const expected = createHmac("sha256", expectedCapability).update(owner).digest("hex");
128
+ if (hasOwnerCapability(req, owner) || safeCapabilityEqual(queryCapability, expected)) return true;
129
+ res.status(401).json({ ok: false, error: "invalid browser owner capability" });
130
+ return false;
131
+ }
132
+
133
+ // Egress proxy is passed via env (not argv) so credentials stay out of `ps`.
134
+ // The parent backend sets NEGOTIUM_BROWSER_PROXY_* from resolveBrowserProxy().
135
+ function proxyFromEnv() {
136
+ const server = process.env.NEGOTIUM_BROWSER_PROXY_SERVER;
137
+ if (!server) return undefined;
138
+ const proxy = { server };
139
+ if (process.env.NEGOTIUM_BROWSER_PROXY_USERNAME)
140
+ proxy.username = process.env.NEGOTIUM_BROWSER_PROXY_USERNAME;
141
+ if (process.env.NEGOTIUM_BROWSER_PROXY_PASSWORD)
142
+ proxy.password = process.env.NEGOTIUM_BROWSER_PROXY_PASSWORD;
143
+ if (process.env.NEGOTIUM_BROWSER_PROXY_BYPASS)
144
+ proxy.bypass = process.env.NEGOTIUM_BROWSER_PROXY_BYPASS;
145
+ return proxy;
146
+ }
147
+
148
+ const options = parseCli();
149
+ const proxy = proxyFromEnv();
150
+ const defaultStartOptions = {
151
+ ...(options.userDataDir ? { userDataDir: options.userDataDir } : {}),
152
+ ...(options.headless !== undefined ? { headless: options.headless } : {}),
153
+ ...(proxy ? { proxy } : {}),
154
+ };
155
+ const sharedManager = new BrowserManager(defaultStartOptions);
156
+ const app = createMcpExpressApp({ host: options.host });
157
+
158
+ const transports = {};
159
+ const servers = {};
160
+ const sessionOwners = {};
161
+ const sseTransports = {};
162
+ const sseServers = {};
163
+ const sseMessageTokens = {};
164
+ let httpServer;
165
+
166
+ app.get("/health", (_req, res) => {
167
+ res.json({
168
+ ok: true,
169
+ name: "mcp-patchright",
170
+ });
171
+ });
172
+
173
+ // Claude Code and Maestro use the legacy two-endpoint SSE transport. The
174
+ // initial connection is authenticated with the owner capability. A fresh,
175
+ // per-session token embedded in the advertised message endpoint prevents
176
+ // another loopback process from injecting JSON-RPC messages by session ID.
177
+ app.get("/sse", async (req, res) => {
178
+ const owner = sseRequestOwner(req);
179
+ if (!owner) {
180
+ res.status(400).json({ ok: false, error: "browser owner is required in SSE mode" });
181
+ return;
182
+ }
183
+ if (!requireOwnerCapability(req, res, owner)) return;
184
+
185
+ const messageToken = randomBytes(32).toString("hex");
186
+ const transport = new SSEServerTransport(
187
+ `/message?token=${encodeURIComponent(messageToken)}`,
188
+ res,
189
+ );
190
+ const server = createMcpServer(defaultStartOptions, sharedManager, owner);
191
+ sseTransports[transport.sessionId] = transport;
192
+ sseServers[transport.sessionId] = server;
193
+ sseMessageTokens[transport.sessionId] = messageToken;
194
+ transport.onclose = () => {
195
+ const sessionId = transport.sessionId;
196
+ delete sseTransports[sessionId];
197
+ delete sseMessageTokens[sessionId];
198
+ const managedServer = sseServers[sessionId];
199
+ delete sseServers[sessionId];
200
+ void managedServer?.close().catch(() => undefined);
201
+ };
202
+ await server.connect(transport);
203
+ });
204
+
205
+ app.post("/message", async (req, res) => {
206
+ const sessionId = typeof req.query.sessionId === "string" ? req.query.sessionId : "";
207
+ const messageToken = typeof req.query.token === "string" ? req.query.token : "";
208
+ const transport = sseTransports[sessionId];
209
+ const expectedMessageToken = sseMessageTokens[sessionId];
210
+ if (!transport) {
211
+ res.status(404).send("SSE session not found");
212
+ return;
213
+ }
214
+ if (!expectedMessageToken || !safeCapabilityEqual(messageToken, expectedMessageToken)) {
215
+ res.status(401).send("invalid SSE message token");
216
+ return;
217
+ }
218
+ await transport.handlePostMessage(req, res, req.body);
219
+ });
220
+
221
+ app.all("/mcp", async (req, res) => {
222
+ try {
223
+ const owner = requestOwner(req);
224
+ if (!owner) {
225
+ res.status(400).json({
226
+ jsonrpc: "2.0",
227
+ error: { code: -32600, message: "browser owner is required in HTTP mode" },
228
+ id: null,
229
+ });
230
+ return;
231
+ }
232
+ if (!requireOwnerCapability(req, res, owner)) return;
233
+ const sessionId = req.headers["mcp-session-id"];
234
+ const normalizedSessionId = Array.isArray(sessionId)
235
+ ? sessionId[0]
236
+ : typeof sessionId === "string"
237
+ ? sessionId
238
+ : undefined;
239
+ let transport;
240
+ if (normalizedSessionId && transports[normalizedSessionId]) {
241
+ const existingTransport = transports[normalizedSessionId];
242
+ if (!(existingTransport instanceof StreamableHTTPServerTransport)) {
243
+ res.status(400).json({
244
+ jsonrpc: "2.0",
245
+ error: { code: -32000, message: "Session uses a different transport protocol" },
246
+ id: null,
247
+ });
248
+ return;
249
+ }
250
+ if (sessionOwners[normalizedSessionId] !== owner) {
251
+ res.status(403).json({
252
+ jsonrpc: "2.0",
253
+ error: { code: -32001, message: "Browser owner does not match this session" },
254
+ id: null,
255
+ });
256
+ return;
257
+ }
258
+ transport = existingTransport;
259
+ } else if (!normalizedSessionId && req.method === "POST" && isInitializeRequest(req.body)) {
260
+ let server;
261
+ transport = new StreamableHTTPServerTransport({
262
+ sessionIdGenerator: () => randomUUID(),
263
+ onsessioninitialized: (newSessionId) => {
264
+ if (!transport || !server) return;
265
+ transports[newSessionId] = transport;
266
+ servers[newSessionId] = server;
267
+ sessionOwners[newSessionId] = owner;
268
+ },
269
+ });
270
+ server = createMcpServer(defaultStartOptions, sharedManager, owner);
271
+ transport.onclose = () => {
272
+ const sid = transport?.sessionId;
273
+ if (sid) {
274
+ delete transports[sid];
275
+ delete sessionOwners[sid];
276
+ const managedServer = servers[sid];
277
+ delete servers[sid];
278
+ void managedServer?.close().catch(() => undefined);
279
+ }
280
+ };
281
+ await server.connect(transport);
282
+ } else {
283
+ res.status(400).json({
284
+ jsonrpc: "2.0",
285
+ error: { code: -32000, message: "Bad Request: No valid session ID provided" },
286
+ id: null,
287
+ });
288
+ return;
289
+ }
290
+ await transport.handleRequest(req, res, req.body);
291
+ } catch (error) {
292
+ console.error("Error handling /mcp request:", error);
293
+ if (!res.headersSent) {
294
+ res.status(500).json({
295
+ jsonrpc: "2.0",
296
+ error: { code: -32603, message: "Internal server error" },
297
+ id: null,
298
+ });
299
+ }
300
+ }
301
+ });
302
+
303
+ app.delete("/owners", async (req, res) => {
304
+ if (!requireCapability(req, res)) return;
305
+ const owner = requestOwner(req);
306
+ if (!owner) {
307
+ res.status(400).json({ ok: false, error: "owner is required" });
308
+ return;
309
+ }
310
+ const closed = await sharedManager.closeOwnerPages(owner);
311
+ res.json({ ok: true, owner, closed });
312
+ });
313
+
314
+ async function shutdown() {
315
+ for (const [sessionId, transport] of Object.entries(sseTransports)) {
316
+ await transport.close().catch(() => undefined);
317
+ delete sseTransports[sessionId];
318
+ delete sseMessageTokens[sessionId];
319
+ }
320
+ for (const [sessionId, server] of Object.entries(sseServers)) {
321
+ await server.close().catch(() => undefined);
322
+ delete sseServers[sessionId];
323
+ }
324
+ for (const [sessionId, transport] of Object.entries(transports)) {
325
+ await transport.close().catch(() => undefined);
326
+ delete transports[sessionId];
327
+ delete sessionOwners[sessionId];
328
+ }
329
+ for (const [sessionId, server] of Object.entries(servers)) {
330
+ await server.close().catch(() => undefined);
331
+ delete servers[sessionId];
332
+ }
333
+ await sharedManager.close().catch(() => undefined);
334
+ await new Promise((resolveClose) => httpServer?.close(() => resolveClose()));
335
+ process.exit(0);
336
+ }
337
+
338
+ httpServer = app.listen(options.port, options.host, () => {
339
+ console.error(`mcp-patchright listening on http://${options.host}:${options.port}`);
340
+ console.error(` SSE: http://${options.host}:${options.port}/sse`);
341
+ console.error(` Streamable: http://${options.host}:${options.port}/mcp`);
342
+ });
343
+ httpServer.on("error", (error) => {
344
+ console.error("mcp-patchright HTTP server error:", error);
345
+ process.exit(1);
346
+ });
347
+
348
+ process.on("SIGINT", () => void shutdown());
349
+ process.on("SIGTERM", () => void shutdown());
@@ -0,0 +1,178 @@
1
+ import { unlinkSync } from "node:fs";
2
+ import { checkAgentAuth } from "#agents/auth-check";
3
+ import { resolveModelForAgent } from "#agents/model-catalog";
4
+ import { getRegistry } from "#agents/registry";
5
+ import { logger } from "#platform/logger";
6
+ import { setApiTopicConfig, type TopicConfig } from "#storage/api-topic-config";
7
+ import { clearTopicSessionId, setApiTopicAgent, setTopicSessionId } from "#storage/api-topics";
8
+ import {
9
+ appendConversationEventStrict,
10
+ findLastSessionIdForAgent,
11
+ readConversation,
12
+ } from "#storage/conversations";
13
+ import type { AgentKind, EffortLevel } from "#types";
14
+
15
+ export type ApiTopicSwitchOutcome =
16
+ | { kind: "fresh"; agent: AgentKind; reason: "no-history" | "bridge-failed" }
17
+ | { kind: "bridged"; agent: AgentKind; bridgedSessionId: string; rolloutPath: string };
18
+
19
+ export type ApiTopicSwitchResult =
20
+ | { ok: true; outcome: ApiTopicSwitchOutcome }
21
+ | { ok: false; error: string };
22
+
23
+ export interface SwitchApiTopicAgentOptions {
24
+ topicId: string;
25
+ topicTitle: string;
26
+ userId: string;
27
+ fromAgent?: AgentKind;
28
+ agent: AgentKind;
29
+ cwd: string;
30
+ config: TopicConfig;
31
+ /** Topic base values used when config has no explicit override. */
32
+ defaultModel?: string;
33
+ defaultEffort?: EffortLevel;
34
+ reason: string;
35
+ }
36
+
37
+ function commitApiTopicSwitch(
38
+ opts: SwitchApiTopicAgentOptions,
39
+ bridgedSessionId: string | null,
40
+ ): void {
41
+ const registry = getRegistry(opts.agent);
42
+ const model = resolveModelForAgent(opts.agent, opts.defaultModel, registry);
43
+ const effort =
44
+ opts.defaultEffort && registry.validateEffort(opts.defaultEffort)
45
+ ? opts.defaultEffort
46
+ : registry.defaultEffort;
47
+ // Keep the durable topic DTO internally consistent too. The turn runner's
48
+ // model-owner guard remains the final safety net for legacy rows, but a
49
+ // Codex topic should never continue advertising Maestro's deepseek-pro.
50
+ setApiTopicAgent(opts.topicId, opts.agent, { model, effort });
51
+ setApiTopicConfig(opts.topicId, opts.config);
52
+ if (bridgedSessionId) {
53
+ setTopicSessionId(opts.topicId, bridgedSessionId, {
54
+ reason: opts.reason,
55
+ agent: opts.agent,
56
+ });
57
+ } else {
58
+ clearTopicSessionId(opts.topicId, opts.reason);
59
+ }
60
+ }
61
+
62
+ /**
63
+ * API-topic equivalent of `switchTopicAgent`: all web/MCP agent changes must
64
+ * pass through this path so the synthetic SDK rollout, unified-log manifest,
65
+ * and durable topic session stay in sync.
66
+ */
67
+ export function switchApiTopicAgent(opts: SwitchApiTopicAgentOptions): ApiTopicSwitchResult {
68
+ const auth = checkAgentAuth(opts.agent);
69
+ if (!auth.ok) return { ok: false, error: auth.error };
70
+
71
+ const entries = readConversation(opts.userId, opts.topicTitle);
72
+ if (entries.length === 0) {
73
+ try {
74
+ commitApiTopicSwitch(opts, null);
75
+ } catch (err) {
76
+ logger.warn({ err, topicId: opts.topicId, agent: opts.agent }, "api-topic switch failed");
77
+ return { ok: false, error: `failed to set agent for "${opts.topicTitle}"` };
78
+ }
79
+ return { ok: true, outcome: { kind: "fresh", agent: opts.agent, reason: "no-history" } };
80
+ }
81
+
82
+ let bridgedSessionId: string;
83
+ let rolloutPath: string;
84
+ try {
85
+ const reuseSessionId = findLastSessionIdForAgent(entries, opts.agent) ?? undefined;
86
+ const registry = getRegistry(opts.agent);
87
+ const model = resolveModelForAgent(
88
+ opts.agent,
89
+ opts.config.model ?? opts.defaultModel,
90
+ registry,
91
+ );
92
+ const requestedEffort = opts.config.effort ?? opts.defaultEffort;
93
+ const effort =
94
+ requestedEffort && registry.validateEffort(requestedEffort)
95
+ ? requestedEffort
96
+ : registry.defaultEffort;
97
+ const result = registry.writeRollout({
98
+ cwd: opts.cwd,
99
+ entries,
100
+ model,
101
+ ...(effort ? { effort } : {}),
102
+ ...(reuseSessionId ? { reuseSessionId } : {}),
103
+ });
104
+ bridgedSessionId = result.sessionId;
105
+ rolloutPath = result.rolloutPath;
106
+ } catch (err) {
107
+ logger.warn(
108
+ {
109
+ err,
110
+ topicId: opts.topicId,
111
+ topicTitle: opts.topicTitle,
112
+ from: opts.fromAgent,
113
+ to: opts.agent,
114
+ },
115
+ "api-topic switch: rollout encoding failed — falling back to fresh session",
116
+ );
117
+ try {
118
+ commitApiTopicSwitch(opts, null);
119
+ } catch (commitErr) {
120
+ logger.warn(
121
+ { err: commitErr, topicId: opts.topicId, agent: opts.agent },
122
+ "api-topic switch fallback commit failed",
123
+ );
124
+ return { ok: false, error: `failed to set agent for "${opts.topicTitle}"` };
125
+ }
126
+ return { ok: true, outcome: { kind: "fresh", agent: opts.agent, reason: "bridge-failed" } };
127
+ }
128
+
129
+ try {
130
+ appendConversationEventStrict(opts.userId, opts.topicTitle, opts.agent, {
131
+ type: "session",
132
+ sessionId: bridgedSessionId,
133
+ });
134
+ } catch (err) {
135
+ try {
136
+ unlinkSync(rolloutPath);
137
+ } catch {
138
+ // best-effort
139
+ }
140
+ logger.warn(
141
+ { err, topicId: opts.topicId, rolloutPath },
142
+ "api-topic switch: manifest append failed, removed orphan rollout",
143
+ );
144
+ return { ok: false, error: `failed to record agent switch for "${opts.topicTitle}"` };
145
+ }
146
+
147
+ try {
148
+ commitApiTopicSwitch(opts, bridgedSessionId);
149
+ } catch (err) {
150
+ try {
151
+ unlinkSync(rolloutPath);
152
+ } catch {
153
+ // best-effort
154
+ }
155
+ logger.warn(
156
+ { err, topicId: opts.topicId, rolloutPath },
157
+ "api-topic switch: config/session commit failed, removed orphan rollout",
158
+ );
159
+ return { ok: false, error: `failed to commit agent switch for "${opts.topicTitle}"` };
160
+ }
161
+
162
+ logger.info(
163
+ {
164
+ topicId: opts.topicId,
165
+ from: opts.fromAgent,
166
+ to: opts.agent,
167
+ sessionId: bridgedSessionId,
168
+ rolloutPath,
169
+ entries: entries.length,
170
+ },
171
+ "api-topic switch: rollout bridged from unified log",
172
+ );
173
+
174
+ return {
175
+ ok: true,
176
+ outcome: { kind: "bridged", agent: opts.agent, bridgedSessionId, rolloutPath },
177
+ };
178
+ }