@sagentlab/navarch-runtime 0.1.68 → 0.1.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,48 @@
1
1
  # navarch-runtime
2
2
 
3
+ ## Managed visual evidence
4
+
5
+ Host-mode build, verify, and QA leases requiring `browser-use` receive a
6
+ `navarch_browser` MCP server with `capture_visual_evidence` in Codex, Claude,
7
+ Gemini, OpenCode, and ACP sessions. It runs Chromium in the runtime process's
8
+ environment, outside the coding agent's OS sandbox, with a fresh browser
9
+ context for desktop and touch-enabled mobile captures. It neither uses the
10
+ operator's browser profile nor executes agent-provided scripts.
11
+
12
+ Install the runtime's pinned browser once on the agent host (and after a
13
+ Playwright version update):
14
+
15
+ ```sh
16
+ npx @sagentlab/navarch-runtime browser-install
17
+ ```
18
+
19
+ The source installer does this automatically. Linux hosts must also have
20
+ Playwright's Chromium system libraries installed. Browser-startup failures
21
+ are returned as capture errors; the runtime does not relax host permissions.
22
+ Docker sessions continue to use their image's tooling, and an operator-supplied
23
+ `NAVARCH_MCP_CONFIG_PATH` remains authoritative.
24
+
25
+ Call the tool with `url`, `source_commit_sha`, and `evidence_role` (`before` or
26
+ `after`). Optional `wait_for_selector` and `settle_ms` let a scene finish loading.
27
+ The URL's deployed SHA must be checked by the agent: the supplied SHA is metadata,
28
+ not platform attestation. Authenticated/interactive journeys still use their
29
+ existing browser tools; this capture tool starts with no cookies.
30
+
31
+ Each successful image is registered through lease-scoped `register_artifact`
32
+ and uploaded to R2. The response contains durable URLs; the agent must inspect
33
+ the images. Before session completion the runtime posts the uploaded evidence
34
+ to the matching delivery PR using its scoped GitHub credential. A publication
35
+ failure keeps verification blocked and preserves the uploaded artifact URLs.
36
+ The capture endpoint binds only to loopback, requires a per-lease bearer, and
37
+ is revoked when the lease ends. Captures are serialized and limited to ten
38
+ attempts per lease. No app-repository capture script or GitHub browser upload
39
+ is required.
40
+
41
+ Run the real Chromium smoke test with
42
+ `NAVARCH_TEST_REAL_BROWSER=1 npm test -- tests/browser-evidence.test.cts` from
43
+ `runtime/`. It renders desktop/mobile PNGs through the same HTTP MCP tool while
44
+ substituting the artifact upload destination.
45
+
3
46
  The machine-side half of Navarch (WP-07): registers this machine with the
4
47
  control plane, then loops claim → sandbox → agent CLI → report until you stop
5
48
  it. Plain Node/TypeScript, no Next.js coupling, and no shared app-code coupling
@@ -0,0 +1,240 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.captureSchema = void 0;
7
+ exports.installEvidenceBrowser = installEvidenceBrowser;
8
+ exports.startBrowserEvidence = startBrowserEvidence;
9
+ const node_http_1 = require("node:http");
10
+ const node_crypto_1 = require("node:crypto");
11
+ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
12
+ const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
13
+ const playwright_1 = require("playwright");
14
+ const zod_1 = require("zod");
15
+ const node_child_process_1 = require("node:child_process");
16
+ const node_util_1 = require("node:util");
17
+ const node_path_1 = __importDefault(require("node:path"));
18
+ async function installEvidenceBrowser() {
19
+ const cli = node_path_1.default.join(node_path_1.default.dirname(require.resolve("playwright/package.json")), "cli.js");
20
+ await (0, node_util_1.promisify)(node_child_process_1.execFile)(process.execPath, [cli, "install", "chromium"], {
21
+ timeout: 180_000,
22
+ });
23
+ }
24
+ const captureShape = {
25
+ url: zod_1.z.string().url().refine((value) => {
26
+ const url = new URL(value);
27
+ return ["http:", "https:"].includes(url.protocol) && !url.username && !url.password;
28
+ }, "Use an HTTP(S) preview or local app URL without embedded credentials."),
29
+ source_commit_sha: zod_1.z.string().regex(/^[0-9a-f]{40}$/).describe("Exact commit deployed at the URL; verify it before capturing. Agent-reported, not attested."),
30
+ evidence_role: zod_1.z.enum(["before", "after"]),
31
+ wait_for_selector: zod_1.z.string().min(1).max(500).optional(),
32
+ settle_ms: zod_1.z.number().int().min(0).max(5000).default(1000),
33
+ };
34
+ exports.captureSchema = zod_1.z.object(captureShape);
35
+ /** Runs in the supervisor, outside the coding agent's OS sandbox. Fresh
36
+ * contexts have no user cookies, arbitrary script execution, or file access.
37
+ * Only the narrow capture operation is exposed to the lease. */
38
+ async function startBrowserEvidence(options) {
39
+ const token = (0, node_crypto_1.randomBytes)(32).toString("hex");
40
+ const fetchImpl = options.fetchImpl ?? fetch;
41
+ const evidence = [];
42
+ const published = new Set();
43
+ let busy = false;
44
+ let closed = false;
45
+ let browser;
46
+ let activeCapture;
47
+ let attempts = 0;
48
+ const assertActive = () => {
49
+ if (closed || !options.isActive())
50
+ throw new Error("Browser evidence lease is no longer active.");
51
+ };
52
+ async function capture(args) {
53
+ assertActive();
54
+ if (busy)
55
+ throw new Error("A capture is already running. Wait for it to finish.");
56
+ if (++attempts > 10)
57
+ throw new Error("This lease reached its 10 capture attempts.");
58
+ busy = true;
59
+ const captured = [];
60
+ const images = [];
61
+ try {
62
+ try {
63
+ browser = await (options.launch?.() ?? playwright_1.chromium.launch({
64
+ headless: true,
65
+ chromiumSandbox: true,
66
+ timeout: 20_000,
67
+ // Do not pass the supervisor's provider or machine credentials to Chrome.
68
+ env: Object.fromEntries(["HOME", "PATH", "TMPDIR", "TEMP", "TMP", "SystemRoot"]
69
+ .flatMap((key) => process.env[key] ? [[key, process.env[key]]] : [])),
70
+ }));
71
+ }
72
+ catch {
73
+ throw new Error("Runtime browser could not start. On the agent host run navarch-runtime browser-install. Check host browser permissions; do not change the task repository or CI to install capture tooling.");
74
+ }
75
+ for (const viewport of ["desktop", "mobile"]) {
76
+ assertActive();
77
+ const context = await browser.newContext({
78
+ ...(viewport === "mobile" ? playwright_1.devices["iPhone 13"] : { viewport: { width: 1440, height: 900 } }),
79
+ acceptDownloads: false,
80
+ serviceWorkers: "block",
81
+ });
82
+ try {
83
+ await context.route("**/*", (route) => {
84
+ const scheme = new URL(route.request().url()).protocol;
85
+ return ["http:", "https:"].includes(scheme) ? route.continue() : route.abort();
86
+ });
87
+ const page = await context.newPage();
88
+ const response = await page.goto(args.url, { waitUntil: "load", timeout: 30_000 });
89
+ if (!response?.ok())
90
+ throw new Error(`Preview returned HTTP ${response?.status() ?? "unknown"}.`);
91
+ if (args.wait_for_selector)
92
+ await page.locator(args.wait_for_selector).first().waitFor({ timeout: 10_000 });
93
+ await page.waitForTimeout(args.settle_ms);
94
+ const bytes = await page.screenshot({ type: "png", timeout: 10_000 });
95
+ if (bytes.length > 25 * 1024 * 1024)
96
+ throw new Error("Screenshot exceeds 25 MiB.");
97
+ assertActive();
98
+ const fileName = `${args.evidence_role}-${viewport}-${(0, node_crypto_1.randomUUID)()}.png`;
99
+ const registration = await fetchImpl(`${options.apiBase.replace(/\/+$/, "")}/api/mcp`, {
100
+ method: "POST",
101
+ signal: AbortSignal.timeout(30_000),
102
+ headers: {
103
+ authorization: `Bearer ${options.machineToken}`,
104
+ "x-navarch-lease-id": options.leaseId,
105
+ "content-type": "application/json",
106
+ accept: "application/json, text/event-stream",
107
+ "mcp-protocol-version": "2025-06-18",
108
+ },
109
+ body: JSON.stringify({ jsonrpc: "2.0", id: (0, node_crypto_1.randomUUID)(), method: "tools/call", params: {
110
+ name: "register_artifact", arguments: {
111
+ file_name: fileName, mime_type: "image/png", size_bytes: bytes.length,
112
+ checksum_sha256: (0, node_crypto_1.createHash)("sha256").update(bytes).digest("hex"),
113
+ source_commit_sha: args.source_commit_sha, evidence_role: args.evidence_role,
114
+ },
115
+ } }),
116
+ });
117
+ if (!registration.ok)
118
+ throw new Error(`Artifact registration returned HTTP ${registration.status}.`);
119
+ const result = await registration.json();
120
+ const destination = result.result?.structuredContent;
121
+ if (result.error || result.result?.isError || !destination?.upload_url || !destination.public_url
122
+ || new URL(destination.upload_url).protocol !== "https:" || new URL(destination.public_url).protocol !== "https:") {
123
+ throw new Error("Artifact registration did not provide secure upload and public URLs.");
124
+ }
125
+ assertActive();
126
+ const upload = await fetchImpl(destination.upload_url, {
127
+ method: "PUT", headers: { "content-type": "image/png" },
128
+ body: new Uint8Array(bytes), signal: AbortSignal.timeout(30_000),
129
+ });
130
+ if (!upload.ok)
131
+ throw new Error(`Screenshot upload returned HTTP ${upload.status}.`);
132
+ const item = { fileName, publicUrl: destination.public_url,
133
+ sourceCommitSha: args.source_commit_sha, role: args.evidence_role, viewport };
134
+ evidence.push(item);
135
+ captured.push(item);
136
+ images.push({ type: "image", data: bytes.toString("base64"), mimeType: "image/png" });
137
+ }
138
+ finally {
139
+ await context.close();
140
+ }
141
+ }
142
+ return { captured, images };
143
+ }
144
+ finally {
145
+ try {
146
+ await browser?.close();
147
+ }
148
+ finally {
149
+ browser = undefined;
150
+ busy = false;
151
+ }
152
+ }
153
+ }
154
+ const http = (0, node_http_1.createServer)(async (req, res) => {
155
+ // Bound to loopback with an unguessable per-lease bearer. Browser-origin
156
+ // calls are rejected even if another page can reach the local port.
157
+ if (req.headers.origin || req.headers.authorization !== `Bearer ${token}`) {
158
+ res.writeHead(403).end();
159
+ return;
160
+ }
161
+ if (closed || !options.isActive()) {
162
+ res.writeHead(410).end();
163
+ return;
164
+ }
165
+ if (req.url !== "/mcp" || req.method !== "POST") {
166
+ res.writeHead(405).end();
167
+ return;
168
+ }
169
+ const server = new mcp_js_1.McpServer({ name: "navarch-browser", version: "1.0.0" });
170
+ const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true });
171
+ server.registerTool("capture_visual_evidence", {
172
+ description: "Capture and upload desktop and mobile screenshots using Navarch's managed local browser. Works outside the coding-agent sandbox. Returns durable artifact URLs. Capture does not certify acceptance or that the URL serves the supplied commit. Evidence is automatically attached to the delivery PR at session completion.",
173
+ inputSchema: captureShape,
174
+ }, async (args) => {
175
+ try {
176
+ if (busy)
177
+ throw new Error("A capture is already running. Wait for it to finish.");
178
+ const pending = capture(args);
179
+ activeCapture = pending;
180
+ const { captured, images } = await pending;
181
+ return { content: [{ type: "text", text: JSON.stringify(captured) }, ...images] };
182
+ }
183
+ catch (error) {
184
+ return { isError: true, content: [{ type: "text", text: String(error) }] };
185
+ }
186
+ });
187
+ res.on("close", () => { void transport.close(); void server.close(); });
188
+ try {
189
+ await server.connect(transport);
190
+ await transport.handleRequest(req, res);
191
+ }
192
+ catch {
193
+ if (!res.headersSent)
194
+ res.writeHead(500).end();
195
+ }
196
+ });
197
+ await new Promise((resolve, reject) => {
198
+ http.once("error", reject);
199
+ http.listen(0, "127.0.0.1", resolve);
200
+ });
201
+ const address = http.address();
202
+ if (!address || typeof address === "string")
203
+ throw new Error("Browser evidence listener unavailable.");
204
+ return {
205
+ token,
206
+ config: { type: "http", url: `http://127.0.0.1:${address.port}/mcp`, headers: { Authorization: `Bearer ${token}` } },
207
+ evidence,
208
+ async publishToPr(prUrl, repository, githubToken) {
209
+ const pending = evidence.filter((item) => !published.has(item.publicUrl));
210
+ if (!pending.length)
211
+ return;
212
+ assertActive();
213
+ const pr = new URL(prUrl);
214
+ const match = pr.pathname.match(/^\/([^/]+\/[^/]+)\/pull\/(\d+)$/);
215
+ if (pr.origin !== "https://github.com" || match?.[1]?.toLowerCase() !== repository.toLowerCase() || !githubToken) {
216
+ throw new Error("Cannot publish browser evidence without a scoped GitHub credential and matching delivery PR.");
217
+ }
218
+ const response = await fetchImpl(`https://api.github.com/repos/${repository}/issues/${match[2]}/comments`, {
219
+ method: "POST", signal: AbortSignal.timeout(30_000),
220
+ headers: { authorization: `Bearer ${githubToken}`, "content-type": "application/json", accept: "application/vnd.github+json" },
221
+ body: JSON.stringify({ body: ["## Navarch browser evidence", "", ...pending.flatMap((item) => [
222
+ `### ${item.role} · ${item.viewport}`, `![${item.viewport}](${item.publicUrl})`,
223
+ `Source commit (agent-reported): \`${item.sourceCommitSha}\``, "",
224
+ ])].join("\n") }),
225
+ });
226
+ if (!response.ok)
227
+ throw new Error(`Evidence comment returned HTTP ${response.status}.`);
228
+ for (const item of pending)
229
+ published.add(item.publicUrl);
230
+ },
231
+ async close() {
232
+ closed = true;
233
+ const stopped = new Promise((resolve) => http.close(() => resolve()));
234
+ http.closeAllConnections();
235
+ await browser?.close().catch(() => undefined);
236
+ await activeCapture?.catch(() => undefined);
237
+ await stopped;
238
+ },
239
+ };
240
+ }
package/dist/cli.cjs CHANGED
@@ -18,6 +18,7 @@ const logger_cjs_1 = require("./logger.cjs");
18
18
  const update_coordinator_cjs_1 = require("./update-coordinator.cjs");
19
19
  const supervisor_cjs_1 = require("./supervisor.cjs");
20
20
  const worktree_janitor_cjs_1 = require("./worktree-janitor.cjs");
21
+ const browser_evidence_cjs_1 = require("./browser-evidence.cjs");
21
22
  const log = (0, logger_cjs_1.createLogger)("cli");
22
23
  const PACKAGE_NAME = "@sagentlab/navarch-runtime";
23
24
  /** Accepted `--agent` ids as usage metavariables, generated from one source of truth. */
@@ -394,6 +395,7 @@ Usage:
394
395
  navarch-runtime start [--config-dir <path>] [--agent ${AGENT_TYPE_USAGE}] [--model <model>]
395
396
  navarch-runtime supervise [--config-dir <path>] [--agent ${AGENT_TYPE_USAGE}] [--model <model>]
396
397
  navarch-runtime doctor [--config-dir <path>] [--agent ${AGENT_TYPE_USAGE}]
398
+ navarch-runtime browser-install
397
399
  navarch-runtime code-graph --query <symbol-or-file> [--mode search|callers|callees|impact] [--commit HEAD] [--depth 1..5] [--repo <path>]
398
400
 
399
401
  Use a different --config-dir (or NAVARCH_CONFIG_DIR) for every agent instance.
@@ -424,6 +426,10 @@ async function main(argv = process.argv.slice(2)) {
424
426
  case "doctor":
425
427
  await doctorCommand(flags);
426
428
  break;
429
+ case "browser-install":
430
+ await (0, browser_evidence_cjs_1.installEvidenceBrowser)();
431
+ console.log("Navarch capture browser installed.");
432
+ break;
427
433
  case "help":
428
434
  case "--help":
429
435
  case "-h":
package/dist/session.cjs CHANGED
@@ -26,6 +26,7 @@ const worktree_guard_cjs_1 = require("./worktree-guard.cjs");
26
26
  const adapter_watchdog_cjs_1 = require("./adapter-watchdog.cjs");
27
27
  const lease_heartbeat_cjs_1 = require("./lease-heartbeat.cjs");
28
28
  const adapter_capacity_cjs_1 = require("./adapter-capacity.cjs");
29
+ const browser_evidence_cjs_1 = require("./browser-evidence.cjs");
29
30
  /** Filename the generated platform MCP config is written under inside the session metadata directory. */
30
31
  const MCP_CONFIG_FILENAME = "mcp-config.json";
31
32
  const GIT_CREDENTIAL_HELPER_FILENAME = "git-credential-navarch.cjs";
@@ -430,9 +431,24 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
430
431
  if (deps.shutdownSignal?.aborted)
431
432
  abortForRuntimeShutdown();
432
433
  lifecycle.enteredMainSessionTry = true;
434
+ let browserEvidence;
433
435
  try {
434
436
  reportActivity("preparing");
435
437
  await gitWorktree.prepare();
438
+ if (!config.mcpConfigPath && config.sandboxMode === "host"
439
+ && ["build", "verify", "qa"].includes(task.task_type)
440
+ && task.required_capabilities?.includes("browser-use")
441
+ && ["codex", "claude-code", "gemini", "opencode", "acp"].includes(runtime)) {
442
+ browserEvidence = await (0, browser_evidence_cjs_1.startBrowserEvidence)({
443
+ apiBase: api.getBaseUrl(), machineToken: api.getToken() ?? "", leaseId,
444
+ isActive: () => !leaseLost && !sessionClosing && !deps.shutdownSignal?.aborted,
445
+ });
446
+ registry.register(browserEvidence.token);
447
+ const mcp = JSON.parse(await node_fs_1.promises.readFile(mcpConfigPath, "utf8"));
448
+ mcp.mcpServers.navarch_browser = browserEvidence.config;
449
+ await node_fs_1.promises.writeFile(mcpConfigPath, JSON.stringify(mcp), { mode: 0o600 });
450
+ promptText += "\n\nNavarch supplies capture_visual_evidence through navarch_browser. Use it first for desktop/mobile screenshots of the verified preview URL and source commit. It runs outside the coding-agent sandbox, uploads images as task artifacts, and the runtime attaches them to the delivery PR before completion. Inspect the returned images before accepting the UI. Do not use GitHub's upload UI or add capture tools to the target repository. If the base commit has no runnable UI, report before evidence as not applicable with the base SHA and reason; still capture the current head. Authentication or interactive walkthroughs may require the existing browser tools.\n";
451
+ }
436
452
  if (gitWorktree.hostedCheckpointRestoreFailure) {
437
453
  log.warn(`hosted checkpoint for ${leaseId} could not be restored: ` +
438
454
  `${(0, redact_cjs_1.redactText)(gitWorktree.hostedCheckpointRestoreFailure, registry.list())}; starting without it.`);
@@ -595,6 +611,19 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
595
611
  log.warn(`head-branch PR lookup failed for ${leaseId}: ${String(err)}`);
596
612
  }
597
613
  // -- delivery gate -----------------------------------------------------
614
+ if (browserEvidence && !leaseLost) {
615
+ mapping.evidenceUrls.push(...browserEvidence.evidence.map((item) => item.publicUrl));
616
+ if (pullRequestUrl) {
617
+ try {
618
+ await browserEvidence.publishToPr(pullRequestUrl, bundle.repository?.full_name ?? task.repo, githubToken);
619
+ }
620
+ catch (error) {
621
+ mapping.leaseOutcome = "failed";
622
+ mapping.exitStatus = "failed";
623
+ mapping.reportSummary = `${task.task_type === "verify" && headSha ? `Verification verdict: BLOCKED ${headSha}\n\n` : ""}Browser evidence was uploaded but could not be attached to the PR: ${String(error)}\n\n${mapping.reportSummary}`;
624
+ }
625
+ }
626
+ }
598
627
  // A clean exit is not delivery. Read what the worktree actually holds
599
628
  // and, when work was started but never shipped, spend a correction turn
600
629
  // in the same worktree before failing the lease. See delivery.cjs for
@@ -787,6 +816,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
787
816
  clearInterval(activityTimer);
788
817
  heartbeatGuard.stop();
789
818
  activeAbortController?.abort();
819
+ await browserEvidence?.close().catch((error) => log.warn(`browser evidence cleanup failed: ${String(error)}`));
790
820
  // Drain only an already-started filesystem update, never a stalled network
791
821
  // request. A later heartbeat response sees sessionClosing and does nothing.
792
822
  await Promise.resolve(heartbeatWorkspaceUpdate).catch(() => undefined);
@@ -60,9 +60,9 @@ async function stageRuntimeUpdate(configDir, directive, runner = defaultRunner)
60
60
  await runner(process.execPath, [staged.bin_path, "--help"]);
61
61
  return staged;
62
62
  }
63
- catch (error) {
64
- if (error.code !== "ENOENT")
65
- throw error;
63
+ catch {
64
+ // A missing, partial, or unrunnable earlier staging (for example one that lost its
65
+ // dependencies) is replaced by a fresh, integrity-checked install instead of failing forever.
66
66
  await node_fs_1.promises.rm(targetDir, { recursive: true, force: true });
67
67
  }
68
68
  const stagingDir = node_path_1.default.join(versionsDir, `.staging-${(0, node_crypto_1.randomUUID)()}`);
@@ -99,6 +99,11 @@ async function stageRuntimeUpdate(configDir, directive, runner = defaultRunner)
99
99
  const installedPackage = node_path_1.default.join(installDir, "node_modules", "@sagentlab", "navarch-runtime");
100
100
  await node_fs_1.promises.writeFile(node_path_1.default.join(installedPackage, ".navarch-integrity"), `${directive.integrity}\n`, { mode: 0o600 });
101
101
  await node_fs_1.promises.rename(installedPackage, targetDir);
102
+ // npm hoists dependencies next to the package; carry them along so the moved package can
103
+ // resolve them from its own node_modules.
104
+ const hoistedModules = node_path_1.default.join(installDir, "node_modules");
105
+ await node_fs_1.promises.rm(node_path_1.default.join(hoistedModules, "@sagentlab"), { recursive: true, force: true });
106
+ await node_fs_1.promises.rename(hoistedModules, node_path_1.default.join(targetDir, "node_modules"));
102
107
  const staged = await verifyInstalledPackage(targetDir, directive.target_version, directive.integrity);
103
108
  await runner(process.execPath, [staged.bin_path, "--help"]);
104
109
  return staged;
@@ -219,8 +219,6 @@ function codexWorktreeGuardArgs(options) {
219
219
  "-c",
220
220
  'approvals_reviewer="auto_review"',
221
221
  "-c",
222
- `projects.${tomlString(node_path_1.default.resolve(options.worktreePath))}.trust_level="untrusted"`,
223
- "-c",
224
222
  `default_permissions=${tomlString(CODEX_GUARD_PROFILE)}`,
225
223
  "-c",
226
224
  `permissions.${CODEX_GUARD_PROFILE}.filesystem=${tomlInlineTable(filesystem)}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sagentlab/navarch-runtime",
3
- "version": "0.1.68",
3
+ "version": "0.1.70",
4
4
  "description": "Navarch machine-side session manager: claims delivery tasks and runs them through local coding agents.",
5
5
  "type": "commonjs",
6
6
  "license": "MIT",
@@ -53,6 +53,9 @@
53
53
  },
54
54
  "dependencies": {
55
55
  "@agentclientprotocol/sdk": "^1.4.0",
56
- "typescript": "^5.7.2"
56
+ "@modelcontextprotocol/sdk": "^1.30.1",
57
+ "playwright": "1.63.0",
58
+ "typescript": "^5.7.2",
59
+ "zod": "^3.25.76"
57
60
  }
58
61
  }