@cabane/companion 0.6.128 → 0.6.129
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 +151 -18
- 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)
|
|
@@ -10487,7 +10566,8 @@ async function prepareHarnessSandbox(input) {
|
|
|
10487
10566
|
runtime: input.runtime,
|
|
10488
10567
|
policy: input.policy,
|
|
10489
10568
|
credentials: input.credentials,
|
|
10490
|
-
runtimePaths: harnessRuntimePaths()
|
|
10569
|
+
runtimePaths: harnessRuntimePaths(),
|
|
10570
|
+
foundStores: input.companionStores ?? []
|
|
10491
10571
|
});
|
|
10492
10572
|
await SandboxManager.initialize(config);
|
|
10493
10573
|
if (input.policy.filesystem?.mode === "strict") {
|
|
@@ -10559,7 +10639,7 @@ exec ${shellQuote(script)} ${shellQuote(real)} "$@"
|
|
|
10559
10639
|
}
|
|
10560
10640
|
};
|
|
10561
10641
|
}
|
|
10562
|
-
var SECRET_PATHS_UNDER_HOME;
|
|
10642
|
+
var SECRET_PATHS_UNDER_HOME, STORE_WALK_TIMEOUT_MS, STORE_WALK_MAX_AGE_MS, CompanionStoreWatch;
|
|
10563
10643
|
var init_harness_sandbox = __esm({
|
|
10564
10644
|
"src/runner/harness-sandbox.ts"() {
|
|
10565
10645
|
"use strict";
|
|
@@ -10582,6 +10662,50 @@ var init_harness_sandbox = __esm({
|
|
|
10582
10662
|
".config/op",
|
|
10583
10663
|
".password-store"
|
|
10584
10664
|
];
|
|
10665
|
+
STORE_WALK_TIMEOUT_MS = 6e4;
|
|
10666
|
+
STORE_WALK_MAX_AGE_MS = 5 * 6e4;
|
|
10667
|
+
CompanionStoreWatch = class {
|
|
10668
|
+
constructor(opts) {
|
|
10669
|
+
this.opts = opts;
|
|
10670
|
+
}
|
|
10671
|
+
opts;
|
|
10672
|
+
latest;
|
|
10673
|
+
running;
|
|
10674
|
+
// Start a walk unless one is running; resolves with the stores held after it.
|
|
10675
|
+
refresh() {
|
|
10676
|
+
this.running ??= (this.opts.walk ?? walkCompanionStores)(this.opts.roots()).then((walk2) => {
|
|
10677
|
+
const stores = walk2.complete ? walk2.stores : [.../* @__PURE__ */ new Set([...this.latest?.stores ?? [], ...walk2.stores])].sort();
|
|
10678
|
+
this.latest = { stores, at: this.now() };
|
|
10679
|
+
if (!walk2.complete) {
|
|
10680
|
+
this.opts.log.warn(
|
|
10681
|
+
{ err: walk2.error, ms: walk2.ms, stores: walk2.stores.length },
|
|
10682
|
+
"runners: couldn't search all of this machine for other companions' stores; the ones found and those matched by name are denied"
|
|
10683
|
+
);
|
|
10684
|
+
} else {
|
|
10685
|
+
this.opts.log.debug(
|
|
10686
|
+
{ ms: walk2.ms, stores: walk2.stores.length },
|
|
10687
|
+
"runners: companion stores found"
|
|
10688
|
+
);
|
|
10689
|
+
}
|
|
10690
|
+
return stores;
|
|
10691
|
+
}).finally(() => {
|
|
10692
|
+
this.running = void 0;
|
|
10693
|
+
});
|
|
10694
|
+
return this.running;
|
|
10695
|
+
}
|
|
10696
|
+
// The stores a turn starting now denies.
|
|
10697
|
+
async stores() {
|
|
10698
|
+
const latest = this.latest;
|
|
10699
|
+
if (!latest) return this.refresh();
|
|
10700
|
+
if (this.now() - latest.at > (this.opts.maxAgeMs ?? STORE_WALK_MAX_AGE_MS)) {
|
|
10701
|
+
void this.refresh();
|
|
10702
|
+
}
|
|
10703
|
+
return latest.stores;
|
|
10704
|
+
}
|
|
10705
|
+
now() {
|
|
10706
|
+
return (this.opts.now ?? Date.now)();
|
|
10707
|
+
}
|
|
10708
|
+
};
|
|
10585
10709
|
}
|
|
10586
10710
|
});
|
|
10587
10711
|
|
|
@@ -11013,6 +11137,7 @@ function runnerDispatcherOptions(input, log, emit, extras = {}) {
|
|
|
11013
11137
|
runtime,
|
|
11014
11138
|
policy: spec.sandboxPolicy ?? {},
|
|
11015
11139
|
credentials: input.sandboxCredentials ?? [],
|
|
11140
|
+
...spec.companionStores ? { companionStores: spec.companionStores } : {},
|
|
11016
11141
|
log: turnLog
|
|
11017
11142
|
});
|
|
11018
11143
|
if (sandbox) onSandbox?.(sandbox);
|
|
@@ -15087,6 +15212,7 @@ var CompanionSupervisor = class {
|
|
|
15087
15212
|
const summary = posture.join("; ");
|
|
15088
15213
|
if (policy.ok) this.log.info({ posture }, `runners: OS sandbox policy \u2014 ${summary}`);
|
|
15089
15214
|
else this.log.error({ posture }, `runners: ${summary}`);
|
|
15215
|
+
void this.companionStores().refresh();
|
|
15090
15216
|
}
|
|
15091
15217
|
const startupHarnessRefresh = this.refreshHarnessStatuses();
|
|
15092
15218
|
this.trackHeartbeat(startupHarnessRefresh);
|
|
@@ -15536,6 +15662,12 @@ var CompanionSupervisor = class {
|
|
|
15536
15662
|
this.parsedSandboxPolicy ??= parseSandboxPolicy(this.config.runners?.policy);
|
|
15537
15663
|
return this.parsedSandboxPolicy;
|
|
15538
15664
|
}
|
|
15665
|
+
// CT1691: the walk for other companions' stores, run off the turn path.
|
|
15666
|
+
storeWatch;
|
|
15667
|
+
companionStores() {
|
|
15668
|
+
this.storeWatch ??= companionStoreWatch(this.log);
|
|
15669
|
+
return this.storeWatch;
|
|
15670
|
+
}
|
|
15539
15671
|
runnerSandbox() {
|
|
15540
15672
|
if (this.config.runners?.sandbox !== "os" || this.dockerRunners()) return "none";
|
|
15541
15673
|
this.sandboxUsable ??= osSandboxUsable(this.log);
|
|
@@ -15656,7 +15788,8 @@ var CompanionSupervisor = class {
|
|
|
15656
15788
|
connectorHealth: this.connectorHealth,
|
|
15657
15789
|
locks: this.runnerLocks,
|
|
15658
15790
|
sandbox: this.runnerSandbox(),
|
|
15659
|
-
policy: this.sandboxPolicy()
|
|
15791
|
+
policy: this.sandboxPolicy(),
|
|
15792
|
+
companionStores: () => this.companionStores().stores()
|
|
15660
15793
|
}
|
|
15661
15794
|
} : {}
|
|
15662
15795
|
});
|
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.129",
|
|
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",
|