@brutalsystems/muster 0.9.2 → 0.10.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 +24 -0
- package/dist/guard.js +57 -3
- package/dist/run.js +5 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -492,6 +492,30 @@ Every launch also records how strongly its containment was imposed:
|
|
|
492
492
|
| `tool-policy` | OpenCode — rules the agent is asked to respect, with no kernel enforcement. |
|
|
493
493
|
| `none` | `full-access` — the sandbox is deliberately disabled. |
|
|
494
494
|
|
|
495
|
+
#### What a sandboxed launch may write
|
|
496
|
+
|
|
497
|
+
Muster names the write roots rather than leaving them to default. Under
|
|
498
|
+
`workspace-write` a launched Claude session may write its cwd, `$TMPDIR`, and
|
|
499
|
+
**its own configuration directory** — the `CLAUDE_CONFIG_DIR` the child will
|
|
500
|
+
actually use, which an identity launch or a requester profile's `env_defaults`
|
|
501
|
+
may relocate. That last root is granted because an agent's memory lives there:
|
|
502
|
+
without it every Bash write to memory failed with `EPERM` while the in-process
|
|
503
|
+
file tools succeeded, which reads from inside the session as a path bug rather
|
|
504
|
+
than a boundary.
|
|
505
|
+
|
|
506
|
+
Under `read` nothing is writable, including the config dir. Under `full-access`
|
|
507
|
+
there is no sandbox to grant into.
|
|
508
|
+
|
|
509
|
+
One path stays denied that muster cannot open: **`$CLAUDE_CONFIG_DIR/projects/`**
|
|
510
|
+
(and `shell-snapshots/`). Claude Code refuses writes there in a sandboxed Bash
|
|
511
|
+
regardless of what muster grants — naming those directories explicitly in
|
|
512
|
+
`allowWrite` changes nothing, which is presumably deliberate, since a session's
|
|
513
|
+
own transcripts live there. Project-scoped memory under
|
|
514
|
+
`projects/<cwd-slug>/memory` is therefore reachable only through the file tools,
|
|
515
|
+
which are in-process and not kernel-sandboxed. Note that `work` implies auto
|
|
516
|
+
mode, which asks the model to prefer shell commands for file edits, so a session
|
|
517
|
+
saving project-scoped memory has to be told to use `Write`/`Edit` instead.
|
|
518
|
+
|
|
495
519
|
### Requester profiles and enrolments
|
|
496
520
|
|
|
497
521
|
A non-local caller — one reached through a transport such as Tin Can rather
|
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 {
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
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,47 @@ export function runtimeArgs(req) {
|
|
|
316
318
|
}
|
|
317
319
|
return result;
|
|
318
320
|
}
|
|
319
|
-
|
|
321
|
+
/**
|
|
322
|
+
* The config dir the launched agent will actually use. `launchEnv`'s blocklist
|
|
323
|
+
* is /^(CLAUDECODE|CLAUDE_CODE_|...)/, which does not match CLAUDE_CONFIG_DIR,
|
|
324
|
+
* so the child inherits whatever the caller set and this resolves to the same
|
|
325
|
+
* dir the child will open.
|
|
326
|
+
*/
|
|
327
|
+
export function claudeConfigDir(env) {
|
|
328
|
+
const configured = env.CLAUDE_CONFIG_DIR;
|
|
329
|
+
if (configured && configured.trim().length > 0)
|
|
330
|
+
return resolve(configured);
|
|
331
|
+
return join(env.HOME ?? homedir(), ".claude");
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* The keys of a profile's own settings.json that name its skill set rather than
|
|
335
|
+
* its policy. `--setting-sources ""` is what keeps `permissions`, `sandbox` and
|
|
336
|
+
* `hooks` muster's alone — forwarding those would let a settings file re-widen
|
|
337
|
+
* what `assertClaudePolicy` refuses to take on trust — so only these two are
|
|
338
|
+
* carried across, and muster's keys are spread after them and win regardless.
|
|
339
|
+
*/
|
|
340
|
+
const forwardedSettings = ["enabledPlugins", "extraKnownMarketplaces"];
|
|
341
|
+
function profilePluginSettings(configDir) {
|
|
342
|
+
let parsed;
|
|
343
|
+
try {
|
|
344
|
+
parsed = JSON.parse(readFileSync(join(configDir, "settings.json"), "utf8"));
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
// No settings file, unreadable, or not JSON: a launch with built-in skills
|
|
348
|
+
// only is degraded but correct, and failing here would take the sandbox fix
|
|
349
|
+
// down with it.
|
|
350
|
+
return {};
|
|
351
|
+
}
|
|
352
|
+
if (parsed === null || typeof parsed !== "object")
|
|
353
|
+
return {};
|
|
354
|
+
const source = parsed;
|
|
355
|
+
const out = {};
|
|
356
|
+
for (const key of forwardedSettings)
|
|
357
|
+
if (source[key] !== undefined)
|
|
358
|
+
out[key] = source[key];
|
|
359
|
+
return out;
|
|
360
|
+
}
|
|
361
|
+
export function launchArgs(req, config, mcp = [], bridgePath, env = process.env) {
|
|
320
362
|
// Before `runtimeArgs`, so this function owns every model error on its own
|
|
321
363
|
// rather than by its caller's discipline: `Muster.launch` happens to resolve
|
|
322
364
|
// first, but `launchArgs` is exported and an input carrying both a model
|
|
@@ -357,7 +399,9 @@ export function launchArgs(req, config, mcp = [], bridgePath) {
|
|
|
357
399
|
: policy.permissions === "bypass"
|
|
358
400
|
? "bypassPermissions"
|
|
359
401
|
: "dontAsk";
|
|
402
|
+
const configDir = claudeConfigDir(env);
|
|
360
403
|
const settings = {
|
|
404
|
+
...profilePluginSettings(configDir),
|
|
361
405
|
disableAllHooks: true,
|
|
362
406
|
permissions: {
|
|
363
407
|
defaultMode: mode,
|
|
@@ -374,7 +418,17 @@ export function launchArgs(req, config, mcp = [], bridgePath) {
|
|
|
374
418
|
autoAllowBashIfSandboxed: true,
|
|
375
419
|
allowUnsandboxedCommands: false,
|
|
376
420
|
excludedCommands: [],
|
|
377
|
-
filesystem: {
|
|
421
|
+
filesystem: {
|
|
422
|
+
denyWrite: readonly ? ["/"] : [],
|
|
423
|
+
// The agent's own memory lives under the config dir, outside the cwd
|
|
424
|
+
// that Claude Code defaults the write root to, so Bash writes to it
|
|
425
|
+
// failed with EPERM while the in-process file tools succeeded (#80).
|
|
426
|
+
// Granted only under workspace-write: read-only must stay read-only,
|
|
427
|
+
// and full-access has no sandbox to grant into.
|
|
428
|
+
...(policy.sandbox === "workspace-write"
|
|
429
|
+
? { allowWrite: [configDir] }
|
|
430
|
+
: {}),
|
|
431
|
+
},
|
|
378
432
|
},
|
|
379
433
|
};
|
|
380
434
|
return [
|
package/dist/run.js
CHANGED
|
@@ -240,7 +240,7 @@ export class Muster {
|
|
|
240
240
|
if (req.runtime === "opencode")
|
|
241
241
|
runtimeArgs(effective);
|
|
242
242
|
else
|
|
243
|
-
argv = launchArgs(effective, this.config);
|
|
243
|
+
argv = launchArgs(effective, this.config, [], undefined, this.sourceEnv);
|
|
244
244
|
const driver = req.kind === "session"
|
|
245
245
|
? await selectHost(req.open ? "tmux" : requestedHost, this.drivers)
|
|
246
246
|
: undefined;
|
|
@@ -389,7 +389,10 @@ export class Muster {
|
|
|
389
389
|
await writeFile(bridgePath, JSON.stringify(Object.fromEntries(prepared.servers.map((s) => [s.name, s.config]))), { mode: 0o600 });
|
|
390
390
|
}
|
|
391
391
|
if (req.runtime !== "opencode")
|
|
392
|
-
|
|
392
|
+
// `runtimeEnv`, not the host's: an identity launch relocates
|
|
393
|
+
// CLAUDE_CONFIG_DIR to the per-launch copy, and the sandbox must grant
|
|
394
|
+
// the dir the child will actually open, not the one Muster runs under.
|
|
395
|
+
argv = launchArgs(effective, this.config, prepared.servers, bridgePath, runtimeEnv);
|
|
393
396
|
if (req.runtime === "codex") {
|
|
394
397
|
codexPolicy = await codexPolicyArgs(runtimeEnv, req.cwd, deadline, prepared.servers, req.kind === "task");
|
|
395
398
|
argv.splice(1, 0, ...codexPolicy);
|