@brutalsystems/muster 0.6.0 → 0.7.1
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/CONTRACT_PROVENANCE.md +69 -0
- package/README.md +76 -22
- package/dist/format.js +2 -0
- package/dist/guard.js +9 -5
- package/dist/identity/opencode.js +86 -0
- package/dist/muster.js +13 -3
- package/dist/naming.js +3 -2
- package/dist/opencode-http.js +241 -0
- package/dist/opencode-policy.js +382 -0
- package/dist/reach/opencode.js +17 -0
- package/dist/run.js +87 -12
- package/package.json +5 -2
- package/test/fixtures/canonical-id.json +2 -2
package/dist/run.js
CHANGED
|
@@ -4,19 +4,23 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
6
|
import { loadConfig } from "./config.js";
|
|
7
|
-
import { runSchema, launchArgs, launchEnv, resolvePermissions, } from "./guard.js";
|
|
7
|
+
import { runSchema, launchArgs, launchEnv, resolvePermissions, runtimeArgs, } from "./guard.js";
|
|
8
8
|
import { Registry } from "./registry.js";
|
|
9
9
|
import { LaunchLog } from "./log.js";
|
|
10
10
|
import { hosts, selectHost } from "./hosts/index.js";
|
|
11
11
|
import { processRef, isSame, stopTree, delay, descendants, } from "./identity/processes.js";
|
|
12
12
|
import { resolveCodex } from "./identity/codex.js";
|
|
13
13
|
import { resolveClaude } from "./identity/claude.js";
|
|
14
|
+
import { openCodeEndpointOwnedBy, resolveOpenCode, } from "./identity/opencode.js";
|
|
14
15
|
import { codexReachable } from "./reach/codex.js";
|
|
15
16
|
import { claudeReachable } from "./reach/claude.js";
|
|
17
|
+
import { openCodeReachable } from "./reach/opencode.js";
|
|
16
18
|
import { assertClaudePolicy } from "./claude-policy.js";
|
|
17
19
|
import { codexPolicyArgs } from "./codex-policy.js";
|
|
18
20
|
import { CodexRpc } from "./codex-rpc.js";
|
|
19
|
-
import {
|
|
21
|
+
import { allocateLoopbackPort, OpenCodeHttp } from "./opencode-http.js";
|
|
22
|
+
import { assertOpenCodeVersion, openCodeMcpNames, openCodeSessionLaunch, openCodeTaskLaunch, } from "./opencode-policy.js";
|
|
23
|
+
import { assignNames, resolvePeer, runtimeName, } from "./naming.js";
|
|
20
24
|
import { selectMcp, prepareMcp, mcpEnvironment } from "./mcp.js";
|
|
21
25
|
export class Muster {
|
|
22
26
|
config;
|
|
@@ -64,7 +68,11 @@ export class Muster {
|
|
|
64
68
|
throw new Error("--open requires a tmux session on macOS");
|
|
65
69
|
const permissions = resolvePermissions(req, this.config);
|
|
66
70
|
const selectedMcp = selectMcp(req, this.config);
|
|
67
|
-
let argv =
|
|
71
|
+
let argv = [];
|
|
72
|
+
if (req.runtime === "opencode")
|
|
73
|
+
runtimeArgs(req);
|
|
74
|
+
else
|
|
75
|
+
argv = launchArgs(req, this.config);
|
|
68
76
|
const driver = req.kind === "session"
|
|
69
77
|
? await selectHost(req.open ? "tmux" : requestedHost, this.drivers)
|
|
70
78
|
: undefined;
|
|
@@ -74,6 +82,8 @@ export class Muster {
|
|
|
74
82
|
let policy = [];
|
|
75
83
|
if (req.runtime === "claude")
|
|
76
84
|
await assertClaudePolicy(this.env);
|
|
85
|
+
if (req.runtime === "opencode")
|
|
86
|
+
await assertOpenCodeVersion(this.env, "1.18.31", Date.now() + 4000);
|
|
77
87
|
const reservation = await this.registry.reserve({ runtime: req.runtime, kind: req.kind, cwd: req.cwd, ...permissions }, this.config.max_concurrent);
|
|
78
88
|
this.owned.add(reservation.launchId);
|
|
79
89
|
let entry = reservation;
|
|
@@ -96,7 +106,7 @@ export class Muster {
|
|
|
96
106
|
...permissions,
|
|
97
107
|
});
|
|
98
108
|
const prepared = await prepareMcp(selectedMcp, mcpEnvironment(selectedMcp, this.env, this.sourceEnv), req.cwd, deadline);
|
|
99
|
-
|
|
109
|
+
let runtimeEnv = mcpEnvironment(prepared.servers, this.env, this.sourceEnv);
|
|
100
110
|
entry = await this.registry.update(entry.launchId, prepared.summary);
|
|
101
111
|
let bridgePath;
|
|
102
112
|
if (req.runtime === "claude" && prepared.servers.length) {
|
|
@@ -108,11 +118,29 @@ export class Muster {
|
|
|
108
118
|
});
|
|
109
119
|
await writeFile(bridgePath, JSON.stringify(Object.fromEntries(prepared.servers.map((s) => [s.name, s.config]))), { mode: 0o600 });
|
|
110
120
|
}
|
|
111
|
-
|
|
121
|
+
if (req.runtime !== "opencode")
|
|
122
|
+
argv = launchArgs(req, this.config, prepared.servers, bridgePath);
|
|
112
123
|
if (req.runtime === "codex") {
|
|
113
124
|
policy = await codexPolicyArgs(runtimeEnv, req.cwd, deadline, prepared.servers);
|
|
114
125
|
argv.splice(1, 0, ...policy);
|
|
115
126
|
}
|
|
127
|
+
else if (req.runtime === "opencode" && req.kind === "session") {
|
|
128
|
+
const port = await allocateLoopbackPort();
|
|
129
|
+
const inheritedMcpNames = await openCodeMcpNames(runtimeEnv, req.cwd, deadline);
|
|
130
|
+
const launch = await openCodeSessionLaunch(req, this.config, prepared.servers, inheritedMcpNames, port, runtimeEnv, req.cwd, deadline);
|
|
131
|
+
argv = launch.argv;
|
|
132
|
+
runtimeEnv = { ...runtimeEnv, ...launch.envPatch };
|
|
133
|
+
entry = await this.registry.update(entry.launchId, {
|
|
134
|
+
server_url: launch.serverUrl,
|
|
135
|
+
opencode_port: launch.port,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
else if (req.runtime === "opencode" && req.kind === "task") {
|
|
139
|
+
const inheritedMcpNames = await openCodeMcpNames(runtimeEnv, req.cwd, deadline);
|
|
140
|
+
const launch = await openCodeTaskLaunch(req, this.config, prepared.servers, inheritedMcpNames, runtimeEnv, req.cwd, deadline);
|
|
141
|
+
argv = launch.argv;
|
|
142
|
+
runtimeEnv = { ...runtimeEnv, ...launch.envPatch };
|
|
143
|
+
}
|
|
116
144
|
if (this.closing)
|
|
117
145
|
throw new Error("Launch cancelled: Muster is closing");
|
|
118
146
|
if (req.kind === "task")
|
|
@@ -133,7 +161,9 @@ export class Muster {
|
|
|
133
161
|
await this.registry.update(entry.launchId, entry);
|
|
134
162
|
let diagnostic = req.runtime === "claude"
|
|
135
163
|
? "no Claude session registry found (check terminal for workspace trust or login prompts)"
|
|
136
|
-
:
|
|
164
|
+
: req.runtime === "opencode"
|
|
165
|
+
? "no OpenCode session matched the launch endpoint, cwd, and creation window"
|
|
166
|
+
: "no descendant runtime identity found";
|
|
137
167
|
if (req.runtime === "codex")
|
|
138
168
|
rpc = new CodexRpc(this.env, req.cwd);
|
|
139
169
|
while (Date.now() < deadline) {
|
|
@@ -169,15 +199,21 @@ export class Muster {
|
|
|
169
199
|
return false;
|
|
170
200
|
}
|
|
171
201
|
})
|
|
172
|
-
:
|
|
202
|
+
: req.runtime === "opencode"
|
|
203
|
+
? await resolveOpenCode(root.pid, entry.server_url, req.cwd, startedAt, deadline)
|
|
204
|
+
: await resolveClaude(root.pid, this.env, deadline);
|
|
173
205
|
if (identity) {
|
|
174
206
|
diagnostic =
|
|
175
207
|
req.runtime === "codex"
|
|
176
208
|
? "thread id reserved but no rollout"
|
|
177
|
-
:
|
|
209
|
+
: req.runtime === "opencode"
|
|
210
|
+
? "OpenCode session exists but its endpoint status is not readable"
|
|
211
|
+
: "registry exists but inbox socket is not accepting connections";
|
|
178
212
|
let metadata;
|
|
179
213
|
if (req.runtime === "codex")
|
|
180
214
|
metadata = await codexReachable(rpc, identity.id, deadline);
|
|
215
|
+
else if (req.runtime === "opencode")
|
|
216
|
+
metadata = await openCodeReachable(new OpenCodeHttp(entry.server_url), identity, deadline);
|
|
181
217
|
else {
|
|
182
218
|
const claude = identity;
|
|
183
219
|
if (!(await claudeReachable(claude.socketPath, deadline)))
|
|
@@ -188,7 +224,10 @@ export class Muster {
|
|
|
188
224
|
throw new Error("readiness completed after launch deadline");
|
|
189
225
|
if (!(await isSame(root)) || !(await processRef(identity.pid)))
|
|
190
226
|
throw new Error("process exited before readiness completed");
|
|
191
|
-
|
|
227
|
+
if (req.runtime === "opencode" &&
|
|
228
|
+
!(await openCodeEndpointOwnedBy(root.pid, entry.server_url, deadline)))
|
|
229
|
+
throw new Error("OpenCode endpoint listener is not owned by the launched process tree");
|
|
230
|
+
const runtime = runtimeName(req.runtime);
|
|
192
231
|
const named = assignNames([
|
|
193
232
|
{ runtime, uuid: identity.id, rawName: metadata.rawName },
|
|
194
233
|
])[0];
|
|
@@ -203,7 +242,12 @@ export class Muster {
|
|
|
203
242
|
cwd: req.cwd,
|
|
204
243
|
...(req.runtime === "codex"
|
|
205
244
|
? { thread_id: identity.id }
|
|
206
|
-
:
|
|
245
|
+
: req.runtime === "opencode"
|
|
246
|
+
? {
|
|
247
|
+
session_id: identity.id,
|
|
248
|
+
server_url: entry.server_url,
|
|
249
|
+
}
|
|
250
|
+
: { session_id: identity.id }),
|
|
207
251
|
pid: identity.pid,
|
|
208
252
|
host: driver.id,
|
|
209
253
|
capabilities: driver.capabilities(),
|
|
@@ -318,6 +362,17 @@ export class Muster {
|
|
|
318
362
|
}
|
|
319
363
|
async stopEntry(entry) {
|
|
320
364
|
try {
|
|
365
|
+
const openCodeSessionId = entry.peer?.session_id;
|
|
366
|
+
const abortDeadline = Date.now() + 1000;
|
|
367
|
+
if (entry.runtime === "opencode" &&
|
|
368
|
+
entry.root &&
|
|
369
|
+
entry.server_url &&
|
|
370
|
+
typeof openCodeSessionId === "string" &&
|
|
371
|
+
(await isSame(entry.root)) &&
|
|
372
|
+
(await openCodeEndpointOwnedBy(entry.root.pid, entry.server_url, abortDeadline)))
|
|
373
|
+
await new OpenCodeHttp(entry.server_url)
|
|
374
|
+
.abort(openCodeSessionId, abortDeadline)
|
|
375
|
+
.catch(() => { });
|
|
321
376
|
await this.stopEntryProcesses(entry);
|
|
322
377
|
}
|
|
323
378
|
finally {
|
|
@@ -353,7 +408,7 @@ export class Muster {
|
|
|
353
408
|
throw new Error(`Ambiguous durable id: ${exact.map((e) => e.runtime + ":" + e.id).join(", ")}`);
|
|
354
409
|
const peers = all.filter((e) => e.kind === "session" && e.status === "running" && e.peer);
|
|
355
410
|
const named = peers.map((e) => ({
|
|
356
|
-
runtime: e.runtime
|
|
411
|
+
runtime: runtimeName(e.runtime),
|
|
357
412
|
uuid: e.id,
|
|
358
413
|
rawName: String(e.peer.name),
|
|
359
414
|
slug: String(e.peer.canonical_id)
|
|
@@ -389,6 +444,23 @@ export class Muster {
|
|
|
389
444
|
rpc = new CodexRpc(this.env, entry.cwd);
|
|
390
445
|
metadata = await codexReachable(rpc, entry.id, deadline);
|
|
391
446
|
}
|
|
447
|
+
else if (entry.runtime === "opencode") {
|
|
448
|
+
if (!entry.server_url)
|
|
449
|
+
throw new Error("session endpoint missing");
|
|
450
|
+
if (!(await isSame(entry.root)) ||
|
|
451
|
+
!(await openCodeEndpointOwnedBy(entry.root.pid, entry.server_url, deadline)))
|
|
452
|
+
throw new Error("session endpoint is not owned by the launch");
|
|
453
|
+
metadata = await openCodeReachable(new OpenCodeHttp(entry.server_url), {
|
|
454
|
+
id: entry.id,
|
|
455
|
+
pid: entry.root.pid,
|
|
456
|
+
rawName: null,
|
|
457
|
+
cwd: entry.cwd,
|
|
458
|
+
serverUrl: entry.server_url,
|
|
459
|
+
}, deadline);
|
|
460
|
+
if (!(await isSame(entry.root)) ||
|
|
461
|
+
!(await openCodeEndpointOwnedBy(entry.root.pid, entry.server_url, deadline)))
|
|
462
|
+
throw new Error("session endpoint ownership changed during refresh");
|
|
463
|
+
}
|
|
392
464
|
else {
|
|
393
465
|
const identity = await resolveClaude(entry.root.pid, this.env, deadline);
|
|
394
466
|
if (!identity ||
|
|
@@ -399,7 +471,7 @@ export class Muster {
|
|
|
399
471
|
}
|
|
400
472
|
const named = assignNames([
|
|
401
473
|
{
|
|
402
|
-
runtime: entry.runtime
|
|
474
|
+
runtime: runtimeName(entry.runtime),
|
|
403
475
|
uuid: entry.id,
|
|
404
476
|
rawName: metadata.rawName,
|
|
405
477
|
},
|
|
@@ -410,6 +482,9 @@ export class Muster {
|
|
|
410
482
|
state: metadata.state,
|
|
411
483
|
name: named.display,
|
|
412
484
|
canonical_id: named.canonicalId,
|
|
485
|
+
...(entry.runtime === "opencode"
|
|
486
|
+
? { server_url: entry.server_url }
|
|
487
|
+
: {}),
|
|
413
488
|
},
|
|
414
489
|
});
|
|
415
490
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brutalsystems/muster",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Launch instructed, reachable local agent sessions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -62,5 +62,8 @@
|
|
|
62
62
|
"CANONICAL_ID.md",
|
|
63
63
|
"CONTRACT_PROVENANCE.md",
|
|
64
64
|
"test/fixtures/canonical-id.json"
|
|
65
|
-
]
|
|
65
|
+
],
|
|
66
|
+
"allowScripts": {
|
|
67
|
+
"node-pty@1.1.0": true
|
|
68
|
+
}
|
|
66
69
|
}
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
33
|
"case": "claude peers are qualified with their runtime",
|
|
34
|
-
"peers": [ { "runtime": "claude-code", "rawName": "
|
|
35
|
-
"expect": [ { "display": "
|
|
34
|
+
"peers": [ { "runtime": "claude-code", "rawName": "billing-api", "uuid": "5af69d42-2214-41d9-b13f-9c3177eb60ce" } ],
|
|
35
|
+
"expect": [ { "display": "billing-api", "canonicalId": "claude-code:billing-api.0ce" } ]
|
|
36
36
|
},
|
|
37
37
|
{
|
|
38
38
|
"case": "collision: every colliding peer is suffixed, and only those",
|