@brutalsystems/muster 0.11.1 → 1.0.1

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