@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/LICENSE +1 -1
- package/README.md +110 -1048
- package/TINCAN_LICENSE +1 -1
- package/dist/agents.js +47 -0
- package/dist/claude-policy.js +25 -0
- package/dist/codex-policy.js +19 -1
- package/dist/enforcement.js +2 -1
- package/dist/guard.js +10 -19
- package/dist/help.js +4 -0
- package/dist/identity/workspace-key.js +59 -0
- package/dist/mcp-tools.js +5 -0
- package/dist/mcp.js +0 -44
- package/dist/muster.js +1 -0
- package/dist/options.js +63 -0
- package/dist/refresh/index.js +66 -0
- package/dist/run.js +53 -81
- package/dist/stop/opencode.js +32 -0
- package/package.json +1 -1
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)
|
|
4
9
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
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
|
|
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
|