@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 +5 -2
- package/CONTRACT_PROVENANCE.md +81 -0
- 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 +7 -3
- package/package.json +1 -1
- package/test/fixtures/canonical-id.json +2 -2
package/CANONICAL_ID.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Tin Can — Address Format
|
|
2
2
|
|
|
3
|
-
> **Normative for `@brutalsystems/tincan`
|
|
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.
|
package/CONTRACT_PROVENANCE.md
CHANGED
|
@@ -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)"],
|
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
|
|
@@ -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
|
"$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": "
|
|
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": "
|
|
579
|
+
"rawName": "billing-api",
|
|
580
580
|
"uuid": "33333333-3333-4333-8333-333333333333"
|
|
581
581
|
}
|
|
582
582
|
],
|