@cabane/companion 0.6.132 → 0.6.133
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 +23 -3
- package/dist/cli.js +105 -19
- package/dist/pairing-config.js +20 -3
- package/dist/runtime.js +80 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -232,8 +232,9 @@ A `coding` agent needs a directory to run in — a path that only exists on your
|
|
|
232
232
|
{
|
|
233
233
|
// …device identity, managed by `pair`; don't hand-edit those fields…
|
|
234
234
|
|
|
235
|
-
// Per-agent machine-local overrides. Only `cwd
|
|
236
|
-
// everything else about the agent (mode, MCP servers,
|
|
235
|
+
// Per-agent machine-local overrides. Only `cwd`, `prepareHook`, `claudeCode` and
|
|
236
|
+
// `sandbox` live here — everything else about the agent (mode, MCP servers,
|
|
237
|
+
// model) is set in Cabane.
|
|
237
238
|
"agents": {
|
|
238
239
|
"my-coding-agent": {
|
|
239
240
|
"cwd": "/Users/you/code/project", // working directory the agent runs in
|
|
@@ -317,6 +318,25 @@ What the sandbox allows is `runners.policy` in `~/.cabane/config.json`. Every ke
|
|
|
317
318
|
| **Reads**, `blocklist` mode (the default) | Everything is readable except every companion's store (`~/.cabane`, and any `.cabane` under your home or beside this companion's home; see below), the `.claude` and `.codex` of the other companion homes those stores sit in, `~/.ssh`, `~/.aws`, `~/.gnupg`, `~/.config/gh`, `~/.git-credentials`, `~/.netrc`, `~/.npmrc`, `~/.pypirc`, `~/.docker/config.json`, `~/.kube`, `~/.config/gcloud`, `~/.azure`, `~/.config/op`, `~/.password-store`, any `.env` or `.env.*` in the working directory, and your `denyRead` paths. `allowRead` reopens a path inside a denied one. |
|
|
318
319
|
| **Writes** | The working directory, the temp dir, `~/.claude`, `~/.claude.json`, `~/.codex`, plus `allowWrite`. |
|
|
319
320
|
|
|
321
|
+
**One agent can be excepted.** The sandbox is set per device, and one agent's row in `agents` can say otherwise — for example an ops agent whose job is the machine itself, on a device whose other agents stay sandboxed:
|
|
322
|
+
|
|
323
|
+
```jsonc
|
|
324
|
+
{
|
|
325
|
+
"runners": {
|
|
326
|
+
"enabled": true,
|
|
327
|
+
"sandbox": "os",
|
|
328
|
+
"policy": {
|
|
329
|
+
/* unchanged */
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
"agents": {
|
|
333
|
+
"014d18a7-6395-48e3-a2f2-313d5baa2a5e": { "sandbox": "none" }, // the ops agent, by its id: it runs with the machine; everyone else stays sandboxed
|
|
334
|
+
},
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
It works the other way too: `"agents": { "<agent id>": { "sandbox": "os" } }` sandboxes one agent on a device that otherwise isn't. **A sandbox row is keyed by the agent's id**, the last part of the agent's settings page URL in Cabane (`/<workspace>/settings/agents/<agent id>`). A username, or `workspace-slug/username`, can match more than one agent: slugs are unique only per workspace owner, so two workspaces this device serves can share one. A `sandbox` row keyed any other way is refused at start, with the field named. `cwd` and `prepareHook` rows still take all three keys. The agent's row wins, then `runners.sandbox`, then no sandbox. The policy stays the device's. Under the `docker` provider the container is the sandbox, so a row's `sandbox` is ignored, and the log says so at start. The exception is set only here: Cabane shows it on the device page and can't change it.
|
|
339
|
+
|
|
320
340
|
Paths may start with `~/` and may be globs. An invalid `policy` doesn't stop the Companion from starting. Instead, every sandboxed turn is refused and the conversation names the field that's wrong, for example `runners.policy.filesystem.mode`.
|
|
321
341
|
|
|
322
342
|
**Other companions on the same machine.** A second companion (another account, a test device) keeps its own secrets in its own `.cabane`, so a turn can't read any companion's store, not only this device's. Every turn denies this companion's store and `~/.cabane` (both, when the Companion runs from a home set with `CABANE_COMPANION_HOME`), plus any `.cabane` up to two levels under your home or beside this companion's home, where companion homes conventionally sit. Stores anywhere deeper are found by a search of your home with `find`, along with the `.claude` and `.codex` beside each one, which hold that companion's harness logins. That search takes seconds over a large home, so the Companion runs it in the background: when it starts, and again whenever a turn starts more than 5 minutes after the last search finished. Only the first turn after a start waits for it. **A store created deeper than two levels is denied within about 5 minutes plus one search, not instantly.** If a search can't finish (`find` is missing, runs past 60 seconds, or hits a directory it can't list), the stores it did find are still denied, and the log says so. The search runs in strict mode too, because strict mode reopens the working directory, and a store inside it must stay hidden.
|
|
@@ -336,7 +356,7 @@ Paths may start with `~/` and may be globs. An invalid `policy` doesn't stop the
|
|
|
336
356
|
|
|
337
357
|
A `gh auth login` session on the machine isn't used: `~/.config/gh` is unreadable in the sandbox. Only the token in `secrets.json` is.
|
|
338
358
|
|
|
339
|
-
`cabane-companion status` prints the effective posture: the filesystem mode, extra hosts, denied hosts and reads,
|
|
359
|
+
`cabane-companion status` prints the effective posture: the filesystem mode, extra hosts, denied hosts and reads, the **names** of masked credentials, never their values, and each agent excepted on its row. The Companion also logs it once at start.
|
|
340
360
|
|
|
341
361
|
## Harnesses
|
|
342
362
|
|
package/dist/cli.js
CHANGED
|
@@ -295,6 +295,28 @@ function localAgentConfig(cfg, agent) {
|
|
|
295
295
|
if (!cfg.prepareHook || row.prepareHook) return row;
|
|
296
296
|
return { ...row, prepareHook: cfg.prepareHook };
|
|
297
297
|
}
|
|
298
|
+
function agentSandbox(cfg, agent) {
|
|
299
|
+
const sandbox = cfg.agents?.[agent.agentId]?.sandbox;
|
|
300
|
+
return sandbox ? { sandbox } : {};
|
|
301
|
+
}
|
|
302
|
+
function runnerSandboxFor(cfg, row, host) {
|
|
303
|
+
const asked = row.sandbox ?? cfg.runners?.sandbox ?? "none";
|
|
304
|
+
if (asked !== "os" || cfg.runners?.provider === "docker") return "none";
|
|
305
|
+
return host.osSandboxUsable() ? "os" : "none";
|
|
306
|
+
}
|
|
307
|
+
function runnerSandboxReport(cfg, agents, host) {
|
|
308
|
+
const device = runnerSandboxFor(cfg, {}, host);
|
|
309
|
+
const sandboxOverrides = agents.flatMap((agent) => {
|
|
310
|
+
const sandbox = runnerSandboxFor(cfg, agentSandbox(cfg, agent), host);
|
|
311
|
+
return sandbox === device ? [] : [{ agentId: agent.agentId, sandbox }];
|
|
312
|
+
});
|
|
313
|
+
return { sandbox: device, sandboxOverrides };
|
|
314
|
+
}
|
|
315
|
+
function agentSandboxRows(cfg) {
|
|
316
|
+
return Object.entries(cfg.agents ?? {}).flatMap(
|
|
317
|
+
([key, row]) => row.sandbox ? [[key, row.sandbox]] : []
|
|
318
|
+
);
|
|
319
|
+
}
|
|
298
320
|
function loadConfig() {
|
|
299
321
|
const path = configPath();
|
|
300
322
|
if (!existsSync(path)) return null;
|
|
@@ -370,7 +392,7 @@ function loadConfigTolerant() {
|
|
|
370
392
|
}
|
|
371
393
|
const obj = parsed && typeof parsed === "object" ? parsed : {};
|
|
372
394
|
const local = {};
|
|
373
|
-
const agents =
|
|
395
|
+
const agents = agentsConfigSchema.safeParse(obj.agents);
|
|
374
396
|
if (agents.success) local.agents = agents.data;
|
|
375
397
|
const prepareHook = prepareHookSchema.safeParse(obj.prepareHook);
|
|
376
398
|
if (prepareHook.success) local.prepareHook = prepareHook.data;
|
|
@@ -426,7 +448,7 @@ function deleteConfig() {
|
|
|
426
448
|
writeFileSync(path, "", { mode: 384 });
|
|
427
449
|
}
|
|
428
450
|
}
|
|
429
|
-
var realHomeCache, localAgentConfigSchema, companionConfigSchema;
|
|
451
|
+
var realHomeCache, localAgentConfigSchema, AGENT_ID_KEY, agentsConfigSchema, companionConfigSchema;
|
|
430
452
|
var init_config = __esm({
|
|
431
453
|
"src/config.ts"() {
|
|
432
454
|
"use strict";
|
|
@@ -443,8 +465,25 @@ var init_config = __esm({
|
|
|
443
465
|
// auto-memory back to your own `~/.claude/settings.json` — Cabane then stops
|
|
444
466
|
// injecting the off switch and your normal Claude Code memory workflow applies
|
|
445
467
|
// (in coding mode, where the checkout's project settings are read).
|
|
446
|
-
claudeCode: z2.object({ autoMemory: z2.boolean().optional() }).strict().optional()
|
|
468
|
+
claudeCode: z2.object({ autoMemory: z2.boolean().optional() }).strict().optional(),
|
|
469
|
+
// CT1693: this agent's exception to `runners.sandbox` — an ops agent that
|
|
470
|
+
// runs with the machine on a sandboxed device, or one sandboxed agent on a
|
|
471
|
+
// device that isn't. Absent is "inherit"; see `runnerSandboxFor`. Only on a
|
|
472
|
+
// row keyed by the agent's id (`agentsConfigSchema`).
|
|
473
|
+
sandbox: z2.enum(["os", "none"]).optional()
|
|
447
474
|
}).strict();
|
|
475
|
+
AGENT_ID_KEY = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
476
|
+
agentsConfigSchema = z2.record(z2.string(), localAgentConfigSchema).superRefine((agents, ctx) => {
|
|
477
|
+
for (const [key, row] of Object.entries(agents)) {
|
|
478
|
+
if (row.sandbox === void 0) continue;
|
|
479
|
+
if (AGENT_ID_KEY.test(key)) continue;
|
|
480
|
+
ctx.addIssue({
|
|
481
|
+
code: z2.ZodIssueCode.custom,
|
|
482
|
+
path: [key, "sandbox"],
|
|
483
|
+
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"
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
});
|
|
448
487
|
companionConfigSchema = z2.object({
|
|
449
488
|
// The cabane instance this device is paired with. SJ515: https-enforced
|
|
450
489
|
// (loopback exempt) so a hand-edited config can't smuggle a plaintext-http
|
|
@@ -465,7 +504,7 @@ var init_config = __esm({
|
|
|
465
504
|
// Optional per-agent machine-local overrides (cwd / prepareHook), keyed by
|
|
466
505
|
// agentId / username / `slug/username`. Hand-added by the operator; the companion
|
|
467
506
|
// never writes this (it only persists credentials + the device, elsewhere).
|
|
468
|
-
agents:
|
|
507
|
+
agents: agentsConfigSchema.optional(),
|
|
469
508
|
// CT1095: the DEVICE-LEVEL prepare hook — one entry that applies to every agent
|
|
470
509
|
// assigned to this device, unless that agent's own `agents` row names a hook of
|
|
471
510
|
// its own.
|
|
@@ -11652,7 +11691,7 @@ function buildCompanionManifest(opts) {
|
|
|
11652
11691
|
runtimes,
|
|
11653
11692
|
capabilities: { ...DEVICE_MANIFEST.capabilities },
|
|
11654
11693
|
runtimeIssues: opts.runtimeIssues ?? [],
|
|
11655
|
-
...opts.
|
|
11694
|
+
...opts.runner ? { runner: opts.runner } : {}
|
|
11656
11695
|
};
|
|
11657
11696
|
}
|
|
11658
11697
|
function runtimeIssuesFor(gates) {
|
|
@@ -15279,10 +15318,21 @@ var CompanionSupervisor = class {
|
|
|
15279
15318
|
}
|
|
15280
15319
|
this.latchBootBundledState();
|
|
15281
15320
|
}
|
|
15282
|
-
|
|
15321
|
+
const sandboxRows = agentSandboxRows(this.config);
|
|
15322
|
+
if (sandboxRows.length > 0 && this.dockerRunners()) {
|
|
15323
|
+
this.log.warn(
|
|
15324
|
+
{ agents: sandboxRows.map(([key]) => key) },
|
|
15325
|
+
`runners: ${sandboxRows.map(([key]) => `agents.${key}.sandbox`).join(", ")} ignored \u2014 under Docker the container is the sandbox`
|
|
15326
|
+
);
|
|
15327
|
+
}
|
|
15328
|
+
const anySandboxed = [{}, ...sandboxRows.map(([, sandbox]) => ({ sandbox }))].some(
|
|
15329
|
+
(row) => this.runnerSandboxFor(row) === "os"
|
|
15330
|
+
);
|
|
15331
|
+
if (anySandboxed) {
|
|
15283
15332
|
const policy = this.sandboxPolicy();
|
|
15284
15333
|
const posture = postureLines(policy);
|
|
15285
|
-
const
|
|
15334
|
+
const perAgent = sandboxRows.map(([key, sandbox]) => `${key} \u2192 ${sandbox}`).join(", ");
|
|
15335
|
+
const summary = posture.join("; ") + (perAgent ? `; per-agent: ${perAgent}` : "");
|
|
15286
15336
|
if (policy.ok) this.log.info({ posture }, `runners: OS sandbox policy \u2014 ${summary}`);
|
|
15287
15337
|
else this.log.error({ posture }, `runners: ${summary}`);
|
|
15288
15338
|
void this.companionStores().refresh();
|
|
@@ -15396,7 +15446,7 @@ var CompanionSupervisor = class {
|
|
|
15396
15446
|
// (fail-soft to null). Informational only — the server matches on name.
|
|
15397
15447
|
versions: this.harnessVersions,
|
|
15398
15448
|
// CT1681: shown on the device page.
|
|
15399
|
-
...runnersEnabled(this.config) ? {
|
|
15449
|
+
...runnersEnabled(this.config) ? { runner: this.runnerManifest() } : {}
|
|
15400
15450
|
}),
|
|
15401
15451
|
// CT566: echo the last classified credential state per runtime, when the
|
|
15402
15452
|
// companion has seen any. Omitted (undefined) until the first observed
|
|
@@ -15728,6 +15778,9 @@ var CompanionSupervisor = class {
|
|
|
15728
15778
|
// CT1679: `runners.sandbox`, checked against this host once. A host that
|
|
15729
15779
|
// can't make the sandbox says so once and runs its turns unwrapped.
|
|
15730
15780
|
sandboxUsable;
|
|
15781
|
+
sandboxHost = {
|
|
15782
|
+
osSandboxUsable: () => this.sandboxUsable ??= osSandboxUsable(this.log)
|
|
15783
|
+
};
|
|
15731
15784
|
// CT1684: `runners.policy`, parsed once. An invalid one is kept as the
|
|
15732
15785
|
// error, which every sandboxed turn is refused with.
|
|
15733
15786
|
parsedSandboxPolicy;
|
|
@@ -15741,10 +15794,26 @@ var CompanionSupervisor = class {
|
|
|
15741
15794
|
this.storeWatch ??= companionStoreWatch(this.log);
|
|
15742
15795
|
return this.storeWatch;
|
|
15743
15796
|
}
|
|
15744
|
-
|
|
15745
|
-
|
|
15746
|
-
|
|
15747
|
-
|
|
15797
|
+
// CT1693: the sandbox one agent's turns run under — its `agents` row, then
|
|
15798
|
+
// the device's. Resolved from the current config every time it's asked, so
|
|
15799
|
+
// a reload changes it; only the host probe is kept.
|
|
15800
|
+
runnerSandboxFor(row) {
|
|
15801
|
+
return runnerSandboxFor(this.config, row, this.sandboxHost);
|
|
15802
|
+
}
|
|
15803
|
+
// CT1693: what the heartbeat says about this device's runners, resolved
|
|
15804
|
+
// against the agents it runs right now.
|
|
15805
|
+
runnerManifest() {
|
|
15806
|
+
const agents = [...this.workspaces.values()].flatMap(
|
|
15807
|
+
(wr) => [...wr.agents.values()].map((agent) => ({
|
|
15808
|
+
agentId: agent.agentId,
|
|
15809
|
+
agentUsername: agent.username,
|
|
15810
|
+
workspaceSlug: wr.slug
|
|
15811
|
+
}))
|
|
15812
|
+
);
|
|
15813
|
+
return {
|
|
15814
|
+
provider: this.config.runners?.provider ?? "local-process",
|
|
15815
|
+
...runnerSandboxReport(this.config, agents, this.sandboxHost)
|
|
15816
|
+
};
|
|
15748
15817
|
}
|
|
15749
15818
|
// CT1679: one provider for the device, built on first use.
|
|
15750
15819
|
runnerProvider() {
|
|
@@ -15860,7 +15929,7 @@ var CompanionSupervisor = class {
|
|
|
15860
15929
|
reportRunnerGone: (report) => this.reportRunnerGone(report),
|
|
15861
15930
|
connectorHealth: this.connectorHealth,
|
|
15862
15931
|
locks: this.runnerLocks,
|
|
15863
|
-
sandbox: this.
|
|
15932
|
+
sandbox: this.runnerSandboxFor(agentSandbox(this.config, ctx)),
|
|
15864
15933
|
policy: this.sandboxPolicy(),
|
|
15865
15934
|
companionStores: () => this.companionStores().stores()
|
|
15866
15935
|
}
|
|
@@ -17205,19 +17274,36 @@ async function status() {
|
|
|
17205
17274
|
process.stdout.write(`prepare hook: ${cfg.prepareHook.command} (device-level, all agents)
|
|
17206
17275
|
`);
|
|
17207
17276
|
}
|
|
17208
|
-
|
|
17209
|
-
process.stdout.write("sandbox: os\n");
|
|
17210
|
-
for (const line of postureLines(parseSandboxPolicy(cfg.runners.policy))) {
|
|
17211
|
-
process.stdout.write(` ${line}
|
|
17277
|
+
for (const line of sandboxStatusLines(cfg)) process.stdout.write(`${line}
|
|
17212
17278
|
`);
|
|
17213
|
-
}
|
|
17214
|
-
}
|
|
17215
17279
|
const overrides2 = Object.keys(cfg.agents ?? {});
|
|
17216
17280
|
if (overrides2.length > 0) {
|
|
17217
17281
|
process.stdout.write(`local overrides for: ${overrides2.join(", ")}
|
|
17218
17282
|
`);
|
|
17219
17283
|
}
|
|
17220
17284
|
}
|
|
17285
|
+
function sandboxStatusLines(cfg) {
|
|
17286
|
+
const rows = agentSandboxRows(cfg);
|
|
17287
|
+
const device = cfg.runners?.sandbox ?? "none";
|
|
17288
|
+
if (!runnersEnabled(cfg)) {
|
|
17289
|
+
if (rows.length === 0) return [];
|
|
17290
|
+
const fields = rows.map(([key]) => `agents.${key}.sandbox`).join(", ");
|
|
17291
|
+
return [`sandbox: off (runners.enabled is false; ${fields} has no effect)`];
|
|
17292
|
+
}
|
|
17293
|
+
if (cfg.runners?.provider === "docker") {
|
|
17294
|
+
if (rows.length === 0) return [];
|
|
17295
|
+
const fields = rows.map(([key]) => `agents.${key}.sandbox`).join(", ");
|
|
17296
|
+
return [`sandbox: docker (the container is the sandbox; ${fields} is ignored)`];
|
|
17297
|
+
}
|
|
17298
|
+
if (device !== "os" && !rows.some(([, sandbox]) => sandbox === "os")) return [];
|
|
17299
|
+
return [
|
|
17300
|
+
`sandbox: ${device}`,
|
|
17301
|
+
...postureLines(parseSandboxPolicy(cfg.runners?.policy)).map((line) => ` ${line}`),
|
|
17302
|
+
...rows.map(
|
|
17303
|
+
([key, sandbox]) => ` ${key}: ${sandbox} (this agent runs ${sandbox === "os" ? "sandboxed" : "unsandboxed"})`
|
|
17304
|
+
)
|
|
17305
|
+
];
|
|
17306
|
+
}
|
|
17221
17307
|
function transcriptsLine() {
|
|
17222
17308
|
const dir2 = transcriptsDir();
|
|
17223
17309
|
let count = 0;
|
package/dist/pairing-config.js
CHANGED
|
@@ -129,8 +129,25 @@ var localAgentConfigSchema = z2.object({
|
|
|
129
129
|
// auto-memory back to your own `~/.claude/settings.json` — Cabane then stops
|
|
130
130
|
// injecting the off switch and your normal Claude Code memory workflow applies
|
|
131
131
|
// (in coding mode, where the checkout's project settings are read).
|
|
132
|
-
claudeCode: z2.object({ autoMemory: z2.boolean().optional() }).strict().optional()
|
|
132
|
+
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`).
|
|
137
|
+
sandbox: z2.enum(["os", "none"]).optional()
|
|
133
138
|
}).strict();
|
|
139
|
+
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;
|
|
140
|
+
var agentsConfigSchema = z2.record(z2.string(), localAgentConfigSchema).superRefine((agents, ctx) => {
|
|
141
|
+
for (const [key, row] of Object.entries(agents)) {
|
|
142
|
+
if (row.sandbox === void 0) continue;
|
|
143
|
+
if (AGENT_ID_KEY.test(key)) continue;
|
|
144
|
+
ctx.addIssue({
|
|
145
|
+
code: z2.ZodIssueCode.custom,
|
|
146
|
+
path: [key, "sandbox"],
|
|
147
|
+
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"
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
});
|
|
134
151
|
var companionConfigSchema = z2.object({
|
|
135
152
|
// The cabane instance this device is paired with. SJ515: https-enforced
|
|
136
153
|
// (loopback exempt) so a hand-edited config can't smuggle a plaintext-http
|
|
@@ -151,7 +168,7 @@ var companionConfigSchema = z2.object({
|
|
|
151
168
|
// Optional per-agent machine-local overrides (cwd / prepareHook), keyed by
|
|
152
169
|
// agentId / username / `slug/username`. Hand-added by the operator; the companion
|
|
153
170
|
// never writes this (it only persists credentials + the device, elsewhere).
|
|
154
|
-
agents:
|
|
171
|
+
agents: agentsConfigSchema.optional(),
|
|
155
172
|
// CT1095: the DEVICE-LEVEL prepare hook — one entry that applies to every agent
|
|
156
173
|
// assigned to this device, unless that agent's own `agents` row names a hook of
|
|
157
174
|
// its own.
|
|
@@ -343,7 +360,7 @@ function loadConfigTolerant() {
|
|
|
343
360
|
}
|
|
344
361
|
const obj = parsed && typeof parsed === "object" ? parsed : {};
|
|
345
362
|
const local = {};
|
|
346
|
-
const agents =
|
|
363
|
+
const agents = agentsConfigSchema.safeParse(obj.agents);
|
|
347
364
|
if (agents.success) local.agents = agents.data;
|
|
348
365
|
const prepareHook = prepareHookSchema.safeParse(obj.prepareHook);
|
|
349
366
|
if (prepareHook.success) local.prepareHook = prepareHook.data;
|
package/dist/runtime.js
CHANGED
|
@@ -260,8 +260,25 @@ var localAgentConfigSchema = z2.object({
|
|
|
260
260
|
// auto-memory back to your own `~/.claude/settings.json` — Cabane then stops
|
|
261
261
|
// injecting the off switch and your normal Claude Code memory workflow applies
|
|
262
262
|
// (in coding mode, where the checkout's project settings are read).
|
|
263
|
-
claudeCode: z2.object({ autoMemory: z2.boolean().optional() }).strict().optional()
|
|
263
|
+
claudeCode: z2.object({ autoMemory: z2.boolean().optional() }).strict().optional(),
|
|
264
|
+
// CT1693: this agent's exception to `runners.sandbox` — an ops agent that
|
|
265
|
+
// runs with the machine on a sandboxed device, or one sandboxed agent on a
|
|
266
|
+
// device that isn't. Absent is "inherit"; see `runnerSandboxFor`. Only on a
|
|
267
|
+
// row keyed by the agent's id (`agentsConfigSchema`).
|
|
268
|
+
sandbox: z2.enum(["os", "none"]).optional()
|
|
264
269
|
}).strict();
|
|
270
|
+
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;
|
|
271
|
+
var agentsConfigSchema = z2.record(z2.string(), localAgentConfigSchema).superRefine((agents, ctx) => {
|
|
272
|
+
for (const [key, row] of Object.entries(agents)) {
|
|
273
|
+
if (row.sandbox === void 0) continue;
|
|
274
|
+
if (AGENT_ID_KEY.test(key)) continue;
|
|
275
|
+
ctx.addIssue({
|
|
276
|
+
code: z2.ZodIssueCode.custom,
|
|
277
|
+
path: [key, "sandbox"],
|
|
278
|
+
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"
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
});
|
|
265
282
|
var companionConfigSchema = z2.object({
|
|
266
283
|
// The cabane instance this device is paired with. SJ515: https-enforced
|
|
267
284
|
// (loopback exempt) so a hand-edited config can't smuggle a plaintext-http
|
|
@@ -282,7 +299,7 @@ var companionConfigSchema = z2.object({
|
|
|
282
299
|
// Optional per-agent machine-local overrides (cwd / prepareHook), keyed by
|
|
283
300
|
// agentId / username / `slug/username`. Hand-added by the operator; the companion
|
|
284
301
|
// never writes this (it only persists credentials + the device, elsewhere).
|
|
285
|
-
agents:
|
|
302
|
+
agents: agentsConfigSchema.optional(),
|
|
286
303
|
// CT1095: the DEVICE-LEVEL prepare hook — one entry that applies to every agent
|
|
287
304
|
// assigned to this device, unless that agent's own `agents` row names a hook of
|
|
288
305
|
// its own.
|
|
@@ -421,6 +438,28 @@ function localAgentConfig(cfg, agent) {
|
|
|
421
438
|
if (!cfg.prepareHook || row.prepareHook) return row;
|
|
422
439
|
return { ...row, prepareHook: cfg.prepareHook };
|
|
423
440
|
}
|
|
441
|
+
function agentSandbox(cfg, agent) {
|
|
442
|
+
const sandbox = cfg.agents?.[agent.agentId]?.sandbox;
|
|
443
|
+
return sandbox ? { sandbox } : {};
|
|
444
|
+
}
|
|
445
|
+
function runnerSandboxFor(cfg, row, host) {
|
|
446
|
+
const asked = row.sandbox ?? cfg.runners?.sandbox ?? "none";
|
|
447
|
+
if (asked !== "os" || cfg.runners?.provider === "docker") return "none";
|
|
448
|
+
return host.osSandboxUsable() ? "os" : "none";
|
|
449
|
+
}
|
|
450
|
+
function runnerSandboxReport(cfg, agents, host) {
|
|
451
|
+
const device = runnerSandboxFor(cfg, {}, host);
|
|
452
|
+
const sandboxOverrides = agents.flatMap((agent) => {
|
|
453
|
+
const sandbox = runnerSandboxFor(cfg, agentSandbox(cfg, agent), host);
|
|
454
|
+
return sandbox === device ? [] : [{ agentId: agent.agentId, sandbox }];
|
|
455
|
+
});
|
|
456
|
+
return { sandbox: device, sandboxOverrides };
|
|
457
|
+
}
|
|
458
|
+
function agentSandboxRows(cfg) {
|
|
459
|
+
return Object.entries(cfg.agents ?? {}).flatMap(
|
|
460
|
+
([key, row]) => row.sandbox ? [[key, row.sandbox]] : []
|
|
461
|
+
);
|
|
462
|
+
}
|
|
424
463
|
function loadConfig() {
|
|
425
464
|
const path = configPath();
|
|
426
465
|
if (!existsSync(path)) return null;
|
|
@@ -814,7 +853,7 @@ function buildCompanionManifest(opts) {
|
|
|
814
853
|
runtimes,
|
|
815
854
|
capabilities: { ...DEVICE_MANIFEST.capabilities },
|
|
816
855
|
runtimeIssues: opts.runtimeIssues ?? [],
|
|
817
|
-
...opts.
|
|
856
|
+
...opts.runner ? { runner: opts.runner } : {}
|
|
818
857
|
};
|
|
819
858
|
}
|
|
820
859
|
function runtimeIssuesFor(gates) {
|
|
@@ -13173,10 +13212,21 @@ var CompanionSupervisor = class {
|
|
|
13173
13212
|
}
|
|
13174
13213
|
this.latchBootBundledState();
|
|
13175
13214
|
}
|
|
13176
|
-
|
|
13215
|
+
const sandboxRows = agentSandboxRows(this.config);
|
|
13216
|
+
if (sandboxRows.length > 0 && this.dockerRunners()) {
|
|
13217
|
+
this.log.warn(
|
|
13218
|
+
{ agents: sandboxRows.map(([key]) => key) },
|
|
13219
|
+
`runners: ${sandboxRows.map(([key]) => `agents.${key}.sandbox`).join(", ")} ignored \u2014 under Docker the container is the sandbox`
|
|
13220
|
+
);
|
|
13221
|
+
}
|
|
13222
|
+
const anySandboxed = [{}, ...sandboxRows.map(([, sandbox]) => ({ sandbox }))].some(
|
|
13223
|
+
(row) => this.runnerSandboxFor(row) === "os"
|
|
13224
|
+
);
|
|
13225
|
+
if (anySandboxed) {
|
|
13177
13226
|
const policy = this.sandboxPolicy();
|
|
13178
13227
|
const posture = postureLines(policy);
|
|
13179
|
-
const
|
|
13228
|
+
const perAgent = sandboxRows.map(([key, sandbox]) => `${key} \u2192 ${sandbox}`).join(", ");
|
|
13229
|
+
const summary = posture.join("; ") + (perAgent ? `; per-agent: ${perAgent}` : "");
|
|
13180
13230
|
if (policy.ok) this.log.info({ posture }, `runners: OS sandbox policy \u2014 ${summary}`);
|
|
13181
13231
|
else this.log.error({ posture }, `runners: ${summary}`);
|
|
13182
13232
|
void this.companionStores().refresh();
|
|
@@ -13290,7 +13340,7 @@ var CompanionSupervisor = class {
|
|
|
13290
13340
|
// (fail-soft to null). Informational only — the server matches on name.
|
|
13291
13341
|
versions: this.harnessVersions,
|
|
13292
13342
|
// CT1681: shown on the device page.
|
|
13293
|
-
...runnersEnabled(this.config) ? {
|
|
13343
|
+
...runnersEnabled(this.config) ? { runner: this.runnerManifest() } : {}
|
|
13294
13344
|
}),
|
|
13295
13345
|
// CT566: echo the last classified credential state per runtime, when the
|
|
13296
13346
|
// companion has seen any. Omitted (undefined) until the first observed
|
|
@@ -13622,6 +13672,9 @@ var CompanionSupervisor = class {
|
|
|
13622
13672
|
// CT1679: `runners.sandbox`, checked against this host once. A host that
|
|
13623
13673
|
// can't make the sandbox says so once and runs its turns unwrapped.
|
|
13624
13674
|
sandboxUsable;
|
|
13675
|
+
sandboxHost = {
|
|
13676
|
+
osSandboxUsable: () => this.sandboxUsable ??= osSandboxUsable(this.log)
|
|
13677
|
+
};
|
|
13625
13678
|
// CT1684: `runners.policy`, parsed once. An invalid one is kept as the
|
|
13626
13679
|
// error, which every sandboxed turn is refused with.
|
|
13627
13680
|
parsedSandboxPolicy;
|
|
@@ -13635,10 +13688,26 @@ var CompanionSupervisor = class {
|
|
|
13635
13688
|
this.storeWatch ??= companionStoreWatch(this.log);
|
|
13636
13689
|
return this.storeWatch;
|
|
13637
13690
|
}
|
|
13638
|
-
|
|
13639
|
-
|
|
13640
|
-
|
|
13641
|
-
|
|
13691
|
+
// CT1693: the sandbox one agent's turns run under — its `agents` row, then
|
|
13692
|
+
// the device's. Resolved from the current config every time it's asked, so
|
|
13693
|
+
// a reload changes it; only the host probe is kept.
|
|
13694
|
+
runnerSandboxFor(row) {
|
|
13695
|
+
return runnerSandboxFor(this.config, row, this.sandboxHost);
|
|
13696
|
+
}
|
|
13697
|
+
// CT1693: what the heartbeat says about this device's runners, resolved
|
|
13698
|
+
// against the agents it runs right now.
|
|
13699
|
+
runnerManifest() {
|
|
13700
|
+
const agents = [...this.workspaces.values()].flatMap(
|
|
13701
|
+
(wr) => [...wr.agents.values()].map((agent) => ({
|
|
13702
|
+
agentId: agent.agentId,
|
|
13703
|
+
agentUsername: agent.username,
|
|
13704
|
+
workspaceSlug: wr.slug
|
|
13705
|
+
}))
|
|
13706
|
+
);
|
|
13707
|
+
return {
|
|
13708
|
+
provider: this.config.runners?.provider ?? "local-process",
|
|
13709
|
+
...runnerSandboxReport(this.config, agents, this.sandboxHost)
|
|
13710
|
+
};
|
|
13642
13711
|
}
|
|
13643
13712
|
// CT1679: one provider for the device, built on first use.
|
|
13644
13713
|
runnerProvider() {
|
|
@@ -13754,7 +13823,7 @@ var CompanionSupervisor = class {
|
|
|
13754
13823
|
reportRunnerGone: (report) => this.reportRunnerGone(report),
|
|
13755
13824
|
connectorHealth: this.connectorHealth,
|
|
13756
13825
|
locks: this.runnerLocks,
|
|
13757
|
-
sandbox: this.
|
|
13826
|
+
sandbox: this.runnerSandboxFor(agentSandbox(this.config, ctx)),
|
|
13758
13827
|
policy: this.sandboxPolicy(),
|
|
13759
13828
|
companionStores: () => this.companionStores().stores()
|
|
13760
13829
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cabane/companion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.133",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The Cabane Companion (headless): connect a coding agent on your machine to your Cabane workspace as a responder — drive work against your own codebase, files, and MCP servers without putting any of it in Cabane.",
|
|
6
6
|
"license": "UNLICENSED",
|