@brutalsystems/muster 0.8.1 → 0.8.3
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 -3
- package/dist/identity/claude.js +68 -2
- package/dist/run.js +12 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -55,9 +55,24 @@ driver is an unavailable v1 stub.
|
|
|
55
55
|
|
|
56
56
|
Before launching a Claude session, open Claude normally in the target directory
|
|
57
57
|
and complete its workspace-trust review yourself, then exit that setup session.
|
|
58
|
-
This is a one-time prerequisite for each directory Claude requires you to trust
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
This is a one-time prerequisite for each directory Claude requires you to trust,
|
|
59
|
+
and trust is **not inherited**: accepting `~/Source/arm` says nothing about
|
|
60
|
+
`~/Source/arm/centrumx`, which still stops at its own dialog. Muster never
|
|
61
|
+
accepts trust prompts or changes trust settings. An untrusted directory blocks
|
|
62
|
+
startup; Muster times out and cleans up that launch.
|
|
63
|
+
|
|
64
|
+
A session sitting on that dialog has no registry record at all, so the timeout
|
|
65
|
+
used to look identical to a hung or crashed launch. Muster now reads
|
|
66
|
+
`hasTrustDialogAccepted` for the working directory out of the profile's
|
|
67
|
+
`.claude.json` when a Claude launch times out with nothing registered, and
|
|
68
|
+
either names the untrusted directory and the one-time fix or rules trust out
|
|
69
|
+
and sends you to the terminal for whatever else is holding the session — a
|
|
70
|
+
login prompt, or another one-time confirmation. It deliberately does not guess
|
|
71
|
+
which: trust is the only one of them Muster can check, and a message that names
|
|
72
|
+
the wrong screen costs what the vague one did. That read happens only on the
|
|
73
|
+
failure path and only ever reads. A
|
|
74
|
+
`--kind task` launch into the same directory is the cheap discriminator by
|
|
75
|
+
hand: it runs headless and never sees the dialog.
|
|
61
76
|
|
|
62
77
|
```sh
|
|
63
78
|
npm ci
|
|
@@ -405,6 +420,19 @@ arrive after startup; this v1 is not an enterprise policy-enforcement layer.
|
|
|
405
420
|
Claude permissions and its command sandbox are different mechanisms; neither
|
|
406
421
|
claim implies that every external tool is OS-sandboxed.
|
|
407
422
|
|
|
423
|
+
**`workspace-write` does not fence Claude's Write tool, only its Bash.** This
|
|
424
|
+
is measured, not inferred — see "Write/Bash fence asymmetry" in
|
|
425
|
+
[PROBE_RESULTS.md](./PROBE_RESULTS.md). At `--level work` an agent's Bash write
|
|
426
|
+
outside the working tree is refused with `Operation not permitted`, while the
|
|
427
|
+
Write tool creates the same path successfully. The practical consequence is
|
|
428
|
+
that **an agent can create files outside the tree that it cannot then delete**,
|
|
429
|
+
because creation goes through Write and cleanup goes through Bash. No setting
|
|
430
|
+
Muster writes expresses the missing fence: the alternative, `deny`, blocks the
|
|
431
|
+
Write tool everywhere rather than by path, including inside the tree. Treat
|
|
432
|
+
`work` as "Bash is confined to the workspace", not "writes are". If that
|
|
433
|
+
distinction matters for a given launch, the containment that does hold for both
|
|
434
|
+
is `read`.
|
|
435
|
+
|
|
408
436
|
OpenCode keeps normal provider/model configuration and launches with `--pure`
|
|
409
437
|
unless plugins are selected, which disables inherited external plugins.
|
|
410
438
|
Providers declared under `[opencode.provider]` and a model named by
|
|
@@ -668,6 +696,18 @@ need it), and resume/fork (#32) cannot assume a copy is still there.
|
|
|
668
696
|
Launching without `--identity` sets none of this: behaviour is exactly as
|
|
669
697
|
before, using the ambient environment.
|
|
670
698
|
|
|
699
|
+
**That is the right choice for profile work, which is counter-intuitive.** If
|
|
700
|
+
you already work in a shell whose `CLAUDE_CONFIG_DIR` selects an account —
|
|
701
|
+
`~/.claude-armdev`, say — a launch with no `--identity` inherits it, and the
|
|
702
|
+
session lands on that account with no flag at all. `launchEnv` blocks
|
|
703
|
+
`CLAUDECODE`, `CLAUDE_CODE_*`, `CODEX_THREAD_ID`, `TMUX`, `TMUX_PANE` and
|
|
704
|
+
`MCP_*` from reaching the child, and deliberately lets `CLAUDE_CONFIG_DIR`
|
|
705
|
+
through for exactly this reason. Passing `--identity` there does not add
|
|
706
|
+
containment to that account — it **replaces** it, pointing the runtime at a
|
|
707
|
+
copy of a different one, so the session appears under an account you did not
|
|
708
|
+
mean to use. Reach for `--identity` to choose an account Muster manages, not to
|
|
709
|
+
harden the one your shell already selected.
|
|
710
|
+
|
|
671
711
|
Session discovery follows the same relocated path, so a Claude identity's
|
|
672
712
|
sessions are found even though `CLAUDE_CONFIG_DIR` no longer points at
|
|
673
713
|
`~/.claude` — this was previously refused outright as an unsupported
|
package/dist/identity/claude.js
CHANGED
|
@@ -13,10 +13,76 @@ import { descendants } from "./processes.js";
|
|
|
13
13
|
* reporting anything useful.
|
|
14
14
|
*/
|
|
15
15
|
export function claudeSessionsDir(env) {
|
|
16
|
+
return join(claudeConfigDir(env), "sessions");
|
|
17
|
+
}
|
|
18
|
+
/** The configuration directory itself, which `claudeSessionsDir` and the
|
|
19
|
+
* workspace-trust read below both hang off. Split out so the two cannot drift:
|
|
20
|
+
* a trust decision read from a different directory than the one the launch
|
|
21
|
+
* records its session in would describe a profile nobody is launching. */
|
|
22
|
+
export function claudeConfigDir(env) {
|
|
16
23
|
const configured = env.CLAUDE_CONFIG_DIR;
|
|
17
24
|
if (typeof configured === "string" && configured.length)
|
|
18
|
-
return
|
|
19
|
-
return join(env.HOME ?? homedir(), ".claude"
|
|
25
|
+
return configured;
|
|
26
|
+
return join(env.HOME ?? homedir(), ".claude");
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Whether Claude Code has recorded an accepted workspace-trust dialog for one
|
|
30
|
+
* directory, in the profile a launch would use. Trust is per-directory and is
|
|
31
|
+
* NOT inherited from a parent: a trusted `/Source/arm` says nothing about
|
|
32
|
+
* `/Source/arm/centrumx`, and the child still stops at the dialog.
|
|
33
|
+
*
|
|
34
|
+
* This only ever READS. Muster does not accept the dialog on the operator's
|
|
35
|
+
* behalf — `setWorkspaceTrust` writes that flag into an identity copy Muster
|
|
36
|
+
* itself created, never into the live `~/.claude.json`, and this must not
|
|
37
|
+
* become a way around that.
|
|
38
|
+
*
|
|
39
|
+
* Every failure answers `false`, because the only caller is a diagnostic on a
|
|
40
|
+
* launch that has already failed. A missing, unreadable or malformed file
|
|
41
|
+
* means Muster cannot say trust was accepted, which is exactly what `false`
|
|
42
|
+
* reports here; throwing would replace a useful message with a worse one.
|
|
43
|
+
*/
|
|
44
|
+
export async function claudeWorkspaceTrusted(env, cwd) {
|
|
45
|
+
let raw;
|
|
46
|
+
try {
|
|
47
|
+
raw = await readFile(join(claudeConfigDir(env), ".claude.json"), "utf8");
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const data = JSON.parse(raw);
|
|
54
|
+
return data.projects?.[cwd]?.hasTrustDialogAccepted === true;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The message a Claude launch dies with when no session ever registered.
|
|
62
|
+
*
|
|
63
|
+
* A session sitting on a consent screen has no registry record at all, so the
|
|
64
|
+
* timeout looks identical to a hung or crashed launch. Several unrelated
|
|
65
|
+
* screens can produce it, and the old message guessed at two of them together.
|
|
66
|
+
* Trust is the one Muster can actually check, so it is either named or ruled
|
|
67
|
+
* out. Nothing else is named: a first-use bypass-permissions confirmation was
|
|
68
|
+
* a plausible account of one report, but no profile on this machine carries a
|
|
69
|
+
* key for it the way they carry `hasSeenAutoModeEntryWarning` and its
|
|
70
|
+
* siblings, so this says "another one-time confirmation" rather than sending
|
|
71
|
+
* the reader after a screen nobody has demonstrated.
|
|
72
|
+
*
|
|
73
|
+
* A `--kind task` launch into the same directory is the cheap discriminator:
|
|
74
|
+
* it runs headless and never sees the dialog, so it succeeds where a session
|
|
75
|
+
* hangs.
|
|
76
|
+
*/
|
|
77
|
+
export function claudeRegistryDiagnostic(trusted, cwd, configDir) {
|
|
78
|
+
if (!trusted)
|
|
79
|
+
return (`no Claude session registry found: workspace trust has not been ` +
|
|
80
|
+
`accepted for ${cwd} in ${configDir} — accept it once with ` +
|
|
81
|
+
`\`cd ${cwd} && claude\`, then retry (trust is per-directory and is ` +
|
|
82
|
+
`not inherited from a parent directory)`);
|
|
83
|
+
return (`no Claude session registry found (${cwd} is trusted for this profile, ` +
|
|
84
|
+
`so the session is stopped on something else — check the terminal for a ` +
|
|
85
|
+
`login prompt or another one-time confirmation)`);
|
|
20
86
|
}
|
|
21
87
|
export async function resolveClaude(root, env, deadline) {
|
|
22
88
|
const dir = claudeSessionsDir(env);
|
package/dist/run.js
CHANGED
|
@@ -16,7 +16,7 @@ import { LaunchLog } from "./log.js";
|
|
|
16
16
|
import { hosts, selectHost } from "./hosts/index.js";
|
|
17
17
|
import { processRef, isSame, stopTree, delay, descendants, ownedGroup, } from "./identity/processes.js";
|
|
18
18
|
import { resolveCodex } from "./identity/codex.js";
|
|
19
|
-
import { resolveClaude } from "./identity/claude.js";
|
|
19
|
+
import { claudeConfigDir, claudeRegistryDiagnostic, claudeWorkspaceTrusted, resolveClaude, } from "./identity/claude.js";
|
|
20
20
|
import { openCodeEndpointOwnedBy, resolveOpenCode, } from "./identity/opencode.js";
|
|
21
21
|
import { codexReachable, threadRejection } from "./reach/codex.js";
|
|
22
22
|
import { claudeReachable } from "./reach/claude.js";
|
|
@@ -486,6 +486,10 @@ export class Muster {
|
|
|
486
486
|
: req.runtime === "opencode"
|
|
487
487
|
? "no OpenCode session matched the launch endpoint, cwd, and creation window"
|
|
488
488
|
: "no descendant runtime identity found";
|
|
489
|
+
// Kept so the throw below can tell "nothing ever registered" from a
|
|
490
|
+
// diagnosis the loop actually earned. Only the first case can be about a
|
|
491
|
+
// consent screen; anything later means a session existed.
|
|
492
|
+
const neverRegistered = diagnostic;
|
|
489
493
|
// `resolveEnv`, not `hostEnv`: this connection is how the thread
|
|
490
494
|
// `resolveCodex` found gets READ — both its `rejectionOf` screen, where a
|
|
491
495
|
// non-null answer is fatal, and `codexReachable` at readiness. An
|
|
@@ -620,6 +624,13 @@ export class Muster {
|
|
|
620
624
|
}
|
|
621
625
|
await delay(Math.min(250, Math.max(0, deadline - Date.now())));
|
|
622
626
|
}
|
|
627
|
+
// Resolved here rather than before the launch: this reads the operator's
|
|
628
|
+
// live `.claude.json`, and a launch that is going to work should not pay
|
|
629
|
+
// for it. Failing closed is also wrong before a launch — `--kind task`
|
|
630
|
+
// runs headless and never sees the dialog, so refusing an untrusted
|
|
631
|
+
// directory up front would break task launches that work today.
|
|
632
|
+
if (req.runtime === "claude" && diagnostic === neverRegistered)
|
|
633
|
+
diagnostic = claudeRegistryDiagnostic(await claudeWorkspaceTrusted(resolveEnv, req.cwd), req.cwd, claudeConfigDir(resolveEnv));
|
|
623
634
|
throw new Error(`${diagnostic} after ${this.config.launch_timeout_sec}s`);
|
|
624
635
|
}
|
|
625
636
|
catch (e) {
|