@cabane/companion 0.6.128 → 0.6.130
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 +8 -6
- package/dist/cli.js +156 -22
- package/dist/runtime.js +131 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -310,15 +310,17 @@ What the sandbox allows is `runners.policy` in `~/.cabane/config.json`. Every ke
|
|
|
310
310
|
}
|
|
311
311
|
```
|
|
312
312
|
|
|
313
|
-
| Setting | Default
|
|
314
|
-
| ----------------------------------------- |
|
|
315
|
-
| **Hosts**, always reachable | Cabane, and the model provider of the turn's harness (Anthropic for Claude Code, OpenAI for Codex). `allow` and `defaults` never remove these; a `deny` entry naming one does, and blocks the turn from reaching it.
|
|
316
|
-
| **Hosts**, `defaults` | `github.com`, `*.github.com`, `*.githubusercontent.com`, `registry.npmjs.org`, `pypi.org`, `files.pythonhosted.org`, `crates.io`, `static.crates.io`, `index.crates.io`. `allow` adds to them, `deny` removes a host (and wins), `defaults: false` drops the list.
|
|
317
|
-
| **Reads**, `blocklist` mode (the default) | Everything is readable except `~/.cabane`, `~/.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
|
-
| **Writes** | The working directory, the temp dir, `~/.claude`, `~/.claude.json`, `~/.codex`, plus `allowWrite`.
|
|
313
|
+
| Setting | Default |
|
|
314
|
+
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
315
|
+
| **Hosts**, always reachable | Cabane, and the model provider of the turn's harness (Anthropic for Claude Code, OpenAI for Codex). `allow` and `defaults` never remove these; a `deny` entry naming one does, and blocks the turn from reaching it. |
|
|
316
|
+
| **Hosts**, `defaults` | `github.com`, `*.github.com`, `*.githubusercontent.com`, `registry.npmjs.org`, `pypi.org`, `files.pythonhosted.org`, `crates.io`, `static.crates.io`, `index.crates.io`. `allow` adds to them, `deny` removes a host (and wins), `defaults: false` drops the list. |
|
|
317
|
+
| **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
|
+
| **Writes** | The working directory, the temp dir, `~/.claude`, `~/.claude.json`, `~/.codex`, plus `allowWrite`. |
|
|
319
319
|
|
|
320
320
|
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
321
|
|
|
322
|
+
**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.
|
|
323
|
+
|
|
322
324
|
**`.env` files in other projects.** Only the working directory's `.env` files are denied by default. Another project's `.env` stays readable by its absolute path. If your other projects hold secrets, list their roots, for example `"denyRead": ["~/work/**/.env", "~/work/**/.env.*"]`. Each glob is expanded when the turn starts, which costs a walk of that tree on every turn and misses files created during the turn. When other projects' secrets really matter, strict mode is the better answer, because it hides them all without a walk.
|
|
323
325
|
|
|
324
326
|
**Strict mode.** `"filesystem": { "mode": "strict" }` hides your whole home directory except the working directory, the harness state (`~/.claude`, `~/.codex`), what the Companion and its harnesses run from, and the paths you list in `allowRead`. Toolchains live all over a home directory (`~/.nvm`, `~/.local/share/pnpm`, `~/.cargo`, `~/.pyenv`, mise or asdf shims), so list yours, or builds will fail with "No such file or directory" on a path you didn't expect. On Linux a hidden path looks missing rather than denied, so the Companion log prints the readable list at the start of each strict turn. Compare it against the path the build couldn't find.
|
package/dist/cli.js
CHANGED
|
@@ -2592,8 +2592,8 @@ var init_policy = __esm({
|
|
|
2592
2592
|
// packages/agent-runtime/src/claude-code/model.ts
|
|
2593
2593
|
function parseClaudeCodeModel(model) {
|
|
2594
2594
|
if (!model) return null;
|
|
2595
|
-
const
|
|
2596
|
-
const bare =
|
|
2595
|
+
const sep3 = model.indexOf("/");
|
|
2596
|
+
const bare = sep3 === -1 || model.slice(0, sep3) !== "anthropic" ? model : model.slice(sep3 + 1);
|
|
2597
2597
|
return bare === CLAUDE_CODE_DEFAULT_MODEL_BARE ? null : model;
|
|
2598
2598
|
}
|
|
2599
2599
|
var CLAUDE_CODE_DEFAULT_MODEL_BARE;
|
|
@@ -4040,9 +4040,9 @@ var init_policy2 = __esm({
|
|
|
4040
4040
|
|
|
4041
4041
|
// packages/agent-runtime/src/opencode/model.ts
|
|
4042
4042
|
function parseOpencodeModel(model) {
|
|
4043
|
-
const
|
|
4044
|
-
if (
|
|
4045
|
-
return { providerID: model.slice(0,
|
|
4043
|
+
const sep3 = model.indexOf("/");
|
|
4044
|
+
if (sep3 === -1) return { providerID: "", modelID: model };
|
|
4045
|
+
return { providerID: model.slice(0, sep3), modelID: model.slice(sep3 + 1) };
|
|
4046
4046
|
}
|
|
4047
4047
|
var init_model2 = __esm({
|
|
4048
4048
|
"packages/agent-runtime/src/opencode/model.ts"() {
|
|
@@ -4311,10 +4311,10 @@ async function* parseSseStream(body, sessionId, signal) {
|
|
|
4311
4311
|
const { value, done } = await reader.read();
|
|
4312
4312
|
if (done) return;
|
|
4313
4313
|
buf += decoder.decode(value, { stream: true });
|
|
4314
|
-
let
|
|
4315
|
-
while ((
|
|
4316
|
-
const frame = buf.slice(0,
|
|
4317
|
-
buf = buf.slice(
|
|
4314
|
+
let sep3;
|
|
4315
|
+
while ((sep3 = buf.indexOf("\n\n")) !== -1) {
|
|
4316
|
+
const frame = buf.slice(0, sep3);
|
|
4317
|
+
buf = buf.slice(sep3 + 2);
|
|
4318
4318
|
const data = frame.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trim()).join("");
|
|
4319
4319
|
if (!data) continue;
|
|
4320
4320
|
let ev;
|
|
@@ -5340,8 +5340,8 @@ var init_policy3 = __esm({
|
|
|
5340
5340
|
|
|
5341
5341
|
// packages/agent-runtime/src/codex/model.ts
|
|
5342
5342
|
function parseCodexModel(model) {
|
|
5343
|
-
const
|
|
5344
|
-
const bare =
|
|
5343
|
+
const sep3 = model.indexOf("/");
|
|
5344
|
+
const bare = sep3 === -1 || model.slice(0, sep3) !== "openai" ? model : model.slice(sep3 + 1);
|
|
5345
5345
|
return CODEX_DEFAULT_MODEL_BARES.has(bare) ? null : bare;
|
|
5346
5346
|
}
|
|
5347
5347
|
var CODEX_DEFAULT_MODEL_BARES;
|
|
@@ -7486,6 +7486,9 @@ var init_protocol = __esm({
|
|
|
7486
7486
|
// CT1684: the device's sandbox policy, validated by the launcher. Absent
|
|
7487
7487
|
// when the sandbox is off.
|
|
7488
7488
|
sandboxPolicy: sandboxPolicySchema.optional(),
|
|
7489
|
+
// CT1691: every companion store the launcher's latest walk found, denied
|
|
7490
|
+
// to the harness. Absent when the sandbox is off.
|
|
7491
|
+
companionStores: z16.array(z16.string()).optional(),
|
|
7489
7492
|
logLevel: z16.string(),
|
|
7490
7493
|
// The launcher's connector-health observations, by runtime, as of the start.
|
|
7491
7494
|
connectorHealth: z16.record(z16.string(), runtimeHealthSchema),
|
|
@@ -7549,7 +7552,7 @@ function healthSnapshot(store) {
|
|
|
7549
7552
|
}
|
|
7550
7553
|
return out;
|
|
7551
7554
|
}
|
|
7552
|
-
function runnerSpec(opts, launch, duties, runnerId) {
|
|
7555
|
+
function runnerSpec(opts, launch, duties, runnerId, companionStores) {
|
|
7553
7556
|
return {
|
|
7554
7557
|
runnerId,
|
|
7555
7558
|
...opts.deviceId ? { deviceId: opts.deviceId } : {},
|
|
@@ -7574,6 +7577,7 @@ function runnerSpec(opts, launch, duties, runnerId) {
|
|
|
7574
7577
|
...launch.provider.bindAttempt ? { attemptState: true } : {},
|
|
7575
7578
|
...launch.provider.inheritsEnvironment === false ? { secretsFromLauncher: true } : {},
|
|
7576
7579
|
...launch.sandbox === "os" && launch.policy?.ok ? { sandboxPolicy: launch.policy.policy } : {},
|
|
7580
|
+
...launch.sandbox === "os" && companionStores ? { companionStores: [...companionStores] } : {},
|
|
7577
7581
|
logLevel: opts.log.level,
|
|
7578
7582
|
connectorHealth: healthSnapshot(launch.connectorHealth),
|
|
7579
7583
|
sessionWriteNoticeClaimed: duties.sessionWriteNoticeClaimed()
|
|
@@ -7772,6 +7776,7 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
|
7772
7776
|
if (v !== void 0) env[k] = v;
|
|
7773
7777
|
}
|
|
7774
7778
|
}
|
|
7779
|
+
const companionStores = launch.sandbox === "os" ? await launch.companionStores?.() : void 0;
|
|
7775
7780
|
const input = {
|
|
7776
7781
|
baseUrl: launch.runnerBaseUrl ?? opts.baseUrl,
|
|
7777
7782
|
credential: minted.credential,
|
|
@@ -7779,7 +7784,7 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
|
7779
7784
|
dispatch: payload,
|
|
7780
7785
|
turnId,
|
|
7781
7786
|
resumed: handleOpts.resumed,
|
|
7782
|
-
spec: runnerSpec(opts, launch, duties, minted.runnerId)
|
|
7787
|
+
spec: runnerSpec(opts, launch, duties, minted.runnerId, companionStores)
|
|
7783
7788
|
};
|
|
7784
7789
|
Object.assign(env, runnerEnv(input));
|
|
7785
7790
|
const running = launch.provider.exec(handle, launch.command, env, {
|
|
@@ -10266,6 +10271,7 @@ var init_git_credential = __esm({
|
|
|
10266
10271
|
});
|
|
10267
10272
|
|
|
10268
10273
|
// src/runner/harness-sandbox.ts
|
|
10274
|
+
import { execFile as execFile2 } from "child_process";
|
|
10269
10275
|
import { lookup } from "dns/promises";
|
|
10270
10276
|
import {
|
|
10271
10277
|
chmodSync as chmodSync6,
|
|
@@ -10279,7 +10285,7 @@ import {
|
|
|
10279
10285
|
import { createRequire as createRequire2 } from "module";
|
|
10280
10286
|
import { isIP } from "net";
|
|
10281
10287
|
import { homedir as homedir3, networkInterfaces, tmpdir as tmpdir3, userInfo as userInfo2 } from "os";
|
|
10282
|
-
import { dirname as dirname8, join as join18 } from "path";
|
|
10288
|
+
import { dirname as dirname8, join as join18, sep as sep2 } from "path";
|
|
10283
10289
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
10284
10290
|
import { SandboxManager } from "@anthropic-ai/sandbox-runtime";
|
|
10285
10291
|
function accountHome() {
|
|
@@ -10345,6 +10351,74 @@ function harnessRuntimePaths() {
|
|
|
10345
10351
|
function ownAddresses() {
|
|
10346
10352
|
return Object.values(networkInterfaces()).flat().flatMap((i) => i ? [i.address] : []);
|
|
10347
10353
|
}
|
|
10354
|
+
function storeWalkRoots(homes, cabaneDir2) {
|
|
10355
|
+
const companionHome = dirname8(cabaneDir2);
|
|
10356
|
+
const parent = dirname8(companionHome);
|
|
10357
|
+
const beside = homes.includes(companionHome) || dirname8(parent) === parent ? [] : [parent];
|
|
10358
|
+
const candidates = [.../* @__PURE__ */ new Set([...homes, ...beside])];
|
|
10359
|
+
return candidates.filter(
|
|
10360
|
+
(root) => !candidates.some((other) => other !== root && isAtOrUnder(root, other))
|
|
10361
|
+
);
|
|
10362
|
+
}
|
|
10363
|
+
async function walkCompanionStores(roots, opts = {}) {
|
|
10364
|
+
const started = Date.now();
|
|
10365
|
+
const { stdout, error } = await new Promise((resolve2) => {
|
|
10366
|
+
execFile2(
|
|
10367
|
+
opts.command ?? "find",
|
|
10368
|
+
// `-prune`: nothing inside a store needs looking at. `-print0`: a
|
|
10369
|
+
// directory name may hold a newline.
|
|
10370
|
+
[...roots, "-type", "d", "-name", ".cabane", "-prune", "-print0"],
|
|
10371
|
+
{
|
|
10372
|
+
encoding: "buffer",
|
|
10373
|
+
timeout: opts.timeoutMs ?? STORE_WALK_TIMEOUT_MS,
|
|
10374
|
+
maxBuffer: 16 * 1024 * 1024
|
|
10375
|
+
},
|
|
10376
|
+
(err, out) => resolve2({
|
|
10377
|
+
stdout: Buffer.isBuffer(out) ? out : Buffer.alloc(0),
|
|
10378
|
+
...err ? { error: err.message } : {}
|
|
10379
|
+
})
|
|
10380
|
+
);
|
|
10381
|
+
});
|
|
10382
|
+
const ms = Date.now() - started;
|
|
10383
|
+
const stores = [];
|
|
10384
|
+
let complete = error === void 0;
|
|
10385
|
+
for (let at = 0, end = stdout.indexOf(0); end !== -1; at = end + 1, end = stdout.indexOf(0, at)) {
|
|
10386
|
+
const raw = stdout.subarray(at, end);
|
|
10387
|
+
const path = raw.toString("utf8");
|
|
10388
|
+
if (Buffer.from(path, "utf8").equals(raw)) stores.push(path);
|
|
10389
|
+
else complete = false;
|
|
10390
|
+
}
|
|
10391
|
+
return { stores: [...new Set(stores)].sort(), complete, ms, ...error ? { error } : {} };
|
|
10392
|
+
}
|
|
10393
|
+
function companionStoreDenies(input) {
|
|
10394
|
+
const { homes, cabaneDir: cabaneDir2, foundStores } = input;
|
|
10395
|
+
const parent = dirname8(dirname8(cabaneDir2));
|
|
10396
|
+
const always = [
|
|
10397
|
+
cabaneDir2,
|
|
10398
|
+
...homes.flatMap((home) => [
|
|
10399
|
+
join18(home, ".cabane"),
|
|
10400
|
+
join18(home, "*", ".cabane"),
|
|
10401
|
+
join18(home, "*", "*", ".cabane")
|
|
10402
|
+
]),
|
|
10403
|
+
// A glob anchored at the root has no directory to start from.
|
|
10404
|
+
...dirname8(parent) === parent ? [] : [join18(parent, "*", ".cabane")]
|
|
10405
|
+
];
|
|
10406
|
+
const companionHome = dirname8(cabaneDir2);
|
|
10407
|
+
const otherLogins = foundStores.map((store) => dirname8(store)).filter((home) => home !== companionHome).flatMap((home) => [join18(home, ".claude"), join18(home, ".codex")]).filter((path) => !input.ownState.some((own) => isAtOrUnder(own, path)));
|
|
10408
|
+
return [.../* @__PURE__ */ new Set([...always, ...foundStores, ...otherLogins])];
|
|
10409
|
+
}
|
|
10410
|
+
function companionStoreWatch(log) {
|
|
10411
|
+
return new CompanionStoreWatch({
|
|
10412
|
+
log,
|
|
10413
|
+
roots: () => {
|
|
10414
|
+
const home = homedir3();
|
|
10415
|
+
return storeWalkRoots([.../* @__PURE__ */ new Set([home, accountHome() ?? home])], cabaneDir());
|
|
10416
|
+
}
|
|
10417
|
+
});
|
|
10418
|
+
}
|
|
10419
|
+
function isAtOrUnder(path, dir2) {
|
|
10420
|
+
return path === dir2 || path.startsWith(dir2 + sep2);
|
|
10421
|
+
}
|
|
10348
10422
|
function expandHome(path, home) {
|
|
10349
10423
|
if (path === "~") return home;
|
|
10350
10424
|
return path.startsWith("~/") ? join18(home, path.slice(2)) : path;
|
|
@@ -10378,7 +10452,12 @@ function harnessSandboxConfig(input) {
|
|
|
10378
10452
|
const allowedDomains = [.../* @__PURE__ */ new Set([...cabane, ...models, ...reachable])];
|
|
10379
10453
|
const envGlobs = input.cwd ? [join18(input.cwd, "**/.env"), join18(input.cwd, "**/.env.*")] : [];
|
|
10380
10454
|
const secrets = [
|
|
10381
|
-
|
|
10455
|
+
...companionStoreDenies({
|
|
10456
|
+
homes,
|
|
10457
|
+
cabaneDir: input.cabaneDir,
|
|
10458
|
+
foundStores: input.foundStores,
|
|
10459
|
+
ownState: [claudeDir, codexDir]
|
|
10460
|
+
}),
|
|
10382
10461
|
...homes.flatMap((home) => SECRET_PATHS_UNDER_HOME.map((p) => join18(home, p))),
|
|
10383
10462
|
...envGlobs,
|
|
10384
10463
|
...expand(policy.filesystem?.denyRead)
|
|
@@ -10441,11 +10520,12 @@ function gitCredentialExports(credentials) {
|
|
|
10441
10520
|
const git = credentials.filter((c) => c.gitHost);
|
|
10442
10521
|
if (!git.length) return "";
|
|
10443
10522
|
const lines2 = ["n=${GIT_CONFIG_COUNT:-0}"];
|
|
10523
|
+
const set = (key, value) => lines2.push(`export "GIT_CONFIG_KEY_$n=${key}" "GIT_CONFIG_VALUE_$n=${value}"`, "n=$((n+1))");
|
|
10444
10524
|
for (const c of git) {
|
|
10445
|
-
|
|
10446
|
-
|
|
10447
|
-
|
|
10448
|
-
);
|
|
10525
|
+
const h = c.gitHost;
|
|
10526
|
+
set(`http.https://${h}/.extraHeader`, `Authorization: Basic $${c.name}`);
|
|
10527
|
+
set(`url.https://${h}/.insteadOf`, `git@${h}:`);
|
|
10528
|
+
set(`url.https://${h}/.insteadOf`, `ssh://git@${h}/`);
|
|
10449
10529
|
}
|
|
10450
10530
|
lines2.push("export GIT_CONFIG_COUNT=$n");
|
|
10451
10531
|
return lines2.join("; ") + "; ";
|
|
@@ -10487,7 +10567,8 @@ async function prepareHarnessSandbox(input) {
|
|
|
10487
10567
|
runtime: input.runtime,
|
|
10488
10568
|
policy: input.policy,
|
|
10489
10569
|
credentials: input.credentials,
|
|
10490
|
-
runtimePaths: harnessRuntimePaths()
|
|
10570
|
+
runtimePaths: harnessRuntimePaths(),
|
|
10571
|
+
foundStores: input.companionStores ?? []
|
|
10491
10572
|
});
|
|
10492
10573
|
await SandboxManager.initialize(config);
|
|
10493
10574
|
if (input.policy.filesystem?.mode === "strict") {
|
|
@@ -10559,7 +10640,7 @@ exec ${shellQuote(script)} ${shellQuote(real)} "$@"
|
|
|
10559
10640
|
}
|
|
10560
10641
|
};
|
|
10561
10642
|
}
|
|
10562
|
-
var SECRET_PATHS_UNDER_HOME;
|
|
10643
|
+
var SECRET_PATHS_UNDER_HOME, STORE_WALK_TIMEOUT_MS, STORE_WALK_MAX_AGE_MS, CompanionStoreWatch;
|
|
10563
10644
|
var init_harness_sandbox = __esm({
|
|
10564
10645
|
"src/runner/harness-sandbox.ts"() {
|
|
10565
10646
|
"use strict";
|
|
@@ -10582,6 +10663,50 @@ var init_harness_sandbox = __esm({
|
|
|
10582
10663
|
".config/op",
|
|
10583
10664
|
".password-store"
|
|
10584
10665
|
];
|
|
10666
|
+
STORE_WALK_TIMEOUT_MS = 6e4;
|
|
10667
|
+
STORE_WALK_MAX_AGE_MS = 5 * 6e4;
|
|
10668
|
+
CompanionStoreWatch = class {
|
|
10669
|
+
constructor(opts) {
|
|
10670
|
+
this.opts = opts;
|
|
10671
|
+
}
|
|
10672
|
+
opts;
|
|
10673
|
+
latest;
|
|
10674
|
+
running;
|
|
10675
|
+
// Start a walk unless one is running; resolves with the stores held after it.
|
|
10676
|
+
refresh() {
|
|
10677
|
+
this.running ??= (this.opts.walk ?? walkCompanionStores)(this.opts.roots()).then((walk2) => {
|
|
10678
|
+
const stores = walk2.complete ? walk2.stores : [.../* @__PURE__ */ new Set([...this.latest?.stores ?? [], ...walk2.stores])].sort();
|
|
10679
|
+
this.latest = { stores, at: this.now() };
|
|
10680
|
+
if (!walk2.complete) {
|
|
10681
|
+
this.opts.log.warn(
|
|
10682
|
+
{ err: walk2.error, ms: walk2.ms, stores: walk2.stores.length },
|
|
10683
|
+
"runners: couldn't search all of this machine for other companions' stores; the ones found and those matched by name are denied"
|
|
10684
|
+
);
|
|
10685
|
+
} else {
|
|
10686
|
+
this.opts.log.debug(
|
|
10687
|
+
{ ms: walk2.ms, stores: walk2.stores.length },
|
|
10688
|
+
"runners: companion stores found"
|
|
10689
|
+
);
|
|
10690
|
+
}
|
|
10691
|
+
return stores;
|
|
10692
|
+
}).finally(() => {
|
|
10693
|
+
this.running = void 0;
|
|
10694
|
+
});
|
|
10695
|
+
return this.running;
|
|
10696
|
+
}
|
|
10697
|
+
// The stores a turn starting now denies.
|
|
10698
|
+
async stores() {
|
|
10699
|
+
const latest = this.latest;
|
|
10700
|
+
if (!latest) return this.refresh();
|
|
10701
|
+
if (this.now() - latest.at > (this.opts.maxAgeMs ?? STORE_WALK_MAX_AGE_MS)) {
|
|
10702
|
+
void this.refresh();
|
|
10703
|
+
}
|
|
10704
|
+
return latest.stores;
|
|
10705
|
+
}
|
|
10706
|
+
now() {
|
|
10707
|
+
return (this.opts.now ?? Date.now)();
|
|
10708
|
+
}
|
|
10709
|
+
};
|
|
10585
10710
|
}
|
|
10586
10711
|
});
|
|
10587
10712
|
|
|
@@ -11013,6 +11138,7 @@ function runnerDispatcherOptions(input, log, emit, extras = {}) {
|
|
|
11013
11138
|
runtime,
|
|
11014
11139
|
policy: spec.sandboxPolicy ?? {},
|
|
11015
11140
|
credentials: input.sandboxCredentials ?? [],
|
|
11141
|
+
...spec.companionStores ? { companionStores: spec.companionStores } : {},
|
|
11016
11142
|
log: turnLog
|
|
11017
11143
|
});
|
|
11018
11144
|
if (sandbox) onSandbox?.(sandbox);
|
|
@@ -15087,6 +15213,7 @@ var CompanionSupervisor = class {
|
|
|
15087
15213
|
const summary = posture.join("; ");
|
|
15088
15214
|
if (policy.ok) this.log.info({ posture }, `runners: OS sandbox policy \u2014 ${summary}`);
|
|
15089
15215
|
else this.log.error({ posture }, `runners: ${summary}`);
|
|
15216
|
+
void this.companionStores().refresh();
|
|
15090
15217
|
}
|
|
15091
15218
|
const startupHarnessRefresh = this.refreshHarnessStatuses();
|
|
15092
15219
|
this.trackHeartbeat(startupHarnessRefresh);
|
|
@@ -15536,6 +15663,12 @@ var CompanionSupervisor = class {
|
|
|
15536
15663
|
this.parsedSandboxPolicy ??= parseSandboxPolicy(this.config.runners?.policy);
|
|
15537
15664
|
return this.parsedSandboxPolicy;
|
|
15538
15665
|
}
|
|
15666
|
+
// CT1691: the walk for other companions' stores, run off the turn path.
|
|
15667
|
+
storeWatch;
|
|
15668
|
+
companionStores() {
|
|
15669
|
+
this.storeWatch ??= companionStoreWatch(this.log);
|
|
15670
|
+
return this.storeWatch;
|
|
15671
|
+
}
|
|
15539
15672
|
runnerSandbox() {
|
|
15540
15673
|
if (this.config.runners?.sandbox !== "os" || this.dockerRunners()) return "none";
|
|
15541
15674
|
this.sandboxUsable ??= osSandboxUsable(this.log);
|
|
@@ -15656,7 +15789,8 @@ var CompanionSupervisor = class {
|
|
|
15656
15789
|
connectorHealth: this.connectorHealth,
|
|
15657
15790
|
locks: this.runnerLocks,
|
|
15658
15791
|
sandbox: this.runnerSandbox(),
|
|
15659
|
-
policy: this.sandboxPolicy()
|
|
15792
|
+
policy: this.sandboxPolicy(),
|
|
15793
|
+
companionStores: () => this.companionStores().stores()
|
|
15660
15794
|
}
|
|
15661
15795
|
} : {}
|
|
15662
15796
|
});
|
package/dist/runtime.js
CHANGED
|
@@ -4011,8 +4011,8 @@ function readThinking(runtimeOptions) {
|
|
|
4011
4011
|
var CLAUDE_CODE_DEFAULT_MODEL_BARE = "default";
|
|
4012
4012
|
function parseClaudeCodeModel(model) {
|
|
4013
4013
|
if (!model) return null;
|
|
4014
|
-
const
|
|
4015
|
-
const bare =
|
|
4014
|
+
const sep3 = model.indexOf("/");
|
|
4015
|
+
const bare = sep3 === -1 || model.slice(0, sep3) !== "anthropic" ? model : model.slice(sep3 + 1);
|
|
4016
4016
|
return bare === CLAUDE_CODE_DEFAULT_MODEL_BARE ? null : model;
|
|
4017
4017
|
}
|
|
4018
4018
|
|
|
@@ -5363,9 +5363,9 @@ function readOpencodeDialect(runtimeOptions) {
|
|
|
5363
5363
|
|
|
5364
5364
|
// packages/agent-runtime/src/opencode/model.ts
|
|
5365
5365
|
function parseOpencodeModel(model) {
|
|
5366
|
-
const
|
|
5367
|
-
if (
|
|
5368
|
-
return { providerID: model.slice(0,
|
|
5366
|
+
const sep3 = model.indexOf("/");
|
|
5367
|
+
if (sep3 === -1) return { providerID: "", modelID: model };
|
|
5368
|
+
return { providerID: model.slice(0, sep3), modelID: model.slice(sep3 + 1) };
|
|
5369
5369
|
}
|
|
5370
5370
|
|
|
5371
5371
|
// packages/agent-runtime/src/opencode/run-spec.ts
|
|
@@ -5617,10 +5617,10 @@ async function* parseSseStream(body, sessionId, signal) {
|
|
|
5617
5617
|
const { value, done } = await reader.read();
|
|
5618
5618
|
if (done) return;
|
|
5619
5619
|
buf += decoder.decode(value, { stream: true });
|
|
5620
|
-
let
|
|
5621
|
-
while ((
|
|
5622
|
-
const frame = buf.slice(0,
|
|
5623
|
-
buf = buf.slice(
|
|
5620
|
+
let sep3;
|
|
5621
|
+
while ((sep3 = buf.indexOf("\n\n")) !== -1) {
|
|
5622
|
+
const frame = buf.slice(0, sep3);
|
|
5623
|
+
buf = buf.slice(sep3 + 2);
|
|
5624
5624
|
const data = frame.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trim()).join("");
|
|
5625
5625
|
if (!data) continue;
|
|
5626
5626
|
let ev;
|
|
@@ -6580,8 +6580,8 @@ function readCodexDialect(runtimeOptions) {
|
|
|
6580
6580
|
// packages/agent-runtime/src/codex/model.ts
|
|
6581
6581
|
var CODEX_DEFAULT_MODEL_BARES = /* @__PURE__ */ new Set(["default"]);
|
|
6582
6582
|
function parseCodexModel(model) {
|
|
6583
|
-
const
|
|
6584
|
-
const bare =
|
|
6583
|
+
const sep3 = model.indexOf("/");
|
|
6584
|
+
const bare = sep3 === -1 || model.slice(0, sep3) !== "openai" ? model : model.slice(sep3 + 1);
|
|
6585
6585
|
return CODEX_DEFAULT_MODEL_BARES.has(bare) ? null : bare;
|
|
6586
6586
|
}
|
|
6587
6587
|
|
|
@@ -8543,6 +8543,9 @@ var runnerSpecSchema = z16.object({
|
|
|
8543
8543
|
// CT1684: the device's sandbox policy, validated by the launcher. Absent
|
|
8544
8544
|
// when the sandbox is off.
|
|
8545
8545
|
sandboxPolicy: sandboxPolicySchema.optional(),
|
|
8546
|
+
// CT1691: every companion store the launcher's latest walk found, denied
|
|
8547
|
+
// to the harness. Absent when the sandbox is off.
|
|
8548
|
+
companionStores: z16.array(z16.string()).optional(),
|
|
8546
8549
|
logLevel: z16.string(),
|
|
8547
8550
|
// The launcher's connector-health observations, by runtime, as of the start.
|
|
8548
8551
|
connectorHealth: z16.record(z16.string(), runtimeHealthSchema),
|
|
@@ -8634,7 +8637,7 @@ function healthSnapshot(store) {
|
|
|
8634
8637
|
}
|
|
8635
8638
|
return out;
|
|
8636
8639
|
}
|
|
8637
|
-
function runnerSpec(opts, launch, duties, runnerId) {
|
|
8640
|
+
function runnerSpec(opts, launch, duties, runnerId, companionStores) {
|
|
8638
8641
|
return {
|
|
8639
8642
|
runnerId,
|
|
8640
8643
|
...opts.deviceId ? { deviceId: opts.deviceId } : {},
|
|
@@ -8659,6 +8662,7 @@ function runnerSpec(opts, launch, duties, runnerId) {
|
|
|
8659
8662
|
...launch.provider.bindAttempt ? { attemptState: true } : {},
|
|
8660
8663
|
...launch.provider.inheritsEnvironment === false ? { secretsFromLauncher: true } : {},
|
|
8661
8664
|
...launch.sandbox === "os" && launch.policy?.ok ? { sandboxPolicy: launch.policy.policy } : {},
|
|
8665
|
+
...launch.sandbox === "os" && companionStores ? { companionStores: [...companionStores] } : {},
|
|
8662
8666
|
logLevel: opts.log.level,
|
|
8663
8667
|
connectorHealth: healthSnapshot(launch.connectorHealth),
|
|
8664
8668
|
sessionWriteNoticeClaimed: duties.sessionWriteNoticeClaimed()
|
|
@@ -8865,6 +8869,7 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
|
8865
8869
|
if (v !== void 0) env[k] = v;
|
|
8866
8870
|
}
|
|
8867
8871
|
}
|
|
8872
|
+
const companionStores = launch.sandbox === "os" ? await launch.companionStores?.() : void 0;
|
|
8868
8873
|
const input = {
|
|
8869
8874
|
baseUrl: launch.runnerBaseUrl ?? opts.baseUrl,
|
|
8870
8875
|
credential: minted.credential,
|
|
@@ -8872,7 +8877,7 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
|
8872
8877
|
dispatch: payload,
|
|
8873
8878
|
turnId,
|
|
8874
8879
|
resumed: handleOpts.resumed,
|
|
8875
|
-
spec: runnerSpec(opts, launch, duties, minted.runnerId)
|
|
8880
|
+
spec: runnerSpec(opts, launch, duties, minted.runnerId, companionStores)
|
|
8876
8881
|
};
|
|
8877
8882
|
Object.assign(env, runnerEnv(input));
|
|
8878
8883
|
const running = launch.provider.exec(handle, launch.command, env, {
|
|
@@ -12451,6 +12456,7 @@ var DeviceLocks = class {
|
|
|
12451
12456
|
};
|
|
12452
12457
|
|
|
12453
12458
|
// src/runner/harness-sandbox.ts
|
|
12459
|
+
import { execFile as execFile2 } from "child_process";
|
|
12454
12460
|
import { lookup } from "dns/promises";
|
|
12455
12461
|
import {
|
|
12456
12462
|
chmodSync as chmodSync6,
|
|
@@ -12464,9 +12470,111 @@ import {
|
|
|
12464
12470
|
import { createRequire as createRequire2 } from "module";
|
|
12465
12471
|
import { isIP } from "net";
|
|
12466
12472
|
import { homedir as homedir3, networkInterfaces, tmpdir as tmpdir3, userInfo as userInfo2 } from "os";
|
|
12467
|
-
import { dirname as dirname8, join as join18 } from "path";
|
|
12473
|
+
import { dirname as dirname8, join as join18, sep as sep2 } from "path";
|
|
12468
12474
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
12469
12475
|
import { SandboxManager } from "@anthropic-ai/sandbox-runtime";
|
|
12476
|
+
function accountHome() {
|
|
12477
|
+
try {
|
|
12478
|
+
return userInfo2().homedir;
|
|
12479
|
+
} catch {
|
|
12480
|
+
return null;
|
|
12481
|
+
}
|
|
12482
|
+
}
|
|
12483
|
+
var STORE_WALK_TIMEOUT_MS = 6e4;
|
|
12484
|
+
var STORE_WALK_MAX_AGE_MS = 5 * 6e4;
|
|
12485
|
+
function storeWalkRoots(homes, cabaneDir2) {
|
|
12486
|
+
const companionHome = dirname8(cabaneDir2);
|
|
12487
|
+
const parent = dirname8(companionHome);
|
|
12488
|
+
const beside = homes.includes(companionHome) || dirname8(parent) === parent ? [] : [parent];
|
|
12489
|
+
const candidates = [.../* @__PURE__ */ new Set([...homes, ...beside])];
|
|
12490
|
+
return candidates.filter(
|
|
12491
|
+
(root) => !candidates.some((other) => other !== root && isAtOrUnder(root, other))
|
|
12492
|
+
);
|
|
12493
|
+
}
|
|
12494
|
+
async function walkCompanionStores(roots, opts = {}) {
|
|
12495
|
+
const started = Date.now();
|
|
12496
|
+
const { stdout, error } = await new Promise((resolve2) => {
|
|
12497
|
+
execFile2(
|
|
12498
|
+
opts.command ?? "find",
|
|
12499
|
+
// `-prune`: nothing inside a store needs looking at. `-print0`: a
|
|
12500
|
+
// directory name may hold a newline.
|
|
12501
|
+
[...roots, "-type", "d", "-name", ".cabane", "-prune", "-print0"],
|
|
12502
|
+
{
|
|
12503
|
+
encoding: "buffer",
|
|
12504
|
+
timeout: opts.timeoutMs ?? STORE_WALK_TIMEOUT_MS,
|
|
12505
|
+
maxBuffer: 16 * 1024 * 1024
|
|
12506
|
+
},
|
|
12507
|
+
(err, out) => resolve2({
|
|
12508
|
+
stdout: Buffer.isBuffer(out) ? out : Buffer.alloc(0),
|
|
12509
|
+
...err ? { error: err.message } : {}
|
|
12510
|
+
})
|
|
12511
|
+
);
|
|
12512
|
+
});
|
|
12513
|
+
const ms = Date.now() - started;
|
|
12514
|
+
const stores = [];
|
|
12515
|
+
let complete = error === void 0;
|
|
12516
|
+
for (let at = 0, end = stdout.indexOf(0); end !== -1; at = end + 1, end = stdout.indexOf(0, at)) {
|
|
12517
|
+
const raw = stdout.subarray(at, end);
|
|
12518
|
+
const path = raw.toString("utf8");
|
|
12519
|
+
if (Buffer.from(path, "utf8").equals(raw)) stores.push(path);
|
|
12520
|
+
else complete = false;
|
|
12521
|
+
}
|
|
12522
|
+
return { stores: [...new Set(stores)].sort(), complete, ms, ...error ? { error } : {} };
|
|
12523
|
+
}
|
|
12524
|
+
var CompanionStoreWatch = class {
|
|
12525
|
+
constructor(opts) {
|
|
12526
|
+
this.opts = opts;
|
|
12527
|
+
}
|
|
12528
|
+
opts;
|
|
12529
|
+
latest;
|
|
12530
|
+
running;
|
|
12531
|
+
// Start a walk unless one is running; resolves with the stores held after it.
|
|
12532
|
+
refresh() {
|
|
12533
|
+
this.running ??= (this.opts.walk ?? walkCompanionStores)(this.opts.roots()).then((walk2) => {
|
|
12534
|
+
const stores = walk2.complete ? walk2.stores : [.../* @__PURE__ */ new Set([...this.latest?.stores ?? [], ...walk2.stores])].sort();
|
|
12535
|
+
this.latest = { stores, at: this.now() };
|
|
12536
|
+
if (!walk2.complete) {
|
|
12537
|
+
this.opts.log.warn(
|
|
12538
|
+
{ err: walk2.error, ms: walk2.ms, stores: walk2.stores.length },
|
|
12539
|
+
"runners: couldn't search all of this machine for other companions' stores; the ones found and those matched by name are denied"
|
|
12540
|
+
);
|
|
12541
|
+
} else {
|
|
12542
|
+
this.opts.log.debug(
|
|
12543
|
+
{ ms: walk2.ms, stores: walk2.stores.length },
|
|
12544
|
+
"runners: companion stores found"
|
|
12545
|
+
);
|
|
12546
|
+
}
|
|
12547
|
+
return stores;
|
|
12548
|
+
}).finally(() => {
|
|
12549
|
+
this.running = void 0;
|
|
12550
|
+
});
|
|
12551
|
+
return this.running;
|
|
12552
|
+
}
|
|
12553
|
+
// The stores a turn starting now denies.
|
|
12554
|
+
async stores() {
|
|
12555
|
+
const latest = this.latest;
|
|
12556
|
+
if (!latest) return this.refresh();
|
|
12557
|
+
if (this.now() - latest.at > (this.opts.maxAgeMs ?? STORE_WALK_MAX_AGE_MS)) {
|
|
12558
|
+
void this.refresh();
|
|
12559
|
+
}
|
|
12560
|
+
return latest.stores;
|
|
12561
|
+
}
|
|
12562
|
+
now() {
|
|
12563
|
+
return (this.opts.now ?? Date.now)();
|
|
12564
|
+
}
|
|
12565
|
+
};
|
|
12566
|
+
function companionStoreWatch(log) {
|
|
12567
|
+
return new CompanionStoreWatch({
|
|
12568
|
+
log,
|
|
12569
|
+
roots: () => {
|
|
12570
|
+
const home = homedir3();
|
|
12571
|
+
return storeWalkRoots([.../* @__PURE__ */ new Set([home, accountHome() ?? home])], cabaneDir());
|
|
12572
|
+
}
|
|
12573
|
+
});
|
|
12574
|
+
}
|
|
12575
|
+
function isAtOrUnder(path, dir2) {
|
|
12576
|
+
return path === dir2 || path.startsWith(dir2 + sep2);
|
|
12577
|
+
}
|
|
12470
12578
|
function osSandboxUsable(log) {
|
|
12471
12579
|
if (!SandboxManager.isSupportedPlatform()) {
|
|
12472
12580
|
log.warn(
|
|
@@ -12999,6 +13107,7 @@ var CompanionSupervisor = class {
|
|
|
12999
13107
|
const summary = posture.join("; ");
|
|
13000
13108
|
if (policy.ok) this.log.info({ posture }, `runners: OS sandbox policy \u2014 ${summary}`);
|
|
13001
13109
|
else this.log.error({ posture }, `runners: ${summary}`);
|
|
13110
|
+
void this.companionStores().refresh();
|
|
13002
13111
|
}
|
|
13003
13112
|
const startupHarnessRefresh = this.refreshHarnessStatuses();
|
|
13004
13113
|
this.trackHeartbeat(startupHarnessRefresh);
|
|
@@ -13448,6 +13557,12 @@ var CompanionSupervisor = class {
|
|
|
13448
13557
|
this.parsedSandboxPolicy ??= parseSandboxPolicy(this.config.runners?.policy);
|
|
13449
13558
|
return this.parsedSandboxPolicy;
|
|
13450
13559
|
}
|
|
13560
|
+
// CT1691: the walk for other companions' stores, run off the turn path.
|
|
13561
|
+
storeWatch;
|
|
13562
|
+
companionStores() {
|
|
13563
|
+
this.storeWatch ??= companionStoreWatch(this.log);
|
|
13564
|
+
return this.storeWatch;
|
|
13565
|
+
}
|
|
13451
13566
|
runnerSandbox() {
|
|
13452
13567
|
if (this.config.runners?.sandbox !== "os" || this.dockerRunners()) return "none";
|
|
13453
13568
|
this.sandboxUsable ??= osSandboxUsable(this.log);
|
|
@@ -13568,7 +13683,8 @@ var CompanionSupervisor = class {
|
|
|
13568
13683
|
connectorHealth: this.connectorHealth,
|
|
13569
13684
|
locks: this.runnerLocks,
|
|
13570
13685
|
sandbox: this.runnerSandbox(),
|
|
13571
|
-
policy: this.sandboxPolicy()
|
|
13686
|
+
policy: this.sandboxPolicy(),
|
|
13687
|
+
companionStores: () => this.companionStores().stores()
|
|
13572
13688
|
}
|
|
13573
13689
|
} : {}
|
|
13574
13690
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cabane/companion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.130",
|
|
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",
|