@brutalsystems/muster 0.9.2 → 0.11.0
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 +40 -0
- package/dist/config.js +14 -0
- package/dist/guard.js +111 -3
- package/dist/help.js +8 -2
- package/dist/hosts/index.js +2 -2
- package/dist/hosts/tmux.js +28 -2
- package/dist/muster.js +9 -1
- package/dist/run.js +9 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -343,6 +343,8 @@ Missing configuration uses these defaults:
|
|
|
343
343
|
|
|
344
344
|
```toml
|
|
345
345
|
host = "auto"
|
|
346
|
+
tmux_status = "off"
|
|
347
|
+
terminal = "terminal"
|
|
346
348
|
launch_timeout_sec = 30
|
|
347
349
|
max_concurrent = 4
|
|
348
350
|
permissions = "deny"
|
|
@@ -472,6 +474,20 @@ without adding a retention policy nobody asked for.
|
|
|
472
474
|
|
|
473
475
|
### Containment levels
|
|
474
476
|
|
|
477
|
+
Two axes, answering different questions:
|
|
478
|
+
|
|
479
|
+
- **`--permissions` decides whether the agent stops to ask a human.** It
|
|
480
|
+
configures the runtime's own approval prompting — a setting the agent's
|
|
481
|
+
process honours.
|
|
482
|
+
- **`--sandbox` decides what the agent may touch, and the kernel enforces it.**
|
|
483
|
+
On macOS that is seatbelt. Nothing the agent does talks its way past it.
|
|
484
|
+
|
|
485
|
+
The difference is not academic: a permissions refusal arrives as a prompt or a
|
|
486
|
+
tool error the agent can read and react to, while a sandbox refusal arrives as
|
|
487
|
+
`EPERM` from the kernel, with no negotiation and often no explanation the agent
|
|
488
|
+
can interpret. `enforcement` below records which of the two a launch actually
|
|
489
|
+
got.
|
|
490
|
+
|
|
475
491
|
`--level` names a legal `--permissions` / `--sandbox` pair. It is a spelling,
|
|
476
492
|
not a second policy path, and naming both a level and either flag is an error.
|
|
477
493
|
|
|
@@ -492,6 +508,30 @@ Every launch also records how strongly its containment was imposed:
|
|
|
492
508
|
| `tool-policy` | OpenCode — rules the agent is asked to respect, with no kernel enforcement. |
|
|
493
509
|
| `none` | `full-access` — the sandbox is deliberately disabled. |
|
|
494
510
|
|
|
511
|
+
#### What a sandboxed launch may write
|
|
512
|
+
|
|
513
|
+
Muster names the write roots rather than leaving them to default. Under
|
|
514
|
+
`workspace-write` a launched Claude session may write its cwd, `$TMPDIR`, and
|
|
515
|
+
**its own configuration directory** — the `CLAUDE_CONFIG_DIR` the child will
|
|
516
|
+
actually use, which an identity launch or a requester profile's `env_defaults`
|
|
517
|
+
may relocate. That last root is granted because an agent's memory lives there:
|
|
518
|
+
without it every Bash write to memory failed with `EPERM` while the in-process
|
|
519
|
+
file tools succeeded, which reads from inside the session as a path bug rather
|
|
520
|
+
than a boundary.
|
|
521
|
+
|
|
522
|
+
Under `read` nothing is writable, including the config dir. Under `full-access`
|
|
523
|
+
there is no sandbox to grant into.
|
|
524
|
+
|
|
525
|
+
One path stays denied that muster cannot open: **`$CLAUDE_CONFIG_DIR/projects/`**
|
|
526
|
+
(and `shell-snapshots/`). Claude Code refuses writes there in a sandboxed Bash
|
|
527
|
+
regardless of what muster grants — naming those directories explicitly in
|
|
528
|
+
`allowWrite` changes nothing, which is presumably deliberate, since a session's
|
|
529
|
+
own transcripts live there. Project-scoped memory under
|
|
530
|
+
`projects/<cwd-slug>/memory` is therefore reachable only through the file tools,
|
|
531
|
+
which are in-process and not kernel-sandboxed. Note that `work` implies auto
|
|
532
|
+
mode, which asks the model to prefer shell commands for file edits, so a session
|
|
533
|
+
saving project-scoped memory has to be told to use `Write`/`Edit` instead.
|
|
534
|
+
|
|
495
535
|
### Requester profiles and enrolments
|
|
496
536
|
|
|
497
537
|
A non-local caller — one reached through a transport such as Tin Can rather
|
package/dist/config.js
CHANGED
|
@@ -64,6 +64,20 @@ const sessionSectionSchema = z
|
|
|
64
64
|
export const configSchema = z
|
|
65
65
|
.object({
|
|
66
66
|
host: z.enum(["auto", "tmux", "pty", "macos-terminal"]).default("auto"),
|
|
67
|
+
/**
|
|
68
|
+
* Off by default. Muster's tmux runs on its own server with `-f /dev/null`,
|
|
69
|
+
* so this bar is never the developer's configured one, and a Muster session
|
|
70
|
+
* is one window running one agent.
|
|
71
|
+
*/
|
|
72
|
+
tmux_status: z.enum(["on", "off"]).default("off"),
|
|
73
|
+
/**
|
|
74
|
+
* Which terminal `--open` uses when the launch does not name one. Was
|
|
75
|
+
* hardcoded to Terminal.app, so a developer on Ghostty or iTerm2 had to pass
|
|
76
|
+
* `--terminal` on every launch with no way to state it once.
|
|
77
|
+
*/
|
|
78
|
+
terminal: z
|
|
79
|
+
.enum(["auto", "terminal", "iterm2", "ghostty"])
|
|
80
|
+
.default("terminal"),
|
|
67
81
|
launch_timeout_sec: z.number().finite().positive().max(600).default(30),
|
|
68
82
|
max_concurrent: z.number().int().positive().max(1000).default(4),
|
|
69
83
|
allowed_roots: z.array(z.string().min(1)).default([]),
|
package/dist/guard.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
3
4
|
import { realpath } from "node:fs/promises";
|
|
4
|
-
import { isAbsolute, relative, resolve } from "node:path";
|
|
5
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
6
|
+
import { claudeConfigDir } from "./identity/claude.js";
|
|
5
7
|
import { mcpName, claudeMcpConfig } from "./mcp.js";
|
|
6
8
|
import { pluginName } from "./plugins.js";
|
|
7
9
|
import { RequesterConfigError } from "./requester-policy.js";
|
|
@@ -316,7 +318,90 @@ export function runtimeArgs(req) {
|
|
|
316
318
|
}
|
|
317
319
|
return result;
|
|
318
320
|
}
|
|
319
|
-
|
|
321
|
+
/**
|
|
322
|
+
* The keys of a profile's own settings.json that name its skill set rather than
|
|
323
|
+
* its policy. `--setting-sources ""` is what keeps `permissions`, `sandbox` and
|
|
324
|
+
* `hooks` muster's alone — forwarding those would let a settings file re-widen
|
|
325
|
+
* what `assertClaudePolicy` refuses to take on trust — so only these two are
|
|
326
|
+
* carried across, and muster's keys are spread after them and win regardless.
|
|
327
|
+
*/
|
|
328
|
+
const forwardedSettings = ["enabledPlugins", "extraKnownMarketplaces"];
|
|
329
|
+
function profilePluginSettings(configDir) {
|
|
330
|
+
let parsed;
|
|
331
|
+
try {
|
|
332
|
+
parsed = JSON.parse(readFileSync(join(configDir, "settings.json"), "utf8"));
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
// No settings file, unreadable, or not JSON: a launch with built-in skills
|
|
336
|
+
// only is degraded but correct, and failing here would take the sandbox fix
|
|
337
|
+
// down with it.
|
|
338
|
+
return {};
|
|
339
|
+
}
|
|
340
|
+
if (parsed === null || typeof parsed !== "object")
|
|
341
|
+
return {};
|
|
342
|
+
const source = parsed;
|
|
343
|
+
const out = {};
|
|
344
|
+
for (const key of forwardedSettings)
|
|
345
|
+
if (source[key] !== undefined)
|
|
346
|
+
out[key] = source[key];
|
|
347
|
+
return out;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Whether the caller typed `--terminal` themselves, read from the raw argv
|
|
351
|
+
* BEFORE `normalizeOpenFlag` runs. Every `--open <app>` becomes `--terminal
|
|
352
|
+
* <app>`, so after normalisation the two are indistinguishable and a deprecation
|
|
353
|
+
* notice would fire on the spelling Muster is steering callers towards.
|
|
354
|
+
*/
|
|
355
|
+
export function usesTerminalFlag(argv) {
|
|
356
|
+
const end = argv.indexOf("--");
|
|
357
|
+
return (end === -1 ? argv : argv.slice(0, end)).some((a) => a === "--terminal" || a.startsWith("--terminal="));
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Lifts `--open <app>` and `--open=<app>` onto `--terminal <app>` before
|
|
361
|
+
* `parseArgs` sees them, so `--open` can carry an optional value at all: in
|
|
362
|
+
* strict mode a boolean option refuses `=value` and a string option refuses a
|
|
363
|
+
* bare flag, and there is no third kind. Everything after `--` belongs to the
|
|
364
|
+
* runtime and is copied through untouched.
|
|
365
|
+
*
|
|
366
|
+
* An unrecognised app is lifted too rather than left behind, so the schema names
|
|
367
|
+
* it in the error instead of it becoming a silent runtime argument.
|
|
368
|
+
*/
|
|
369
|
+
export function normalizeOpenFlag(argv) {
|
|
370
|
+
const out = [];
|
|
371
|
+
for (let i = 0; i < argv.length; i++) {
|
|
372
|
+
const arg = argv[i];
|
|
373
|
+
if (arg === "--") {
|
|
374
|
+
out.push(...argv.slice(i));
|
|
375
|
+
return out;
|
|
376
|
+
}
|
|
377
|
+
if (arg.startsWith("--open=")) {
|
|
378
|
+
out.push("--open", "--terminal", arg.slice("--open=".length));
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (arg === "--open") {
|
|
382
|
+
const next = argv[i + 1];
|
|
383
|
+
if (next !== undefined && next !== "--" && !next.startsWith("-")) {
|
|
384
|
+
out.push("--open", "--terminal", next);
|
|
385
|
+
i++;
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
out.push(arg);
|
|
390
|
+
}
|
|
391
|
+
return out;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Which terminal `--open` uses: the launch's own choice, else config's, and
|
|
395
|
+
* Terminal.app only when neither names one — it is the one terminal every macOS
|
|
396
|
+
* is guaranteed to have. `auto` is a request to decide rather than a literal
|
|
397
|
+
* app, so it defers to config exactly as an absent value does.
|
|
398
|
+
*/
|
|
399
|
+
export function resolveTerminal(req, config) {
|
|
400
|
+
if (req.terminal && req.terminal !== "auto")
|
|
401
|
+
return req.terminal;
|
|
402
|
+
return config.terminal === "auto" ? "terminal" : config.terminal;
|
|
403
|
+
}
|
|
404
|
+
export function launchArgs(req, config, mcp = [], bridgePath, env = process.env) {
|
|
320
405
|
// Before `runtimeArgs`, so this function owns every model error on its own
|
|
321
406
|
// rather than by its caller's discipline: `Muster.launch` happens to resolve
|
|
322
407
|
// first, but `launchArgs` is exported and an input carrying both a model
|
|
@@ -357,7 +442,15 @@ export function launchArgs(req, config, mcp = [], bridgePath) {
|
|
|
357
442
|
: policy.permissions === "bypass"
|
|
358
443
|
? "bypassPermissions"
|
|
359
444
|
: "dontAsk";
|
|
445
|
+
// The dir the launched agent will actually open: `launchEnv`'s blocklist is
|
|
446
|
+
// /^(CLAUDECODE|CLAUDE_CODE_|...)/, which does not match CLAUDE_CONFIG_DIR, so
|
|
447
|
+
// the child inherits whatever the caller set. Shared with the trust read and
|
|
448
|
+
// the sessions dir rather than restated — a second copy here drifted from that
|
|
449
|
+
// one already (it resolve()d, and read a blank value as unset), which is how a
|
|
450
|
+
// write root ends up naming a directory nobody is launching in.
|
|
451
|
+
const configDir = claudeConfigDir(env);
|
|
360
452
|
const settings = {
|
|
453
|
+
...profilePluginSettings(configDir),
|
|
361
454
|
disableAllHooks: true,
|
|
362
455
|
permissions: {
|
|
363
456
|
defaultMode: mode,
|
|
@@ -374,7 +467,22 @@ export function launchArgs(req, config, mcp = [], bridgePath) {
|
|
|
374
467
|
autoAllowBashIfSandboxed: true,
|
|
375
468
|
allowUnsandboxedCommands: false,
|
|
376
469
|
excludedCommands: [],
|
|
377
|
-
filesystem: {
|
|
470
|
+
filesystem: {
|
|
471
|
+
denyWrite: readonly ? ["/"] : [],
|
|
472
|
+
// The agent's own memory lives under the config dir, outside the cwd
|
|
473
|
+
// that Claude Code defaults the write root to, so Bash writes to it
|
|
474
|
+
// failed with EPERM while the in-process file tools succeeded (#80).
|
|
475
|
+
// Granted only under workspace-write: read-only must stay read-only,
|
|
476
|
+
// and full-access has no sandbox to grant into.
|
|
477
|
+
// Absolute only here: the sandbox is an OS boundary evaluated against a
|
|
478
|
+
// path, and a relative CLAUDE_CONFIG_DIR would grant whatever the
|
|
479
|
+
// launching process's cwd happened to be. The shared resolver stays
|
|
480
|
+
// as-written so the trust read and the sessions dir keep their
|
|
481
|
+
// behaviour.
|
|
482
|
+
...(policy.sandbox === "workspace-write"
|
|
483
|
+
? { allowWrite: [resolve(configDir)] }
|
|
484
|
+
: {}),
|
|
485
|
+
},
|
|
378
486
|
},
|
|
379
487
|
};
|
|
380
488
|
return [
|
package/dist/help.js
CHANGED
|
@@ -43,8 +43,14 @@ export const COMMANDS = [
|
|
|
43
43
|
["--kind KIND", "session (default) or task"],
|
|
44
44
|
["--host ID", "tmux, pty, or auto (default: tmux, then pty)"],
|
|
45
45
|
["--identity NAME", "run as a configured identity"],
|
|
46
|
-
[
|
|
47
|
-
|
|
46
|
+
[
|
|
47
|
+
"--open [APP]",
|
|
48
|
+
"display the tmux session in a terminal (macOS); APP is terminal, iterm2 or ghostty, defaulting to the configured terminal",
|
|
49
|
+
],
|
|
50
|
+
[
|
|
51
|
+
"--terminal APP",
|
|
52
|
+
"deprecated, removed in v2: pass the app to --open instead",
|
|
53
|
+
],
|
|
48
54
|
[
|
|
49
55
|
"--level LEVEL",
|
|
50
56
|
"read, work, or open: a legal permissions and sandbox pair",
|
package/dist/hosts/index.js
CHANGED
|
@@ -9,9 +9,9 @@ import { MacosTerminalHost } from "./macos-terminal.js";
|
|
|
9
9
|
* muster` and stay there, since a tmux launch is meant to survive its parent
|
|
10
10
|
* exiting (#57).
|
|
11
11
|
*/
|
|
12
|
-
export function hosts(env = process.env) {
|
|
12
|
+
export function hosts(env = process.env, tmuxStatus = "off") {
|
|
13
13
|
return [
|
|
14
|
-
new TmuxHost(env.MUSTER_TMUX_SERVER || undefined),
|
|
14
|
+
new TmuxHost(env.MUSTER_TMUX_SERVER || undefined, tmuxStatus),
|
|
15
15
|
new PtyHost(),
|
|
16
16
|
new MacosTerminalHost(),
|
|
17
17
|
];
|
package/dist/hosts/tmux.js
CHANGED
|
@@ -72,9 +72,19 @@ export function tmuxCommand(argv) {
|
|
|
72
72
|
}
|
|
73
73
|
export class TmuxHost {
|
|
74
74
|
server;
|
|
75
|
+
status;
|
|
75
76
|
id = "tmux";
|
|
76
|
-
|
|
77
|
+
/**
|
|
78
|
+
* `status` is set explicitly on every session rather than left to tmux's
|
|
79
|
+
* default, so the bar's state is what Muster asked for whatever version of
|
|
80
|
+
* tmux is installed. Off by default: Muster's server is its own (`-L`, with
|
|
81
|
+
* `-f /dev/null`), so the bar here is never the developer's configured one,
|
|
82
|
+
* and a session is one window running one agent — the bar reports only what
|
|
83
|
+
* the caller already knows. `tmux_status = "on"` in config.toml restores it.
|
|
84
|
+
*/
|
|
85
|
+
constructor(server = "muster", status = "off") {
|
|
77
86
|
this.server = server;
|
|
87
|
+
this.status = status;
|
|
78
88
|
if (!/^[a-zA-Z0-9_-]+$/.test(server))
|
|
79
89
|
throw new Error("Invalid tmux server");
|
|
80
90
|
}
|
|
@@ -155,7 +165,23 @@ export class TmuxHost {
|
|
|
155
165
|
try {
|
|
156
166
|
// Window selection is session-wide: sharing a session makes every
|
|
157
167
|
// attached terminal follow whichever agent was opened most recently.
|
|
158
|
-
const
|
|
168
|
+
const session = `muster-${randomUUID()}`;
|
|
169
|
+
// One tmux invocation, not two: a second round trip would leave the bar
|
|
170
|
+
// visible for as long as it took, and `;` keeps set-option in the same
|
|
171
|
+
// command list. Targeted by session name rather than relying on "current",
|
|
172
|
+
// which is not meaningful for a detached session.
|
|
173
|
+
const output = await this.call([
|
|
174
|
+
"new-session",
|
|
175
|
+
"-s",
|
|
176
|
+
session,
|
|
177
|
+
...common,
|
|
178
|
+
";",
|
|
179
|
+
"set-option",
|
|
180
|
+
"-t",
|
|
181
|
+
session,
|
|
182
|
+
"status",
|
|
183
|
+
this.status,
|
|
184
|
+
], opts.deadline);
|
|
159
185
|
return parseWindowRow(output);
|
|
160
186
|
}
|
|
161
187
|
catch (e) {
|
package/dist/muster.js
CHANGED
|
@@ -13,6 +13,7 @@ import { loadConfig, musterHome } from "./config.js";
|
|
|
13
13
|
import { describeIdentities, interactiveSetup, setupIdentity, } from "./identity-cli.js";
|
|
14
14
|
import { IdentityError } from "./identity-store.js";
|
|
15
15
|
import { MCP_TOOLS } from "./mcp-tools.js";
|
|
16
|
+
import { normalizeOpenFlag, usesTerminalFlag } from "./guard.js";
|
|
16
17
|
import { commandHelp, topLevelHelp } from "./help.js";
|
|
17
18
|
import { promptForMissing, terminalReader } from "./prompt.js";
|
|
18
19
|
const listSchema = z
|
|
@@ -147,8 +148,13 @@ async function main() {
|
|
|
147
148
|
}
|
|
148
149
|
try {
|
|
149
150
|
const command = argv.shift();
|
|
151
|
+
// Read before normalisation, which turns `--open <app>` into `--terminal
|
|
152
|
+
// <app>` and would otherwise make the notice fire on the replacement.
|
|
153
|
+
const typedTerminalFlag = usesTerminalFlag(argv);
|
|
150
154
|
const { values: parsed, positionals } = parseArgs({
|
|
151
|
-
|
|
155
|
+
// `--open` carries an optional terminal name, which parseArgs has no kind
|
|
156
|
+
// for; the value is lifted onto `--terminal` first. See normalizeOpenFlag.
|
|
157
|
+
args: normalizeOpenFlag(argv),
|
|
152
158
|
allowPositionals: true,
|
|
153
159
|
strict: true,
|
|
154
160
|
options: {
|
|
@@ -221,6 +227,8 @@ async function main() {
|
|
|
221
227
|
? formatHuman(value, humanColorEnabled(process.stdout.isTTY, process.env))
|
|
222
228
|
: JSON.stringify(value) + "\n");
|
|
223
229
|
if (command === "run") {
|
|
230
|
+
if (typedTerminalFlag)
|
|
231
|
+
process.stderr.write("[muster] --terminal is deprecated and will be removed in v2; pass the app to --open instead, or set terminal in config.toml\n");
|
|
224
232
|
const runtime = positionals[0];
|
|
225
233
|
const args = positionals.slice(1);
|
|
226
234
|
const record = await muster.run({
|
package/dist/run.js
CHANGED
|
@@ -4,7 +4,7 @@ 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, agentBaseEnv, assertAllowedWorkspace, assertLevelExclusive, resolveLifecycle, resolveModel, launchArgs, launchEnv, paramsFingerprint, resolvePermissions, runtimeArgs, } from "./guard.js";
|
|
7
|
+
import { runSchema, agentBaseEnv, assertAllowedWorkspace, assertLevelExclusive, resolveLifecycle, resolveModel, launchArgs, launchEnv, paramsFingerprint, resolvePermissions, resolveTerminal, runtimeArgs, } from "./guard.js";
|
|
8
8
|
import { enforcementGrade } from "./enforcement.js";
|
|
9
9
|
import { LOCAL } from "./requester.js";
|
|
10
10
|
import { assertEnforcementMeetsMinimum, assertLevelWithinCeiling, assertMayAct, resolveIdentityForRequest, resolveRequesterPolicy, RequesterPolicyRefusal, } from "./requester-policy.js";
|
|
@@ -98,7 +98,8 @@ export class Muster {
|
|
|
98
98
|
}
|
|
99
99
|
static async create(opts = {}) {
|
|
100
100
|
const env = launchEnv(opts.env ?? process.env), home = opts.home ?? join(env.HOME ?? homedir(), ".muster");
|
|
101
|
-
|
|
101
|
+
const config = await loadConfig(home);
|
|
102
|
+
return new Muster(config, home, env, opts.drivers ?? hosts(opts.env ?? process.env, config.tmux_status), { ...(opts.env ?? process.env) });
|
|
102
103
|
}
|
|
103
104
|
run(input, requester = LOCAL) {
|
|
104
105
|
if (this.closing)
|
|
@@ -217,7 +218,7 @@ export class Muster {
|
|
|
217
218
|
throw new Error("host applies only to session launches");
|
|
218
219
|
if (req.terminal && !req.open)
|
|
219
220
|
throw new Error("--terminal requires --open");
|
|
220
|
-
const terminal =
|
|
221
|
+
const terminal = resolveTerminal(req, this.config);
|
|
221
222
|
const requestedHost = req.host ?? this.config.host;
|
|
222
223
|
if (req.open &&
|
|
223
224
|
(req.kind !== "session" || !["auto", "tmux"].includes(requestedHost)))
|
|
@@ -240,7 +241,7 @@ export class Muster {
|
|
|
240
241
|
if (req.runtime === "opencode")
|
|
241
242
|
runtimeArgs(effective);
|
|
242
243
|
else
|
|
243
|
-
argv = launchArgs(effective, this.config);
|
|
244
|
+
argv = launchArgs(effective, this.config, [], undefined, this.sourceEnv);
|
|
244
245
|
const driver = req.kind === "session"
|
|
245
246
|
? await selectHost(req.open ? "tmux" : requestedHost, this.drivers)
|
|
246
247
|
: undefined;
|
|
@@ -389,7 +390,10 @@ export class Muster {
|
|
|
389
390
|
await writeFile(bridgePath, JSON.stringify(Object.fromEntries(prepared.servers.map((s) => [s.name, s.config]))), { mode: 0o600 });
|
|
390
391
|
}
|
|
391
392
|
if (req.runtime !== "opencode")
|
|
392
|
-
|
|
393
|
+
// `runtimeEnv`, not the host's: an identity launch relocates
|
|
394
|
+
// CLAUDE_CONFIG_DIR to the per-launch copy, and the sandbox must grant
|
|
395
|
+
// the dir the child will actually open, not the one Muster runs under.
|
|
396
|
+
argv = launchArgs(effective, this.config, prepared.servers, bridgePath, runtimeEnv);
|
|
393
397
|
if (req.runtime === "codex") {
|
|
394
398
|
codexPolicy = await codexPolicyArgs(runtimeEnv, req.cwd, deadline, prepared.servers, req.kind === "task");
|
|
395
399
|
argv.splice(1, 0, ...codexPolicy);
|