@sagentlab/navarch-runtime 0.1.91 → 0.1.93
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 +20 -3
- package/dist/adapters/claude-conversation.cjs +269 -0
- package/dist/adapters/claude.cjs +2 -0
- package/dist/api.cjs +14 -0
- package/dist/browser-evidence.cjs +9 -1
- package/dist/browser-probe.cjs +20 -5
- package/dist/claim-loop.cjs +19 -7
- package/dist/cli.cjs +16 -1
- package/dist/config.cjs +5 -0
- package/dist/conversation/capability.cjs +53 -0
- package/dist/conversation/journal.cjs +153 -0
- package/dist/conversation/loop.cjs +232 -0
- package/dist/conversation/prompt.cjs +53 -0
- package/dist/conversation/turn-runner.cjs +402 -0
- package/dist/conversation/workspace.cjs +103 -0
- package/dist/local-browser.cjs +279 -0
- package/dist/prompt.cjs +35 -0
- package/dist/session.cjs +16 -6
- package/dist/update-coordinator.cjs +6 -2
- package/dist/worktree-guard.cjs +37 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,6 +9,16 @@ environment, outside the coding agent's OS sandbox, with a fresh browser
|
|
|
9
9
|
context for desktop and touch-enabled mobile captures. It neither uses the
|
|
10
10
|
operator's browser profile nor executes agent-provided scripts.
|
|
11
11
|
|
|
12
|
+
The same server offers interactive `local_browser_open`, `local_browser_click`,
|
|
13
|
+
`local_browser_fill`, `local_browser_press`, `local_browser_snapshot`, and
|
|
14
|
+
`local_browser_screenshot` tools for walking through a service the agent starts
|
|
15
|
+
on this machine. That browser is limited to loopback (`localhost`,
|
|
16
|
+
`*.localhost`, `127.0.0.0/8`, `::1`). Every request, WebSocket, or navigation
|
|
17
|
+
to any other host is aborted and listed in the tool result. The agent's own
|
|
18
|
+
sandbox can already reach these services, so the tools add a browser without
|
|
19
|
+
adding network reach. Each lease gets one tab at a time, a fresh context per
|
|
20
|
+
viewport, and at most 300 actions.
|
|
21
|
+
|
|
12
22
|
Install the runtime's pinned browser once on the agent host (and after a
|
|
13
23
|
Playwright version update):
|
|
14
24
|
|
|
@@ -36,9 +46,13 @@ lifetime, so restart the agent after fixing the browser to re-probe.
|
|
|
36
46
|
`navarch-runtime doctor` runs the same check.
|
|
37
47
|
|
|
38
48
|
Codex's macOS seatbelt denies Chromium's Mach port registration
|
|
39
|
-
(`bootstrap_check_in ... MachPortRendezvousServer: Permission denied`),
|
|
40
|
-
|
|
41
|
-
|
|
49
|
+
(`bootstrap_check_in ... MachPortRendezvousServer: Permission denied`), and
|
|
50
|
+
`--single-process` crashes once a second page opens. When Node runs in the
|
|
51
|
+
sandbox and only Chromium fails, the probe still passes, with that reason. Codex
|
|
52
|
+
sessions then browse local services through the `local_browser_*` tools above,
|
|
53
|
+
and the session prompt tells them to. With an operator `NAVARCH_MCP_CONFIG_PATH`
|
|
54
|
+
those tools are absent, so the machine does not take browser tasks; run those
|
|
55
|
+
with another agent such as Claude Code, or use Docker mode. Docker mode is not
|
|
42
56
|
probed because its image supplies the browser. Set `NAVARCH_BROWSER_PROBE=off`
|
|
43
57
|
to trust the configured capability list without a check.
|
|
44
58
|
|
|
@@ -460,6 +474,9 @@ unchanged across the deployment.
|
|
|
460
474
|
| `NAVARCH_QODER_EXTRA_ARGS` | — | Comma list of extra CLI args appended after the unattended defaults (`--setting-sources`, `--permission-mode auto`, `--output-format json`). An explicit `--output-format`, `--permission-mode`, `--settings`, `--model`, or `--reasoning-effort` here replaces the per-session default. Qoder selects its own account models; pin one via `--model` here or `connect --model` to override. |
|
|
461
475
|
| `NAVARCH_MCP_CONFIG_PATH` | — | Path to the platform MCP config passed as `--mcp-config`. |
|
|
462
476
|
| `NAVARCH_WORKTREE_GUARD` | on | Host-mode sessions get an adapter-native per-session worktree boundary guard (see below). Set `off` to disable it for every runtime on the machine — required to run OpenCode on the host. |
|
|
477
|
+
| `NAVARCH_NAVI_CONVERSATIONS` | on | Serve read-only Navi repository conversations on this machine. Only the `claude-code` adapter on the host sandbox with the worktree guard on is supported; `navarch-runtime doctor` prints the `navi_chat:` state and reason. Set `off` to stop polling for conversations. |
|
|
478
|
+
| `NAVARCH_CONVERSATION_POLL_MS` | `5000` | Idle poll interval for queued conversation turns (1000–30000). |
|
|
479
|
+
| `NAVARCH_CONVERSATION_TURN_TIMEOUT_MS` | `540000` | Time limit for one conversation turn's agent process (at most nine minutes). |
|
|
463
480
|
| `NAVARCH_GUARD_EXTRA_ROOTS` | — | `path.delimiter`-separated (`:` on POSIX) extra directories the worktree guard allows beyond the session worktree, shared bare repo, and temp dirs. |
|
|
464
481
|
|
|
465
482
|
## Worktree boundary guard (host mode)
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.claudeConversationRunner = exports.ClaudeStreamParser = exports.CLAUDE_CONVERSATION_TOOLS = void 0;
|
|
4
|
+
exports.claudeConversationArgs = claudeConversationArgs;
|
|
5
|
+
exports.runClaudeConversationTurn = runClaudeConversationTurn;
|
|
6
|
+
const node_child_process_1 = require("node:child_process");
|
|
7
|
+
const node_perf_hooks_1 = require("node:perf_hooks");
|
|
8
|
+
const kill_agent_cjs_1 = require("../kill-agent.cjs");
|
|
9
|
+
/**
|
|
10
|
+
* Read-only tools a Navi repository question may use. Anything not listed
|
|
11
|
+
* (Bash, Edit, Write, WebFetch, Task, MCP tools) is absent from the model's
|
|
12
|
+
* tool list, so a question cannot itself authorize a code change, PR,
|
|
13
|
+
* deployment, or new task. Machine-wide NAVARCH_CLAUDE_EXTRA_ARGS are
|
|
14
|
+
* deliberately not applied here: they could add --dangerously-skip-permissions
|
|
15
|
+
* or --allowedTools and widen this boundary.
|
|
16
|
+
*/
|
|
17
|
+
exports.CLAUDE_CONVERSATION_TOOLS = "Read,Grep,Glob";
|
|
18
|
+
const STDERR_TAIL_BYTES = 8 * 1024;
|
|
19
|
+
/**
|
|
20
|
+
* Incremental parser for `claude -p --output-format stream-json --verbose
|
|
21
|
+
* --include-partial-messages`. Chunks may split a record anywhere. Text is
|
|
22
|
+
* emitted from `text_delta` records the moment they are parsed. A complete
|
|
23
|
+
* `assistant` message is used only for a message that streamed no deltas
|
|
24
|
+
* (a CLI without partial-message support), so text is never emitted twice.
|
|
25
|
+
*/
|
|
26
|
+
class ClaudeStreamParser {
|
|
27
|
+
emit;
|
|
28
|
+
onRecord;
|
|
29
|
+
buffer = "";
|
|
30
|
+
currentMessageId = null;
|
|
31
|
+
streamedMessages = new Set();
|
|
32
|
+
emittedText = false;
|
|
33
|
+
separatorPending = false;
|
|
34
|
+
result = null;
|
|
35
|
+
/** `onRecord` fires for each complete record before any event it produces is emitted. */
|
|
36
|
+
constructor(emit, onRecord = () => undefined) {
|
|
37
|
+
this.emit = emit;
|
|
38
|
+
this.onRecord = onRecord;
|
|
39
|
+
}
|
|
40
|
+
/** Returns the number of complete records parsed from this chunk. */
|
|
41
|
+
push(chunk) {
|
|
42
|
+
this.buffer += chunk;
|
|
43
|
+
let records = 0;
|
|
44
|
+
for (;;) {
|
|
45
|
+
const newline = this.buffer.indexOf("\n");
|
|
46
|
+
if (newline < 0)
|
|
47
|
+
break;
|
|
48
|
+
const line = this.buffer.slice(0, newline).trim();
|
|
49
|
+
this.buffer = this.buffer.slice(newline + 1);
|
|
50
|
+
if (!line)
|
|
51
|
+
continue;
|
|
52
|
+
records += 1;
|
|
53
|
+
this.onRecord();
|
|
54
|
+
this.record(line);
|
|
55
|
+
}
|
|
56
|
+
return records;
|
|
57
|
+
}
|
|
58
|
+
/** Parses a final record that lacks a trailing newline. */
|
|
59
|
+
flush() {
|
|
60
|
+
const line = this.buffer.trim();
|
|
61
|
+
this.buffer = "";
|
|
62
|
+
if (line) {
|
|
63
|
+
this.onRecord();
|
|
64
|
+
this.record(line);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Used when the agent finished without any streamed text. */
|
|
68
|
+
get hasEmittedText() {
|
|
69
|
+
return this.emittedText;
|
|
70
|
+
}
|
|
71
|
+
text(text) {
|
|
72
|
+
if (!text)
|
|
73
|
+
return;
|
|
74
|
+
this.emit({ type: "text", text: this.separatorPending ? `\n\n${text}` : text });
|
|
75
|
+
this.separatorPending = false;
|
|
76
|
+
this.emittedText = true;
|
|
77
|
+
}
|
|
78
|
+
record(line) {
|
|
79
|
+
let parsed;
|
|
80
|
+
try {
|
|
81
|
+
parsed = JSON.parse(line);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (!parsed || typeof parsed !== "object")
|
|
87
|
+
return;
|
|
88
|
+
const record = parsed;
|
|
89
|
+
switch (record.type) {
|
|
90
|
+
case "stream_event":
|
|
91
|
+
this.streamEvent(record.event, record.parent_tool_use_id);
|
|
92
|
+
break;
|
|
93
|
+
case "assistant":
|
|
94
|
+
this.assistant(record);
|
|
95
|
+
break;
|
|
96
|
+
case "user":
|
|
97
|
+
this.toolResults(record);
|
|
98
|
+
break;
|
|
99
|
+
case "result":
|
|
100
|
+
this.result = {
|
|
101
|
+
isError: record.is_error === true || (typeof record.subtype === "string" && record.subtype !== "success"),
|
|
102
|
+
text: typeof record.result === "string" ? record.result : null,
|
|
103
|
+
};
|
|
104
|
+
if (this.result.isError)
|
|
105
|
+
this.emit({ type: "failure", reason: String(record.subtype ?? "error") });
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
streamEvent(event, parentToolUseId) {
|
|
110
|
+
// Sub-agent output is not part of the user's answer.
|
|
111
|
+
if (!event || typeof event !== "object" || parentToolUseId)
|
|
112
|
+
return;
|
|
113
|
+
const e = event;
|
|
114
|
+
const message = asRec(e.message);
|
|
115
|
+
const delta = asRec(e.delta);
|
|
116
|
+
if (e.type === "message_start") {
|
|
117
|
+
this.currentMessageId = typeof message?.id === "string" ? message.id : null;
|
|
118
|
+
}
|
|
119
|
+
else if (e.type === "content_block_start") {
|
|
120
|
+
const block = asRec(e.content_block);
|
|
121
|
+
if (block?.type === "tool_use" && typeof block.name === "string") {
|
|
122
|
+
this.emit({ type: "tool_status", tool: block.name, phase: "started" });
|
|
123
|
+
}
|
|
124
|
+
else if (block?.type === "text" && this.emittedText) {
|
|
125
|
+
this.separatorPending = true;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
else if (e.type === "content_block_delta" && delta?.type === "text_delta" && typeof delta.text === "string") {
|
|
129
|
+
if (this.currentMessageId)
|
|
130
|
+
this.streamedMessages.add(this.currentMessageId);
|
|
131
|
+
this.text(delta.text);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
assistant(record) {
|
|
135
|
+
const message = asRec(record.message);
|
|
136
|
+
if (record.parent_tool_use_id || !message || !Array.isArray(message.content))
|
|
137
|
+
return;
|
|
138
|
+
if (typeof message.id === "string" && this.streamedMessages.has(message.id))
|
|
139
|
+
return;
|
|
140
|
+
for (const item of message.content) {
|
|
141
|
+
const block = asRec(item);
|
|
142
|
+
if (block?.type === "text" && typeof block.text === "string") {
|
|
143
|
+
if (this.emittedText)
|
|
144
|
+
this.separatorPending = true;
|
|
145
|
+
this.text(block.text);
|
|
146
|
+
}
|
|
147
|
+
else if (block?.type === "tool_use" && typeof block.name === "string" && !(typeof message.id === "string" && this.streamedMessages.has(message.id))) {
|
|
148
|
+
this.emit({ type: "tool_status", tool: block.name, phase: "started" });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
toolResults(record) {
|
|
153
|
+
const content = asRec(record.message)?.content;
|
|
154
|
+
if (!Array.isArray(content))
|
|
155
|
+
return;
|
|
156
|
+
for (const item of content) {
|
|
157
|
+
const block = asRec(item);
|
|
158
|
+
if (block?.type === "tool_result" && block.is_error === true) {
|
|
159
|
+
this.emit({ type: "tool_status", tool: "tool", phase: "failed" });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
exports.ClaudeStreamParser = ClaudeStreamParser;
|
|
165
|
+
function asRec(value) {
|
|
166
|
+
return value && typeof value === "object" ? value : null;
|
|
167
|
+
}
|
|
168
|
+
/** Fixed, read-only argument list for one conversation turn. */
|
|
169
|
+
function claudeConversationArgs(options) {
|
|
170
|
+
const args = [
|
|
171
|
+
"-p",
|
|
172
|
+
"--output-format", "stream-json",
|
|
173
|
+
"--verbose",
|
|
174
|
+
"--include-partial-messages",
|
|
175
|
+
// The server holds the bounded history. A local Claude transcript would
|
|
176
|
+
// be a second, unbounded private copy of the conversation.
|
|
177
|
+
"--no-session-persistence",
|
|
178
|
+
// No operator hooks, project settings or MCP servers (including the
|
|
179
|
+
// lease-scoped Navarch task tools) in a conversation.
|
|
180
|
+
"--setting-sources", "",
|
|
181
|
+
"--strict-mcp-config",
|
|
182
|
+
"--tools", exports.CLAUDE_CONVERSATION_TOOLS,
|
|
183
|
+
"--permission-mode", "default",
|
|
184
|
+
];
|
|
185
|
+
if (options.settingsPath)
|
|
186
|
+
args.push("--settings", options.settingsPath);
|
|
187
|
+
if (options.model)
|
|
188
|
+
args.push("--model", options.model);
|
|
189
|
+
return args;
|
|
190
|
+
}
|
|
191
|
+
async function runClaudeConversationTurn(options) {
|
|
192
|
+
const idle = (aborted) => ({
|
|
193
|
+
exitCode: null, timedOut: false, aborted, succeeded: false, firstEventMs: null, firstTextMs: null, stderr: "",
|
|
194
|
+
});
|
|
195
|
+
if (options.signal.aborted)
|
|
196
|
+
return idle(true);
|
|
197
|
+
const startedAt = node_perf_hooks_1.performance.now();
|
|
198
|
+
let firstEventMs = null;
|
|
199
|
+
let firstTextMs = null;
|
|
200
|
+
let timedOut = false;
|
|
201
|
+
let aborted = false;
|
|
202
|
+
let closed = false;
|
|
203
|
+
let stderr = "";
|
|
204
|
+
const deliver = (event) => {
|
|
205
|
+
// After stop, timeout or exit, late output must not reach the caller.
|
|
206
|
+
if (closed || aborted || timedOut)
|
|
207
|
+
return;
|
|
208
|
+
if (event.type === "text" && firstTextMs === null)
|
|
209
|
+
firstTextMs = node_perf_hooks_1.performance.now() - startedAt;
|
|
210
|
+
options.onEvent(event);
|
|
211
|
+
};
|
|
212
|
+
const parser = new ClaudeStreamParser(deliver, () => {
|
|
213
|
+
if (firstEventMs === null)
|
|
214
|
+
firstEventMs = node_perf_hooks_1.performance.now() - startedAt;
|
|
215
|
+
});
|
|
216
|
+
return new Promise((resolve) => {
|
|
217
|
+
const child = (0, node_child_process_1.spawn)(options.bin, claudeConversationArgs(options), {
|
|
218
|
+
detached: process.platform !== "win32",
|
|
219
|
+
cwd: options.cwd,
|
|
220
|
+
env: options.env,
|
|
221
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
222
|
+
});
|
|
223
|
+
child.stdin?.on("error", () => undefined);
|
|
224
|
+
child.stdin?.end(options.prompt);
|
|
225
|
+
options.onProcessStarted?.(child.pid);
|
|
226
|
+
const timer = options.timeoutMs > 0 ? setTimeout(() => {
|
|
227
|
+
timedOut = true;
|
|
228
|
+
(0, kill_agent_cjs_1.killAgent)(child);
|
|
229
|
+
}, options.timeoutMs) : undefined;
|
|
230
|
+
const onAbort = () => {
|
|
231
|
+
aborted = true;
|
|
232
|
+
(0, kill_agent_cjs_1.killAgent)(child);
|
|
233
|
+
};
|
|
234
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
235
|
+
child.stdout.setEncoding("utf8");
|
|
236
|
+
child.stdout.on("data", (chunk) => {
|
|
237
|
+
parser.push(chunk);
|
|
238
|
+
});
|
|
239
|
+
child.stderr.setEncoding("utf8");
|
|
240
|
+
child.stderr.on("data", (chunk) => {
|
|
241
|
+
stderr = (stderr + chunk).slice(-STDERR_TAIL_BYTES);
|
|
242
|
+
});
|
|
243
|
+
const finish = (exitCode) => {
|
|
244
|
+
if (closed)
|
|
245
|
+
return;
|
|
246
|
+
parser.flush();
|
|
247
|
+
// An answer delivered only in the final result record (no stream deltas).
|
|
248
|
+
if (!parser.hasEmittedText && parser.result && !parser.result.isError && parser.result.text) {
|
|
249
|
+
deliver({ type: "text", text: parser.result.text });
|
|
250
|
+
}
|
|
251
|
+
closed = true;
|
|
252
|
+
clearTimeout(timer);
|
|
253
|
+
options.signal.removeEventListener("abort", onAbort);
|
|
254
|
+
resolve({
|
|
255
|
+
exitCode, timedOut, aborted,
|
|
256
|
+
succeeded: exitCode === 0 && !aborted && !timedOut && !!parser.result && !parser.result.isError,
|
|
257
|
+
firstEventMs, firstTextMs, stderr,
|
|
258
|
+
});
|
|
259
|
+
};
|
|
260
|
+
child.on("error", (err) => {
|
|
261
|
+
stderr = (stderr + `\n${String(err)}`).slice(-STDERR_TAIL_BYTES);
|
|
262
|
+
finish(null);
|
|
263
|
+
});
|
|
264
|
+
child.on("close", (code) => finish(code));
|
|
265
|
+
if (options.signal.aborted)
|
|
266
|
+
onAbort();
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
exports.claudeConversationRunner = { run: runClaudeConversationTurn };
|
package/dist/adapters/claude.cjs
CHANGED
|
@@ -5,6 +5,7 @@ exports.runClaudeCodeAdapter = runClaudeCodeAdapter;
|
|
|
5
5
|
const node_child_process_1 = require("node:child_process");
|
|
6
6
|
const exit_conditions_cjs_1 = require("../exit-conditions.cjs");
|
|
7
7
|
const kill_agent_cjs_1 = require("../kill-agent.cjs");
|
|
8
|
+
const claude_conversation_cjs_1 = require("./claude-conversation.cjs");
|
|
8
9
|
const adapter_capacity_cjs_1 = require("../adapter-capacity.cjs");
|
|
9
10
|
const DEFAULT_CLAUDE_MODEL = "claude-opus-5-5";
|
|
10
11
|
const FALLBACK_CLAUDE_MODEL = "claude-opus-5-5";
|
|
@@ -281,4 +282,5 @@ function shellQuote(value) {
|
|
|
281
282
|
exports.claudeCodeAdapter = {
|
|
282
283
|
agentType: "claude-code",
|
|
283
284
|
run: runClaudeCodeAdapter,
|
|
285
|
+
conversation: claude_conversation_cjs_1.claudeConversationRunner,
|
|
284
286
|
};
|
package/dist/api.cjs
CHANGED
|
@@ -158,6 +158,20 @@ class NavarchApiClient {
|
|
|
158
158
|
throw new Error("issueSecrets: empty response from control plane.");
|
|
159
159
|
return result;
|
|
160
160
|
}
|
|
161
|
+
/** Navi runtime v1: advertises conversation support for 90 s and returns queued turns for this machine. */
|
|
162
|
+
async pollConversations(machineId) {
|
|
163
|
+
const result = await this.request("POST", `/api/machines/${encodeURIComponent(machineId)}/conversations`, { protocol: 1 });
|
|
164
|
+
if (!result)
|
|
165
|
+
throw new Error("pollConversations: empty response from control plane.");
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
/** Navi runtime v1 claim / renew / append for one turn. */
|
|
169
|
+
async conversationCommand(machineId, conversationId, command) {
|
|
170
|
+
const result = await this.request("POST", `/api/machines/${encodeURIComponent(machineId)}/conversations/${encodeURIComponent(conversationId)}`, command);
|
|
171
|
+
if (!result)
|
|
172
|
+
throw new Error("conversationCommand: empty response from control plane.");
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
161
175
|
/** Create a signed upload target for this lease's redacted transcript. */
|
|
162
176
|
async getTranscriptUploadUrl(leaseId) {
|
|
163
177
|
const result = await this.request("POST", `/api/dispatch/${encodeURIComponent(leaseId)}/transcript-upload-url`, {});
|
|
@@ -13,6 +13,7 @@ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
|
13
13
|
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
14
14
|
const playwright_1 = require("playwright");
|
|
15
15
|
const zod_1 = require("zod");
|
|
16
|
+
const local_browser_cjs_1 = require("./local-browser.cjs");
|
|
16
17
|
const node_child_process_1 = require("node:child_process");
|
|
17
18
|
const node_util_1 = require("node:util");
|
|
18
19
|
const node_path_1 = __importDefault(require("node:path"));
|
|
@@ -46,7 +47,8 @@ const captureShape = {
|
|
|
46
47
|
exports.captureSchema = zod_1.z.object(captureShape);
|
|
47
48
|
/** Runs in the supervisor, outside the coding agent's OS sandbox. Fresh
|
|
48
49
|
* contexts have no user cookies, arbitrary script execution, or file access.
|
|
49
|
-
*
|
|
50
|
+
* The lease gets the narrow capture operation plus the loopback-only
|
|
51
|
+
* local_browser_* tools (local-browser.cts). */
|
|
50
52
|
async function startBrowserEvidence(options) {
|
|
51
53
|
const token = (0, node_crypto_1.randomBytes)(32).toString("hex");
|
|
52
54
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
@@ -57,6 +59,10 @@ async function startBrowserEvidence(options) {
|
|
|
57
59
|
let browser;
|
|
58
60
|
let activeCapture;
|
|
59
61
|
let attempts = 0;
|
|
62
|
+
const localBrowser = (0, local_browser_cjs_1.createLocalBrowser)({
|
|
63
|
+
launch: options.launch ?? launchCaptureBrowser,
|
|
64
|
+
isActive: () => !closed && options.isActive(),
|
|
65
|
+
});
|
|
60
66
|
const assertActive = () => {
|
|
61
67
|
if (closed || !options.isActive())
|
|
62
68
|
throw new Error("Browser evidence lease is no longer active.");
|
|
@@ -204,6 +210,7 @@ async function startBrowserEvidence(options) {
|
|
|
204
210
|
return { isError: true, content: [{ type: "text", text: String(error) }] };
|
|
205
211
|
}
|
|
206
212
|
});
|
|
213
|
+
(0, local_browser_cjs_1.registerLocalBrowserTools)(server, localBrowser);
|
|
207
214
|
res.on("close", () => { void transport.close(); void server.close(); });
|
|
208
215
|
try {
|
|
209
216
|
await server.connect(transport);
|
|
@@ -253,6 +260,7 @@ async function startBrowserEvidence(options) {
|
|
|
253
260
|
const stopped = new Promise((resolve) => http.close(() => resolve()));
|
|
254
261
|
http.closeAllConnections();
|
|
255
262
|
await browser?.close().catch(() => undefined);
|
|
263
|
+
await localBrowser.close();
|
|
256
264
|
await activeCapture?.catch(() => undefined);
|
|
257
265
|
await stopped;
|
|
258
266
|
},
|
package/dist/browser-probe.cjs
CHANGED
|
@@ -74,8 +74,19 @@ async function probeBrowserCapability(config, deps = {}) {
|
|
|
74
74
|
const platform = deps.platform ?? process.platform;
|
|
75
75
|
if (codexSandboxNeedsProbe(config, platform)) {
|
|
76
76
|
const sandboxFailure = await probeCodexSandbox(config, deps);
|
|
77
|
+
if (sandboxFailure?.browserOnly && !config.mcpConfigPath) {
|
|
78
|
+
// Node runs in the sandbox, so the agent can start its services there;
|
|
79
|
+
// only Chromium cannot. Sessions get the supervisor's local_browser_*
|
|
80
|
+
// tools (local-browser.cts) instead, which reach the same loopback.
|
|
81
|
+
return {
|
|
82
|
+
status: "passed",
|
|
83
|
+
reason: truncate(`Headless Chromium cannot start inside the Codex sandbox (${sandboxFailure.detail}); ` +
|
|
84
|
+
"Codex sessions browse local services through Navarch's local_browser tools, which run outside it."),
|
|
85
|
+
checked_at: now().toISOString(),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
77
88
|
if (sandboxFailure) {
|
|
78
|
-
return { status: "failed", reason: truncate(sandboxFailure), checked_at: now().toISOString() };
|
|
89
|
+
return { status: "failed", reason: truncate(sandboxFailure.reason), checked_at: now().toISOString() };
|
|
79
90
|
}
|
|
80
91
|
}
|
|
81
92
|
return { status: "passed", checked_at: now().toISOString() };
|
|
@@ -215,8 +226,9 @@ async function probeCodexSandbox(config, deps) {
|
|
|
215
226
|
// sandbox view: a path outside the granted read roots, not an old CLI.
|
|
216
227
|
const hidden = /bwrap: execvp .*No such file or directory/.test(output);
|
|
217
228
|
const opensslConfig = /OpenSSL configuration error/.test(output);
|
|
218
|
-
|
|
219
|
-
|
|
229
|
+
const detail = result.timedOut ? "timed out" : output.trim() ? summarizeLaunchFailure(output) : `exit ${result.code}`;
|
|
230
|
+
const reason = ("Could not verify a browser inside the Codex sandbox: `codex sandbox` did not run the probe " +
|
|
231
|
+
`(${detail}). ` +
|
|
220
232
|
(hidden
|
|
221
233
|
? "The sandbox cannot see that executable; add its install directory to NAVARCH_GUARD_EXTRA_ROOTS, " +
|
|
222
234
|
"or run browser tasks with another agent."
|
|
@@ -224,19 +236,22 @@ async function probeCodexSandbox(config, deps) {
|
|
|
224
236
|
? "Node cannot read its OpenSSL config inside the sandbox, so Codex sessions cannot run node either; " +
|
|
225
237
|
"update the Navarch runtime, or add that file's directory to NAVARCH_GUARD_EXTRA_ROOTS."
|
|
226
238
|
: "Update the Codex CLI, or run browser tasks with another agent."));
|
|
239
|
+
return { reason, detail, browserOnly: false };
|
|
227
240
|
}
|
|
228
241
|
const detail = result.timedOut ? "the launch timed out." : summarizeLaunchFailure(output);
|
|
229
242
|
const machPort = /bootstrap_check_in|MachPortRendezvous|mach_port_rendezvous/i.test(output);
|
|
230
|
-
|
|
243
|
+
const reason = (`Headless Chromium cannot start inside the Codex sandbox: ${detail} ` +
|
|
231
244
|
(machPort
|
|
232
245
|
? "Codex's macOS sandbox denies Chromium's Mach port registration. "
|
|
233
246
|
: "") +
|
|
234
247
|
"Run browser tasks with another agent (for example Claude Code), use NAVARCH_SANDBOX_MODE=docker, " +
|
|
235
248
|
"or run Codex without its sandbox.");
|
|
249
|
+
return { reason, detail, browserOnly: true };
|
|
236
250
|
}
|
|
237
251
|
catch (error) {
|
|
238
252
|
log.warn(`codex sandbox browser probe could not run: ${String(error)}`);
|
|
239
|
-
|
|
253
|
+
const detail = summarizeLaunchFailure(String(error));
|
|
254
|
+
return { reason: `Could not verify a browser inside the Codex sandbox: ${detail}`, detail, browserOnly: false };
|
|
240
255
|
}
|
|
241
256
|
finally {
|
|
242
257
|
await node_fs_1.promises.rm(probeRoot, { recursive: true, force: true }).catch(() => undefined);
|
package/dist/claim-loop.cjs
CHANGED
|
@@ -6,6 +6,7 @@ const api_cjs_1 = require("./api.cjs");
|
|
|
6
6
|
const logger_cjs_1 = require("./logger.cjs");
|
|
7
7
|
const log = (0, logger_cjs_1.createLogger)("claim");
|
|
8
8
|
const MAX_CLAIM_BACKOFF_MS = 60_000;
|
|
9
|
+
const pendingClaimSlot = "pending-claim-request";
|
|
9
10
|
/**
|
|
10
11
|
* Polls dispatch/claim on an interval, gated by available capacity
|
|
11
12
|
* (implementation-plan.md WP-07 "Claim loop: poll dispatch, receive context
|
|
@@ -69,13 +70,24 @@ class ClaimLoop {
|
|
|
69
70
|
// the dispatcher records it on the lease and excludes it from review
|
|
70
71
|
// tasks of PRs this session authors.
|
|
71
72
|
const sessionId = (0, node_crypto_1.randomUUID)();
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
73
|
+
// Hold the slot for the duration of the claim request. Navi conversation
|
|
74
|
+
// turns also take slots; without this reservation one could take the last
|
|
75
|
+
// slot while the control plane is granting a lease, stranding that lease.
|
|
76
|
+
const availableCapacity = this.capacity.available();
|
|
77
|
+
this.capacity.acquire(pendingClaimSlot);
|
|
78
|
+
let claimed;
|
|
79
|
+
try {
|
|
80
|
+
claimed = await this.api.claim({
|
|
81
|
+
available_capacity: availableCapacity,
|
|
82
|
+
capabilities: this.config.capabilities,
|
|
83
|
+
agent_type: this.config.agentType,
|
|
84
|
+
runtimes: this.config.runtimes,
|
|
85
|
+
session_id: sessionId,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
this.capacity.release(pendingClaimSlot);
|
|
90
|
+
}
|
|
79
91
|
if (this.consecutiveFailures > 0) {
|
|
80
92
|
log.info(`claim polling recovered after ${this.consecutiveFailures} failed attempt(s)`);
|
|
81
93
|
}
|
package/dist/cli.cjs
CHANGED
|
@@ -12,6 +12,8 @@ const api_cjs_1 = require("./api.cjs");
|
|
|
12
12
|
const capacity_cjs_1 = require("./capacity.cjs");
|
|
13
13
|
const heartbeat_loop_cjs_1 = require("./heartbeat-loop.cjs");
|
|
14
14
|
const claim_loop_cjs_1 = require("./claim-loop.cjs");
|
|
15
|
+
const loop_cjs_1 = require("./conversation/loop.cjs");
|
|
16
|
+
const capability_cjs_1 = require("./conversation/capability.cjs");
|
|
15
17
|
const session_cjs_1 = require("./session.cjs");
|
|
16
18
|
const sandbox_cjs_1 = require("./sandbox.cjs");
|
|
17
19
|
const logger_cjs_1 = require("./logger.cjs");
|
|
@@ -246,7 +248,7 @@ async function startCommand(flags) {
|
|
|
246
248
|
log.warn(`not advertising browser-use: ${browserProbe.reason}`);
|
|
247
249
|
}
|
|
248
250
|
else if (browserProbe.status === "passed") {
|
|
249
|
-
log.info(
|
|
251
|
+
log.info(`browser probe passed; advertising browser-use${browserProbe.reason ? ` (${browserProbe.reason})` : ""}`);
|
|
250
252
|
}
|
|
251
253
|
const config = {
|
|
252
254
|
...configuredConfig,
|
|
@@ -267,6 +269,13 @@ async function startCommand(flags) {
|
|
|
267
269
|
return (0, session_cjs_1.runSession)({ api, config, shutdownSignal: runtimeShutdown.signal }, claimed, sessionId)
|
|
268
270
|
.finally(() => activeSessionIds.delete(sessionId));
|
|
269
271
|
});
|
|
272
|
+
const conversationLoop = new loop_cjs_1.ConversationLoop({
|
|
273
|
+
api,
|
|
274
|
+
machineId: identity.machine_id,
|
|
275
|
+
config,
|
|
276
|
+
capacity,
|
|
277
|
+
shutdownSignal: runtimeShutdown.signal,
|
|
278
|
+
});
|
|
270
279
|
const bootId = (0, node_crypto_1.randomUUID)();
|
|
271
280
|
const updateCoordinatorRef = {};
|
|
272
281
|
let readySent = false;
|
|
@@ -282,6 +291,7 @@ async function startCommand(flags) {
|
|
|
282
291
|
}
|
|
283
292
|
}, (status) => {
|
|
284
293
|
claimLoop.stop();
|
|
294
|
+
conversationLoop.stop();
|
|
285
295
|
worktreeJanitor.stop();
|
|
286
296
|
log.error(status === 410
|
|
287
297
|
? `agent ${identity.name} has been removed from the fleet; it can no longer claim tasks.`
|
|
@@ -298,12 +308,14 @@ async function startCommand(flags) {
|
|
|
298
308
|
updateCoordinatorRef.current = new update_coordinator_cjs_1.RuntimeUpdateCoordinator({
|
|
299
309
|
config,
|
|
300
310
|
claimLoop,
|
|
311
|
+
conversationLoop,
|
|
301
312
|
capacity,
|
|
302
313
|
heartbeat,
|
|
303
314
|
});
|
|
304
315
|
heartbeat.start();
|
|
305
316
|
worktreeJanitor.start();
|
|
306
317
|
claimLoop.start();
|
|
318
|
+
conversationLoop.start();
|
|
307
319
|
log.info(`navarch-runtime started: machine=${identity.name} agent=${config.agentType} max_sessions=${config.maxSessions} api_base=${identity.api_base}`);
|
|
308
320
|
let shuttingDown = false;
|
|
309
321
|
const shutdown = () => {
|
|
@@ -316,6 +328,7 @@ async function startCommand(flags) {
|
|
|
316
328
|
log.info("shutting down: draining active sessions...");
|
|
317
329
|
heartbeat.setUpdateState("draining");
|
|
318
330
|
await claimLoop.drain();
|
|
331
|
+
await conversationLoop.drain();
|
|
319
332
|
await capacity.waitForIdle();
|
|
320
333
|
worktreeJanitor.stop();
|
|
321
334
|
heartbeat.stop();
|
|
@@ -391,6 +404,8 @@ async function doctorCommand(flags) {
|
|
|
391
404
|
console.log(`sandbox_mode: ${config.sandboxMode}`);
|
|
392
405
|
console.log(`worktree_guard: ${config.worktreeGuard ? "on" : "off"}`);
|
|
393
406
|
console.log(`runtimes: ${config.runtimes?.join(", ") ?? config.agentType}`);
|
|
407
|
+
const conversation = (0, capability_cjs_1.conversationCapability)(config);
|
|
408
|
+
console.log(`navi_chat: ${conversation.state.toUpperCase()}${conversation.state === "unsupported" ? ` — ${conversation.reason}` : ` (${config.agentType}, read-only)`}`);
|
|
394
409
|
console.log(`docker: ${dockerOk ? "available" : "NOT AVAILABLE (docker-backed sessions will fail)"}`);
|
|
395
410
|
const browserProbe = await (0, browser_probe_cjs_1.probeBrowserCapability)(config);
|
|
396
411
|
console.log(`browser: ${browserProbe.status.toUpperCase()}${browserProbe.reason ? ` — ${browserProbe.reason}` : ""}`);
|
package/dist/config.cjs
CHANGED
|
@@ -178,6 +178,11 @@ function loadRuntimeConfig(env = process.env) {
|
|
|
178
178
|
// by default; operators who would rather share nothing can opt out.
|
|
179
179
|
reportUsageLimits: !["off", "false", "0"].includes(env.NAVARCH_USAGE_LIMITS ?? ""),
|
|
180
180
|
browserProbe: !["off", "false", "0"].includes(env.NAVARCH_BROWSER_PROBE ?? ""),
|
|
181
|
+
naviConversations: !["off", "false", "0"].includes(env.NAVARCH_NAVI_CONVERSATIONS ?? ""),
|
|
182
|
+
// The server stops waiting for a capability advertisement after 90 s and
|
|
183
|
+
// asks for a poll at least every 30 s; the floor only bounds control-plane load.
|
|
184
|
+
conversationPollIntervalMs: Math.min(30_000, Math.max(1_000, envInt(env, "NAVARCH_CONVERSATION_POLL_MS", 5_000))),
|
|
185
|
+
conversationTurnTimeoutMs: Math.min(9 * 60_000, envInt(env, "NAVARCH_CONVERSATION_TURN_TIMEOUT_MS", 9 * 60_000)),
|
|
181
186
|
codexHome: defaultCodexHome(env),
|
|
182
187
|
claudeConfigPath: defaultClaudeConfigPath(env),
|
|
183
188
|
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.conversationCapability = conversationCapability;
|
|
4
|
+
exports.conversationSupportMatrix = conversationSupportMatrix;
|
|
5
|
+
const index_cjs_1 = require("../adapters/index.cjs");
|
|
6
|
+
/**
|
|
7
|
+
* Whether this runtime can execute a Navi conversation turn, with the reason
|
|
8
|
+
* when it cannot. The runtime polls (and so advertises conversation support
|
|
9
|
+
* to the control plane) only when this returns `supported`.
|
|
10
|
+
*
|
|
11
|
+
* A conversation always runs on the machine's configured primary adapter
|
|
12
|
+
* (`agentType`). An unsupported primary adapter is reported, never replaced
|
|
13
|
+
* by another installed agent: the user selected this runtime for the agent it
|
|
14
|
+
* runs.
|
|
15
|
+
*/
|
|
16
|
+
function conversationCapability(config) {
|
|
17
|
+
if (!config.naviConversations) {
|
|
18
|
+
return { state: "unsupported", reason: "Disabled by NAVARCH_NAVI_CONVERSATIONS=off." };
|
|
19
|
+
}
|
|
20
|
+
const adapter = (0, index_cjs_1.selectAdapter)(config.agentType);
|
|
21
|
+
if (!adapter.conversation) {
|
|
22
|
+
return {
|
|
23
|
+
state: "unsupported",
|
|
24
|
+
reason: `The ${config.agentType} adapter has no streamed read-only conversation path.`,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (config.sandboxMode !== "host") {
|
|
28
|
+
return {
|
|
29
|
+
state: "unsupported",
|
|
30
|
+
reason: "Conversations run on the host; NAVARCH_SANDBOX_MODE=docker is not supported yet.",
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (!config.worktreeGuard) {
|
|
34
|
+
return {
|
|
35
|
+
state: "unsupported",
|
|
36
|
+
reason: "Conversations need the worktree read boundary; NAVARCH_WORKTREE_GUARD is off.",
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return { state: "supported" };
|
|
40
|
+
}
|
|
41
|
+
/** Per-adapter support table for docs and diagnostics (host sandbox, guard on, feature enabled). */
|
|
42
|
+
function conversationSupportMatrix() {
|
|
43
|
+
const agents = ["claude-code", "codex", "gemini", "opencode", "acp", "qoder", "pi-local", "pi"];
|
|
44
|
+
return agents.map((agentType) => ({
|
|
45
|
+
agentType,
|
|
46
|
+
capability: conversationCapability({
|
|
47
|
+
agentType,
|
|
48
|
+
sandboxMode: "host",
|
|
49
|
+
worktreeGuard: true,
|
|
50
|
+
naviConversations: true,
|
|
51
|
+
}),
|
|
52
|
+
}));
|
|
53
|
+
}
|