@brutalsystems/muster 0.7.18 → 0.7.20

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
@@ -105,7 +105,10 @@ piped output and `TERM=dumb` are always uncolored. JSON never includes colors.
105
105
 
106
106
  `run` accepts an optional `--` followed by runtime arguments. The normal
107
107
  allowlist is `--model` / `-m`, plus Claude's `--effort` and OpenCode's `--agent`
108
- and `--variant`. Unknown options, bundled short options, config injection and
108
+ and `--variant`. For Claude and Codex a model named there is *consumed* rather
109
+ than forwarded — it reaches the child through the same path as `--model`, so it
110
+ appears once — and naming a model both ways is refused rather than resolved by
111
+ precedence. Unknown options, bundled short options, config injection and
109
112
  raw permission overrides are refused, even with `allow_dangerous_flags`
110
113
  enabled. Use Muster's normalized permission flags. Prompt strings are always
111
114
  passed as one literal argument. Codex and Claude launches and OpenCode tasks put
@@ -396,7 +399,9 @@ claim implies that every external tool is OS-sandboxed.
396
399
  OpenCode keeps normal provider/model configuration and launches with `--pure`
397
400
  unless plugins are selected, which disables inherited external plugins.
398
401
  Providers declared under `[opencode.provider]` and a model named by
399
- `[opencode] model` or `--model` are supplied in the same verified overlay, so a
402
+ `[opencode] model` or `--model` are supplied in the same verified overlay (a
403
+ model named after `--` instead rides the child's argv and is left out of the
404
+ overlay, so it is delivered once rather than twice), so a
400
405
  launch does not depend on the operator's own OpenCode configuration; provider
401
406
  definitions merge with any the child resolves for itself rather than replacing
402
407
  them. Secrets belong in `api_key_env_var`, read from the launching environment,
package/dist/format.js CHANGED
@@ -38,10 +38,16 @@ function recordHuman(record, color) {
38
38
  if (record.runtime === "opencode" && record.server_url)
39
39
  lines.push(field("Control URL", clean(record.server_url)));
40
40
  lines.push(field("Directory", clean(record.cwd)));
41
+ if (record.identity)
42
+ lines.push(field("Identity", clean(record.identity)));
41
43
  if (record.permissions)
42
44
  lines.push(field("Permissions", clean(record.permissions)));
43
45
  if (record.sandbox)
44
46
  lines.push(field("Sandbox", clean(record.sandbox)));
47
+ // Only when known: a null means the child chose its own model, and "-" in a
48
+ // human listing would read as a value rather than an absence.
49
+ if (record.model)
50
+ lines.push(field("Model", clean(record.model)));
45
51
  if (record.mcp)
46
52
  lines.push(field("MCP", record.mcp.length ? record.mcp.map(clean).join(", ") : "none"));
47
53
  for (const warning of record.mcp_warnings ?? [])
package/dist/guard.js CHANGED
@@ -156,30 +156,121 @@ export function paramsFingerprint(req, permissions, mcp, plugins) {
156
156
  ]))
157
157
  .digest("hex");
158
158
  }
159
+ const MODEL_FLAGS = ["--model", "-m"];
160
+ /**
161
+ * The flag half of any pass-through arg, lowercased with `_` mapped to `-`.
162
+ * Extracted from `runtimeArgs`, which already normalised this way inline, and
163
+ * now shared with the resolver so the two cannot disagree about whether a
164
+ * given spelling names a model: one deciding a model was named while the other
165
+ * forwards it is exactly how a model reaches the child's argv twice.
166
+ */
167
+ export function normaliseFlag(arg) {
168
+ return arg.split("=")[0].toLowerCase().replaceAll("_", "-");
169
+ }
170
+ /** A model named inside the runtime pass-through, if one is there. */
171
+ function modelInArgs(args) {
172
+ const found = [];
173
+ for (let i = 0; i < args.length; i++) {
174
+ const arg = args[i];
175
+ if (!MODEL_FLAGS.includes(normaliseFlag(arg)))
176
+ continue;
177
+ if (arg.includes("=")) {
178
+ const value = arg.slice(arg.indexOf("=") + 1);
179
+ // `--model=` names a model and supplies none. Accepting it would deliver
180
+ // nothing and record `model: null` — "the child chose" — which is the
181
+ // accepted-and-silently-ignored behaviour this feature exists to remove.
182
+ if (!value)
183
+ throw new Error(`Missing value for ${arg}`);
184
+ found.push(value);
185
+ continue;
186
+ }
187
+ const value = args[i + 1];
188
+ // A bare flag with no usable value is left alone: `runtimeArgs` raises
189
+ // "Missing value for --model" on the same input and words it well.
190
+ if (value && !value.startsWith("-"))
191
+ found.push(value);
192
+ }
193
+ // Taking the first would let OpenCode forward both to a child whose last flag
194
+ // wins, leaving the run record naming a model the child did not run.
195
+ if (found.length > 1)
196
+ throw new Error(`Model named twice in runtime options: ${found.join(" and ")}. Name it once.`);
197
+ return found[0];
198
+ }
199
+ /**
200
+ * The one place a launch's model is decided. `launchArgs`, the registry write
201
+ * and the OpenCode overlay all call this, so what is recorded on the run and
202
+ * what is handed to the child are one computation rather than two that agree
203
+ * by luck — which is the whole point of recording it at all.
204
+ */
205
+ export function resolveModel(req, config) {
206
+ const inArgs = modelInArgs(req.args);
207
+ if (req.model !== undefined && inArgs !== undefined)
208
+ throw new Error(`Model named twice: --model ${req.model} and ${inArgs} in runtime options. Name it once.`);
209
+ const named = req.model ?? inArgs;
210
+ if (named !== undefined) {
211
+ // OpenCode's native form IS provider/model; claude and codex take a bare
212
+ // id, and a slash there is a mistake worth catching at this boundary
213
+ // rather than in the child's argument parser.
214
+ if (req.runtime !== "opencode" && named.includes("/"))
215
+ throw new Error(`--model for ${req.runtime} takes a bare model id, not PROVIDER/MODEL (got ${named})`);
216
+ return {
217
+ model: named,
218
+ source: "request",
219
+ origin: req.model !== undefined ? "flag" : "args",
220
+ };
221
+ }
222
+ const configured = req.runtime === "opencode" ? config.opencode.model : undefined;
223
+ return configured === undefined
224
+ ? null
225
+ : { model: configured, source: "config", origin: "config" };
226
+ }
227
+ /**
228
+ * Per runtime: which pass-through options ride to the child, and which are
229
+ * recognised but delivered by `resolveModel` instead. A consumed flag must not
230
+ * be refused — naming a model in `args` is still a supported way to ask, and
231
+ * it is the only way that worked before `--model` reached these runtimes.
232
+ * OpenCode is deliberately absent from `consumed`: it already forwards a model
233
+ * from `args` onto its argv and `test/opencode-policy.test.ts` pins that.
234
+ */
235
+ const RUNTIME_OPTIONS = {
236
+ claude: { forwarded: ["--effort"], consumed: MODEL_FLAGS },
237
+ codex: { forwarded: [], consumed: MODEL_FLAGS },
238
+ opencode: {
239
+ forwarded: ["--model", "-m", "--agent", "--variant"],
240
+ consumed: [],
241
+ },
242
+ };
159
243
  export function runtimeArgs(req) {
160
244
  const result = [];
161
- const allowed = req.runtime === "claude"
162
- ? ["--model", "-m", "--effort"]
163
- : req.runtime === "opencode"
164
- ? ["--model", "-m", "--agent", "--variant"]
165
- : ["--model", "-m"];
245
+ const { forwarded, consumed } = RUNTIME_OPTIONS[req.runtime];
166
246
  for (let i = 0; i < req.args.length; i++) {
167
- const arg = req.args[i], flag = arg.split("=")[0].toLowerCase().replaceAll("_", "-");
168
- if (!allowed.includes(flag))
247
+ const arg = req.args[i], flag = normaliseFlag(arg);
248
+ const keep = forwarded.includes(flag);
249
+ if (!keep && !consumed.includes(flag))
169
250
  throw new Error(`Runtime option refused: ${arg}`);
170
- if (arg.includes("="))
171
- result.push(arg);
251
+ if (arg.includes("=")) {
252
+ if (keep)
253
+ result.push(arg);
254
+ }
172
255
  else {
256
+ // The value is stepped over for a consumed flag exactly as for a
257
+ // forwarded one, so a missing value is still an error and the next
258
+ // option is not mistaken for this one's value.
173
259
  const value = req.args[++i];
174
260
  if (!value || value.startsWith("-"))
175
261
  throw new Error(`Missing value for ${arg}`);
176
- result.push(arg, value);
262
+ if (keep)
263
+ result.push(arg, value);
177
264
  }
178
265
  }
179
266
  return result;
180
267
  }
181
268
  export function launchArgs(req, config, mcp = [], bridgePath) {
182
269
  const extra = runtimeArgs(req);
270
+ const resolved = resolveModel(req, config);
271
+ // OpenCode never reaches here — `run.ts` branches it to the OpenCode launch
272
+ // builders, which carry the model in their config overlay.
273
+ const model = resolved ? ["--model", resolved.model] : [];
183
274
  const policy = resolvePermissions(req, config);
184
275
  if (req.runtime === "codex")
185
276
  return [
@@ -200,6 +291,7 @@ export function launchArgs(req, config, mcp = [], bridgePath) {
200
291
  ? ["--ask-for-approval", "never"]
201
292
  : ["-c", 'approval_policy="never"']),
202
293
  ]),
294
+ ...model,
203
295
  ...extra,
204
296
  "--",
205
297
  req.prompt,
@@ -244,6 +336,7 @@ export function launchArgs(req, config, mcp = [], bridgePath) {
244
336
  JSON.stringify(claudeMcpConfig(mcp, bridgePath)),
245
337
  "--settings",
246
338
  JSON.stringify(settings),
339
+ ...model,
247
340
  ...extra,
248
341
  "--",
249
342
  req.prompt,
package/dist/help.js CHANGED
@@ -55,7 +55,10 @@ export const COMMANDS = [
55
55
  ["--no-mcp", "launch with no MCP servers"],
56
56
  ["--plugin NAME", "repeatable; replaces configured defaults (OpenCode)"],
57
57
  ["--no-plugin", "launch with no plugins"],
58
- ["--model PROVIDER/MODEL", "select the OpenCode model"],
58
+ [
59
+ "--model MODEL",
60
+ "model for the launch: a bare id for claude and codex, PROVIDER/MODEL for opencode",
61
+ ],
59
62
  [
60
63
  "--request-key KEY",
61
64
  "name the request; sending it again returns the same launch",
@@ -1,7 +1,7 @@
1
1
  import { identityAuthState } from "./identity-auth.js";
2
2
  import { IdentityError, identityDirExists, identityMetaSchema, identityNameSchema, identityPath, listIdentities, readIdentity, writeIdentity, } from "./identity-store.js";
3
3
  import { removeStoredToken, writeStoredToken, } from "./identity-token.js";
4
- import { captureClaudeToken, redactToken, } from "./identity-login.js";
4
+ import { captureClaudeToken, redactToken, resolveOnPath, } from "./identity-login.js";
5
5
  /**
6
6
  * The one-time login for each agent. Muster never performs it: each is an
7
7
  * interactive OAuth flow, and a command that appeared to do it for you would be
@@ -25,7 +25,12 @@ export async function setupIdentity(opts) {
25
25
  // a codex or opencode identity, where the credential is an auth file the
26
26
  // agent's own login writes into the template, it has exactly one effect: a new
27
27
  // way to fail. Refused where the operator types it.
28
- if (opts.tokenEnv && opts.agent !== "claude")
28
+ // Checked against undefined, not truthiness: `--token-env ""` is falsy, so a
29
+ // truthiness test let an empty name past every guard and then dropped it,
30
+ // leaving the identity on neither credential route without saying so (#69).
31
+ if (opts.tokenEnv !== undefined && opts.tokenEnv.trim() === "")
32
+ throw new IdentityError(opts.name, "--token-env takes the name of an environment variable holding the token, not the token and not an empty value");
33
+ if (opts.tokenEnv !== undefined && opts.agent !== "claude")
29
34
  throw new IdentityError(name, `--token-env applies only to a claude identity; ${opts.agent} authenticates from an auth file in the template, and a token variable here would only add a way for the launch to fail`);
30
35
  // Distinguish "no such identity" from "an identity that is there but unusable".
31
36
  // Overwriting the second would silently repurpose a directory that still holds
@@ -150,24 +155,30 @@ export async function interactiveSetup(opts) {
150
155
  stored: false,
151
156
  note: `Run this once, then check with \`muster identities\`:\n\n ${created.login}\n`,
152
157
  };
158
+ // Asked before anything is spawned, so "could not be started" is a fact about
159
+ // the filesystem rather than an inference from what the child printed. That
160
+ // inference was platform-dependent: a missing executable emits nothing under
161
+ // macOS and an error into the pty under Linux (#69).
162
+ if (!(await resolveOnPath("claude", opts.env ?? process.env)))
163
+ throw new IdentityError(opts.name, "`claude` is not on PATH, so the login could not be started; nothing was changed");
153
164
  write(`Running: claude setup-token\n${"─".repeat(50)}\n`);
154
165
  const result = await captureClaudeToken({
155
166
  dir: created.path,
156
167
  env: opts.env ?? process.env,
157
168
  onData: write,
158
- ...(opts.stdin ? { stdin: opts.stdin } : {}),
169
+ stdin: opts.stdin,
159
170
  });
160
171
  write(`${"─".repeat(50)}\n`);
161
172
  // Checked BEFORE the token branch, deliberately: a login that failed writes
162
173
  // no credential even if something token-shaped went past on the way down.
163
174
  //
164
- // This is also the case where the child never started at all. node-pty does
165
- // not throw for a missing executable — it exits 1 having emitted zero bytes —
166
- // so an operator with no `claude` on PATH used to be told Muster could not
167
- // parse output that never existed, with no identity name and no hint that
168
- // nothing had run. The child's own output is the error here, whatever it was.
175
+ // Two different events land here and they need different explanations. node-pty
176
+ // does not throw for a missing executable — it exits 1 having emitted zero
177
+ // bytes — which is exactly the exit shape of a login that failed on its own or
178
+ // that the operator interrupted. The exit code cannot tell them apart; whether
179
+ // anything was ever printed can (#69).
169
180
  if (result.exitCode !== 0)
170
- throw new IdentityError(opts.name, `\`claude setup-token\` failed (exit ${result.exitCode}) and no credential was stored; its own output above is the error. If nothing was printed above it could not be started at all — check that \`claude\` is on PATH.`);
181
+ throw new IdentityError(opts.name, `\`claude setup-token\` ended without storing a credential (exit ${result.exitCode}); its own output above is the error. If you interrupted it, nothing was stored and nothing was changed.`);
171
182
  if (result.token) {
172
183
  const { clearedRoute } = await storeIdentityToken({
173
184
  home: opts.home,
@@ -1,3 +1,28 @@
1
+ import { access, constants } from "node:fs/promises";
2
+ import { delimiter, join } from "node:path";
3
+ /**
4
+ * Where `name` is on PATH, or undefined. The same lookup `tmuxExecutable` does,
5
+ * and the reason the login asks it: whether a child "could not be started" has
6
+ * to be decided before spawning, not inferred from what it printed. node-pty
7
+ * does not throw for a missing executable — it exits 1, emitting nothing on
8
+ * macOS and an error into the pty on Linux, so output was never the discriminator
9
+ * it appeared to be (#69).
10
+ */
11
+ export async function resolveOnPath(name, env) {
12
+ for (const dir of (env.PATH ?? "").split(delimiter)) {
13
+ if (!dir)
14
+ continue;
15
+ const candidate = join(dir, name);
16
+ try {
17
+ await access(candidate, constants.X_OK);
18
+ return candidate;
19
+ }
20
+ catch {
21
+ // Not here, or not executable. Keep looking.
22
+ }
23
+ }
24
+ return undefined;
25
+ }
1
26
  /**
2
27
  * A Claude long-lived OAuth token, as `claude setup-token` prints it. The length
3
28
  * floor is what separates a real token from the literal word in help text.
@@ -257,8 +282,8 @@ function attachInput(pty, input, signals) {
257
282
  * status quo.
258
283
  */
259
284
  export async function captureClaudeToken(opts) {
260
- const module = await import("node-pty");
261
- const pty = module.spawn("claude", ["setup-token"], {
285
+ const spawn = opts.spawn ?? (await import("node-pty")).spawn;
286
+ const pty = spawn("claude", ["setup-token"], {
262
287
  cwd: opts.dir,
263
288
  env: { ...opts.env, CLAUDE_CONFIG_DIR: opts.dir },
264
289
  name: "xterm-256color",
@@ -278,7 +303,7 @@ export async function captureClaudeToken(opts) {
278
303
  // up before rethrowing, and enters raw mode last of all.
279
304
  let detachInput = () => { };
280
305
  try {
281
- detachInput = attachInput(pty, opts.stdin ?? process.stdin, opts.signals ?? process);
306
+ detachInput = attachInput(pty, opts.stdin, opts.signals ?? process);
282
307
  const filter = createTokenFilter(opts.onData);
283
308
  const dataSub = pty.onData((chunk) => filter.push(chunk));
284
309
  const exitCode = await new Promise((resolve) => {
package/dist/mcp-tools.js CHANGED
@@ -80,7 +80,7 @@ export const MCP_TOOLS = [
80
80
  model: {
81
81
  type: "string",
82
82
  minLength: 1,
83
- description: "PROVIDER/MODEL, overriding the configured [opencode] model. OpenCode only.",
83
+ description: "Model for the launch. A bare model id for claude and codex; PROVIDER/MODEL for opencode, where it overrides the configured [opencode] model. Recorded on the run and reported by `list`.",
84
84
  },
85
85
  identity: {
86
86
  type: "string",
package/dist/muster.js CHANGED
@@ -247,13 +247,21 @@ async function main() {
247
247
  // year-long token to disk on the machine the operator explicitly asked
248
248
  // to keep no credential at rest, while never recording the variable
249
249
  // they named.
250
- if (tokenEnv)
250
+ if (tokenEnv !== undefined && agent !== "claude")
251
+ // Not the mutual-exclusion message: for codex and opencode
252
+ // `--interactive` captures nothing and `--token-env` does not apply
253
+ // at all, so describing them as two routes to choose between would
254
+ // be describing Claude's behaviour at a different agent (#69).
255
+ throw new IdentityError(identity, `--token-env applies only to a claude identity; ${agent} authenticates from an auth file in the template, and a token variable here would only add a way for the launch to fail`);
256
+ if (tokenEnv !== undefined)
251
257
  throw new IdentityError(identity, "--interactive and --token-env are the two mutually exclusive credential routes: --interactive runs the login and stores a token in the identity, --token-env records the name of a variable to read one from instead. Pass one, not both.");
252
258
  const result = await interactiveSetup({
253
259
  home: musterHome(),
254
260
  name: identity,
255
261
  agent: agent,
256
262
  isTTY: Boolean(process.stdin.isTTY),
263
+ // The one place the operator's real keyboard is named.
264
+ stdin: process.stdin,
257
265
  });
258
266
  process.stdout.write(`\n${result.note}\n`);
259
267
  return;
@@ -265,7 +273,7 @@ async function main() {
265
273
  home: musterHome(),
266
274
  name: identity,
267
275
  agent: agent,
268
- ...(tokenEnv ? { tokenEnv } : {}),
276
+ ...(tokenEnv !== undefined ? { tokenEnv } : {}),
269
277
  })) + "\n");
270
278
  }
271
279
  else if (command === "identities") {
@@ -1,7 +1,7 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
- import { resolvePermissions, runtimeArgs } from "./guard.js";
4
- import { openCodeModel, openCodeProviders } from "./providers.js";
3
+ import { resolveModel, resolvePermissions, runtimeArgs, } from "./guard.js";
4
+ import { openCodeProviders } from "./providers.js";
5
5
  const exec = promisify(execFile);
6
6
  const DEBUG_CONFIG_ERROR = "Cannot verify effective OpenCode MCP configuration";
7
7
  const ISOLATION_ERROR = "OpenCode MCP isolation could not be verified; refusing launch";
@@ -173,7 +173,15 @@ function permissions(req, config, tools) {
173
173
  }
174
174
  export function openCodeConfig(req, config, selected, inheritedMcpNames, plugins = [], source = {}) {
175
175
  const { mcp, tools } = mcpPolicy(selected, inheritedMcpNames);
176
- const model = openCodeModel(req.model, config);
176
+ // An `args`-named model is already forwarded onto the child's argv by
177
+ // `runtimeArgs`, so it must not also enter the verified overlay: that would
178
+ // deliver one model by two routes and rewrite a config the caller never
179
+ // changed. The overlay carries only what `--model` or `[opencode] model`
180
+ // named, exactly as before this resolver existed.
181
+ const resolved = resolveModel(req, config);
182
+ const model = resolved && resolved.origin !== "args"
183
+ ? resolved.model
184
+ : config.opencode.model;
177
185
  const provider = openCodeProviders(config, source);
178
186
  return {
179
187
  share: "disabled",
package/dist/providers.js CHANGED
@@ -75,7 +75,3 @@ export function openCodeProviders(config, source) {
75
75
  ];
76
76
  }));
77
77
  }
78
- /** `--model` beats the configured default; OpenCode decides when neither is set. */
79
- export function openCodeModel(requested, config) {
80
- return requested ?? config.opencode.model;
81
- }
package/dist/run.js CHANGED
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { spawn } from "node:child_process";
6
6
  import { loadConfig } from "./config.js";
7
- import { runSchema, agentBaseEnv, assertAllowedWorkspace, assertLevelExclusive, launchArgs, launchEnv, paramsFingerprint, resolvePermissions, runtimeArgs, } from "./guard.js";
7
+ import { runSchema, agentBaseEnv, assertAllowedWorkspace, assertLevelExclusive, resolveModel, launchArgs, launchEnv, paramsFingerprint, resolvePermissions, runtimeArgs, } from "./guard.js";
8
8
  import { enforcementGrade } from "./enforcement.js";
9
9
  import { LOCAL } from "./requester.js";
10
10
  import { assertEnforcementMeetsMinimum, assertLevelWithinCeiling, assertMayAct, resolveIdentityForRequest, resolveRequesterPolicy, RequesterPolicyRefusal, } from "./requester-policy.js";
@@ -37,6 +37,12 @@ function taskHandle(entry, pid) {
37
37
  mcp: entry.mcp ?? [],
38
38
  ...(entry.mcp_warnings ? { mcp_warnings: entry.mcp_warnings } : {}),
39
39
  ...(entry.enforcement ? { enforcement: entry.enforcement } : {}),
40
+ ...(entry.identity ? { identity: entry.identity } : {}),
41
+ // `run` returns this, while `list` builds its own record: a field added to
42
+ // one and not the other makes the two disagree about the same launch.
43
+ ...(entry.model !== undefined
44
+ ? { model: entry.model, model_source: entry.modelSource ?? null }
45
+ : {}),
40
46
  id: entry.id,
41
47
  runtime: entry.runtime,
42
48
  state: "running",
@@ -226,6 +232,10 @@ export class Muster {
226
232
  const selectedMcp = selectMcp(effective, this.config);
227
233
  const selectedPlugins = req.runtime === "opencode" ? selectPlugins(effective, this.config) : [];
228
234
  let argv = [];
235
+ // Resolved here, at the seam that already exists to refuse a bad launch
236
+ // before anything is reserved: a malformed model must not leave a
237
+ // registry entry behind.
238
+ const model = resolveModel(effective, this.config);
229
239
  if (req.runtime === "opencode")
230
240
  runtimeArgs(effective);
231
241
  else
@@ -247,6 +257,13 @@ export class Muster {
247
257
  cwd: req.cwd,
248
258
  ...permissions,
249
259
  enforcement,
260
+ model: model?.model ?? null,
261
+ modelSource: model?.source ?? null,
262
+ // Recorded here rather than only at the patch below, because `list`
263
+ // now publishes it: an entry that dies between this line and that one
264
+ // would otherwise report no identity, which reads as "ran on the
265
+ // ambient environment" for a launch that named an account.
266
+ ...(identityName ? { identity: identityName } : {}),
250
267
  }, this.config.max_concurrent, req.requestKey
251
268
  ? {
252
269
  requestKey: req.requestKey,
@@ -519,6 +536,10 @@ export class Muster {
519
536
  pid: identity.pid,
520
537
  host: driver.id,
521
538
  capabilities: driver.capabilities(),
539
+ // The RESOLVED name, matching what the entry records and what
540
+ // `list` projects: a launch result and a listing must not
541
+ // disagree about which account this ran as.
542
+ ...(identityName ? { identity: identityName } : {}),
522
543
  attach_hint: driver.attachHint(launched.hostRef),
523
544
  };
524
545
  if (req.open) {
@@ -859,6 +880,13 @@ export class Muster {
859
880
  // From the entry, not the peer: a listing must report the same fact a
860
881
  // launch result does, and an entry predating the field has none.
861
882
  ...(e.enforcement ? { enforcement: e.enforcement } : {}),
883
+ ...(e.identity ? { identity: e.identity } : {}),
884
+ // Absent on an entry written before the field, which is a different
885
+ // fact from an explicit null: that one means Muster resolved nothing
886
+ // and the child chose. Both are more useful than a guess.
887
+ ...(e.model !== undefined
888
+ ? { model: e.model, model_source: e.modelSource ?? null }
889
+ : {}),
862
890
  id: e.id,
863
891
  runtime: e.peer?.runtime ?? e.runtime,
864
892
  state: e.status === "running" ? (e.peer?.state ?? "idle") : e.status,
@@ -879,6 +907,13 @@ export class Muster {
879
907
  ? { permissions: e.permissions, sandbox: e.sandbox }
880
908
  : {}),
881
909
  ...(e.enforcement ? { enforcement: e.enforcement } : {}),
910
+ ...(e.identity ? { identity: e.identity } : {}),
911
+ // Absent on an entry written before the field, which is a different
912
+ // fact from an explicit null: that one means Muster resolved nothing
913
+ // and the child chose. Both are more useful than a guess.
914
+ ...(e.model !== undefined
915
+ ? { model: e.model, model_source: e.modelSource ?? null }
916
+ : {}),
882
917
  id: e.id,
883
918
  runtime: e.runtime,
884
919
  cwd: e.cwd,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brutalsystems/muster",
3
- "version": "0.7.18",
3
+ "version": "0.7.20",
4
4
  "description": "Launch instructed, reachable local agent sessions",
5
5
  "type": "module",
6
6
  "engines": {
@@ -12,7 +12,8 @@
12
12
  "scripts": {
13
13
  "build": "tsc && node scripts/prepare-bin.mjs",
14
14
  "test": "vitest run",
15
- "check": "npm test && node scripts/check-tarball.mjs",
15
+ "typecheck": "tsc -p tsconfig.check.json",
16
+ "check": "npm run typecheck && npm test && node scripts/check-tarball.mjs",
16
17
  "test:contract": "MUSTER_CONTRACT=1 vitest run test/contract.test.ts",
17
18
  "postinstall": "node scripts/prepare-pty.mjs",
18
19
  "pretest": "npm run build",