@brutalsystems/muster 1.0.2 → 1.1.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 +35 -0
- package/README.md +3 -1
- package/dist/guard.js +8 -0
- package/dist/help.js +1 -1
- package/dist/identity/claude.js +11 -6
- package/dist/mcp-tools.js +5 -2
- package/dist/options.js +17 -3
- package/dist/run.js +15 -4
- package/package.json +1 -1
package/CONTRACT_PROVENANCE.md
CHANGED
|
@@ -527,3 +527,38 @@ convenient re-sync: `test/fixtures/canonical-id.json` is in `package.json`'s
|
|
|
527
527
|
fixture moved `publish.yml`, `RELEASING.md` and `docs/verification.md` on
|
|
528
528
|
their own. Only the two frozen hashes in `test/naming.test.ts` are hand-written,
|
|
529
529
|
and those are the third site behaving as documented.
|
|
530
|
+
|
|
531
|
+
## What the vendored copy covers, and what it deliberately does not
|
|
532
|
+
|
|
533
|
+
The copies pin **the address format Muster generates** — slugify, suffix,
|
|
534
|
+
assignment, resolution — and the refusals that resolution can produce:
|
|
535
|
+
`ambiguous`, `self`, `unknown`. That is the whole surface, and the boundary is
|
|
536
|
+
not an accident of what was convenient to copy.
|
|
537
|
+
|
|
538
|
+
They do **not** cover Tin Can's send-path vocabulary, because Muster never calls
|
|
539
|
+
`send_peer` in product code. Muster produces addresses; Tin Can does the
|
|
540
|
+
sending. The only mentions of that call in this repository are a doc comment in
|
|
541
|
+
`naming.ts` describing what an address is for, and the contract suite asserting
|
|
542
|
+
`outcome === "accepted"` on the happy path. Neither enumerates a refusal, so
|
|
543
|
+
there is no unknown-refusal path here for a new one to fall through.
|
|
544
|
+
|
|
545
|
+
Tin Can 2.0.0 is the worked example. It added a `duplicate_peer_moved` refusal,
|
|
546
|
+
splitting the retry-whose-name-has-moved case out of `duplicate_send`. A
|
|
547
|
+
consumer of send outcomes had to act on that — birddog did, and it was a real
|
|
548
|
+
change for them. Muster owed nothing, and the 2.0.0 re-sync above correctly
|
|
549
|
+
shows no such edit. The asymmetry is clean: Muster produces addresses and never
|
|
550
|
+
consumes send outcomes; birddog consumes send outcomes and never produces an
|
|
551
|
+
address. Neither repository inherits the other's obligations, and a vendored
|
|
552
|
+
contract that tried to cover both would be wrong in both directions.
|
|
553
|
+
|
|
554
|
+
**So the question at each re-sync is not "did Tin Can change".** It is: did
|
|
555
|
+
anything Muster *generates or parses* change. Copying more than that is not
|
|
556
|
+
caution, it is over-vendoring — it pins a surface Muster does not use, and the
|
|
557
|
+
frozen hashes in `test/naming.test.ts` then move for reasons that have nothing
|
|
558
|
+
to do with Muster, which is how a freeze stops meaning anything.
|
|
559
|
+
|
|
560
|
+
**If Muster ever grows a send path, this boundary moves and the copy must widen
|
|
561
|
+
with it** — `duplicate_peer_moved` and the rest of the send vocabulary would
|
|
562
|
+
become a genuine re-sync obligation, and a re-sync that kept ignoring them would
|
|
563
|
+
be a defect. Recorded now, while the reasoning is fresh, rather than discovered
|
|
564
|
+
by whoever adds that path.
|
package/README.md
CHANGED
|
@@ -59,7 +59,9 @@ models; behaviour against real ones is verified by hand and recorded in
|
|
|
59
59
|
## Quick start
|
|
60
60
|
|
|
61
61
|
```sh
|
|
62
|
-
|
|
62
|
+
# Allow the install scripts: muster's postinstall builds node-pty, and npm
|
|
63
|
+
# skips install scripts by default — silently, so the pty host fails later.
|
|
64
|
+
npm install -g --allow-scripts=@brutalsystems/muster,node-pty @brutalsystems/muster
|
|
63
65
|
|
|
64
66
|
# A session: an agent you can reach, addressed once it is genuinely alive.
|
|
65
67
|
muster run claude --prompt 'Review this project' --cwd . --level work
|
package/dist/guard.js
CHANGED
|
@@ -443,6 +443,14 @@ export function launchArgs(req, config, mcp = [], bridgePath, env = process.env)
|
|
|
443
443
|
const settings = {
|
|
444
444
|
...profilePluginSettings(configDir),
|
|
445
445
|
disableAllHooks: true,
|
|
446
|
+
// `--setting-sources ""` means an acceptance recorded in the profile's
|
|
447
|
+
// settings.json is never read, so without this the bypass warning stops
|
|
448
|
+
// every bypass launch before it registers (#45). Only on request, and
|
|
449
|
+
// `assertOptionsSupported` has already refused it outside bypass.
|
|
450
|
+
...(req.options.includes("accept-bypass-warning") &&
|
|
451
|
+
policy.permissions === "bypass"
|
|
452
|
+
? { skipDangerousModePermissionPrompt: true }
|
|
453
|
+
: {}),
|
|
446
454
|
permissions: {
|
|
447
455
|
defaultMode: mode,
|
|
448
456
|
allow: mcp.flatMap((s) => s.config.tools.map((t) => `mcp__${s.name}__${t}`)),
|
package/dist/help.js
CHANGED
|
@@ -60,7 +60,7 @@ export const COMMANDS = [
|
|
|
60
60
|
["--mcp NAME", "repeatable; replaces configured defaults"],
|
|
61
61
|
[
|
|
62
62
|
"--options NAME",
|
|
63
|
-
"repeatable; muster-side launch options — auto-approve-path records the launch directory as trusted so its one-time dialog does not block the launch (claude and codex only, and refused if the directory ships hooks or MCP servers)",
|
|
63
|
+
"repeatable; muster-side launch options — auto-approve-path records the launch directory as trusted so its one-time dialog does not block the launch (claude and codex only, and refused if the directory ships hooks or MCP servers); accept-bypass-warning accepts Claude's one-time Bypass Permissions warning so a --level open launch can start (claude with bypass permissions only)",
|
|
64
64
|
],
|
|
65
65
|
["--no-mcp", "launch with no MCP servers"],
|
|
66
66
|
["--plugin NAME", "repeatable; replaces configured defaults (OpenCode)"],
|
package/dist/identity/claude.js
CHANGED
|
@@ -64,22 +64,27 @@ export async function claudeWorkspaceTrusted(env, cwd) {
|
|
|
64
64
|
* timeout looks identical to a hung or crashed launch. Several unrelated
|
|
65
65
|
* screens can produce it, and the old message guessed at two of them together.
|
|
66
66
|
* Trust is the one Muster can actually check, so it is either named or ruled
|
|
67
|
-
* out.
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
67
|
+
* out. The bypass-permissions warning is named only for a launch that ran in
|
|
68
|
+
* bypass without `accept-bypass-warning`: demonstrated in #45, and that launch
|
|
69
|
+
* is the only one it can stop. Anything else says "another one-time
|
|
70
|
+
* confirmation" rather than sending the reader after a screen nobody has
|
|
71
|
+
* demonstrated.
|
|
72
72
|
*
|
|
73
73
|
* A `--kind task` launch into the same directory is the cheap discriminator:
|
|
74
74
|
* it runs headless and never sees the dialog, so it succeeds where a session
|
|
75
75
|
* hangs.
|
|
76
76
|
*/
|
|
77
|
-
export function claudeRegistryDiagnostic(trusted, cwd, configDir) {
|
|
77
|
+
export function claudeRegistryDiagnostic(trusted, cwd, configDir, opts = {}) {
|
|
78
78
|
if (!trusted)
|
|
79
79
|
return (`no Claude session registry found: workspace trust has not been ` +
|
|
80
80
|
`accepted for ${cwd} in ${configDir} — accept it once with ` +
|
|
81
81
|
`\`cd ${cwd} && claude\`, then retry (trust is per-directory and is ` +
|
|
82
82
|
`not inherited from a parent directory)`);
|
|
83
|
+
if (opts.bypassWarningPending)
|
|
84
|
+
return (`no Claude session registry found (${cwd} is trusted for this profile): ` +
|
|
85
|
+
`this launch runs in bypass permissions, and Claude stops on its ` +
|
|
86
|
+
`one-time "Bypass Permissions mode" warning until someone answers it — ` +
|
|
87
|
+
`relaunch with --options accept-bypass-warning to accept it for the launch`);
|
|
83
88
|
return (`no Claude session registry found (${cwd} is trusted for this profile, ` +
|
|
84
89
|
`so the session is stopped on something else — check the terminal for a ` +
|
|
85
90
|
`login prompt or another one-time confirmation)`);
|
package/dist/mcp-tools.js
CHANGED
|
@@ -63,8 +63,11 @@ export const MCP_TOOLS = [
|
|
|
63
63
|
},
|
|
64
64
|
options: {
|
|
65
65
|
type: "array",
|
|
66
|
-
items: {
|
|
67
|
-
|
|
66
|
+
items: {
|
|
67
|
+
type: "string",
|
|
68
|
+
enum: ["auto-approve-path", "accept-bypass-warning"],
|
|
69
|
+
},
|
|
70
|
+
description: "Muster-side launch options. auto-approve-path records the launch directory as trusted in the profile the agent will use, so its one-time dialog does not block the launch; refused for a runtime with no trust gate, and refused if the directory ships hooks or MCP servers. accept-bypass-warning accepts Claude's one-time Bypass Permissions warning for a bypass launch (level open); refused for other runtimes and without bypass permissions. Not expressible remotely.",
|
|
68
71
|
},
|
|
69
72
|
project: {
|
|
70
73
|
type: "string",
|
package/dist/options.js
CHANGED
|
@@ -10,17 +10,31 @@ import { AGENTS } from "./agents.js";
|
|
|
10
10
|
* in its own way, or cannot express at all — so the set is closed, and naming
|
|
11
11
|
* one an agent has no answer for is an error rather than a silent no-op.
|
|
12
12
|
*/
|
|
13
|
-
export const LAUNCH_OPTIONS = [
|
|
13
|
+
export const LAUNCH_OPTIONS = [
|
|
14
|
+
"auto-approve-path",
|
|
15
|
+
"accept-bypass-warning",
|
|
16
|
+
];
|
|
14
17
|
/**
|
|
15
18
|
* Refuses an option the runtime cannot honour, before anything is reserved or
|
|
16
19
|
* spawned. `auto-approve-path` records that the launch directory is trusted, so
|
|
17
20
|
* it needs an agent with a trust gate to record it in; OpenCode has none, and
|
|
18
21
|
* accepting the flag there would promise something muster cannot deliver.
|
|
22
|
+
*
|
|
23
|
+
* `accept-bypass-warning` answers Claude's one-time "Bypass Permissions mode"
|
|
24
|
+
* warning, which only Claude shows and only in bypass. Anywhere else there is
|
|
25
|
+
* no warning to accept, so it is refused rather than carried as a no-op that
|
|
26
|
+
* reads like consent to something.
|
|
19
27
|
*/
|
|
20
|
-
export function assertOptionsSupported(runtime, options) {
|
|
21
|
-
for (const option of options)
|
|
28
|
+
export function assertOptionsSupported(runtime, options, permissions) {
|
|
29
|
+
for (const option of options) {
|
|
22
30
|
if (option === "auto-approve-path" && !AGENTS[runtime].trustWorkspace)
|
|
23
31
|
throw new Error(`--options auto-approve-path is not supported for ${runtime}: it has no workspace-trust gate`);
|
|
32
|
+
if (option === "accept-bypass-warning" && runtime !== "claude")
|
|
33
|
+
throw new Error(`--options accept-bypass-warning is not supported for ${runtime}: only Claude shows a bypass-permissions warning`);
|
|
34
|
+
if (option === "accept-bypass-warning" &&
|
|
35
|
+
permissions.permissions !== "bypass")
|
|
36
|
+
throw new Error(`--options accept-bypass-warning requires bypass permissions (--level open or --permissions bypass); this launch resolves to ${permissions.permissions}`);
|
|
37
|
+
}
|
|
24
38
|
}
|
|
25
39
|
/**
|
|
26
40
|
* What a directory ships that would configure a session it is trusted by.
|
package/dist/run.js
CHANGED
|
@@ -256,10 +256,10 @@ export class Muster {
|
|
|
256
256
|
if (req.open &&
|
|
257
257
|
(req.kind !== "session" || !["auto", "tmux"].includes(requestedHost)))
|
|
258
258
|
throw new Error("--open requires a tmux session on macOS");
|
|
259
|
+
const permissions = resolvePermissions(effective, this.config);
|
|
259
260
|
// Refused here, with the other checks that cost nothing to fail: an option
|
|
260
261
|
// the runtime cannot honour must not leave a registry entry behind.
|
|
261
|
-
assertOptionsSupported(req.runtime, req.options);
|
|
262
|
-
const permissions = resolvePermissions(effective, this.config);
|
|
262
|
+
assertOptionsSupported(req.runtime, req.options, permissions);
|
|
263
263
|
const enforcement = enforcementGrade(req.runtime, permissions);
|
|
264
264
|
assertEnforcementMeetsMinimum(req.runtime, enforcement, policy);
|
|
265
265
|
// Composed here, before any process of any kind is started — the version
|
|
@@ -373,7 +373,14 @@ export class Muster {
|
|
|
373
373
|
const { req, identityName, identityMeta, credential, selectedMcp, agentBase, deadline, } = c;
|
|
374
374
|
let entry = c.entry;
|
|
375
375
|
const prepared = await prepareMcp(selectedMcp, mcpEnvironment(selectedMcp, agentBase, this.sourceEnv), req.cwd, deadline);
|
|
376
|
-
|
|
376
|
+
// PWD names the launch directory, not the caller's. The child is spawned
|
|
377
|
+
// in req.cwd, but an inherited PWD still names wherever muster was run
|
|
378
|
+
// from, and `opencode run` prefers PWD over its real cwd: its model's
|
|
379
|
+
// shell then ran in the caller's directory instead of --cwd.
|
|
380
|
+
let runtimeEnv = {
|
|
381
|
+
...mcpEnvironment(prepared.servers, agentBase, this.sourceEnv),
|
|
382
|
+
PWD: req.cwd,
|
|
383
|
+
};
|
|
377
384
|
entry = await this.registry.update(entry.launchId, prepared.summary);
|
|
378
385
|
if (identityName && identityMeta) {
|
|
379
386
|
// The launch id exists now, so the copy can be made and named after it.
|
|
@@ -736,7 +743,11 @@ export class Muster {
|
|
|
736
743
|
// runs headless and never sees the dialog, so refusing an untrusted
|
|
737
744
|
// directory up front would break task launches that work today.
|
|
738
745
|
if (req.runtime === "claude" && diagnostic === neverRegistered)
|
|
739
|
-
diagnostic = claudeRegistryDiagnostic(await claudeWorkspaceTrusted(resolveEnv, req.cwd), req.cwd, claudeConfigDir(resolveEnv)
|
|
746
|
+
diagnostic = claudeRegistryDiagnostic(await claudeWorkspaceTrusted(resolveEnv, req.cwd), req.cwd, claudeConfigDir(resolveEnv), {
|
|
747
|
+
bypassWarningPending: req.kind === "session" &&
|
|
748
|
+
permissions.permissions === "bypass" &&
|
|
749
|
+
!req.options.includes("accept-bypass-warning"),
|
|
750
|
+
});
|
|
740
751
|
throw new Error(`${diagnostic} after ${this.config.launch_timeout_sec}s`);
|
|
741
752
|
}
|
|
742
753
|
async launch(input, requester) {
|