@cabane/companion 0.6.144 → 0.6.146

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.
@@ -64,18 +64,15 @@ var prepareHookSchema = z.object({
64
64
  // default is generous; a hook that hangs past this is killed and the turn
65
65
  // fails with a clear timeout message rather than pinning the companion.
66
66
  timeoutMs: z.number().int().positive().optional(),
67
- // CT943: re-invoke the hook on turns where a prepared result already exists,
68
- // in VALIDATE mode — the cached result is handed back as `prepared` and the
69
- // hook's stdout is ignored, so the cwd provably cannot move. Its only power
70
- // is to exit non-zero and fail the turn.
67
+ // Re-invoke the hook when a prepared result already exists, in VALIDATE
68
+ // mode: the cached result is handed back as `prepared` and stdout is
69
+ // ignored, so the cwd cannot move. Its only power is to exit non-zero and
70
+ // fail the turn. The hook resolves once per (conversation, agent), so a
71
+ // later message asking for a different environment is otherwise silently
72
+ // ignored; this lets a hook refuse it out loud.
71
73
  //
72
- // Why it exists: the hook runs exactly once per (conversation, agent), so a
73
- // later message asking for a different environment can't be honoured. Without
74
- // this, it is silently ignored — the host knows and says nothing, which is
75
- // the worst available outcome. An opt-in hook can now refuse it out loud.
76
- //
77
- // Default OFF, and deliberately: an operator hook that provisions
78
- // unconditionally must not start running on every turn because it upgraded.
74
+ // Default OFF: a hook that provisions unconditionally must not start
75
+ // running on every turn because the companion upgraded.
79
76
  validateCached: z.boolean().optional(),
80
77
  // Independent operator-owned reporter. Receives sanitized failure metadata
81
78
  // on stdin; never re-enters this hook and cannot change the turn outcome.
@@ -122,18 +119,17 @@ function configPath() {
122
119
  var localAgentConfigSchema = z2.object({
123
120
  cwd: z2.string().optional(),
124
121
  prepareHook: prepareHookSchema.optional(),
125
- // CT289: the Claude Code auto-memory escape hatch. Auto-memory is forced OFF
126
- // by default on every companion (memory belongs in the Cabane workspace, and a
127
- // shared companion would otherwise pool one cwd-keyed memory dir across users).
128
- // On a companion you run yourself, set `claudeCode: { autoMemory: true }` to hand
129
- // auto-memory back to your own `~/.claude/settings.json` — Cabane then stops
122
+ // The Claude Code auto-memory escape hatch. Auto-memory is forced OFF by
123
+ // default (memory belongs in the Cabane workspace, and a shared companion
124
+ // would pool one cwd-keyed memory dir across users). On a companion you run
125
+ // yourself, `claudeCode: { autoMemory: true }` hands auto-memory back to your
126
+ // own `~/.claude/settings.json` — Cabane then stops
130
127
  // injecting the off switch and your normal Claude Code memory workflow applies
131
128
  // (in coding mode, where the checkout's project settings are read).
132
129
  claudeCode: z2.object({ autoMemory: z2.boolean().optional() }).strict().optional(),
133
- // CT1693: this agent's exception to `runners.sandbox` — an ops agent that
134
- // runs with the machine on a sandboxed device, or one sandboxed agent on a
135
- // device that isn't. Absent is "inherit"; see `runnerSandboxFor`. Only on a
136
- // row keyed by the agent's id (`agentsConfigSchema`).
130
+ // This agent's exception to `runners.sandbox`: an ops agent that runs with
131
+ // the machine on a sandboxed device, or one sandboxed agent on a device that
132
+ // isn't. Absent is "inherit". Only on a row keyed by the agent's id.
137
133
  sandbox: z2.enum(["os", "none"]).optional()
138
134
  }).strict();
139
135
  var AGENT_ID_KEY = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -149,9 +145,8 @@ var agentsConfigSchema = z2.record(z2.string(), localAgentConfigSchema).superRef
149
145
  }
150
146
  });
151
147
  var companionConfigSchema = z2.object({
152
- // The cabane instance this device is paired with. SJ515: https-enforced
153
- // (loopback exempt) so a hand-edited config can't smuggle a plaintext-http
154
- // base URL onto the MITM-able channel the device token + prompt ride.
148
+ // https-enforced (loopback exempt), so a hand-edited config can't put the
149
+ // device token and prompt on a plaintext channel.
155
150
  baseUrl: z2.string().url().refine(isAllowedBaseUrl, {
156
151
  message: "baseUrl must be https (loopback http is allowed for local dev only)"
157
152
  }),
@@ -169,91 +164,59 @@ var companionConfigSchema = z2.object({
169
164
  // agentId / username / `slug/username`. Hand-added by the operator; the companion
170
165
  // never writes this (it only persists credentials + the device, elsewhere).
171
166
  agents: agentsConfigSchema.optional(),
172
- // CT1095: the DEVICE-LEVEL prepare hook — one entry that applies to every agent
173
- // assigned to this device, unless that agent's own `agents` row names a hook of
174
- // its own.
175
- //
176
- // Until now `prepareHook` existed only under `agents[<id>]`, which made the
177
- // config a second silent roster: a device whose provisioning is genuinely
178
- // per-device (ours resolves a directory from the conversation and agent ids the
179
- // companion already hands the hook) still had to list every agent by id, and an
180
- // agent nobody remembered to add got no hook at all — no error, no log, just a
181
- // turn that started in the operator's `$HOME`. That is how @dale woke in
182
- // `/home/joshed` on 2026-08-17 with a perfectly correct dispatch.
183
- //
184
- // Absent → nothing changes for anyone, which is why this can land inert: a
185
- // device with no device-level entry behaves exactly as it did before. A hook
186
- // that fails still fails the dispatch loudly, as now.
167
+ // The DEVICE-LEVEL prepare hook, for every agent assigned here unless its own
168
+ // `agents` row names one. Per-agent hooks alone make the config a second
169
+ // silent roster: an agent nobody listed gets no hook and no error, and starts
170
+ // in the operator's `$HOME` (an agent woke in `/home/joshed` this way on
171
+ // 2026-08-17).
187
172
  prepareHook: prepareHookSchema.optional(),
188
- // CT1085 §9: `dashboardPort` and `autoOpen` are READ AND IGNORED by the CLI —
189
- // it binds no port and opens no browser. They stay in the schema (rather than
190
- // becoming a strict-mode parse error on an existing config file) and still mean
191
- // what they say to the Electron shell, the one front-end that renders a
192
- // dashboard. `logLevel` is unaffected: pino level, live-editable.
173
+ // `dashboardPort` and `autoOpen` are IGNORED by the CLI (no port, no browser)
174
+ // but stay in the schema so an existing file still parses; the Electron shell
175
+ // reads them. `logLevel` is pino's level, live-editable.
193
176
  dashboardPort: z2.number().int().min(1).max(65535).optional(),
194
177
  autoOpen: z2.boolean().optional(),
195
178
  logLevel: z2.enum(["warn", "info", "debug"]).optional(),
196
179
  logFormat: z2.enum(["human", "json"]).optional(),
197
- // CT270: the opencode runtime, when the operator runs one on this machine. The
198
- // operator installs opencode, starts `opencode serve` (auth via opencode's own
199
- // `/connect` — Cabane never sees provider keys), and points the companion at it
200
- // here. Setting this makes the device advertise the `opencode` runtime on its
201
- // heartbeat manifest (so the server offers DeepSeek/opencode models here and
202
- // routes those turns to this device) AND registers the opencode adapter in the
203
- // dispatcher. Absent → the device is claude-code-only, exactly as before.
180
+ // An `opencode serve` the operator runs here (auth via opencode's own
181
+ // `/connect`; Cabane never sees provider keys). Set, the device advertises the
182
+ // `opencode` runtime and registers its adapter.
204
183
  opencode: z2.object({
205
184
  serverUrl: z2.string().url()
206
185
  }).strict().optional(),
207
- // CT1082: the Claude Code runtime, when the user connected it on this machine.
208
- // The change this task exists for: Claude Code used to be the one harness a
209
- // device exposed with no configuration — a `claude --version` exit-0 was taken as
210
- // consent — and it is now the third config-driven harness, opted into exactly
211
- // like codex. CT1379: the binary that must be there is the Agent SDK's own
212
- // bundled executable — the file a turn actually launches — not the user's
213
- // `claude` on PATH, which the SDK never runs. The manifest advertises
214
- // claude-code only when this block says so AND that binary resolves.
186
+ // Whether the user connected Claude Code on this machine: consent, opted into
187
+ // like codex. The manifest advertises claude-code only when this says so AND
188
+ // the Agent SDK's bundled executable resolves (not `claude` on PATH, which the
189
+ // SDK never runs).
215
190
  //
216
- // The block's PRESENCE is also the migration marker (see `migrateConnectedHarnesses`).
217
- // Absent means the config predates this task — a device whose user was never
218
- // asked — and the migration grandfathers it on first start. So every config
219
- // written from here on carries the block explicitly, including a freshly-paired
220
- // one, which starts at `enabled: false`: a new device is connected to nothing
221
- // until its user says otherwise.
191
+ // The block's PRESENCE is also the migration marker
192
+ // (`migrateConnectedHarnesses`): absent means a config whose user was never
193
+ // asked. So every config written carries it, a fresh pair at `enabled: false`.
222
194
  //
223
- // Not to be confused with the per-agent `agents.<key>.claudeCode.autoMemory`
224
- // above — that's one agent's memory switch, this is the device's connected set.
195
+ // Not the per-agent `agents.<key>.claudeCode.autoMemory`, one agent's memory
196
+ // switch.
225
197
  claudeCode: z2.object({
226
198
  enabled: z2.boolean().optional()
227
199
  }).strict().optional(),
228
- // CT481: the codex runtime, when the operator runs Codex on this machine. Unlike
229
- // opencode (a long-lived `opencode serve` addressed by URL), Codex is a local CLI
230
- // the `@openai/codex-sdk` spawns per turn — so the config is just an opt-in flag,
231
- // not a server address. The operator installs Codex + logs in (`codex login`, or
232
- // `CODEX_API_KEY` — Cabane never sees the key) and sets `{ "codex": { "enabled":
233
- // true } }`. Presence of the block enables it (an explicit `enabled: false`
234
- // keeps the block but turns it off). Absent → the device doesn't offer codex.
235
- // CT1379: enabling is now half the answer, not all of it — the manifest and the
236
- // adapter registry additionally require the SDK's vendored `codex` binary to
237
- // resolve, because the enable flag alone advertised a runtime a device with a
238
- // half-installed platform package could not start.
200
+ // Codex is a local CLI the SDK spawns per turn, so this is an opt-in flag, not
201
+ // an address. The operator logs in (`codex login` or `CODEX_API_KEY`; Cabane
202
+ // never sees the key). The block's presence enables it; `enabled: false` turns
203
+ // it off. Half the answer: advertising also requires the SDK's vendored
204
+ // `codex` binary to resolve.
239
205
  codex: z2.object({
240
206
  enabled: z2.boolean().optional()
241
207
  }).strict().optional(),
242
- // CT1679: run each turn in a child runner process instead of in this one.
243
- // Absent, or `enabled: false`, is the in-process path, untouched. `sandbox`
244
- // wraps the runner in an OS sandbox where the host supports one; `'none'`
245
- // (the default) runs it as a plain child beside this process, a supported
246
- // configuration in its own right rather than a degraded one.
247
- // CT1684: `policy` is what the OS sandbox lets a turn reach, read and hold
248
- // (`runner/sandbox-policy.ts`). It is validated there rather than here, so a
249
- // bad policy refuses the turns it governs, with the field named, instead of
250
- // keeping the companion from starting at all.
208
+ // Run each turn in a child runner process. Absent or `enabled: false` is the
209
+ // in-process path. `sandbox: 'os'` wraps the runner in an OS sandbox where the
210
+ // host supports one; `'none'` (the default) is a plain child, supported in its
211
+ // own right. `policy` is what the sandbox lets a turn reach, read and hold,
212
+ // validated in `runner/sandbox-policy.ts` so a bad policy refuses the turns it
213
+ // governs, field named, instead of stopping the companion starting.
251
214
  runners: z2.object({
252
215
  enabled: z2.boolean().optional(),
253
216
  provider: z2.enum(["local-process", "docker"]).optional(),
254
217
  sandbox: z2.enum(["os", "none"]).optional(),
255
- // CT1681: the `docker` provider — a container per conversation and agent,
256
- // from one image, on a network whose only way out is an allow-listed proxy.
218
+ // A container per conversation and agent, from one image, on a network
219
+ // whose only way out is an allow-listed proxy.
257
220
  docker: z2.object({
258
221
  // The base image a container runs. Default: the one this companion
259
222
  // builds from its own Dockerfile on first use. One named here must
@@ -526,13 +489,11 @@ function resolvePairBaseUrl(server) {
526
489
  function writePairedConfig(paired) {
527
490
  const { local, note, hadPriorConfig } = loadConfigTolerant();
528
491
  const config = {
529
- // CT1082: a device pairs connected to NOTHING — `claudeCode: { enabled: false }`
530
- // written explicitly, not left absent, because absence is exactly what marks a
531
- // pre-CT1082 config for the grandfathering migration. Omit it and a brand-new
532
- // device gets auto-connected on its first start by the behaviour this task
533
- // removes; stamp it on a RE-pair and an upgrading user's working device gets
534
- // disconnected instead. So it's written only when there was no config here
535
- // before. `local` spreads after, so a re-pair keeps the answer already given.
492
+ // A device pairs connected to NOTHING: `claudeCode: { enabled: false }`
493
+ // written explicitly, since absence is what the harness migration reads.
494
+ // Omitted, a new device is auto-connected on first start; stamped on a
495
+ // RE-pair, an upgrading user's working device is disconnected. So only when
496
+ // there was no config before; `local` spreads after, keeping a prior answer.
536
497
  ...hadPriorConfig ? {} : { claudeCode: { enabled: false } },
537
498
  ...local,
538
499
  baseUrl: paired.baseUrl,