@brutalsystems/muster 0.11.0 → 1.0.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 CHANGED
@@ -1,24 +1,118 @@
1
+ <img src="docs/assets/muster-logo.png" alt="" width="120" align="left" hspace="12" vspace="4">
2
+
1
3
  # Muster
2
4
 
3
5
  [![license](https://img.shields.io/github/license/BrutalSystems/muster)](./LICENSE)
4
6
 
5
- Muster launches instructed Codex, Claude Code, or OpenCode agents on this machine. A
6
- `session` returns an address only after its runtime is reachable. A `task`
7
- runs once, captures output, and never advertises a peer address.
7
+ <br clear="left">
8
+
9
+ **Launch coding agents on your machine, under containment you chose, and get
10
+ back an address you can actually reach.**
11
+
12
+ Muster starts Claude Code, Codex and OpenCode sessions on your behalf. You say
13
+ what the agent may do; Muster arranges it, verifies the session is genuinely
14
+ alive, and hands you a durable address for it. If a session never becomes
15
+ reachable, you get a diagnosis naming what stopped it — not a process that looks
16
+ fine and isn't.
8
17
 
9
- **Live launch verification:** Codex and Claude both launched and answered their
10
- initial prompts without terminal interaction. OpenCode 1.18.31 passed the
11
- loopback API and local-provider protocol checks. Claude requires a directory
12
- already trusted by the operator. The automated suite uses fake runtimes, not
13
- real models.
18
+ It is the launching half of a small family: Muster starts agents,
19
+ [tincan](https://github.com/BrutalSystems/tincan) lets them message each other,
20
+ [birddog](https://github.com/BrutalSystems/birddog) watches what they do.
21
+
22
+ ## What it's for
23
+
24
+ Running more than one agent, deliberately. One session reviewing while another
25
+ builds; a task that runs once and captures its output; an agent launched under a
26
+ separate account so its work never touches your own configuration. Muster is the
27
+ part that makes those launches repeatable, contained and addressable instead of
28
+ a terminal tab you have to remember.
29
+
30
+ A `session` returns an address only after its runtime is reachable. A `task`
31
+ runs once, captures output, and never advertises a peer address.
14
32
 
15
- ## Install
33
+ ## What makes it different
34
+
35
+ - **Containment is two separate questions, and both are answered.** Whether the
36
+ agent stops to ask you, and what it may touch — the second enforced by the OS,
37
+ not by asking nicely. Every launch records which of the two it actually got.
38
+ - **A session is only reported ready when it is reachable.** Muster asks each
39
+ runtime in its own language — a session record, an RPC call, an HTTP endpoint
40
+ — and does not confuse "the process started" with "the agent is listening".
41
+ - **It refuses instead of guessing.** An unrecognised flag, a contradictory pair
42
+ of options, an identity belonging to a different runtime: all errors, before
43
+ anything is started. Nothing half-launched is left behind.
44
+ - **Other machines can ask, within limits you set.** An enrolled caller launches
45
+ inside a profile's ceiling — the directories it may use, the strongest
46
+ containment it may request. Configuration is the grant; the request never
47
+ widens it.
48
+ - **Every claim here was measured.** Runtime behaviour is established by
49
+ driving the real programs and recording what they did — see
50
+ [Verification](docs/verification.md). Where a finding later proved wrong, the
51
+ correction is in the release notes rather than quietly edited away.
52
+
53
+ Claude requires a directory already trusted by the operator, or
54
+ `--options auto-approve-path`. The automated suite uses fake runtimes, not real
55
+ models; behaviour against real ones is verified by hand and recorded in
56
+ `RELEASE_NOTES.md`.
57
+
58
+ ## Quick start
16
59
 
17
60
  ```sh
18
61
  npm install -g @brutalsystems/muster
19
- muster --version
62
+
63
+ # A session: an agent you can reach, addressed once it is genuinely alive.
64
+ muster run claude --prompt 'Review this project' --cwd . --level work
65
+
66
+ # A task: runs once, captures output, advertises nothing.
67
+ muster run codex --kind task --prompt 'Summarise the failing tests' --level read
68
+ muster output <id>
69
+
70
+ # What is running, and stopping it.
71
+ muster list --format human
72
+ muster stop <id>
20
73
  ```
21
74
 
75
+ `--level` names a containment pair. `read` can look but not change; `work` can
76
+ edit its working directory; `open` removes the sandbox and needs
77
+ `allow_dangerous_flags` in config.
78
+
79
+ | Level | The agent asks before acting | What it may touch |
80
+ | ------ | ---------------------------- | ----------------- |
81
+ | `read` | always | nothing on disk |
82
+ | `work` | no | its cwd |
83
+ | `open` | no | everything |
84
+
85
+ Claude asks to trust a directory the first time. That is a one-time review you
86
+ do yourself — or `--options auto-approve-path`, which records it for you after
87
+ checking the directory ships no hooks. See
88
+ [Claude workspace trust](docs/claude-trust.md).
89
+
90
+ ## Requirements
91
+
92
+ **Node 22.12+** (tested on 24.16), and a POSIX host with `ps` and `lsof`.
93
+ Terminals use tmux or node-pty. The OS sandbox is seatbelt, so kernel-enforced
94
+ containment is macOS only; everywhere else `enforcement` reports what was
95
+ actually applied rather than pretending.
96
+
97
+ Then whichever agents you mean to launch, installed and logged in. Verified
98
+ against **Claude Code 2.1.274**, **Codex 0.155.1** and **OpenCode 1.18.31** or
99
+ newer — a newer OpenCode must keep the CLI and loopback HTTP API Muster uses.
100
+ What that verification covers is in [Verification](docs/verification.md).
101
+
102
+ ## Documentation
103
+
104
+ | | |
105
+ | ---------------------------------------------------- | ------------------------------------------------------ |
106
+ | [How a launch runs](docs/launching.md) | hosts, terminals, lifetimes, repeated requests |
107
+ | [Configuration and permissions](docs/permissions.md) | containment, config file, remote callers |
108
+ | [Agent identities](docs/identities.md) | running as a named account |
109
+ | [Claude workspace trust](docs/claude-trust.md) | the one-time dialog, and the two gates behind it |
110
+ | [Command-line reference](docs/cli.md) | output formats, runtime pass-through |
111
+ | [MCP servers and plugins](docs/mcp.md) | tools for launched agents, and Muster's own MCP server |
112
+ | [Verification](docs/verification.md) | what is tested, what is checked by hand |
113
+
114
+ ## Keeping it up to date
115
+
22
116
  Publishing a release does not touch an installed copy: `muster --version` keeps
23
117
  reporting the old version until you update it.
24
118
 
@@ -46,33 +140,9 @@ It is not everything CI does: CI additionally runs the suite across the Node
46
140
  version matrix, and the contract suite runs only on release. Passing `check` is
47
141
  necessary rather than sufficient.
48
142
 
49
- Requires Node 22.12+ (tested with 24.16), TypeScript 5, Codex 0.155.1, Claude
50
- Code 2.1.267, and OpenCode 1.18.31 or newer. Newer OpenCode releases must retain
51
- the CLI and loopback HTTP API used by Muster. Terminal drivers use tmux or
52
- node-pty. Process discovery currently requires a POSIX host with `ps` and
53
- `lsof`; the terminal interface itself is platform-neutral. The macOS Terminal
54
- driver is an unavailable v1 stub.
55
-
56
- Before launching a Claude session, open Claude normally in the target directory
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
- 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.
143
+ Beyond the runtime requirements above, building needs TypeScript 5. The
144
+ terminal interface itself is platform-neutral; the macOS Terminal driver is an
145
+ unavailable v1 stub.
76
146
 
77
147
  ```sh
78
148
  npm ci
@@ -80,7 +150,7 @@ npm run build
80
150
  node dist/muster.js run codex --prompt 'Review the authentication flow'
81
151
  node dist/muster.js run claude --prompt 'Summarize the project' --host tmux
82
152
  node dist/muster.js run opencode --prompt 'Review this project'
83
- node dist/muster.js run opencode --prompt 'Build a plan' -- --model coldfire-local/qwen3-30b-a3b
153
+ node dist/muster.js run opencode --prompt 'Build a plan' -- --model local-provider/qwen3-30b
84
154
  node dist/muster.js run codex --kind task --prompt 'Explain the test layout'
85
155
  node dist/muster.js run opencode --kind task --prompt 'Summarize the tests'
86
156
  node dist/muster.js list
@@ -96,1006 +166,6 @@ emit JSON; `output` prints the captured task output. `stop` also accepts an
96
166
  unambiguous peer name or canonical address, refusing ambiguity with candidates.
97
167
  Use the durable ID to stop a session whose runtime has renamed it.
98
168
 
99
- For readable terminal output, add `--format human` to `run`, `list`, `stop`, or
100
- `doctor`:
101
-
102
- ```sh
103
- muster run codex --prompt 'Review this project' --format human
104
- muster list --format human
105
- muster stop THREAD_OR_SESSION_OR_RUN_ID --format human
106
- ```
107
-
108
- Human output keeps full IDs and shows directory, host, state, and relevant
109
- attach/output/stop commands with actual IDs. pty sessions are labeled as not
110
- watchable or attachable. `--format json` is explicit JSON; omitting the flag
111
- still returns JSON. Put Muster options before any `-- RUNTIME_OPTIONS`.
112
- `output` always prints captured task text and does not accept `--format`.
113
- MCP tools retain their existing JSON responses.
114
-
115
- Human output uses subtle colors in interactive terminals: green for idle or
116
- running, yellow for busy or starting, gray for ended states and field labels,
117
- red for failures or nonzero exit codes, and cyan for follow-up commands. IDs
118
- and paths retain the normal text color. Set `NO_COLOR=1` to disable colors;
119
- piped output and `TERM=dumb` are always uncolored. JSON never includes colors.
120
-
121
- `run` accepts an optional `--` followed by runtime arguments. The normal
122
- allowlist is `--model` / `-m`, plus Claude's `--effort` and OpenCode's `--agent`
123
- and `--variant`. For Claude and Codex a model named there is *consumed* rather
124
- than forwarded — it reaches the child through the same path as `--model`, so it
125
- appears once — and naming a model both ways is refused rather than resolved by
126
- precedence. Unknown options, bundled short options, config injection and
127
- raw permission overrides are refused, even with `allow_dangerous_flags`
128
- enabled. Use Muster's normalized permission flags. Prompt strings are always
129
- passed as one literal argument. Codex and Claude launches and OpenCode tasks put
130
- that value after `--`; OpenCode sessions use the dedicated `--prompt` value.
131
-
132
- A tmux session is stopped after 30 minutes with no pane activity. Set
133
- `--idle-timeout 2h` to lengthen it, `--idle-timeout off` to disable it for one
134
- launch, or `[session] idle_timeout` in `config.toml` to change the default for
135
- every launch. `--ttl` adds a hard ceiling regardless of activity, for a session
136
- that looks busy only because something is looping inside it. A session with a
137
- client attached is never stopped. The limit applies to tmux sessions only: pty
138
- and macos-terminal sessions die with the process that launched them, and a task
139
- ends when its prompt does.
140
-
141
- OpenCode inherits the provider and model definitions from its normal user and
142
- project configuration, so configured local OpenAI-compatible providers such as
143
- `coldfire-local/qwen3-30b-a3b` remain available. Muster never rewrites those
144
- files. Put runtime options after `--`, as in the local-model example above.
145
-
146
- ```sh
147
- muster run opencode --prompt 'Review this project'
148
- muster run opencode --prompt 'Build a plan' -- --model coldfire-local/qwen3-30b-a3b
149
- muster run opencode --kind task --prompt 'Summarize the tests'
150
- muster run opencode --mcp tincan --permissions auto --sandbox workspace-write --prompt 'Coordinate'
151
- ```
152
-
153
- ## Terminal lifetime
154
-
155
- Add `--open` to display the launched session in a new Terminal.app window on macOS:
156
-
157
- ```sh
158
- muster run codex --open --prompt "Review this project" --format human
159
- muster run claude --open --terminal ghostty --prompt "Review this project" --format human
160
- muster run opencode --open --terminal iterm2 --prompt "Review this project" --format human
161
- muster run codex --open --terminal iterm2 --prompt "Review this project" --format human
162
- ```
163
-
164
- MCP `run` accepts `open: true`. This requires tmux and selects it when the host
165
- is auto; explicit pty hosts and tasks are rejected. Detached launch remains the
166
- default. `--terminal auto|terminal|iterm2|ghostty` requires `--open`; omitted or
167
- `auto` uses Terminal.app. MCP accepts the same `terminal` values. Explicit
168
- choices never silently fall back. iTerm2 and Ghostty must be installed as
169
- `iTerm.app` or `Ghostty.app` in `/Applications` or `~/Applications`.
170
- The host remains tmux. Successful results include `terminal_opened: true` and
171
- the resolved `terminal` (a launch event, not a live window-status check).
172
- Closing the Terminal window leaves the session running; use `muster stop <id>`
173
- to stop it. Terminal.app and iTerm2 may require macOS Automation permission.
174
- Ghostty opens a separate app instance that exits when its last window closes;
175
- that instance disables saved-window restoration so previous tabs and windows
176
- are not duplicated. It uses the [Ghostty macOS launch interface](https://github.com/ghostty-org/ghostty/discussions/9221).
177
- Terminal.app creates the requested window before activation
178
- to avoid also opening a default shell window on startup.
179
- iTerm2 uses its [documented scripting interface](https://iterm2.com/documentation-scripting.html).
180
- If the launch request to the viewer fails,
181
- Muster stops the launched session and returns an error.
182
-
183
- Auto-selection tries tmux, then pty. Every peer includes `host`, `capabilities`
184
- and `attach_hint`. Watchability is separate from the runtime's idle/busy state.
185
-
186
- - **tmux:** each agent has its own session on the dedicated `muster` tmux
187
- server. `MUSTER_TMUX_SERVER` names a different one, for a caller that must not
188
- share the machine's Muster server — Muster's own test suite sets it, so a gate
189
- run cannot leave sessions beside yours. Opening or exiting one agent does not
190
- switch another agent's viewer.
191
- Watchable and attachable; survives the CLI or MCP server exiting.
192
- Use the returned attach hint to reconnect to that agent.
193
- - **pty:** not watchable or attachable. The CLI prints the peer record and stays
194
- running to own the terminal. Ctrl-C stops it. MCP-owned pty sessions stop when
195
- the MCP server disconnects. There is no persistent pty daemon.
196
- - **task:** a per-run worker captures stdout/stderr and exit status after the
197
- launching CLI exits. It supervises one task, with no retry or restart behavior.
198
-
199
- `list` refreshes live session metadata and shows ended runs distinctly. A timeout
200
- or startup failure cleans the process tree and host window, logs failure, and
201
- returns an error instead of a peer record.
202
-
203
- Each OpenCode session owns a separate HTTP server bound to `127.0.0.1` on a
204
- Muster-selected port. Muster verifies that the listener belongs to the launched
205
- process tree, stores its `server_url` and durable `session_id`, and never attaches
206
- to an existing personal OpenCode server. `list` refreshes the session through
207
- that endpoint; `stop` aborts it best-effort and still performs authoritative
208
- process-tree cleanup. The URL is local control metadata, not a public address.
209
- OpenCode sessions otherwise use the same tmux/pty, viewer, list, stop, and human
210
- output behavior described above; OpenCode tasks use the same detached output
211
- and exit-status lifecycle as other tasks.
212
-
213
- ## Repeating a launch request
214
-
215
- `--request-key KEY` (MCP: `requestKey`) names the launch *request*, not the
216
- session it produces. Sending the same key again returns the launch it already
217
- produced instead of starting a second agent:
218
-
219
- ```sh
220
- muster run codex --prompt "Review the auth flow" --request-key review-auth-7f3a
221
- ```
222
-
223
- A key is bound to the parameters it was first seen with — runtime, kind,
224
- resolved working directory, the instruction, the effective permissions, the
225
- selected MCP servers and plugins, and the model. Sending the same key with any
226
- of those changed is **refused**, rather than answered with a session that does
227
- something other than what was asked. Launching something genuinely different
228
- means using a different key.
229
-
230
- The fingerprint is computed here, after validation, rather than accepted from
231
- the caller: two machines canonicalize a path differently, and a fingerprint
232
- computed before validation would disagree with itself.
233
-
234
- What a repeat returns depends on what the first attempt did:
235
-
236
- | First attempt | A repeat gets |
237
- | --- | --- |
238
- | Reachable session or running task | That session or task, unchanged |
239
- | Still launching | Refused — ask for its status instead |
240
- | Failed, exited or stopped | Refused, naming the recorded outcome |
241
- | Outcome unknown | Refused, and told to inspect first (below) |
242
-
243
- A repeat never consumes launch capacity, including when the concurrency cap is
244
- full of the very launch being asked about. Omitting the key leaves behaviour
245
- exactly as it was: every request is its own launch.
246
-
247
- ## When an outcome is unknown
248
-
249
- If Muster dies between starting a process and recording it, the entry says
250
- `unknown` rather than `failed`, and `muster list` reports it that way.
251
-
252
- The distinction is not cosmetic. `failed` means nothing is running and retrying
253
- is safe; `unknown` means a session may be running that Muster never got to
254
- record, and relaunching would be how a second agent appears in the same
255
- directory. A repeat under the same request key is refused for exactly that
256
- reason.
257
-
258
- Only the half of the window that provably never reached a spawn is still called
259
- `failed` — an owner that died before Muster marked itself about to start a
260
- process. Resolving an `unknown` entry is manual today: look for the session,
261
- stop it if it is there, and launch again under a new key.
262
-
263
- ## Where a launch may run
264
-
265
- By default any directory the account can read is a legal target, which is the
266
- behaviour Muster has always had. Two config settings narrow it:
267
-
268
- ```toml
269
- allowed_roots = ["/Users/you/Source"]
270
-
271
- [projects]
272
- muster = "/Users/you/Source/brutalsystems/muster"
273
- ```
274
-
275
- With either configured, a launch outside all of them is refused. A configured
276
- project is a grant in its own right — naming a directory there permits it
277
- without also permitting its parent.
278
-
279
- ```sh
280
- muster run codex --project muster --prompt "Review the auth flow"
281
- ```
282
-
283
- `--project NAME` (MCP: `project`) is used instead of `--cwd`; naming both is an
284
- error rather than a precedence rule. A project name is also portable in a way an
285
- absolute path is not, since the same name can point somewhere different on
286
- another machine.
287
-
288
- The check resolves symlinks before comparing, so a link inside an allowed root
289
- that points elsewhere does not escape it, and compares path-relatively, so
290
- `/work/project-other` is not treated as part of `/work/project`.
291
-
292
- This constrains **where a session starts, not what it can then reach**. It is
293
- not a sandbox: a launched agent still has whatever access the OS account has.
294
- Permission modes and the sandbox settings below are the controls for that.
295
-
296
- ## Process ownership
297
-
298
- Muster identifies a launched process by its **pid paired with its start time**,
299
- and revalidates that pair whenever the registry is touched — including after
300
- Muster itself restarts.
301
-
302
- This is not a compromise chosen for convenience. On macOS it is the strongest
303
- check the public platform offers:
304
-
305
- - There is no cgroup. Process groups and sessions are the nearest approximation
306
- and are escapable by design, so they bound a tree rather than owning it.
307
- - The kernel's unique process identifier is not exposed in the public SDK.
308
- - The audit-token route requires a message from the target process, which a
309
- supervisor observing arbitrary children never has.
310
-
311
- Two consequences worth stating, because both look like redundant work to a
312
- reader who does not know why they are there:
313
-
314
- - **Identity is revalidated at the moment of use, not at discovery.** Every
315
- signal, every status answer and every registry pass re-checks the pair. A pid
316
- discovered a moment ago may already belong to something else.
317
- - **Revalidation after a Muster restart is deliberate.** Comparable supervisors
318
- trust what they recorded before they died. Muster does not, which is why a
319
- session that ended while Muster was not running is reported as ended rather
320
- than as running.
321
-
322
- Process-tree membership uses two signals, because neither is sufficient alone.
323
- The parent chain is exact while it holds, and stops holding the moment an
324
- intermediate process exits — its children reparent to launchd and are no longer
325
- reachable from the root, though they are still doing the session's work. The
326
- process group survives that, since reparenting changes no process's group.
327
-
328
- Muster records the group the launched root leads and takes the union. A stop
329
- reaches reparented workers, and a session whose root has died but whose group is
330
- still busy is reported as running rather than ended.
331
-
332
- Only a group the root *leads* is used. A process that merely belongs to someone
333
- else's group shares it with whatever else is there — a shell, a tmux pane, the
334
- terminal — and treating that as membership would reach far outside the session.
335
- Both terminal hosts start their root in a new session, so in practice it does
336
- lead its own group. A process that calls `setsid()` for itself still escapes;
337
- nothing on macOS prevents that.
338
-
339
- ## Configuration and permissions
340
-
341
- Muster reads `~/.muster/config.toml` once at startup and **never writes it**.
342
- Missing configuration uses these defaults:
343
-
344
- ```toml
345
- host = "auto"
346
- tmux_status = "off"
347
- terminal = "terminal"
348
- launch_timeout_sec = 30
349
- max_concurrent = 4
350
- permissions = "deny"
351
- sandbox = "read-only"
352
- allow_dangerous_flags = false
353
- default_mcp = []
354
- ```
355
-
356
- Choose `sandbox = "workspace-write"` yourself when agents should edit files.
357
- `full-access` and `permissions = "bypass"` require `allow_dangerous_flags = true`.
358
- The old config spelling `danger-full-access` remains accepted as an alias.
359
-
360
- Per-launch `--permissions` and `--sandbox` override these config defaults. The
361
- same named fields are accepted by MCP `run`. Both session and task launches
362
- support them:
363
-
364
- ```sh
365
- muster run codex --permissions auto --sandbox workspace-write \
366
- --prompt 'Implement the change' --format human
367
- muster run claude --permissions auto --sandbox workspace-write \
368
- --prompt 'Implement the change' --format human
369
- ```
370
-
371
- | Muster permissions | Codex translation | Claude Code translation | OpenCode translation |
372
- | ------------------ | ---------------------------- | ----------------------------------- | ------------------------------------------------------------------------------- |
373
- | `deny` (default) | Approval policy `never` | Permission mode `dontAsk` | Deny write, shell, task, external-directory, web, and unselected MCP operations |
374
- | `auto` | `--approve-for-me` | Permission mode `auto` | `--auto` with explicit deny rules |
375
- | `bypass` | Bypass approvals and sandbox | Permission mode `bypassPermissions` | Allow supported operations |
376
-
377
- `deny` refuses actions that would require approval; it does not prohibit tools
378
- already allowed by the sandbox or permission rules. `auto` delegates permission
379
- review to the runtime and may still reject an action; it is not blanket approval.
380
- Auto mode requires `workspace-write` in Muster because the Codex preset selects
381
- that sandbox. Explicitly select it; Muster never widens a read-only request.
382
- Bypass requires `full-access`; combinations claiming a sandbox while bypassing
383
- it are refused. `deny` can be combined with any authorized sandbox setting.
384
-
385
- `full-access` disables the runtime command sandbox and requires operator-owned
386
- config authorization even without bypass. Muster never writes that config.
387
- Config defaults apply to subsequent launches; overrides apply to one launch.
388
- Resolved `permissions` and `sandbox` are returned in records, shown in human
389
- output, persisted for listing, and written in the pre-launch log. Old records
390
- without those fields remain readable; their settings are not guessed.
391
-
392
- The runtimes' enforcement differs: Codex reviews sandbox escalation requests;
393
- Claude's classifier reviews tool permission requests while its Bash sandbox is
394
- separate. Auto-mode availability and decisions remain subject to runtime,
395
- model, account, and managed policy. These fields describe Muster's resolved
396
- launch settings, not a continuous attestation of remote policy or user changes.
397
- See [Codex auto-review](https://learn.chatgpt.com/docs/sandboxing/auto-review) and
398
- [Claude permission modes](https://code.claude.com/docs/en/permission-modes).
399
-
400
- OpenCode permissions are tool policy, not an OS-level filesystem sandbox.
401
- Muster's read-only OpenCode policy denies editing, shell, subagent,
402
- external-directory, web, and MCP operations except explicitly selected MCP
403
- tools; it does not claim kernel-enforced isolation. Auto still requires
404
- `workspace-write`, and bypass still requires authorized `full-access`.
405
-
406
- The concurrency cap is shared by separate CLI/MCP processes, including pending
407
- launches. Settings apply to both kinds; tasks are read-only by default.
408
-
409
- Codex defaults to an explicit sandbox and never-ask approval policy. Muster enumerates
410
- and explicitly disables inherited MCP servers, enables only the selected
411
- operator definitions, then verifies the effective server selection. Hooks, plugins, app connectors, automatic skill-MCP installation
412
- and external notifications are disabled for the child. Unknown MCP names that
413
- cannot be addressed safely are refused.
414
-
415
- Claude's built-in tools default to enabled. Unless full access is selected, its command sandbox is enabled,
416
- requires availability, and forbids unsandboxed retries. File-writing tools and
417
- sandbox writes are denied in read-only mode; default permissions use `dontAsk` so an
418
- unattended child does not auto-grant escalations. User/project settings and MCP
419
- servers are not inherited. Detected managed policy is refused because the CLI
420
- cannot prove that inline settings override it. Enterprise remote policy can
421
- arrive after startup; this v1 is not an enterprise policy-enforcement layer.
422
- Claude permissions and its command sandbox are different mechanisms; neither
423
- claim implies that every external tool is OS-sandboxed.
424
-
425
- **`workspace-write` does not fence Claude's Write tool, only its Bash.** This
426
- is measured, not inferred — see "Write/Bash fence asymmetry" in
427
- [PROBE_RESULTS.md](./PROBE_RESULTS.md). At `--level work` an agent's Bash write
428
- outside the working tree is refused with `Operation not permitted`, while the
429
- Write tool creates the same path successfully. The practical consequence is
430
- that **an agent can create files outside the tree that it cannot then delete**,
431
- because creation goes through Write and cleanup goes through Bash. No setting
432
- Muster writes expresses the missing fence: the alternative, `deny`, blocks the
433
- Write tool everywhere rather than by path, including inside the tree. Treat
434
- `work` as "Bash is confined to the workspace", not "writes are". If that
435
- distinction matters for a given launch, the containment that does hold for both
436
- is `read`.
437
-
438
- OpenCode keeps normal provider/model configuration and launches with `--pure`
439
- unless plugins are selected, which disables inherited external plugins.
440
- Providers declared under `[opencode.provider]` and a model named by
441
- `[opencode] model` or `--model` are supplied in the same verified overlay (a
442
- model named after `--` instead rides the child's argv and is left out of the
443
- overlay, so it is delivered once rather than twice), so a
444
- launch does not depend on the operator's own OpenCode configuration; provider
445
- definitions merge with any the child resolves for itself rather than replacing
446
- them. Secrets belong in `api_key_env_var`, read from the launching environment,
447
- not in `config.toml`. Before launch, Muster inspects the
448
- effective OpenCode configuration, disables every inherited MCP server and tool
449
- family, enables only the selected Muster definitions, and fails closed if a
450
- higher-precedence managed policy prevents that isolation from being proven.
451
-
452
- These child restrictions prevent automatically propagating spawn authority.
453
- External tools are enabled only through the operator configuration and launch
454
- selection described below. Codex and Claude runtime inboxes remain reachable
455
- from an external Tin Can even when they have no MCP tools of their own. An
456
- OpenCode session is reachable as a Tin Can peer only when the Tin Can plugin is
457
- selected with `--plugin`; the plugin, not the MCP server, is what advertises the
458
- session to other runtimes.
459
- No user-level runtime configuration is rewritten, and workspace-trust dialogs
460
- are never accepted automatically.
461
-
462
- Launch intent is fsynced to `~/.muster/launches.jsonl` before a runtime starts;
463
- ready/failure outcomes follow. For a local requester the log contains the
464
- **full prompt**, cwd, requester, runtime, kind and host. A non-local requester
465
- gets metadata-only logging instead — no prompt text and no environment values,
466
- see [Requester profiles and enrolments](#requester-profiles-and-enrolments)
467
- below. Registry and task outputs also live under `~/.muster`, with private
468
- file permissions. An abandoned `registry.lock` fails closed: verify no Muster
469
- operation is running before removing that directory. There is no automatic
470
- time-based lock theft. `launches.jsonl` rotates at 8 MiB: the file is renamed
471
- to `launches.jsonl.1`, replacing any previous generation, and a fresh file is
472
- opened at mode `0600`. One prior generation is kept; this bounds the file
473
- without adding a retention policy nobody asked for.
474
-
475
- ### Containment levels
476
-
477
- Two axes, answering different questions:
478
-
479
- - **`--permissions` decides whether the agent stops to ask a human.** It
480
- configures the runtime's own approval prompting — a setting the agent's
481
- process honours.
482
- - **`--sandbox` decides what the agent may touch, and the kernel enforces it.**
483
- On macOS that is seatbelt. Nothing the agent does talks its way past it.
484
-
485
- The difference is not academic: a permissions refusal arrives as a prompt or a
486
- tool error the agent can read and react to, while a sandbox refusal arrives as
487
- `EPERM` from the kernel, with no negotiation and often no explanation the agent
488
- can interpret. `enforcement` below records which of the two a launch actually
489
- got.
490
-
491
- `--level` names a legal `--permissions` / `--sandbox` pair. It is a spelling,
492
- not a second policy path, and naming both a level and either flag is an error.
493
-
494
- | Level | Equivalent to |
495
- | ------ | ------------------------------------------------------------------------------------- |
496
- | `read` | `--permissions deny --sandbox read-only` |
497
- | `work` | `--permissions auto --sandbox workspace-write` |
498
- | `open` | `--permissions bypass --sandbox full-access` (still requires `allow_dangerous_flags`) |
499
-
500
- `deny` with `workspace-write` or `full-access` has no level and stays available
501
- through the flags.
502
-
503
- Every launch also records how strongly its containment was imposed:
504
-
505
- | `enforcement` | Meaning |
506
- | ------------- | ---------------------------------------------------------------------------- |
507
- | `kernel` | Codex or Claude Code with an OS sandbox configured and active. |
508
- | `tool-policy` | OpenCode — rules the agent is asked to respect, with no kernel enforcement. |
509
- | `none` | `full-access` — the sandbox is deliberately disabled. |
510
-
511
- #### What a sandboxed launch may write
512
-
513
- Muster names the write roots rather than leaving them to default. Under
514
- `workspace-write` a launched Claude session may write its cwd, `$TMPDIR`, and
515
- **its own configuration directory** — the `CLAUDE_CONFIG_DIR` the child will
516
- actually use, which an identity launch or a requester profile's `env_defaults`
517
- may relocate. That last root is granted because an agent's memory lives there:
518
- without it every Bash write to memory failed with `EPERM` while the in-process
519
- file tools succeeded, which reads from inside the session as a path bug rather
520
- than a boundary.
521
-
522
- Under `read` nothing is writable, including the config dir. Under `full-access`
523
- there is no sandbox to grant into.
524
-
525
- One path stays denied that muster cannot open: **`$CLAUDE_CONFIG_DIR/projects/`**
526
- (and `shell-snapshots/`). Claude Code refuses writes there in a sandboxed Bash
527
- regardless of what muster grants — naming those directories explicitly in
528
- `allowWrite` changes nothing, which is presumably deliberate, since a session's
529
- own transcripts live there. Project-scoped memory under
530
- `projects/<cwd-slug>/memory` is therefore reachable only through the file tools,
531
- which are in-process and not kernel-sandboxed. Note that `work` implies auto
532
- mode, which asks the model to prefer shell commands for file edits, so a session
533
- saving project-scoped memory has to be told to use `Write`/`Edit` instead.
534
-
535
- ### Requester profiles and enrolments
536
-
537
- A non-local caller — one reached through a transport such as Tin Can rather
538
- than the local CLI or a local MCP client — is identified by an `(authority,
539
- subject)` tuple supplied by whatever already authenticated it. That tuple must
540
- be enrolled in `config.toml` and bound to a profile, or the request is
541
- refused. Two additions to `configSchema`, following the existing record style
542
- of `[projects]`, `[mcp_servers]` and `[plugins]`:
543
-
544
- ```toml
545
- [requester_profiles.research]
546
- allowed_roots = ["/Users/mikewilliams/Source/brutalsystems"]
547
- level = "read"
548
- min_enforcement = "kernel" # "tool-policy" to admit OpenCode here
549
- env_allow = ["GH_TOKEN"]
550
- env_defaults = { PATH = "/usr/bin:/bin", LANG = "en_US.UTF-8" }
551
- identities = ["research"] # bounds which identities this requester may name
552
-
553
- [[requesters]]
554
- authority = "example.peer.v1"
555
- subject = "<opaque authenticated key id>"
556
- label = "m4pro"
557
- profile = "research"
558
- ```
559
-
560
- `[[requesters]]` is an array of tables rather than a record keyed by
561
- `"authority:subject"`, so no separator can be ambiguous inside a subject. Two
562
- enrolments with the same `(authority, subject)` are a configuration error
563
- rather than a precedence rule. `label` is optional and never read for policy.
564
-
565
- `allowed_roots` is **required** on a profile and must be non-empty — it is the
566
- one key with no safe default, since the global `allowed_roots` defaults to
567
- empty and empty is read as permitting anything the account can reach. Every
568
- other key defaults to its narrowest value: `level` defaults to `read` and
569
- excludes `open` (no remote request may disable the sandbox), `min_enforcement`
570
- defaults to `kernel` (the default admits Codex and Claude Code; `tool-policy`
571
- is what admits OpenCode), and `env_allow`/`env_defaults` default to empty.
572
- Where a key appears in both, `env_defaults` wins — a profile that pins a value
573
- and also allows the key means the value it pinned, not the accepting shell's. A
574
- profile written as nothing but a required `allowed_roots` is therefore the
575
- most restrictive profile expressible.
576
-
577
- `identities` bounds which named identities (see [Agent
578
- identities](#agent-identities) above) a non-local requester may launch under,
579
- and it also defaults to its narrowest value — empty:
580
-
581
- - **Empty or omitted** refuses any identity the request names — the launch
582
- falls back to lending the operator's own ambient account, same as before
583
- identities existed.
584
- - **Exactly one** makes that identity the default: a request naming no
585
- identity gets it anyway, and a request naming it explicitly gets the same
586
- launch.
587
- - **Several** require the request to choose: a request naming none of them is
588
- refused rather than guessing, and a request naming one outside the list is
589
- refused too.
590
-
591
- A local requester has no profile and no ceiling here either — it may name any
592
- identity that exists, exactly as it may reach any root.
593
-
594
- A request naming no level resolves to its profile's level — never to the
595
- receiving machine's own `config.permissions` / `config.sandbox` defaults,
596
- which would let an unconfigured local default hand a remote caller more than
597
- its profile grants.
598
-
599
- **The `env_allow` trap.** A non-local launch's environment is composed from
600
- nothing — an allowlist, not the accepting shell's environment filtered by a
601
- denylist. That means a remote profile must list in `env_allow` (or supply in
602
- `env_defaults`) everything the runtime's own toolchain needs — not just what
603
- the agent's task needs. A missing entry does not present as a policy refusal;
604
- it presents as a **broken runtime** — a launch that starts and then fails in
605
- ways that look like a bug in Codex, Claude Code or OpenCode rather than a
606
- missing environment variable. This is exactly the diagnosis-confusing failure
607
- shape the rest of this feature avoids, so test a first remote profile with a
608
- trivial launch before relying on it.
609
-
610
- **`env_allow` is not the whole environment.** Two configured things still
611
- contribute variables read from the accepting process's unfiltered environment,
612
- exactly as for a local launch. A selected MCP server contributes its own declared
613
- `env_vars` and its `bearer_token_env_var`. And on every OpenCode launch, each
614
- configured `[opencode.provider]` contributes its API key, because the provider
615
- configuration handed to the runtime is built from them — so a remote OpenCode
616
- launch reaches this machine's model providers even with `env_allow = []`. That is the same principle applied
617
- one level up: a configured credential authenticates, one that merely happens to
618
- be present does not. It does mean a remote request may not choose which servers
619
- or plugins run: naming `mcp` or `plugin` is refused as not expressible
620
- remotely, and a remote launch gets this machine's configured `default_mcp` and
621
- `default_plugins`. To withhold a credential from a remote requester, withhold
622
- the server — a profile cannot withhold the variable alone. Provider keys cannot
623
- be withheld per requester at all today; a machine that must not lend its model
624
- providers should not enrol a remote requester for OpenCode.
625
-
626
- **What this does and does not defend against.** Muster does not authenticate
627
- its caller. A trusted local invoker that has already authenticated a remote
628
- peer passes along the provenance it established, and Muster applies least
629
- privilege to that provenance. This is not a defence against a compromised
630
- same-user process: anything able to exec Muster can already rewrite the very
631
- `config.toml` it reads, including its own enrolment and profile. The value is
632
- in what a legitimately-authenticated remote peer is confined to, not in
633
- resisting an attacker who is already running as the same local user.
634
-
635
- **Stop and output are asymmetric.** A local operator may stop or read the
636
- output of any launch on the machine, as before. A non-local requester may
637
- `stop` or read `output` for only the launches it asked for — an attempt to act
638
- on another requester's launch is refused. An entry with no recorded requester
639
- (older data, or a local launch) reads as local and is therefore protected the
640
- same as any other local launch. `list` is not scoped this way: every caller
641
- sees every launch on the machine, so the asymmetry covers acting on a launch,
642
- not seeing that it exists.
643
-
644
- ## Agent identities
645
-
646
- An **identity** is a named, pre-authenticated template configuration for one
647
- agent, stored under `~/.muster/identities/<name>` at mode `0700`. Launching
648
- under one makes a launch state which account it runs as instead of silently
649
- inheriting whatever the environment happened to imply.
650
-
651
- ```sh
652
- muster setup-identity --identity work --agent claude --interactive
653
- muster setup-identity --identity ci-bot --agent codex
654
- muster setup-identity --identity research --agent opencode
655
- ```
656
-
657
- Claude needs a credential of its own: its keychain credential is keyed to the
658
- template directory's own path, and a launch runs from a per-launch copy whose
659
- path hashes to a different keychain service name, so a token is the only thing
660
- that can authenticate a Claude identity launch. `muster run --identity` refuses
661
- a Claude identity with neither route configured rather than spawning an agent
662
- that cannot log in. There are two ways to give it one:
663
-
664
- - **`--interactive` (recommended).** Runs `claude setup-token` itself inside a
665
- pty with your terminal attached both ways — you see the flow and approve the
666
- browser step, and anything it asks you (the `Paste code here if prompted:`
667
- fallback, say, which is what an ssh session gets) reaches it from your
668
- keyboard. It captures the token from the output and stores it at
669
- `~/.muster/identities/<name>/token`, mode `0600` — the same directory
670
- Codex and OpenCode identities already hold their own `auth.json` in. The
671
- command prints a redacted confirmation of what it stored, never the token
672
- (`muster identities` later reports only that a stored token is present). If
673
- the login's output format ever changes so the token isn't recognised, this
674
- fails open: nothing is redacted or captured, the token appears exactly as it
675
- does today, and the command says it could not store one and points you at
676
- `--token-env` to record the one now on your screen. It needs a terminal, and
677
- refuses when stdin is not one.
678
- - **`--token-env NAME`**, for a machine that should keep no credential at
679
- rest — CI, say. It names the environment variable holding a Claude Code
680
- token (from a `claude setup-token` run yourself) — never the token itself.
681
- It applies to Claude **only**; Codex and OpenCode authenticate from an auth
682
- file inside the template, so `--token-env` is refused for them — it could
683
- only ever add a way for the launch to fail.
684
-
685
- An identity holds exactly one of these routes at a time: storing a token
686
- clears a declared `--token-env`, and passing `--token-env` deletes a stored
687
- token. `setup-identity` says which route it removed, if either. Passing
688
- `--interactive` and `--token-env` in the same command is refused rather than
689
- resolved by precedence — they ask for opposite things, and choosing one for
690
- you would be choosing where the credential lives.
691
-
692
- `setup-identity` creates the template (or reuses an existing one for the same
693
- agent — it refuses to repurpose one that belongs to a different agent) and
694
- reports whether a credential is already configured. Without `--interactive`
695
- it prints the **one-time login** for that agent to run yourself instead of
696
- running it for you:
697
-
698
- ```sh
699
- CLAUDE_CONFIG_DIR=~/.muster/identities/work claude setup-token # then export it, or use --interactive
700
- CODEX_HOME=~/.muster/identities/ci-bot codex login
701
- OPENCODE_CONFIG_DIR=~/.muster/identities/research opencode auth login
702
- ```
703
-
704
- Muster never performs a login without `--interactive` — each is an
705
- interactive OAuth flow, and a command that appeared to do it for you without
706
- showing you the flow would be hiding a credential prompt. Run the printed
707
- command, complete the flow, then check the result:
708
-
709
- ```sh
710
- muster identities
711
- ```
712
-
713
- Launch under a named identity with `--identity`:
714
-
715
- ```sh
716
- muster run claude --identity work --cwd ~/projects/thing --prompt 'Ship it'
717
- ```
718
-
719
- At launch, Muster copies a declared subset of that identity's files into a
720
- fresh per-launch directory under `~/.muster/identities-live/<launchId>/` and
721
- points the runtime's own configuration-location variable
722
- (`CLAUDE_CONFIG_DIR`, `CODEX_HOME`, or `OPENCODE_CONFIG_DIR` +
723
- `XDG_DATA_HOME`) at that copy — never at the template itself, so one launch
724
- can never mutate another's credential.
725
-
726
- The copy lives only as long as the launch. `muster stop` removes it directly,
727
- and otherwise it is removed by the **first `muster list` after the entry
728
- reaches a terminal status** (`exited`, `failed`, `stopped`, `unknown`). There
729
- is no prune operation — an earlier draft of this documentation said the copy
730
- was retained until the entry was pruned, and that was never what was built.
731
- This matters for two reasons: an agent writes its own session transcript into
732
- its configuration directory, so that transcript goes with the copy moments
733
- after the launch ends (capture it while the launch is still running if you
734
- need it), and resume/fork (#32) cannot assume a copy is still there.
735
-
736
- Launching without `--identity` sets none of this: behaviour is exactly as
737
- before, using the ambient environment.
738
-
739
- **That is the right choice for profile work, which is counter-intuitive.** If
740
- you already work in a shell whose `CLAUDE_CONFIG_DIR` selects an account —
741
- `~/.claude-armdev`, say — a launch with no `--identity` inherits it, and the
742
- session lands on that account with no flag at all. `launchEnv` blocks
743
- `CLAUDECODE`, `CLAUDE_CODE_*`, `CODEX_THREAD_ID`, `TMUX`, `TMUX_PANE` and
744
- `MCP_*` from reaching the child, and deliberately lets `CLAUDE_CONFIG_DIR`
745
- through for exactly this reason. Passing `--identity` there does not add
746
- containment to that account — it **replaces** it, pointing the runtime at a
747
- copy of a different one, so the session appears under an account you did not
748
- mean to use. Reach for `--identity` to choose an account Muster manages, not to
749
- harden the one your shell already selected.
750
-
751
- Session discovery follows the same relocated path, so a Claude identity's
752
- sessions are found even though `CLAUDE_CONFIG_DIR` no longer points at
753
- `~/.claude` — this was previously refused outright as an unsupported
754
- configuration.
755
-
756
- Five things worth stating plainly, because each is a sharp edge:
757
-
758
- 1. **`muster identities` reports *configured*, never *valid*.** A keychain
759
- item, an auth file, a stored token or a set variable proves a credential
760
- exists — not that it still works. Tokens expire and are revoked; there is
761
- no way to check that without exercising the credential, and `identities`
762
- does not do that.
763
- 2. **The identity store holds credentials at rest**, for every agent that
764
- keeps one there. Codex and OpenCode always do, because their own logins
765
- write auth files directly into the template. A Claude identity created
766
- with `--interactive` does too, at `~/.muster/identities/<name>/token` —
767
- the same directory, the same `0700`/`0600` protection, nothing more:
768
- storing it does not make it any safer at rest than Codex's or OpenCode's
769
- `auth.json` sitting right beside it, it just makes Claude consistent with
770
- them instead of relying on the operator's shell rc file. Directories are
771
- `0700`, and deleting an identity deletes whatever credential it holds —
772
- there is no separate secret to revoke elsewhere first. The one route with
773
- nothing at rest is `--token-env`: it names a variable instead, so the
774
- credential lives only in whichever environment sets that variable.
775
- 3. **Use `--interactive`, or `claude setup-token` with `--token-env` — never
776
- `ANTHROPIC_API_KEY`.** The OAuth token from `setup-token` bills the
777
- account's Claude subscription. An `ANTHROPIC_API_KEY` silently moves the
778
- same account onto pay-per-token API billing instead — same agent, very
779
- different bill, no error either way.
780
- 4. **A Claude identity's token is briefly written to disk during a launch.**
781
- The tmux host serialises the whole launch specification — environment
782
- included — to a temporary `launch.json` at mode `0600` and removes it once
783
- the launch is recorded. That is the same treatment every other launch
784
- variable gets, not special handling for this one, but it does mean a
785
- long-lived subscription token passes through disk; a launch whose host
786
- temp directory leaks (a crash before cleanup, a misconfigured `TMPDIR`)
787
- leaves it there.
788
- 5. **A requester profile with no `identities` lends the operator's own
789
- account** to that requester, because the launch falls back to the ambient
790
- environment exactly as it did before identities existed. That is preserved
791
- for compatibility, not a recommendation — an operator should set
792
- `identities` on any requester profile whose access they actually care
793
- about bounding.
794
-
795
- A Claude launch under an identity also marks the launch's working directory
796
- trusted *inside that launch's own copy* of the configuration, so a brand-new
797
- directory starts as a registered session without anyone accepting a trust
798
- dialog. This is distinct from the ordinary Claude prerequisite described
799
- above: it only ever writes to the per-launch copy, never to the operator's own
800
- `~/.claude.json`, and only applies to launches under an identity. A launch
801
- without `--identity` still requires the operator to have trusted the directory
802
- themselves beforehand.
803
-
804
- Automated tests use fake runtimes, so they prove the plumbing — the copy, the
805
- variables, the trust flag, the lifecycle — but not that a real agent starts
806
- authenticated from a copy. See
807
- [docs/identity-verification.md](./docs/identity-verification.md) for the
808
- manual checklist that covers that, and for which agents have actually been
809
- verified.
810
-
811
- ## Optional tools for launched agents
812
-
813
- MCP servers are optional. Muster does not require or install Tin Can (or any
814
- other MCP server). Register only servers you want launched agents to use in
815
- `~/.muster/config.toml`. For example, to use an already-installed Tin Can:
816
-
817
- ```toml
818
- # Optional personal defaults for sessions. Omit to keep launches MCP-free.
819
- default_mcp = ["tincan"]
820
-
821
- [mcp_servers.tincan]
822
- command = "tincan"
823
- tools = ["peers", "send_peer", "message_log"]
824
- required = false
825
- ```
826
-
827
- Then any runtime can use it:
828
-
829
- ```sh
830
- muster run codex --mcp tincan --prompt 'Coordinate with the other agent'
831
- muster run claude --mcp tincan --prompt 'Coordinate with the other agent'
832
- muster run opencode --mcp tincan --permissions auto --sandbox workspace-write \
833
- --prompt 'Coordinate'
834
- muster run codex --no-mcp --prompt 'Work without external tools'
835
- ```
836
-
837
- A plugin is named either by `path` or by an `npm` specifier, never both. A path
838
- becomes a file URL; a specifier is passed through untouched for OpenCode to
839
- resolve and install:
840
-
841
- ```toml
842
- [plugins.tincan]
843
- npm = "@brutalsystems/tincan-opencode"
844
-
845
- [plugins.local-thing]
846
- path = "~/.config/opencode/plugin/local-thing.ts"
847
- ```
848
-
849
- Prefer `npm` where the plugin is published — but not because it avoids a stale
850
- copy. Both routes keep one. A copied file goes stale in silence: the package
851
- updates, the copy does not, and the old code keeps running against whatever it
852
- was built for. A specifier keeps its copy somewhere less obvious. OpenCode
853
- resolves the specifier once, writes a generated `package.json` pinning the
854
- resolved version alongside a lockfile, and caches that under the range it was
855
- asked for:
856
-
857
- ```sh
858
- ~/.cache/opencode/packages/@brutalsystems/tincan-opencode@latest
859
- ```
860
-
861
- Publishing a new version does not change that directory and nothing warns, so
862
- the stale plugin keeps running and keeps reporting. Deleting it forces
863
- re-resolution on the next launch:
864
-
865
- ```sh
866
- rm -rf ~/.cache/opencode/packages/@brutalsystems/tincan-opencode@latest
867
- ```
868
-
869
- Muster passes the specifier through untouched, so pin a version where the
870
- staleness should be deliberate and visible in config rather than implicit in a
871
- cache. OpenCode keys a pinned specifier to its own directory, leaving the
872
- `@latest` copy alone:
873
-
874
- ```toml
875
- [plugins.tincan]
876
- npm = "@brutalsystems/tincan-opencode@0.7.1"
877
- ```
878
-
879
- A specifier needs the package to declare an entry point OpenCode's loader
880
- actually reads. It looks for `exports["./server"]` and falls back to `main`; it
881
- does not read `exports["."]`. A package declaring only the latter is fetched,
882
- its `package.json` is read, the session runs — and the plugin never executes,
883
- with nothing logged. If a specifier-named plugin appears to do nothing, check
884
- the package's entry points before looking anywhere else.
885
-
886
- Either way, confirm what is actually loaded. `muster doctor` checks every
887
- path-configured plugin against the package it came from:
888
-
889
- ```sh
890
- muster doctor --format human
891
- ```
892
-
893
- ```
894
- tincan @brutalsystems/tincan-opencode
895
- Status: stale — copy predates 0.7.2, published 2026-09-21
896
- Path: /Users/you/.config/opencode/plugin/tincan.ts
897
-
898
- 1 stale plugin. Replace path with npm = "@brutalsystems/tincan-opencode" in [plugins.tincan], or re-copy the file.
899
- ```
900
-
901
- The copied file carries no version of its own, so the check compares its mtime
902
- against the registry's publish time for the current release. That means a file
903
- re-copied recently reads as current even when its contents are old: doctor
904
- under-reports and never cries wolf. It exits 1 when anything is stale, and an
905
- unreachable registry is reported as such rather than failing the command.
906
-
907
- Muster knows the packages for the plugins it ships with. Name anything else,
908
- or override the default, with `published`:
909
-
910
- ```toml
911
- [plugins.mine]
912
- path = "~/src/mine.ts"
913
- published = "@acme/mine-opencode"
914
- ```
915
-
916
- Plugins named by `npm` are not checked: they keep no copy of their own, and
917
- clearing OpenCode's cache above is that remedy instead. For what a running
918
- session loaded rather than what is on disk, the records still carry the version:
919
-
920
- ```sh
921
- grep plugin_version ~/.tincan/peers/opencode/ses_*.json
922
- tincan --version
923
- ```
924
-
925
- Registering Muster itself is the one case to think twice about. A definition
926
- whose command runs `muster mcp` gives every session that selects it `run`,
927
- `list`, `stop` and `output` — launch authority, held by an agent rather than by
928
- you. Put it in `default_mcp` and every session muster launches can launch more,
929
- which is the propagation the child restrictions above exist to prevent;
930
- `max_concurrent` caps simultaneous launches but does not stop a tree forming.
931
- Nothing in Muster refuses this, so the restraint has to be yours. If you want
932
- an orchestrating agent, give exactly that one session an explicit
933
- `--mcp muster` and leave `default_mcp` alone. See "MCP installation" below for
934
- the same stance applied to a runtime's own shared configuration.
935
-
936
- `--mcp NAME` is repeatable and **replaces** personal defaults. `--no-mcp` clears
937
- them and cannot be combined with `--mcp`. MCP `run` takes `mcp: ["tincan"]`;
938
- `mcp: []` disables all servers. Omitted selection uses `default_mcp` for sessions.
939
- Tasks have no defaults, but accept an explicit selection.
940
-
941
- Definitions support either `command` plus optional `args`, `env`, and `env_vars`
942
- for stdio, or `url` plus optional `bearer_token_env_var` for Streamable HTTP.
943
- `tools` is a required nonempty list of exact tool names. Use `env_vars` and
944
- `bearer_token_env_var` to reference credentials in the launch environment;
945
- credentials are not written to launch logs. HTTP OAuth setup shared across
946
- runtimes, plugin/app tools, MCP resources and prompts are outside this interface.
947
- Server and tool names must contain only letters, digits, underscores or hyphens.
948
- Relative stdio commands run from the requested working directory; prefer an
949
- absolute executable path or a command on PATH.
950
-
951
- Before agent launch, Muster initializes each selected server and checks its tool
952
- catalog. No tool is called during preflight. An explicit `--mcp` selection is
953
- required: a missing server, missing credential, or missing configured tool fails
954
- that launch. Defaults are optional unless their definition sets `required = true`;
955
- an unavailable optional default is skipped with `mcp_warnings`. Each preflight is
956
- bounded by `startup_timeout_sec` (default 10, maximum 60) and the launch deadline.
957
- JSON results and `list` report selected logical names in `mcp`; human output shows
958
- `MCP:` and any warnings. These describe launch configuration, not continuous
959
- server health.
960
-
961
- Codex receives isolated definitions, using a fresh runtime name when an inherited
962
- server has the same name, and its native tool allowlist. Claude receives a private
963
- Muster stdio connection that filters both tool discovery and calls; newly added
964
- upstream tools cannot bypass the selection. Only selected tools are preapproved.
965
- OpenCode disables inherited definitions and tool families in its verified
966
- per-launch overlay, then enables only selected definitions. A launch that
967
- selects MCP servers omits the wildcard `*` permission entry and names the
968
- built-in tools it denies instead: in OpenCode a `*` entry suppresses MCP tools
969
- outright, and an explicit allow for the tool does not restore them. The
970
- built-in list is fixed, so a tool added by a future OpenCode release would not
971
- be denied by it; launches that select no MCP server keep the wildcard. With no plugins
972
- selected it also uses `--pure` and pins an empty plugin list, so inherited
973
- plugins do not enter the child process. Selecting a plugin with `--plugin`
974
- necessarily relaxes that: OpenCode merges project-local plugin discovery into
975
- any non-empty plugin list, so the child also loads whatever plugins the target
976
- repository ships. It globs both spellings — `.opencode/plugin` and
977
- `.opencode/plugins` — so auditing only one of them misses half of what a
978
- repository can load. Select plugins only for repositories trusted with that.
979
- `--plugin NAME` is repeatable and replaces whatever plugins the configuration
980
- selects by default, rather than adding to them; `--no-plugin` selects none and
981
- is how a launch gets the `--pure` treatment above when the configuration would
982
- otherwise have chosen some. Both apply to OpenCode only.
983
- Claude connection specifications are stored with owner-only permissions under
984
- `~/.muster/mcp`, removed on stop, task completion, or when `list` observes an ended
985
- session. MCP server access is separate from filesystem sandbox permissions: a
986
- selected external tool may have its own write or network capabilities.
987
-
988
- ## MCP installation
989
-
990
- Installed deliberately, in the one session that should hold spawn authority —
991
- never at user scope.
992
-
993
- Muster exposes `run`, `list`, `stop`, and `output` over stdio. `mcp` starts the
994
- server, and so does no arguments when stdin is not a terminal — which is what an
995
- MCP client provides. Typing `muster` at a terminal prints the usage text and a
996
- short status instead, because a server waiting for JSON-RPC on stdin is
997
- indistinguishable from a hang. `muster mcp` starts the server either way, so it
998
- can still be driven by hand to debug it. Diagnostics go to stderr, never protocol stdout.
999
- Schemas match the CLI (`args` is the array of optional runtime arguments).
1000
-
1001
- For a single Codex session, use per-invocation configuration:
1002
-
1003
- ```sh
1004
- codex -c 'mcp_servers.muster.command="node"' \
1005
- -c 'mcp_servers.muster.args=["/absolute/path/to/muster/dist/muster.js","mcp"]'
1006
- ```
1007
-
1008
- For a single Claude session:
1009
-
1010
- ```sh
1011
- claude --mcp-config '{"mcpServers":{"muster":{"command":"node","args":["/absolute/path/to/muster/dist/muster.js","mcp"]}}}'
1012
- ```
1013
-
1014
- Do not add Muster to `~/.codex/config.toml`, Claude's user-scope MCP registry,
1015
- or another shared configuration that grants launch authority to every agent.
1016
- The same applies to Muster's own `[mcp_servers]`: a definition that runs
1017
- `muster mcp`, selected by `default_mcp`, grants that authority to every session
1018
- Muster launches.
1019
-
1020
- ## Tin Can compatibility
1021
-
1022
- Muster has no Tin Can build or runtime dependency. Each tool implements the
1023
- written address contract independently. Vendored mechanics carry source-commit
1024
- headers and the original MIT license in `TINCAN_LICENSE`.
1025
-
1026
- Addresses belong to running sessions and can expire or collide. Store
1027
- `thread_id` / `session_id`, and re-resolve through Tin Can's `peers` before
1028
- sending instead of caching a launch address. Canonical IDs are not unique keys.
1029
-
1030
- One OpenCode behaviour is worth knowing if you deliver to a session yourself
1031
- rather than through Tin Can. A message posted to the v2
1032
- `/api/session/{id}/prompt` of a **TUI-hosted** session is admitted with a 200
1033
- and an `admittedSeq`, schedules a turn, and then dies with
1034
- `ModelUnavailableError` naming the session's own model — while the TUI resolves
1035
- that same model in the same process. A session hosted by `opencode serve` does
1036
- not do this, and the v1 `/session/{id}/prompt_async` route does not either.
1037
- Tin Can 0.6.0 delivers over v1, so nothing here depends on the v2 path.
1038
-
1039
- The failure is silent from the caller's side: a success response, nothing
1040
- written into the session, and the only trace a line in OpenCode's own log. The
1041
- investigation, what it did and did not establish, and why it was not filed
1042
- upstream are recorded in
1043
- [CONTRACT_PROVENANCE.md](./CONTRACT_PROVENANCE.md).
1044
- The frozen cases intentionally preserve Tin Can's known naming defects.
1045
-
1046
- Codex `idle` means reachable and not known to be busy, not guaranteed free.
1047
- The querying app-server can report `notLoaded` for a live thread; Muster maps
1048
- that to `idle`. Claude state comes from its session registry.
1049
-
1050
- OpenCode peers expose a durable `session_id` and loopback-only `server_url` so a
1051
- future Tin Can adapter has a stable integration seam. Tin Can does not yet send
1052
- to OpenCode through Muster: choosing native OpenCode messaging versus A2A, and
1053
- updating Tin Can for that transport, are explicitly deferred follow-up work.
1054
-
1055
- ## Verification
1056
-
1057
- ```sh
1058
- npm test
1059
- npm run build
1060
- npm run test:contract
1061
- ```
1062
-
1063
- The ordinary suite requires tmux, node-pty, POSIX `ps`/`lsof`, and Python 3 for
1064
- real file locks in the fake Codex executable. It uses isolated runtime homes and
1065
- no model APIs. It skips the four explicitly invoked compatibility cases.
1066
-
1067
- The verified contract baseline is the Tin Can named by `tincan_version` in the
1068
- frozen fixture, `test/fixtures/canonical-id.json`. That field is the single place
1069
- recording which release the vendored copies describe, and both `RELEASING.md` and
1070
- `publish.yml` derive their pin from it. The suite spawns an installed binary as
1071
- an MCP subprocess, compares durable IDs, and verifies delivery to fake runtimes
1072
- through both terminal hosts. A missing binary or missing durable field fails the
1073
- test. For reproducible verification, install that exact release outside Muster —
1074
- derived here too, rather than typed, so this command cannot name a release the
1075
- fixture has moved past:
1076
-
1077
- ```sh
1078
- contract_dir=$(mktemp -d)
1079
- ver=$(node -p "require('./test/fixtures/canonical-id.json').tincan_version")
1080
- npm install --prefix "$contract_dir" --no-save "@brutalsystems/tincan@$ver"
1081
- MUSTER_TINCAN_BIN="$contract_dir/node_modules/.bin/tincan" npm run test:contract
1082
- ```
1083
-
1084
- Verification uses that published release installed in an isolated temporary
1085
- directory. No Tin Can source edits are made.
1086
- Fixture version metadata alone does not indicate address-format drift: compare
1087
- the case arrays. Those arrays cover naming only, not peer-list membership or
1088
- state semantics. Upgrades require separate compatibility verification; passing
1089
- fixture hashes alone does not establish compatibility. The tests locate the
1090
- launched peer by canonical ID and compare its durable ID; they do not assert a
1091
- total peer count, exclude same-runtime peers, or assert Tin Can's busy state.
1092
- `CONTRACT_PROVENANCE.md` records both types of integrity checks.
1093
-
1094
- One-time live probes are separate from the automated suite. Both runtimes
1095
- launched and answered an initial prompt with the versions listed above; Claude
1096
- used a directory already trusted by its operator. `npm test` does not run
1097
- real models.
1098
-
1099
169
  ## License and releases
1100
170
 
1101
171
  MIT © 2026 Mike Williams. See [LICENSE](./LICENSE). Vendored Tin Can code