@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.
- package/dist/cli.js +650 -1022
- package/dist/pairing-config.js +56 -95
- package/dist/runtime.js +644 -1014
- package/dist/static/app.js +9 -11
- package/package.json +1 -1
package/dist/pairing-config.js
CHANGED
|
@@ -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
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
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
|
-
//
|
|
73
|
-
//
|
|
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
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
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
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
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
|
-
//
|
|
153
|
-
//
|
|
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
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
//
|
|
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
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
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
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
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
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
//
|
|
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
|
|
217
|
-
//
|
|
218
|
-
// asked
|
|
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
|
|
224
|
-
//
|
|
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
|
-
//
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
// `
|
|
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
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
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
|
-
//
|
|
256
|
-
//
|
|
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
|
-
//
|
|
530
|
-
// written explicitly,
|
|
531
|
-
//
|
|
532
|
-
//
|
|
533
|
-
//
|
|
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,
|