@brutalsystems/muster 1.0.1 → 1.1.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/CANONICAL_ID.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Tin Can — Address Format
2
2
 
3
- > **Normative for `@brutalsystems/tincan` 1.9.2.**
3
+ > **Normative for `@brutalsystems/tincan` 2.0.0.**
4
4
  >
5
5
  > This describes what Tin Can does today, precisely enough for another tool to
6
6
  > produce addresses Tin Can will resolve. It is a write-down of shipped
@@ -335,7 +335,10 @@ afterwards that the other tool disagreed.
335
335
  So:
336
336
 
337
337
  - **A change to any behaviour described here is a breaking change**, and ships
338
- as a major version under semver.
338
+ as a major version under semver. The converse does not hold, and 2.0.0 is the
339
+ standing example: it marks the repository being republished with a new
340
+ history, and **nothing in this document changed with it**. If you implement
341
+ against this format, 2.0.0 requires nothing of you.
339
342
  - Update this document and the fixture in the same commit as the code.
340
343
  - Adding cases to the fixture is not a breaking change. Changing an existing
341
344
  expected value is.
@@ -481,3 +481,84 @@ as a re-copy signal.
481
481
  updating the fixture moved CI on its own — the first cut where the failure class
482
482
  that produced v0.9.0 had nothing to act on. The drift hashes still moved, which
483
483
  is the third site behaving exactly as documented above.
484
+
485
+ ## Re-sync to Tin Can 2.0.0 — 2026-09-26
486
+
487
+ Copies are from tag `v2.0.0`, commit `c588e8b3e119c530e55c8e59be892133f7b49631`.
488
+
489
+ | File | SHA-256 |
490
+ | --- | --- |
491
+ | `CANONICAL_ID.md` | `609634a58df2e288542732a3c95037a3e57efcaf63a36c26f6a4a7251c4804f2` |
492
+ | `test/fixtures/canonical-id.json` | `bf686e97d91cba3bd3ee51ecb6d74ccf5e4978b3fcb783e441b1c01de5135285` |
493
+
494
+ `tincan_version: 2.0.0`, still 40 cases: 8 slugify, 7 suffix, 12 assignment,
495
+ 13 resolution. The format did not move. 2.0.0 is a major because Tin Can's
496
+ repository was republished with a new history and npm reserves a version
497
+ number forever — not because anything in the address format changed. Upstream
498
+ says so in the copy itself, in a paragraph added to `CANONICAL_ID.md`'s
499
+ "Changing this format": 2.0.0 "requires nothing of you".
500
+
501
+ **The usual verification was not available, and the substitute is better here.**
502
+ Every previous entry checked the claim by diffing upstream across the two tags
503
+ — `v1.7.0..v1.9.2` last time. That is impossible now: the republish left
504
+ tincan with a two-commit history and exactly one tag, so `v1.9.2` is not a
505
+ revision in that repository any more and never will be. What was diffed instead
506
+ is muster's own vendored copies, which *are* still in muster's history, against
507
+ the `v2.0.0` tag content. That answers muster's actual question — what changed
508
+ between what was vendored and what is being vendored — without depending on
509
+ upstream's history at all, and it is the form future re-syncs should take.
510
+
511
+ The entire delta across both files is three hunks:
512
+
513
+ - `CANONICAL_ID.md`: the normative version line, `1.9.2` → `2.0.0`.
514
+ - `CANONICAL_ID.md`: the new paragraph described above.
515
+ - the fixture: `tincan_version`, and one `rawName` in the prefix-collision
516
+ case, `cxx-be` → `billing-api`.
517
+
518
+ `naming.ts` does not appear and no expected value moved. The renamed peer is
519
+ **input**, not an expected output, and that case's expected `canonicalId`
520
+ names the *other* peer (`muster-b1`), so the rename cannot change what the
521
+ case asserts. It is upstream removing a string that reads as a client project
522
+ name, which is also why it was worth taking promptly rather than at the next
523
+ convenient re-sync: `test/fixtures/canonical-id.json` is in `package.json`'s
524
+ `files`, so the old string shipped in every tarball up to and including 1.0.1.
525
+
526
+ **No pin edit, again.** The pins derive from `tincan_version`, so updating the
527
+ fixture moved `publish.yml`, `RELEASING.md` and `docs/verification.md` on
528
+ their own. Only the two frozen hashes in `test/naming.test.ts` are hand-written,
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/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)"],
@@ -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. 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.
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: { type: "string", enum: ["auto-approve-path"] },
67
- 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. Not expressible remotely.",
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 = ["auto-approve-path"];
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
@@ -736,7 +736,11 @@ export class Muster {
736
736
  // runs headless and never sees the dialog, so refusing an untrusted
737
737
  // directory up front would break task launches that work today.
738
738
  if (req.runtime === "claude" && diagnostic === neverRegistered)
739
- diagnostic = claudeRegistryDiagnostic(await claudeWorkspaceTrusted(resolveEnv, req.cwd), req.cwd, claudeConfigDir(resolveEnv));
739
+ diagnostic = claudeRegistryDiagnostic(await claudeWorkspaceTrusted(resolveEnv, req.cwd), req.cwd, claudeConfigDir(resolveEnv), {
740
+ bypassWarningPending: req.kind === "session" &&
741
+ permissions.permissions === "bypass" &&
742
+ !req.options.includes("accept-bypass-warning"),
743
+ });
740
744
  throw new Error(`${diagnostic} after ${this.config.launch_timeout_sec}s`);
741
745
  }
742
746
  async launch(input, requester) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brutalsystems/muster",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Launch instructed, reachable local agent sessions",
5
5
  "type": "module",
6
6
  "engines": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$comment": "FROZEN. Describes @brutalsystems/tincan address behaviour as shipped. Adding cases is fine; changing an expected value means the address format changed - see CANONICAL_ID.md, 'Changing this format'.",
3
- "tincan_version": "1.9.2",
3
+ "tincan_version": "2.0.0",
4
4
  "spec": "CANONICAL_ID.md",
5
5
  "slugify": [
6
6
  {
@@ -576,7 +576,7 @@
576
576
  },
577
577
  {
578
578
  "runtime": "claude-code",
579
- "rawName": "cxx-be",
579
+ "rawName": "billing-api",
580
580
  "uuid": "33333333-3333-4333-8333-333333333333"
581
581
  }
582
582
  ],