@sagentlab/navarch-runtime 0.1.67 → 0.1.69
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 +43 -0
- package/dist/api.cjs +2 -2
- package/dist/browser-evidence.cjs +240 -0
- package/dist/cli.cjs +6 -0
- package/dist/session.cjs +43 -4
- package/dist/worktree-guard.cjs +0 -2
- package/package.json +5 -2
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
|
package/dist/api.cjs
CHANGED
|
@@ -142,8 +142,8 @@ class NavarchApiClient {
|
|
|
142
142
|
return result;
|
|
143
143
|
}
|
|
144
144
|
/** Best-effort, lease-scoped activity signal for connected machines. */
|
|
145
|
-
async reportActivity(leaseId, phase) {
|
|
146
|
-
await this.request("POST", `/api/dispatch/${encodeURIComponent(leaseId)}/activity`, { phase }, { allowEmpty: true });
|
|
145
|
+
async reportActivity(leaseId, phase, detail) {
|
|
146
|
+
await this.request("POST", `/api/dispatch/${encodeURIComponent(leaseId)}/activity`, { phase, ...(detail ? { detail } : {}) }, { allowEmpty: true });
|
|
147
147
|
}
|
|
148
148
|
/** schema-design.md §7 — `POST /api/dispatch/:leaseId/complete {status, report, evidence_urls, cost}`. */
|
|
149
149
|
async completeLease(leaseId, req) {
|
|
@@ -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}`, ``,
|
|
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";
|
|
@@ -401,16 +402,22 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
401
402
|
let activityPending = Promise.resolve();
|
|
402
403
|
let outputObservedAt = 0;
|
|
403
404
|
let outputReportedAt = 0;
|
|
405
|
+
let outputUpdates = 0;
|
|
406
|
+
let agentTurnStartedAt = 0;
|
|
404
407
|
let adapterRunning = false;
|
|
405
|
-
const reportActivity = (phase) => {
|
|
406
|
-
activityPending = activityPending.then(() => api.reportActivity(leaseId, phase)).catch((error) => {
|
|
408
|
+
const reportActivity = (phase, detail) => {
|
|
409
|
+
activityPending = activityPending.then(() => api.reportActivity(leaseId, phase, detail)).catch((error) => {
|
|
407
410
|
log.warn(`activity update failed for ${leaseId}: ${(0, redact_cjs_1.redactText)(String(error), registry.list())}`);
|
|
408
411
|
});
|
|
409
412
|
};
|
|
410
413
|
const activityTimer = setInterval(() => {
|
|
411
414
|
if (adapterRunning && outputObservedAt > outputReportedAt && Date.now() - outputReportedAt >= 60_000) {
|
|
412
415
|
outputReportedAt = Date.now();
|
|
413
|
-
reportActivity("output"
|
|
416
|
+
reportActivity("output", {
|
|
417
|
+
outputUpdates,
|
|
418
|
+
elapsedSeconds: Math.floor((outputReportedAt - agentTurnStartedAt) / 1000),
|
|
419
|
+
});
|
|
420
|
+
outputUpdates = 0;
|
|
414
421
|
}
|
|
415
422
|
}, 15_000);
|
|
416
423
|
const heartbeatGuard = new lease_heartbeat_cjs_1.LeaseHeartbeatGuard(claimed.expires_at, () => {
|
|
@@ -424,9 +431,24 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
424
431
|
if (deps.shutdownSignal?.aborted)
|
|
425
432
|
abortForRuntimeShutdown();
|
|
426
433
|
lifecycle.enteredMainSessionTry = true;
|
|
434
|
+
let browserEvidence;
|
|
427
435
|
try {
|
|
428
436
|
reportActivity("preparing");
|
|
429
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
|
+
}
|
|
430
452
|
if (gitWorktree.hostedCheckpointRestoreFailure) {
|
|
431
453
|
log.warn(`hosted checkpoint for ${leaseId} could not be restored: ` +
|
|
432
454
|
`${(0, redact_cjs_1.redactText)(gitWorktree.hostedCheckpointRestoreFailure, registry.list())}; starting without it.`);
|
|
@@ -485,6 +507,9 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
485
507
|
await node_fs_1.promises.writeFile(node_path_1.default.join(workDir, "prompt.md"), runPrompt, "utf8");
|
|
486
508
|
adapterStarted = true;
|
|
487
509
|
adapterRunning = true;
|
|
510
|
+
agentTurnStartedAt = Date.now();
|
|
511
|
+
outputReportedAt = agentTurnStartedAt;
|
|
512
|
+
outputUpdates = 0;
|
|
488
513
|
reportActivity("running");
|
|
489
514
|
const remainingRuntimeMs = maxRuntimeMs > 0
|
|
490
515
|
? maxRuntimeMs - (node_perf_hooks_1.performance.now() - agentBudgetStartedAt)
|
|
@@ -509,7 +534,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
509
534
|
cwd: gitWorktree.worktreePath,
|
|
510
535
|
dockerExec: sandbox ? { containerName: sandbox.name, runner: sandbox_cjs_1.nodeCommandRunner } : undefined,
|
|
511
536
|
signal: activeAbortController.signal,
|
|
512
|
-
onActivity: () => { outputObservedAt = Date.now(); },
|
|
537
|
+
onActivity: () => { outputObservedAt = Date.now(); outputUpdates += 1; },
|
|
513
538
|
}, task.idle_timeout_ms ?? config.sessionIdleTimeoutMs ?? 45 * 60_000, runtime === "opencode" ? config.opencodeStartupTimeoutMs : 0);
|
|
514
539
|
activeAbortController = null;
|
|
515
540
|
adapterRunning = false;
|
|
@@ -586,6 +611,19 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
586
611
|
log.warn(`head-branch PR lookup failed for ${leaseId}: ${String(err)}`);
|
|
587
612
|
}
|
|
588
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
|
+
}
|
|
589
627
|
// A clean exit is not delivery. Read what the worktree actually holds
|
|
590
628
|
// and, when work was started but never shipped, spend a correction turn
|
|
591
629
|
// in the same worktree before failing the lease. See delivery.cjs for
|
|
@@ -778,6 +816,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
778
816
|
clearInterval(activityTimer);
|
|
779
817
|
heartbeatGuard.stop();
|
|
780
818
|
activeAbortController?.abort();
|
|
819
|
+
await browserEvidence?.close().catch((error) => log.warn(`browser evidence cleanup failed: ${String(error)}`));
|
|
781
820
|
// Drain only an already-started filesystem update, never a stalled network
|
|
782
821
|
// request. A later heartbeat response sees sessionClosing and does nothing.
|
|
783
822
|
await Promise.resolve(heartbeatWorkspaceUpdate).catch(() => undefined);
|
package/dist/worktree-guard.cjs
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.1.69",
|
|
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
|
-
"
|
|
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
|
}
|