@cabane/companion 0.6.185 → 0.6.187
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 +31 -50
- package/dist/cli.js +1166 -3746
- package/dist/launch.js +12 -61
- package/dist/pairing-config.js +52 -63
- package/dist/runtime.js +947 -5617
- package/dist/static/app.js +1 -2
- package/dist/static/styles.css +0 -7
- package/package.json +2 -2
package/dist/launch.js
CHANGED
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
existsSync as existsSync3
|
|
42
42
|
} from "fs";
|
|
43
43
|
import { homedir as homedir5, userInfo } from "os";
|
|
44
|
-
import { dirname as dirname3, join as join4 } from "path";
|
|
44
|
+
import { dirname as dirname3, isAbsolute, join as join4 } from "path";
|
|
45
45
|
import { z as z2 } from "zod";
|
|
46
46
|
|
|
47
47
|
// src/pairing.ts
|
|
@@ -131,25 +131,13 @@ var localAgentConfigSchema = z2.object({
|
|
|
131
131
|
// yourself, `claudeCode: { autoMemory: true }` hands auto-memory back to your
|
|
132
132
|
// own `~/.claude/settings.json` — Cabane then stops
|
|
133
133
|
// injecting the off switch and your normal Claude Code memory workflow applies
|
|
134
|
-
// (
|
|
135
|
-
claudeCode: z2.object({ autoMemory: z2.boolean().optional() }).strict().optional()
|
|
136
|
-
// This agent's exception to `runners.sandbox`: an ops agent that runs with
|
|
137
|
-
// the machine on a sandboxed device, or one sandboxed agent on a device that
|
|
138
|
-
// isn't. Absent is "inherit". Only on a row keyed by the agent's id.
|
|
139
|
-
sandbox: z2.enum(["os", "none"]).optional()
|
|
134
|
+
// (the user and project settings load on every Claude Code turn).
|
|
135
|
+
claudeCode: z2.object({ autoMemory: z2.boolean().optional() }).strict().optional()
|
|
140
136
|
}).strict();
|
|
141
|
-
var
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (AGENT_ID_KEY.test(key)) continue;
|
|
146
|
-
ctx.addIssue({
|
|
147
|
-
code: z2.ZodIssueCode.custom,
|
|
148
|
-
path: [key, "sandbox"],
|
|
149
|
-
message: "a sandbox exception must name one agent \u2014 key the row by the agent's id (shown in Cabane), not a username or workspace-slug/username"
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
});
|
|
137
|
+
var agentsConfigSchema = z2.record(z2.string(), localAgentConfigSchema);
|
|
138
|
+
function isRootPath(value) {
|
|
139
|
+
return isAbsolute(value) || value === "~" || value.startsWith("~/");
|
|
140
|
+
}
|
|
153
141
|
var companionConfigSchema = z2.object({
|
|
154
142
|
// https-enforced (loopback exempt), so a hand-edited config can't put the
|
|
155
143
|
// device token and prompt on a plaintext channel.
|
|
@@ -204,48 +192,11 @@ var companionConfigSchema = z2.object({
|
|
|
204
192
|
// The answer to `start`'s "start at boot?" when it was no, so it isn't asked again.
|
|
205
193
|
// `cabane service install` still installs it.
|
|
206
194
|
serviceDeclined: z2.boolean().optional(),
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
// governs, field named, instead of stopping the companion starting.
|
|
213
|
-
runners: z2.object({
|
|
214
|
-
enabled: z2.boolean().optional(),
|
|
215
|
-
provider: z2.enum(["local-process", "docker"]).optional(),
|
|
216
|
-
sandbox: z2.enum(["os", "none"]).optional(),
|
|
217
|
-
// A container per conversation and agent, from one image, on a network
|
|
218
|
-
// whose only way out is an allow-listed proxy.
|
|
219
|
-
docker: z2.object({
|
|
220
|
-
// The base image a container runs. Default: the one this companion
|
|
221
|
-
// builds from its own Dockerfile on first use. One named here must
|
|
222
|
-
// have that one's layout (the `runner` user, `cabane-companion` on
|
|
223
|
-
// PATH reading `CABANE_COMPANION_ROOT`).
|
|
224
|
-
image: z2.string().min(1).optional(),
|
|
225
|
-
// The companion install mounted into every container. Default, on
|
|
226
|
-
// Linux: this companion's own. Needed for one run from a checkout
|
|
227
|
-
// (a `pnpm --filter @cabane/companion deploy --prod <dir>`).
|
|
228
|
-
companionDir: z2.string().min(1).optional(),
|
|
229
|
-
// Hosts a turn may reach besides the Cabane API, the model providers
|
|
230
|
-
// and the git host. `*.example.com` matches any subdomain.
|
|
231
|
-
egressAllow: z2.array(z2.string().min(1)).optional(),
|
|
232
|
-
// Keep a turn's container between turns and start it again for the
|
|
233
|
-
// next one. Off by default: the container goes after every turn and
|
|
234
|
-
// the runtime's session files are restored into the next.
|
|
235
|
-
keepContainers: z2.boolean().optional(),
|
|
236
|
-
// The Cabane URL as a container reaches it, for a device whose own
|
|
237
|
-
// `baseUrl` is a loopback address the container can't see.
|
|
238
|
-
apiUrl: z2.string().url().refine(isAllowedBaseUrl).optional(),
|
|
239
|
-
// Env set on each turn's runner, `${NAME}` resolved from secrets.json
|
|
240
|
-
// at exec: the runtime's own auth, which the image never carries.
|
|
241
|
-
env: z2.record(z2.string(), z2.string()).optional(),
|
|
242
|
-
// A git credential helper run on this device to answer the
|
|
243
|
-
// container's git for an allowed host, e.g. `!gh auth git-credential`.
|
|
244
|
-
// Absent: git's own configured helpers on this device.
|
|
245
|
-
gitCredentialHelper: z2.string().min(1).optional()
|
|
246
|
-
}).strict().optional(),
|
|
247
|
-
policy: z2.unknown().optional()
|
|
248
|
-
}).strict().optional()
|
|
195
|
+
// Where agents stand on this device. Every (agent, conversation) pair gets a directory under it,
|
|
196
|
+
// and an agent with no folder of its own and no prepare hook works there
|
|
197
|
+
// (`turn-execution.ts`, `prepareEnvironment`). Default `~/.cabane/work`: hidden, so pairing a
|
|
198
|
+
// laptop creates no new visible folder; set it to make the work visible.
|
|
199
|
+
root: z2.string().min(1).refine(isRootPath, { message: "must be an absolute path, or start with ~/" }).optional()
|
|
249
200
|
});
|
|
250
201
|
var legacyOfferSchema = z2.object({
|
|
251
202
|
claudeCode: z2.object({ enabled: z2.boolean().optional() }).passthrough().optional(),
|
package/dist/pairing-config.js
CHANGED
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
existsSync as existsSync3
|
|
40
40
|
} from "fs";
|
|
41
41
|
import { homedir as homedir5, userInfo } from "os";
|
|
42
|
-
import { dirname as dirname3, join as join4 } from "path";
|
|
42
|
+
import { dirname as dirname3, isAbsolute, join as join4 } from "path";
|
|
43
43
|
import { z as z2 } from "zod";
|
|
44
44
|
|
|
45
45
|
// src/errors.ts
|
|
@@ -156,25 +156,13 @@ var localAgentConfigSchema = z2.object({
|
|
|
156
156
|
// yourself, `claudeCode: { autoMemory: true }` hands auto-memory back to your
|
|
157
157
|
// own `~/.claude/settings.json` — Cabane then stops
|
|
158
158
|
// injecting the off switch and your normal Claude Code memory workflow applies
|
|
159
|
-
// (
|
|
160
|
-
claudeCode: z2.object({ autoMemory: z2.boolean().optional() }).strict().optional()
|
|
161
|
-
// This agent's exception to `runners.sandbox`: an ops agent that runs with
|
|
162
|
-
// the machine on a sandboxed device, or one sandboxed agent on a device that
|
|
163
|
-
// isn't. Absent is "inherit". Only on a row keyed by the agent's id.
|
|
164
|
-
sandbox: z2.enum(["os", "none"]).optional()
|
|
159
|
+
// (the user and project settings load on every Claude Code turn).
|
|
160
|
+
claudeCode: z2.object({ autoMemory: z2.boolean().optional() }).strict().optional()
|
|
165
161
|
}).strict();
|
|
166
|
-
var
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if (AGENT_ID_KEY.test(key)) continue;
|
|
171
|
-
ctx.addIssue({
|
|
172
|
-
code: z2.ZodIssueCode.custom,
|
|
173
|
-
path: [key, "sandbox"],
|
|
174
|
-
message: "a sandbox exception must name one agent \u2014 key the row by the agent's id (shown in Cabane), not a username or workspace-slug/username"
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
});
|
|
162
|
+
var agentsConfigSchema = z2.record(z2.string(), localAgentConfigSchema);
|
|
163
|
+
function isRootPath(value) {
|
|
164
|
+
return isAbsolute(value) || value === "~" || value.startsWith("~/");
|
|
165
|
+
}
|
|
178
166
|
var companionConfigSchema = z2.object({
|
|
179
167
|
// https-enforced (loopback exempt), so a hand-edited config can't put the
|
|
180
168
|
// device token and prompt on a plaintext channel.
|
|
@@ -229,49 +217,40 @@ var companionConfigSchema = z2.object({
|
|
|
229
217
|
// The answer to `start`'s "start at boot?" when it was no, so it isn't asked again.
|
|
230
218
|
// `cabane service install` still installs it.
|
|
231
219
|
serviceDeclined: z2.boolean().optional(),
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
// governs, field named, instead of stopping the companion starting.
|
|
238
|
-
runners: z2.object({
|
|
239
|
-
enabled: z2.boolean().optional(),
|
|
240
|
-
provider: z2.enum(["local-process", "docker"]).optional(),
|
|
241
|
-
sandbox: z2.enum(["os", "none"]).optional(),
|
|
242
|
-
// A container per conversation and agent, from one image, on a network
|
|
243
|
-
// whose only way out is an allow-listed proxy.
|
|
244
|
-
docker: z2.object({
|
|
245
|
-
// The base image a container runs. Default: the one this companion
|
|
246
|
-
// builds from its own Dockerfile on first use. One named here must
|
|
247
|
-
// have that one's layout (the `runner` user, `cabane-companion` on
|
|
248
|
-
// PATH reading `CABANE_COMPANION_ROOT`).
|
|
249
|
-
image: z2.string().min(1).optional(),
|
|
250
|
-
// The companion install mounted into every container. Default, on
|
|
251
|
-
// Linux: this companion's own. Needed for one run from a checkout
|
|
252
|
-
// (a `pnpm --filter @cabane/companion deploy --prod <dir>`).
|
|
253
|
-
companionDir: z2.string().min(1).optional(),
|
|
254
|
-
// Hosts a turn may reach besides the Cabane API, the model providers
|
|
255
|
-
// and the git host. `*.example.com` matches any subdomain.
|
|
256
|
-
egressAllow: z2.array(z2.string().min(1)).optional(),
|
|
257
|
-
// Keep a turn's container between turns and start it again for the
|
|
258
|
-
// next one. Off by default: the container goes after every turn and
|
|
259
|
-
// the runtime's session files are restored into the next.
|
|
260
|
-
keepContainers: z2.boolean().optional(),
|
|
261
|
-
// The Cabane URL as a container reaches it, for a device whose own
|
|
262
|
-
// `baseUrl` is a loopback address the container can't see.
|
|
263
|
-
apiUrl: z2.string().url().refine(isAllowedBaseUrl).optional(),
|
|
264
|
-
// Env set on each turn's runner, `${NAME}` resolved from secrets.json
|
|
265
|
-
// at exec: the runtime's own auth, which the image never carries.
|
|
266
|
-
env: z2.record(z2.string(), z2.string()).optional(),
|
|
267
|
-
// A git credential helper run on this device to answer the
|
|
268
|
-
// container's git for an allowed host, e.g. `!gh auth git-credential`.
|
|
269
|
-
// Absent: git's own configured helpers on this device.
|
|
270
|
-
gitCredentialHelper: z2.string().min(1).optional()
|
|
271
|
-
}).strict().optional(),
|
|
272
|
-
policy: z2.unknown().optional()
|
|
273
|
-
}).strict().optional()
|
|
220
|
+
// Where agents stand on this device. Every (agent, conversation) pair gets a directory under it,
|
|
221
|
+
// and an agent with no folder of its own and no prepare hook works there
|
|
222
|
+
// (`turn-execution.ts`, `prepareEnvironment`). Default `~/.cabane/work`: hidden, so pairing a
|
|
223
|
+
// laptop creates no new visible folder; set it to make the work visible.
|
|
224
|
+
root: z2.string().min(1).refine(isRootPath, { message: "must be an absolute path, or start with ~/" }).optional()
|
|
274
225
|
});
|
|
226
|
+
function retiredConfigKeys(raw) {
|
|
227
|
+
if (!raw || typeof raw !== "object") return [];
|
|
228
|
+
const obj = raw;
|
|
229
|
+
const keys = [];
|
|
230
|
+
if ("runners" in obj) keys.push("runners");
|
|
231
|
+
const agents = obj.agents;
|
|
232
|
+
if (agents && typeof agents === "object") {
|
|
233
|
+
for (const [key, row] of Object.entries(agents)) {
|
|
234
|
+
if (row && typeof row === "object" && "sandbox" in row) keys.push(`agents.${key}.sandbox`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return keys;
|
|
238
|
+
}
|
|
239
|
+
function withoutRetiredKeys(raw) {
|
|
240
|
+
if (retiredConfigKeys(raw).length === 0) return raw;
|
|
241
|
+
const { runners: _runners, ...rest } = raw;
|
|
242
|
+
if (!rest.agents || typeof rest.agents !== "object") return rest;
|
|
243
|
+
const agents = {};
|
|
244
|
+
for (const [key, row] of Object.entries(rest.agents)) {
|
|
245
|
+
if (row && typeof row === "object" && "sandbox" in row) {
|
|
246
|
+
const { sandbox: _sandbox, ...kept } = row;
|
|
247
|
+
agents[key] = kept;
|
|
248
|
+
} else {
|
|
249
|
+
agents[key] = row;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return { ...rest, agents };
|
|
253
|
+
}
|
|
275
254
|
var legacyOfferSchema = z2.object({
|
|
276
255
|
claudeCode: z2.object({ enabled: z2.boolean().optional() }).passthrough().optional(),
|
|
277
256
|
codex: z2.object({ enabled: z2.boolean().optional() }).passthrough().optional(),
|
|
@@ -307,7 +286,7 @@ function loadConfig() {
|
|
|
307
286
|
if (raw.trim().length === 0) return null;
|
|
308
287
|
let parsed;
|
|
309
288
|
try {
|
|
310
|
-
parsed = JSON.parse(raw);
|
|
289
|
+
parsed = withoutRetiredKeys(JSON.parse(raw));
|
|
311
290
|
} catch (err) {
|
|
312
291
|
throw new ConfigError(
|
|
313
292
|
`${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -321,6 +300,12 @@ function loadConfig() {
|
|
|
321
300
|
`${path}: invalid "agents" config at \`${agentIssue.path.join(".")}\` \u2014 ${agentIssue.message}. Fix the agents block in the config and retry.`
|
|
322
301
|
);
|
|
323
302
|
}
|
|
303
|
+
const rootIssue = result.error.issues.find((i) => i.path[0] === "root");
|
|
304
|
+
if (rootIssue) {
|
|
305
|
+
throw new ConfigError(
|
|
306
|
+
`${path}: invalid "root" \u2014 ${rootIssue.message}. Fix it in the config and retry.`
|
|
307
|
+
);
|
|
308
|
+
}
|
|
324
309
|
throw new ConfigError(
|
|
325
310
|
`${path} is from an incompatible or older version of the companion, or was hand-edited. Run \`${cabane("setup")}\` to pair again, or \`${cabane("logout --purge")}\` to reset.`
|
|
326
311
|
);
|
|
@@ -350,7 +335,7 @@ function loadConfigTolerant() {
|
|
|
350
335
|
if (raw.trim().length === 0) return fresh;
|
|
351
336
|
let parsed;
|
|
352
337
|
try {
|
|
353
|
-
parsed = JSON.parse(raw);
|
|
338
|
+
parsed = withoutRetiredKeys(JSON.parse(raw));
|
|
354
339
|
} catch {
|
|
355
340
|
return {
|
|
356
341
|
local: empty,
|
|
@@ -363,6 +348,7 @@ function loadConfigTolerant() {
|
|
|
363
348
|
const {
|
|
364
349
|
agents: agents2,
|
|
365
350
|
prepareHook: prepareHook2,
|
|
351
|
+
root: root2,
|
|
366
352
|
dashboardPort,
|
|
367
353
|
autoOpen,
|
|
368
354
|
logLevel,
|
|
@@ -374,6 +360,7 @@ function loadConfigTolerant() {
|
|
|
374
360
|
local: {
|
|
375
361
|
...agents2 !== void 0 ? { agents: agents2 } : {},
|
|
376
362
|
...prepareHook2 !== void 0 ? { prepareHook: prepareHook2 } : {},
|
|
363
|
+
...root2 !== void 0 ? { root: root2 } : {},
|
|
377
364
|
...dashboardPort !== void 0 ? { dashboardPort } : {},
|
|
378
365
|
...autoOpen !== void 0 ? { autoOpen } : {},
|
|
379
366
|
...logLevel !== void 0 ? { logLevel } : {},
|
|
@@ -391,6 +378,8 @@ function loadConfigTolerant() {
|
|
|
391
378
|
if (agents.success) local.agents = agents.data;
|
|
392
379
|
const prepareHook = prepareHookSchema.safeParse(obj.prepareHook);
|
|
393
380
|
if (prepareHook.success) local.prepareHook = prepareHook.data;
|
|
381
|
+
const root = companionConfigSchema.shape.root.safeParse(obj.root);
|
|
382
|
+
if (root.success && root.data !== void 0) local.root = root.data;
|
|
394
383
|
if (typeof obj.dashboardPort === "number") local.dashboardPort = obj.dashboardPort;
|
|
395
384
|
if (typeof obj.autoOpen === "boolean") local.autoOpen = obj.autoOpen;
|
|
396
385
|
if (obj.logFormat === "human" || obj.logFormat === "json") local.logFormat = obj.logFormat;
|