@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 +43 -0
- package/dist/browser-evidence.cjs +240 -0
- package/dist/cli.cjs +6 -0
- package/dist/session.cjs +30 -0
- package/dist/update-installer.cjs +8 -3
- 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
|
|
@@ -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";
|
|
@@ -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
|
|
64
|
-
|
|
65
|
-
|
|
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;
|
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.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
|
-
"
|
|
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
|
}
|