@sagentlab/navarch-runtime 0.1.116 → 0.1.118
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 -17
- package/bin/muse-mcp-bridge.cjs +3 -0
- package/bin/worktree-guard-hook.cjs +74 -2
- package/dist/adapters/index.cjs +6 -1
- package/dist/adapters/muse.cjs +433 -0
- package/dist/agent-auth.cjs +2 -1
- package/dist/config.cjs +8 -1
- package/dist/conversation/capability.cjs +1 -1
- package/dist/exit-conditions.cjs +1 -1
- package/dist/muse-mcp-bridge.cjs +167 -0
- package/dist/session.cjs +30 -8
- package/node_modules/@muse-code/sdk/LICENSE +21 -0
- package/node_modules/@muse-code/sdk/README.md +88 -0
- package/node_modules/@muse-code/sdk/dist/src/connection/connection.d.ts +149 -0
- package/node_modules/@muse-code/sdk/dist/src/connection/connection.js +581 -0
- package/node_modules/@muse-code/sdk/dist/src/connection/spawn.d.ts +188 -0
- package/node_modules/@muse-code/sdk/dist/src/connection/spawn.js +496 -0
- package/node_modules/@muse-code/sdk/dist/src/errors.d.ts +71 -0
- package/node_modules/@muse-code/sdk/dist/src/errors.js +76 -0
- package/node_modules/@muse-code/sdk/dist/src/facade/approval.d.ts +96 -0
- package/node_modules/@muse-code/sdk/dist/src/facade/approval.js +293 -0
- package/node_modules/@muse-code/sdk/dist/src/facade/client.d.ts +171 -0
- package/node_modules/@muse-code/sdk/dist/src/facade/client.js +442 -0
- package/node_modules/@muse-code/sdk/dist/src/facade/discarded.d.ts +36 -0
- package/node_modules/@muse-code/sdk/dist/src/facade/discarded.js +36 -0
- package/node_modules/@muse-code/sdk/dist/src/facade/gap-fill.d.ts +80 -0
- package/node_modules/@muse-code/sdk/dist/src/facade/gap-fill.js +388 -0
- package/node_modules/@muse-code/sdk/dist/src/facade/host-death.d.ts +139 -0
- package/node_modules/@muse-code/sdk/dist/src/facade/host-death.js +96 -0
- package/node_modules/@muse-code/sdk/dist/src/facade/host-start.d.ts +62 -0
- package/node_modules/@muse-code/sdk/dist/src/facade/host-start.js +81 -0
- package/node_modules/@muse-code/sdk/dist/src/facade/session.d.ts +309 -0
- package/node_modules/@muse-code/sdk/dist/src/facade/session.js +654 -0
- package/node_modules/@muse-code/sdk/dist/src/facade/turn-handle.d.ts +156 -0
- package/node_modules/@muse-code/sdk/dist/src/facade/turn-handle.js +271 -0
- package/node_modules/@muse-code/sdk/dist/src/facade/turn-submit.d.ts +72 -0
- package/node_modules/@muse-code/sdk/dist/src/facade/turn-submit.js +183 -0
- package/node_modules/@muse-code/sdk/dist/src/fingerprint.d.ts +37 -0
- package/node_modules/@muse-code/sdk/dist/src/fingerprint.js +54 -0
- package/node_modules/@muse-code/sdk/dist/src/fold/item-store.d.ts +110 -0
- package/node_modules/@muse-code/sdk/dist/src/fold/item-store.js +169 -0
- package/node_modules/@muse-code/sdk/dist/src/fold/session-fold.d.ts +536 -0
- package/node_modules/@muse-code/sdk/dist/src/fold/session-fold.js +427 -0
- package/node_modules/@muse-code/sdk/dist/src/fold/state-store.d.ts +68 -0
- package/node_modules/@muse-code/sdk/dist/src/fold/state-store.js +89 -0
- package/node_modules/@muse-code/sdk/dist/src/index.d.ts +65 -0
- package/node_modules/@muse-code/sdk/dist/src/index.js +57 -0
- package/node_modules/@muse-code/sdk/dist/src/msp.d.ts +3380 -0
- package/node_modules/@muse-code/sdk/dist/src/pending/pending-command-set.d.ts +361 -0
- package/node_modules/@muse-code/sdk/dist/src/pending/pending-command-set.js +468 -0
- package/node_modules/@muse-code/sdk/package.json +47 -0
- package/package.json +8 -4
|
@@ -0,0 +1,433 @@
|
|
|
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.museAdapter = exports.MUSE_HOST_GUARD_MESSAGE = void 0;
|
|
7
|
+
exports.runMuseAdapter = runMuseAdapter;
|
|
8
|
+
exports.sumMuseSessionCost = sumMuseSessionCost;
|
|
9
|
+
const node_child_process_1 = require("node:child_process");
|
|
10
|
+
const node_fs_1 = require("node:fs");
|
|
11
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
+
const node_crypto_1 = require("node:crypto");
|
|
13
|
+
const kill_agent_cjs_1 = require("../kill-agent.cjs");
|
|
14
|
+
const version_cjs_1 = require("../version.cjs");
|
|
15
|
+
const CANCEL_GRACE_MS = 2_000;
|
|
16
|
+
const SHUTDOWN_GRACE_MS = 30_000;
|
|
17
|
+
const TERMINATE_GRACE_MS = 2_000;
|
|
18
|
+
exports.MUSE_HOST_GUARD_MESSAGE = "Muse Code cannot yet prove Navarch's host worktree boundary. Use a custom " +
|
|
19
|
+
"NAVARCH_SANDBOX_MODE=docker image with Muse installed, or explicitly set " +
|
|
20
|
+
"NAVARCH_WORKTREE_GUARD=off only on an otherwise isolated machine.";
|
|
21
|
+
function record(value) {
|
|
22
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
|
|
23
|
+
}
|
|
24
|
+
function count(value) {
|
|
25
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
|
|
26
|
+
}
|
|
27
|
+
function deferred() {
|
|
28
|
+
let resolve;
|
|
29
|
+
const promise = new Promise((done) => { resolve = done; });
|
|
30
|
+
return { promise, resolve };
|
|
31
|
+
}
|
|
32
|
+
async function waitWithin(promise, ms) {
|
|
33
|
+
let timer;
|
|
34
|
+
await Promise.race([promise, new Promise((resolve) => { timer = setTimeout(resolve, ms); })]);
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Muse Session Protocol through the pinned official SDK's framed Connection.
|
|
39
|
+
* Own the child here (rather than the facade's private child) so Navarch can
|
|
40
|
+
* register its PID, stream raw output and kill its complete process group.
|
|
41
|
+
* The facade intentionally does not forward session-scoped MCP configuration.
|
|
42
|
+
* No `exec --json` fallback: its event contract is a different surface.
|
|
43
|
+
*/
|
|
44
|
+
async function runMuseAdapter(options) {
|
|
45
|
+
const empty = { exitCode: null, timedOut: false, killedByLeaseLoss: false, stdout: "", stderr: "" };
|
|
46
|
+
if (options.signal?.aborted)
|
|
47
|
+
return { ...empty, killedByLeaseLoss: true };
|
|
48
|
+
if (!options.dockerExec && options.museHostGuard !== false)
|
|
49
|
+
throw new Error(exports.MUSE_HOST_GUARD_MESSAGE);
|
|
50
|
+
if (!options.cwd || !node_path_1.default.isAbsolute(options.cwd))
|
|
51
|
+
throw new Error("Muse requires an absolute session worktree.");
|
|
52
|
+
if (options.extraArgs.some((arg) => /^(--yolo|--disable-sandbox|--disable-approval)(=|$)/.test(arg))) {
|
|
53
|
+
throw new Error("Muse permission and sandbox bypass flags are not supported by Navarch.");
|
|
54
|
+
}
|
|
55
|
+
const sourceServers = await readMcpServers(options.mcpConfigPath, options.env);
|
|
56
|
+
const sdk = await import("@muse-code/sdk");
|
|
57
|
+
const bridge = await prepareBridge(sourceServers, options);
|
|
58
|
+
try {
|
|
59
|
+
return await runMuseProcess(options, bridge.servers, sdk);
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
if (bridge.dir)
|
|
63
|
+
await node_fs_1.promises.rm(bridge.dir, { recursive: true, force: true });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function runMuseProcess(options, mcpServers, sdk) {
|
|
67
|
+
const child = spawnMuse(options);
|
|
68
|
+
child.stdout.setEncoding("utf8");
|
|
69
|
+
child.stderr.setEncoding("utf8");
|
|
70
|
+
child.stdin.on("error", () => undefined); // A dead host is handled by its exit/EOF.
|
|
71
|
+
let stdout = "";
|
|
72
|
+
let stderr = "";
|
|
73
|
+
let timedOut = false;
|
|
74
|
+
let killedByLeaseLoss = false;
|
|
75
|
+
let processCode = null;
|
|
76
|
+
let sessionId;
|
|
77
|
+
let turnId;
|
|
78
|
+
let durable = false;
|
|
79
|
+
let terminal;
|
|
80
|
+
let authError;
|
|
81
|
+
let protocolFailed = false;
|
|
82
|
+
const closed = deferred();
|
|
83
|
+
const stopped = deferred();
|
|
84
|
+
const completed = deferred();
|
|
85
|
+
const items = new Map();
|
|
86
|
+
const usage = new Map();
|
|
87
|
+
const pending = [];
|
|
88
|
+
let cancelling;
|
|
89
|
+
const diagnostic = (message) => { stderr += `${stderr ? "\n" : ""}${message}`; };
|
|
90
|
+
child.on("error", (error) => { diagnostic(`Muse process failed: ${error.message}`); closed.resolve(); });
|
|
91
|
+
child.on("exit", (code) => {
|
|
92
|
+
processCode = code;
|
|
93
|
+
// A descendant may keep stdio open after its root exits.
|
|
94
|
+
if (!options.dockerExec)
|
|
95
|
+
(0, kill_agent_cjs_1.killAgent)(child);
|
|
96
|
+
});
|
|
97
|
+
child.on("close", (code) => { processCode = code; closed.resolve(); });
|
|
98
|
+
child.stderr.on("data", (chunk) => {
|
|
99
|
+
stderr += chunk;
|
|
100
|
+
options.onActivity?.();
|
|
101
|
+
options.onOutput?.("stderr", chunk);
|
|
102
|
+
});
|
|
103
|
+
const connection = new sdk.Connection({
|
|
104
|
+
incoming: (async function* () {
|
|
105
|
+
for await (const chunk of child.stdout) {
|
|
106
|
+
const text = String(chunk);
|
|
107
|
+
stdout += text;
|
|
108
|
+
options.onActivity?.();
|
|
109
|
+
options.onOutput?.("stdout", text);
|
|
110
|
+
yield text;
|
|
111
|
+
}
|
|
112
|
+
})(),
|
|
113
|
+
write: (chunk) => new Promise((resolve, reject) => {
|
|
114
|
+
child.stdin.write(chunk, (error) => error ? reject(error) : resolve());
|
|
115
|
+
}),
|
|
116
|
+
});
|
|
117
|
+
const cancel = () => cancelling ??= (async () => {
|
|
118
|
+
if (sessionId && turnId) {
|
|
119
|
+
// Admission is not cancellation completion. Give the host a bounded
|
|
120
|
+
// chance to settle before killing; never keep a lost lease alive.
|
|
121
|
+
void connection.command("turn/cancel", { sessionId, turnId }, { maxAttempts: 1 }).catch(() => undefined);
|
|
122
|
+
await waitWithin(Promise.race([completed.promise, closed.promise]), CANCEL_GRACE_MS);
|
|
123
|
+
}
|
|
124
|
+
if (options.dockerExec) {
|
|
125
|
+
await options.dockerExec.runner.run("docker", ["kill", options.dockerExec.containerName], { timeoutMs: 10_000 }).catch(() => undefined);
|
|
126
|
+
}
|
|
127
|
+
(0, kill_agent_cjs_1.killAgent)(child);
|
|
128
|
+
})();
|
|
129
|
+
const stop = () => { stopped.resolve(); void cancel(); };
|
|
130
|
+
const onAbort = () => { killedByLeaseLoss = true; stop(); };
|
|
131
|
+
const timer = options.timeoutMs > 0 ? setTimeout(() => { timedOut = true; stop(); }, options.timeoutMs) : undefined;
|
|
132
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
133
|
+
if (options.signal?.aborted)
|
|
134
|
+
onAbort();
|
|
135
|
+
const consume = (notification) => {
|
|
136
|
+
const params = record(notification.params);
|
|
137
|
+
if (!params || params.sessionId !== sessionId)
|
|
138
|
+
return;
|
|
139
|
+
if (notification.method === "view/gap") {
|
|
140
|
+
protocolFailed = true;
|
|
141
|
+
diagnostic("Muse event stream has a gap; refusing an incomplete result.");
|
|
142
|
+
stop();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (notification.method === "item/completed") {
|
|
146
|
+
const item = record(params.item);
|
|
147
|
+
if (!item || item.turnId !== turnId || item.kind !== "agentMessage" || typeof item.itemId !== "string")
|
|
148
|
+
return;
|
|
149
|
+
const previous = items.get(item.itemId);
|
|
150
|
+
if (!previous || Number(item.revision) >= Number(previous.revision))
|
|
151
|
+
items.set(item.itemId, item);
|
|
152
|
+
}
|
|
153
|
+
else if (params.turnId === turnId && notification.method === "session/tokenUsage") {
|
|
154
|
+
if (typeof params.viewCursor === "string")
|
|
155
|
+
usage.set(params.viewCursor, params);
|
|
156
|
+
}
|
|
157
|
+
else if (params.turnId === turnId && notification.method === "turn/completed") {
|
|
158
|
+
terminal = params;
|
|
159
|
+
const error = record(params.error);
|
|
160
|
+
if (params.terminal === "failed" && error?.kind === "authRequired") {
|
|
161
|
+
authError = "Muse authentication is required. Open muse and run /login on this machine.";
|
|
162
|
+
}
|
|
163
|
+
completed.resolve();
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
connection.onNotification((notification) => {
|
|
167
|
+
if (!turnId) {
|
|
168
|
+
// Events can precede the acknowledgement. Do not assume commandId is
|
|
169
|
+
// turnId; use the authoritative ack, then drain in wire order.
|
|
170
|
+
if (pending.length >= 10_000) {
|
|
171
|
+
protocolFailed = true;
|
|
172
|
+
diagnostic("Muse sent too many events before acknowledging the turn.");
|
|
173
|
+
stop();
|
|
174
|
+
}
|
|
175
|
+
else
|
|
176
|
+
pending.push(notification);
|
|
177
|
+
}
|
|
178
|
+
else
|
|
179
|
+
consume(notification);
|
|
180
|
+
});
|
|
181
|
+
connection.onProtocolError(() => {
|
|
182
|
+
protocolFailed = true;
|
|
183
|
+
diagnostic("Muse sent an invalid MSP frame; refusing to continue.");
|
|
184
|
+
stop();
|
|
185
|
+
});
|
|
186
|
+
// Unattended agents cannot invent approval or answer interactive questions.
|
|
187
|
+
// denyUnmatched is also selected below; unexpected requests abort the turn.
|
|
188
|
+
connection.onServerRequest(async () => {
|
|
189
|
+
protocolFailed = true;
|
|
190
|
+
diagnostic("Muse requested interactive approval or input; operator action is required.");
|
|
191
|
+
stop();
|
|
192
|
+
throw new sdk.MspError({ code: -32601, message: "Navarch cannot approve interactive requests", data: { kind: "methodNotFound", retryable: false } });
|
|
193
|
+
});
|
|
194
|
+
try {
|
|
195
|
+
options.onProcessStarted?.(child.pid);
|
|
196
|
+
const run = async () => {
|
|
197
|
+
const initialized = await connection.request("initialize", {
|
|
198
|
+
clientInfo: { name: "navarch_runtime", version: version_cjs_1.RUNTIME_VERSION },
|
|
199
|
+
capabilities: { requestedCapabilities: Object.keys(mcpServers).length ? ["sessionMcp"] : [], userInputDialogs: false },
|
|
200
|
+
});
|
|
201
|
+
const schema = record(initialized.schema);
|
|
202
|
+
if (typeof schema?.fingerprint !== "string")
|
|
203
|
+
throw new Error("Muse initialization did not provide a schema fingerprint.");
|
|
204
|
+
if (sdk.checkServedFingerprint(schema.fingerprint))
|
|
205
|
+
diagnostic("Muse schema fingerprint differs from the pinned SDK; unknown fields are tolerated.");
|
|
206
|
+
if (Object.keys(mcpServers).length && (!Array.isArray(initialized.grantedCapabilities) || !initialized.grantedCapabilities.includes("sessionMcp"))) {
|
|
207
|
+
throw new Error("Muse did not grant sessionMcp; the task cannot run without its lease-scoped tools.");
|
|
208
|
+
}
|
|
209
|
+
durable = initialized.sessionDurability === undefined || initialized.sessionDurability === "durable";
|
|
210
|
+
connection.notify("initialized");
|
|
211
|
+
await connection.flush();
|
|
212
|
+
if (timedOut || killedByLeaseLoss || protocolFailed)
|
|
213
|
+
return;
|
|
214
|
+
const config = Object.keys(mcpServers).length ? { mcpServers } : undefined;
|
|
215
|
+
const opened = options.resumeSessionId
|
|
216
|
+
? await connection.command("session/resume", { sessionId: options.resumeSessionId, excludeItems: true, ...(config ? { config } : {}) }, { maxAttempts: 1 })
|
|
217
|
+
: await connection.command("session/start", {
|
|
218
|
+
workspaceRoot: options.cwd,
|
|
219
|
+
approvalMode: "denyUnmatched",
|
|
220
|
+
...(options.model && options.model !== "default" ? { modelId: options.model } : {}),
|
|
221
|
+
...(config ? { config } : {}),
|
|
222
|
+
}, { maxAttempts: 1 });
|
|
223
|
+
const session = record(opened.session);
|
|
224
|
+
if (typeof session?.sessionId !== "string" || typeof session.workspaceRoot !== "string"
|
|
225
|
+
|| !node_path_1.default.isAbsolute(session.workspaceRoot) || await node_fs_1.promises.realpath(session.workspaceRoot) !== await node_fs_1.promises.realpath(options.cwd)
|
|
226
|
+
|| session.activeTurnId !== null) {
|
|
227
|
+
throw new Error("Muse returned an unexpected session identity, worktree, or active turn.");
|
|
228
|
+
}
|
|
229
|
+
if (options.resumeSessionId && session.sessionId !== options.resumeSessionId)
|
|
230
|
+
throw new Error("Muse resumed a different session.");
|
|
231
|
+
sessionId = session.sessionId;
|
|
232
|
+
if (options.resumeSessionId) {
|
|
233
|
+
if (!durable)
|
|
234
|
+
throw new Error("Muse cannot resume this session on an ephemeral host.");
|
|
235
|
+
if (record(opened.history)?.mode !== "none")
|
|
236
|
+
throw new Error("Muse resume returned unexpected history; refusing replay.");
|
|
237
|
+
await connection.command("session/setApprovalMode", { sessionId, mode: "denyUnmatched" }, { maxAttempts: 1 });
|
|
238
|
+
if (options.model && options.model !== "default" && session.modelId !== options.model) {
|
|
239
|
+
throw new Error("Muse resumed with a different model than the dispatched policy.");
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// An older host may ignore additive fields. Prove the selected approval
|
|
243
|
+
// posture before sending any model-visible prompt.
|
|
244
|
+
let approvalMode = options.resumeSessionId ? undefined : record(session.approvalMode)?.mode;
|
|
245
|
+
if (approvalMode !== "denyUnmatched" && durable) {
|
|
246
|
+
const state = await connection.request("session/read", { sessionId, excludeItems: true });
|
|
247
|
+
approvalMode = record(record(state.session)?.approvalMode)?.mode;
|
|
248
|
+
}
|
|
249
|
+
if (approvalMode !== "denyUnmatched") {
|
|
250
|
+
throw new Error("Muse did not confirm denyUnmatched approval mode; refusing to start a turn.");
|
|
251
|
+
}
|
|
252
|
+
if (timedOut || killedByLeaseLoss || protocolFailed)
|
|
253
|
+
return;
|
|
254
|
+
const ack = await connection.command("turn/start", {
|
|
255
|
+
sessionId,
|
|
256
|
+
input: [{ type: "text", text: options.prompt }],
|
|
257
|
+
...(options.reasoningEffort ? { reasoningEffort: options.reasoningEffort } : {}),
|
|
258
|
+
}, { maxAttempts: 1 });
|
|
259
|
+
if (typeof ack.turnId !== "string" || ack.disposition !== "started")
|
|
260
|
+
throw new Error("Muse did not start the requested turn.");
|
|
261
|
+
turnId = ack.turnId;
|
|
262
|
+
for (const notification of pending.splice(0))
|
|
263
|
+
consume(notification);
|
|
264
|
+
await Promise.race([completed.promise, connection.closed]);
|
|
265
|
+
if (!terminal && !timedOut && !killedByLeaseLoss)
|
|
266
|
+
throw new Error("Muse closed before the turn completed.");
|
|
267
|
+
};
|
|
268
|
+
await Promise.race([run(), stopped.promise]);
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
if (error instanceof sdk.MspError && error.kind === "authRequired") {
|
|
272
|
+
authError = "Muse authentication is required. Open muse and run /login on this machine.";
|
|
273
|
+
}
|
|
274
|
+
if (!timedOut && !killedByLeaseLoss)
|
|
275
|
+
diagnostic(error instanceof Error ? error.message : "Muse protocol request failed.");
|
|
276
|
+
}
|
|
277
|
+
finally {
|
|
278
|
+
await cancelling;
|
|
279
|
+
// EOF releases Muse's writer lease. A bounded shutdown always reaps the
|
|
280
|
+
// child, including a host that ignores EOF or died during a request.
|
|
281
|
+
child.stdin.end();
|
|
282
|
+
await waitWithin(Promise.race([closed.promise, stopped.promise]), SHUTDOWN_GRACE_MS);
|
|
283
|
+
await cancelling;
|
|
284
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
285
|
+
(0, kill_agent_cjs_1.killAgent)(child, "SIGTERM");
|
|
286
|
+
await waitWithin(closed.promise, TERMINATE_GRACE_MS);
|
|
287
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
288
|
+
(0, kill_agent_cjs_1.killAgent)(child);
|
|
289
|
+
if (options.dockerExec)
|
|
290
|
+
await options.dockerExec.runner.run("docker", ["kill", options.dockerExec.containerName], { timeoutMs: 10_000 }).catch(() => undefined);
|
|
291
|
+
await waitWithin(closed.promise, TERMINATE_GRACE_MS);
|
|
292
|
+
}
|
|
293
|
+
// The root may have exited while detached shell/MCP descendants remain.
|
|
294
|
+
// Reap our owned host process group even after a clean root exit.
|
|
295
|
+
if (!options.dockerExec)
|
|
296
|
+
(0, kill_agent_cjs_1.killAgent)(child);
|
|
297
|
+
await waitWithin(closed.promise, TERMINATE_GRACE_MS);
|
|
298
|
+
await connection.close().catch(() => undefined);
|
|
299
|
+
clearTimeout(timer);
|
|
300
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
301
|
+
}
|
|
302
|
+
if (processCode === 5)
|
|
303
|
+
diagnostic("This Muse build has the experimental SDK/serve surface disabled. No CLI fallback was attempted.");
|
|
304
|
+
const successful = !protocolFailed && terminal?.terminal === "completed" && processCode === 0;
|
|
305
|
+
if (terminal && terminal.terminal !== "completed" && !timedOut && !killedByLeaseLoss && !authError) {
|
|
306
|
+
diagnostic(`Muse turn did not complete successfully (${String(terminal.terminal)}).`);
|
|
307
|
+
}
|
|
308
|
+
const reportText = [...items.values()].flatMap((item) => typeof item.text === "string" ? [item.text] : []).join("\n").trim();
|
|
309
|
+
return {
|
|
310
|
+
exitCode: timedOut || killedByLeaseLoss ? null : successful ? 0 : processCode === null ? null : 1,
|
|
311
|
+
timedOut, killedByLeaseLoss, stdout, stderr,
|
|
312
|
+
...(reportText ? { reportText } : {}),
|
|
313
|
+
...(authError ? { authError } : {}),
|
|
314
|
+
...(durable && sessionId && terminal ? { agentSessionId: sessionId } : {}),
|
|
315
|
+
...normalizedUsage([...usage.values()], !options.resumeSessionId),
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
function normalizedUsage(events, fresh) {
|
|
319
|
+
if (!events.length)
|
|
320
|
+
return {};
|
|
321
|
+
let tokensIn = 0;
|
|
322
|
+
let tokensOut = 0;
|
|
323
|
+
let cacheHitTokensIn = 0;
|
|
324
|
+
let completeCache = true;
|
|
325
|
+
for (const event of events) {
|
|
326
|
+
const usage = record(event.usage);
|
|
327
|
+
const input = count(event.promptTokens);
|
|
328
|
+
const output = count(usage?.outputTokens);
|
|
329
|
+
if (input === undefined || output === undefined)
|
|
330
|
+
return {}; // No fabricated partial total.
|
|
331
|
+
tokensIn += input;
|
|
332
|
+
tokensOut += output;
|
|
333
|
+
const cache = count(usage?.cacheReadTokens);
|
|
334
|
+
if (cache === undefined || cache > input)
|
|
335
|
+
completeCache = false;
|
|
336
|
+
else
|
|
337
|
+
cacheHitTokensIn += cache;
|
|
338
|
+
}
|
|
339
|
+
if (!Number.isSafeInteger(tokensIn) || !Number.isSafeInteger(tokensOut))
|
|
340
|
+
return {};
|
|
341
|
+
const cost = record(record(events.at(-1)?.cumulative)?.cost);
|
|
342
|
+
return {
|
|
343
|
+
tokensIn, tokensOut,
|
|
344
|
+
...(completeCache ? { cacheHitTokensIn } : {}),
|
|
345
|
+
...(cost?.partial === false && typeof cost.usd === "number" && Number.isFinite(cost.usd) && cost.usd >= 0
|
|
346
|
+
? { sessionCostUsd: cost.usd, ...(fresh ? { costUsd: cost.usd } : {}) } : {}),
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
/** Replace each provider session's cumulative estimate, never sum it per turn. */
|
|
350
|
+
function sumMuseSessionCost(attempts) {
|
|
351
|
+
const sessions = new Map();
|
|
352
|
+
attempts.forEach((attempt, index) => sessions.set(attempt.agentSessionId ?? `attempt:${index}`, attempt.sessionCostUsd));
|
|
353
|
+
if (!sessions.size || [...sessions.values()].some((cost) => cost === undefined))
|
|
354
|
+
return undefined;
|
|
355
|
+
return [...sessions.values()].reduce((sum, cost) => sum + cost, 0);
|
|
356
|
+
}
|
|
357
|
+
async function prepareBridge(source, options) {
|
|
358
|
+
if (!Object.keys(source).length)
|
|
359
|
+
return { servers: {} };
|
|
360
|
+
if (!options.mcpMetadataDir || !node_path_1.default.isAbsolute(options.mcpMetadataDir))
|
|
361
|
+
throw new Error("Muse MCP needs a task-owned metadata directory.");
|
|
362
|
+
const dir = await node_fs_1.promises.mkdtemp(node_path_1.default.join(options.mcpMetadataDir, "muse-mcp-"));
|
|
363
|
+
try {
|
|
364
|
+
const configPath = node_path_1.default.join(dir, "servers.json");
|
|
365
|
+
const content = JSON.stringify(source);
|
|
366
|
+
await node_fs_1.promises.writeFile(configPath, content, { mode: 0o600 });
|
|
367
|
+
const digest = (0, node_crypto_1.createHash)("sha256").update(content).digest("hex");
|
|
368
|
+
const servers = Object.fromEntries(Object.keys(source).map((name) => [name, {
|
|
369
|
+
transport: "stdio", framing: "lineDelimitedJson", mode: "required",
|
|
370
|
+
command: options.dockerExec ? "navarch-muse-mcp-bridge" : process.execPath,
|
|
371
|
+
args: [...(options.dockerExec ? [] : [node_path_1.default.resolve(__dirname, "../../bin/muse-mcp-bridge.cjs")]), configPath, name, digest],
|
|
372
|
+
}]));
|
|
373
|
+
return { servers, dir };
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
await node_fs_1.promises.rm(dir, { recursive: true, force: true });
|
|
377
|
+
throw error;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
async function readMcpServers(configPath, env) {
|
|
381
|
+
if (!configPath)
|
|
382
|
+
return {};
|
|
383
|
+
const parsed = record(JSON.parse(await node_fs_1.promises.readFile(configPath, "utf8")));
|
|
384
|
+
const source = record(parsed?.mcpServers);
|
|
385
|
+
if (!source)
|
|
386
|
+
throw new Error("Muse MCP configuration must contain mcpServers.");
|
|
387
|
+
const result = Object.create(null);
|
|
388
|
+
const strings = (value) => {
|
|
389
|
+
const entries = value === undefined ? {} : record(value);
|
|
390
|
+
if (!entries)
|
|
391
|
+
throw new Error("Muse MCP environment/headers must be string maps.");
|
|
392
|
+
return Object.fromEntries(Object.entries(entries).map(([key, raw]) => {
|
|
393
|
+
if (typeof raw !== "string")
|
|
394
|
+
throw new Error("Muse MCP environment/headers must be string maps.");
|
|
395
|
+
const name = raw.match(/^\{env:([A-Z_][A-Z0-9_]*)\}$/)?.[1];
|
|
396
|
+
const value = name ? env[name] ?? process.env[name] : raw;
|
|
397
|
+
if (value === undefined)
|
|
398
|
+
throw new Error(`Muse MCP configuration references missing environment variable ${name}.`);
|
|
399
|
+
return [key, value];
|
|
400
|
+
}));
|
|
401
|
+
};
|
|
402
|
+
for (const [name, value] of Object.entries(source)) {
|
|
403
|
+
const server = record(value);
|
|
404
|
+
if (!server)
|
|
405
|
+
throw new Error("Muse MCP server configuration is invalid.");
|
|
406
|
+
const url = server.httpUrl ?? server.url;
|
|
407
|
+
if (typeof url === "string" && /^https?:\/\//.test(url) && (!server.type || ["http", "streamable-http"].includes(String(server.type)))) {
|
|
408
|
+
result[name] = { transport: "streamableHttp", url, headers: strings(server.headers), mode: "required" };
|
|
409
|
+
}
|
|
410
|
+
else if (typeof server.command === "string" && (!server.type || server.type === "stdio") && (!server.args || Array.isArray(server.args) && server.args.every((arg) => typeof arg === "string"))) {
|
|
411
|
+
result[name] = { transport: "stdio", command: server.command, args: server.args, env: strings(server.env), mode: "required" };
|
|
412
|
+
}
|
|
413
|
+
else
|
|
414
|
+
throw new Error("Muse MCP server transport is unsupported; no servers were silently dropped.");
|
|
415
|
+
}
|
|
416
|
+
return result;
|
|
417
|
+
}
|
|
418
|
+
function spawnMuse(options) {
|
|
419
|
+
const env = { ...process.env, ...options.env };
|
|
420
|
+
delete env.MUSE_PERMISSION_PROFILE_FAIL_OPEN;
|
|
421
|
+
const args = ["serve", ...options.extraArgs];
|
|
422
|
+
if (!options.dockerExec)
|
|
423
|
+
return (0, node_child_process_1.spawn)(options.bin, args, { cwd: options.cwd, env, detached: process.platform !== "win32", stdio: "pipe" });
|
|
424
|
+
return (0, node_child_process_1.spawn)("docker", [
|
|
425
|
+
"exec", "-i", ...Object.keys(options.env).filter((name) => name !== "MUSE_PERMISSION_PROFILE_FAIL_OPEN").flatMap((name) => ["--env", name]),
|
|
426
|
+
options.dockerExec.containerName, "sh", "-c",
|
|
427
|
+
'[ ! -f /tmp/session.env ] || . /tmp/session.env; unset MUSE_PERMISSION_PROFILE_FAIL_OPEN; cd "$1" || exit 1; shift; exec "$@"',
|
|
428
|
+
"sh", options.cwd, options.bin, ...args,
|
|
429
|
+
], { env, detached: process.platform !== "win32", stdio: "pipe" });
|
|
430
|
+
}
|
|
431
|
+
// Read-only Navi conversations are deliberately absent: an approval mode is
|
|
432
|
+
// not a filesystem read boundary, and the SDK cannot yet prove that contract.
|
|
433
|
+
exports.museAdapter = { agentType: "muse", run: runMuseAdapter };
|
package/dist/agent-auth.cjs
CHANGED
|
@@ -43,7 +43,8 @@ function matchingLine(text) {
|
|
|
43
43
|
function detectAgentAuthFailure(result) {
|
|
44
44
|
if (result.killedByLeaseLoss || result.timedOut || result.capacityError)
|
|
45
45
|
return null;
|
|
46
|
-
|
|
46
|
+
// Muse exposes authRequired as a typed terminal error, never assistant prose.
|
|
47
|
+
if (result.authError && result.exitCode !== 0)
|
|
47
48
|
return { detail: result.authError };
|
|
48
49
|
const claude = (0, exit_conditions_cjs_1.parseClaudeJsonResult)(result.stdout) ?? (0, exit_conditions_cjs_1.parseClaudeJsonResult)(result.stderr);
|
|
49
50
|
if (claude) {
|
package/dist/config.cjs
CHANGED
|
@@ -10,6 +10,7 @@ exports.opencodeBlockedOnHost = opencodeBlockedOnHost;
|
|
|
10
10
|
exports.loadRuntimeConfig = loadRuntimeConfig;
|
|
11
11
|
const node_path_1 = __importDefault(require("node:path"));
|
|
12
12
|
const node_os_1 = __importDefault(require("node:os"));
|
|
13
|
+
const muse_cjs_1 = require("./adapters/muse.cjs");
|
|
13
14
|
const sandbox_profile_cjs_1 = require("./sandbox-profile.cjs");
|
|
14
15
|
/** Published image containing git, GitHub CLI, and the pinned Claude Code CLI. */
|
|
15
16
|
exports.DEFAULT_SANDBOX_IMAGE = "ghcr.io/sagentlab/navarch-sandbox-agent:0.1.0";
|
|
@@ -25,6 +26,7 @@ exports.RUNTIME_AGENT_TYPES = [
|
|
|
25
26
|
"opencode",
|
|
26
27
|
"acp",
|
|
27
28
|
"qoder",
|
|
29
|
+
"muse",
|
|
28
30
|
"pi-local",
|
|
29
31
|
];
|
|
30
32
|
function isRuntimeAgentType(value) {
|
|
@@ -116,10 +118,13 @@ function loadRuntimeConfig(env = process.env) {
|
|
|
116
118
|
? sandboxMode === "host" && !worktreeGuard
|
|
117
119
|
: runtime === "antigravity"
|
|
118
120
|
? process.platform !== "win32" && !opencodeBlockedOnHost(sandboxMode, worktreeGuard)
|
|
119
|
-
: runtime !== "opencode" || !opencodeBlockedOnHost(sandboxMode, worktreeGuard));
|
|
121
|
+
: (runtime !== "opencode" && runtime !== "muse") || !opencodeBlockedOnHost(sandboxMode, worktreeGuard));
|
|
120
122
|
if (requestedRuntimes.includes("pi-local") && runnableRuntimes.length === 0) {
|
|
121
123
|
throw new Error("Pi (local) requires NAVARCH_SANDBOX_MODE=host and NAVARCH_WORKTREE_GUARD=off");
|
|
122
124
|
}
|
|
125
|
+
if (requestedRuntimes.includes("muse") && runnableRuntimes.length === 0) {
|
|
126
|
+
throw new Error(muse_cjs_1.MUSE_HOST_GUARD_MESSAGE);
|
|
127
|
+
}
|
|
123
128
|
if (requestedRuntimes.includes("antigravity") && runnableRuntimes.length === 0) {
|
|
124
129
|
if (process.platform === "win32")
|
|
125
130
|
throw new Error("Antigravity's private lease MCP relay requires a Linux or macOS worker; Windows is unsupported.");
|
|
@@ -168,6 +173,8 @@ function loadRuntimeConfig(env = process.env) {
|
|
|
168
173
|
piExtraArgs: envList(env, "NAVARCH_PI_EXTRA_ARGS", []),
|
|
169
174
|
qoderBin: env.NAVARCH_QODER_BIN ?? "qoder",
|
|
170
175
|
qoderExtraArgs: envList(env, "NAVARCH_QODER_EXTRA_ARGS", []),
|
|
176
|
+
museBin: env.NAVARCH_MUSE_BIN ?? "muse",
|
|
177
|
+
museExtraArgs: envList(env, "NAVARCH_MUSE_EXTRA_ARGS", []),
|
|
171
178
|
gitAuthorName: env.NAVARCH_GIT_AUTHOR_NAME ?? "sagentlab",
|
|
172
179
|
gitAuthorEmail: env.NAVARCH_GIT_AUTHOR_EMAIL ?? "z@sagentlab.com",
|
|
173
180
|
mcpConfigPath: env.NAVARCH_MCP_CONFIG_PATH ?? null,
|
|
@@ -40,7 +40,7 @@ function conversationCapability(config) {
|
|
|
40
40
|
}
|
|
41
41
|
/** Per-adapter support table for docs and diagnostics (host sandbox, guard on, feature enabled). */
|
|
42
42
|
function conversationSupportMatrix() {
|
|
43
|
-
const agents = ["claude-code", "codex", "gemini", "antigravity", "opencode", "acp", "qoder", "pi-local", "pi"];
|
|
43
|
+
const agents = ["claude-code", "codex", "gemini", "antigravity", "opencode", "acp", "qoder", "muse", "pi-local", "pi"];
|
|
44
44
|
return agents.map((agentType) => ({
|
|
45
45
|
agentType,
|
|
46
46
|
capability: conversationCapability({
|
package/dist/exit-conditions.cjs
CHANGED
|
@@ -22,7 +22,7 @@ exports.mapExitCondition = mapExitCondition;
|
|
|
22
22
|
exports.AGENT_AUTH_FAILURE_PREFIX = "Coding agent authentication failed";
|
|
23
23
|
function agentAuthFailureReport(detail) {
|
|
24
24
|
return `${exports.AGENT_AUTH_FAILURE_PREFIX}: ${detail} ` +
|
|
25
|
-
"Log the coding agent in again on this machine (`claude /login`, `codex login`,
|
|
25
|
+
"Log the coding agent in again on this machine (`claude /login`, `codex login`, an interactive `agy` session, or open `muse` and run `/login`), then resume the agent.";
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
28
|
* Best-effort parse of `claude -p --output-format json` (or `stream-json`,
|