@cabane/companion 0.6.127 → 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 +187 -26
- package/dist/runtime.js +167 -23
- 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
|
@@ -1037,6 +1037,11 @@ function leaseRefusal(err) {
|
|
|
1037
1037
|
function isWriteFenceRefusal(err) {
|
|
1038
1038
|
return err instanceof ApiError && err.status === 409 && apiErrorCode(err) === "not_running";
|
|
1039
1039
|
}
|
|
1040
|
+
function isTurnEndedRefusal(err) {
|
|
1041
|
+
if (!(err instanceof ApiError) || err.status !== 401) return false;
|
|
1042
|
+
const body = err.body;
|
|
1043
|
+
return typeof body === "object" && body !== null && body.reason === "turn_ended";
|
|
1044
|
+
}
|
|
1040
1045
|
function describeErrorBody(body) {
|
|
1041
1046
|
if (body === void 0 || body === null) return void 0;
|
|
1042
1047
|
let text;
|
|
@@ -1286,6 +1291,15 @@ var init_api = __esm({
|
|
|
1286
1291
|
progressed = true;
|
|
1287
1292
|
} catch (err) {
|
|
1288
1293
|
if (err instanceof ApiError && err.status >= 400 && err.status < 500) {
|
|
1294
|
+
if (entry.kind === "active-run" && isTurnEndedRefusal(err)) {
|
|
1295
|
+
this.opts.log?.debug(
|
|
1296
|
+
{ kind: entry.kind, turnId: entry.turnId, ...apiErrorLogFields(err) },
|
|
1297
|
+
"companion: queued settle refused; the turn had already ended"
|
|
1298
|
+
);
|
|
1299
|
+
outbox.remove(entry.turnId, entry.seq);
|
|
1300
|
+
progressed = true;
|
|
1301
|
+
continue;
|
|
1302
|
+
}
|
|
1289
1303
|
this.opts.log?.warn(
|
|
1290
1304
|
{ kind: entry.kind, turnId: entry.turnId, seq: entry.seq, ...apiErrorLogFields(err) },
|
|
1291
1305
|
"Cabane rejected a saved update; it could not be delivered. This companion may need an update: npm i -g @cabane/companion@latest"
|
|
@@ -2578,8 +2592,8 @@ var init_policy = __esm({
|
|
|
2578
2592
|
// packages/agent-runtime/src/claude-code/model.ts
|
|
2579
2593
|
function parseClaudeCodeModel(model) {
|
|
2580
2594
|
if (!model) return null;
|
|
2581
|
-
const
|
|
2582
|
-
const bare =
|
|
2595
|
+
const sep3 = model.indexOf("/");
|
|
2596
|
+
const bare = sep3 === -1 || model.slice(0, sep3) !== "anthropic" ? model : model.slice(sep3 + 1);
|
|
2583
2597
|
return bare === CLAUDE_CODE_DEFAULT_MODEL_BARE ? null : model;
|
|
2584
2598
|
}
|
|
2585
2599
|
var CLAUDE_CODE_DEFAULT_MODEL_BARE;
|
|
@@ -4026,9 +4040,9 @@ var init_policy2 = __esm({
|
|
|
4026
4040
|
|
|
4027
4041
|
// packages/agent-runtime/src/opencode/model.ts
|
|
4028
4042
|
function parseOpencodeModel(model) {
|
|
4029
|
-
const
|
|
4030
|
-
if (
|
|
4031
|
-
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) };
|
|
4032
4046
|
}
|
|
4033
4047
|
var init_model2 = __esm({
|
|
4034
4048
|
"packages/agent-runtime/src/opencode/model.ts"() {
|
|
@@ -4297,10 +4311,10 @@ async function* parseSseStream(body, sessionId, signal) {
|
|
|
4297
4311
|
const { value, done } = await reader.read();
|
|
4298
4312
|
if (done) return;
|
|
4299
4313
|
buf += decoder.decode(value, { stream: true });
|
|
4300
|
-
let
|
|
4301
|
-
while ((
|
|
4302
|
-
const frame = buf.slice(0,
|
|
4303
|
-
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);
|
|
4304
4318
|
const data = frame.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trim()).join("");
|
|
4305
4319
|
if (!data) continue;
|
|
4306
4320
|
let ev;
|
|
@@ -5326,8 +5340,8 @@ var init_policy3 = __esm({
|
|
|
5326
5340
|
|
|
5327
5341
|
// packages/agent-runtime/src/codex/model.ts
|
|
5328
5342
|
function parseCodexModel(model) {
|
|
5329
|
-
const
|
|
5330
|
-
const bare =
|
|
5343
|
+
const sep3 = model.indexOf("/");
|
|
5344
|
+
const bare = sep3 === -1 || model.slice(0, sep3) !== "openai" ? model : model.slice(sep3 + 1);
|
|
5331
5345
|
return CODEX_DEFAULT_MODEL_BARES.has(bare) ? null : bare;
|
|
5332
5346
|
}
|
|
5333
5347
|
var CODEX_DEFAULT_MODEL_BARES;
|
|
@@ -7472,6 +7486,9 @@ var init_protocol = __esm({
|
|
|
7472
7486
|
// CT1684: the device's sandbox policy, validated by the launcher. Absent
|
|
7473
7487
|
// when the sandbox is off.
|
|
7474
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(),
|
|
7475
7492
|
logLevel: z16.string(),
|
|
7476
7493
|
// The launcher's connector-health observations, by runtime, as of the start.
|
|
7477
7494
|
connectorHealth: z16.record(z16.string(), runtimeHealthSchema),
|
|
@@ -7535,7 +7552,7 @@ function healthSnapshot(store) {
|
|
|
7535
7552
|
}
|
|
7536
7553
|
return out;
|
|
7537
7554
|
}
|
|
7538
|
-
function runnerSpec(opts, launch, duties, runnerId) {
|
|
7555
|
+
function runnerSpec(opts, launch, duties, runnerId, companionStores) {
|
|
7539
7556
|
return {
|
|
7540
7557
|
runnerId,
|
|
7541
7558
|
...opts.deviceId ? { deviceId: opts.deviceId } : {},
|
|
@@ -7560,6 +7577,7 @@ function runnerSpec(opts, launch, duties, runnerId) {
|
|
|
7560
7577
|
...launch.provider.bindAttempt ? { attemptState: true } : {},
|
|
7561
7578
|
...launch.provider.inheritsEnvironment === false ? { secretsFromLauncher: true } : {},
|
|
7562
7579
|
...launch.sandbox === "os" && launch.policy?.ok ? { sandboxPolicy: launch.policy.policy } : {},
|
|
7580
|
+
...launch.sandbox === "os" && companionStores ? { companionStores: [...companionStores] } : {},
|
|
7563
7581
|
logLevel: opts.log.level,
|
|
7564
7582
|
connectorHealth: healthSnapshot(launch.connectorHealth),
|
|
7565
7583
|
sessionWriteNoticeClaimed: duties.sessionWriteNoticeClaimed()
|
|
@@ -7758,6 +7776,7 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
|
7758
7776
|
if (v !== void 0) env[k] = v;
|
|
7759
7777
|
}
|
|
7760
7778
|
}
|
|
7779
|
+
const companionStores = launch.sandbox === "os" ? await launch.companionStores?.() : void 0;
|
|
7761
7780
|
const input = {
|
|
7762
7781
|
baseUrl: launch.runnerBaseUrl ?? opts.baseUrl,
|
|
7763
7782
|
credential: minted.credential,
|
|
@@ -7765,7 +7784,7 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
|
7765
7784
|
dispatch: payload,
|
|
7766
7785
|
turnId,
|
|
7767
7786
|
resumed: handleOpts.resumed,
|
|
7768
|
-
spec: runnerSpec(opts, launch, duties, minted.runnerId)
|
|
7787
|
+
spec: runnerSpec(opts, launch, duties, minted.runnerId, companionStores)
|
|
7769
7788
|
};
|
|
7770
7789
|
Object.assign(env, runnerEnv(input));
|
|
7771
7790
|
const running = launch.provider.exec(handle, launch.command, env, {
|
|
@@ -8998,10 +9017,17 @@ var init_turn_execution = __esm({
|
|
|
8998
9017
|
body
|
|
8999
9018
|
);
|
|
9000
9019
|
} catch (err) {
|
|
9001
|
-
|
|
9002
|
-
|
|
9003
|
-
|
|
9004
|
-
|
|
9020
|
+
if (isTurnEndedRefusal(err)) {
|
|
9021
|
+
turnLog.debug(
|
|
9022
|
+
{ ...apiErrorLogFields(err), turnId },
|
|
9023
|
+
"companion: settle refused; the turn had already ended"
|
|
9024
|
+
);
|
|
9025
|
+
} else {
|
|
9026
|
+
turnLog.warn(
|
|
9027
|
+
{ ...apiErrorLogFields(err), turnId },
|
|
9028
|
+
"Couldn't save the turn's result to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
|
|
9029
|
+
);
|
|
9030
|
+
}
|
|
9005
9031
|
}
|
|
9006
9032
|
const durationMs = Date.now() - startedAt;
|
|
9007
9033
|
this.supervisor.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
|
|
@@ -9809,10 +9835,17 @@ var init_turn_execution = __esm({
|
|
|
9809
9835
|
body
|
|
9810
9836
|
);
|
|
9811
9837
|
} catch (err) {
|
|
9812
|
-
|
|
9813
|
-
|
|
9814
|
-
|
|
9815
|
-
|
|
9838
|
+
if (isTurnEndedRefusal(err)) {
|
|
9839
|
+
turnLog.debug(
|
|
9840
|
+
{ ...apiErrorLogFields(err) },
|
|
9841
|
+
"companion: settle refused; the turn had already ended"
|
|
9842
|
+
);
|
|
9843
|
+
} else {
|
|
9844
|
+
turnLog.warn(
|
|
9845
|
+
{ ...apiErrorLogFields(err) },
|
|
9846
|
+
"Couldn't save the turn's result to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
|
|
9847
|
+
);
|
|
9848
|
+
}
|
|
9816
9849
|
}
|
|
9817
9850
|
this.supervisor.releaseAbort(turnId, abortController);
|
|
9818
9851
|
}
|
|
@@ -10238,6 +10271,7 @@ var init_git_credential = __esm({
|
|
|
10238
10271
|
});
|
|
10239
10272
|
|
|
10240
10273
|
// src/runner/harness-sandbox.ts
|
|
10274
|
+
import { execFile as execFile2 } from "child_process";
|
|
10241
10275
|
import { lookup } from "dns/promises";
|
|
10242
10276
|
import {
|
|
10243
10277
|
chmodSync as chmodSync6,
|
|
@@ -10251,7 +10285,7 @@ import {
|
|
|
10251
10285
|
import { createRequire as createRequire2 } from "module";
|
|
10252
10286
|
import { isIP } from "net";
|
|
10253
10287
|
import { homedir as homedir3, networkInterfaces, tmpdir as tmpdir3, userInfo as userInfo2 } from "os";
|
|
10254
|
-
import { dirname as dirname8, join as join18 } from "path";
|
|
10288
|
+
import { dirname as dirname8, join as join18, sep as sep2 } from "path";
|
|
10255
10289
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
10256
10290
|
import { SandboxManager } from "@anthropic-ai/sandbox-runtime";
|
|
10257
10291
|
function accountHome() {
|
|
@@ -10317,6 +10351,74 @@ function harnessRuntimePaths() {
|
|
|
10317
10351
|
function ownAddresses() {
|
|
10318
10352
|
return Object.values(networkInterfaces()).flat().flatMap((i) => i ? [i.address] : []);
|
|
10319
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
|
+
}
|
|
10320
10422
|
function expandHome(path, home) {
|
|
10321
10423
|
if (path === "~") return home;
|
|
10322
10424
|
return path.startsWith("~/") ? join18(home, path.slice(2)) : path;
|
|
@@ -10350,7 +10452,12 @@ function harnessSandboxConfig(input) {
|
|
|
10350
10452
|
const allowedDomains = [.../* @__PURE__ */ new Set([...cabane, ...models, ...reachable])];
|
|
10351
10453
|
const envGlobs = input.cwd ? [join18(input.cwd, "**/.env"), join18(input.cwd, "**/.env.*")] : [];
|
|
10352
10454
|
const secrets = [
|
|
10353
|
-
|
|
10455
|
+
...companionStoreDenies({
|
|
10456
|
+
homes,
|
|
10457
|
+
cabaneDir: input.cabaneDir,
|
|
10458
|
+
foundStores: input.foundStores,
|
|
10459
|
+
ownState: [claudeDir, codexDir]
|
|
10460
|
+
}),
|
|
10354
10461
|
...homes.flatMap((home) => SECRET_PATHS_UNDER_HOME.map((p) => join18(home, p))),
|
|
10355
10462
|
...envGlobs,
|
|
10356
10463
|
...expand(policy.filesystem?.denyRead)
|
|
@@ -10459,7 +10566,8 @@ async function prepareHarnessSandbox(input) {
|
|
|
10459
10566
|
runtime: input.runtime,
|
|
10460
10567
|
policy: input.policy,
|
|
10461
10568
|
credentials: input.credentials,
|
|
10462
|
-
runtimePaths: harnessRuntimePaths()
|
|
10569
|
+
runtimePaths: harnessRuntimePaths(),
|
|
10570
|
+
foundStores: input.companionStores ?? []
|
|
10463
10571
|
});
|
|
10464
10572
|
await SandboxManager.initialize(config);
|
|
10465
10573
|
if (input.policy.filesystem?.mode === "strict") {
|
|
@@ -10531,7 +10639,7 @@ exec ${shellQuote(script)} ${shellQuote(real)} "$@"
|
|
|
10531
10639
|
}
|
|
10532
10640
|
};
|
|
10533
10641
|
}
|
|
10534
|
-
var SECRET_PATHS_UNDER_HOME;
|
|
10642
|
+
var SECRET_PATHS_UNDER_HOME, STORE_WALK_TIMEOUT_MS, STORE_WALK_MAX_AGE_MS, CompanionStoreWatch;
|
|
10535
10643
|
var init_harness_sandbox = __esm({
|
|
10536
10644
|
"src/runner/harness-sandbox.ts"() {
|
|
10537
10645
|
"use strict";
|
|
@@ -10554,6 +10662,50 @@ var init_harness_sandbox = __esm({
|
|
|
10554
10662
|
".config/op",
|
|
10555
10663
|
".password-store"
|
|
10556
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
|
+
};
|
|
10557
10709
|
}
|
|
10558
10710
|
});
|
|
10559
10711
|
|
|
@@ -10985,6 +11137,7 @@ function runnerDispatcherOptions(input, log, emit, extras = {}) {
|
|
|
10985
11137
|
runtime,
|
|
10986
11138
|
policy: spec.sandboxPolicy ?? {},
|
|
10987
11139
|
credentials: input.sandboxCredentials ?? [],
|
|
11140
|
+
...spec.companionStores ? { companionStores: spec.companionStores } : {},
|
|
10988
11141
|
log: turnLog
|
|
10989
11142
|
});
|
|
10990
11143
|
if (sandbox) onSandbox?.(sandbox);
|
|
@@ -15059,6 +15212,7 @@ var CompanionSupervisor = class {
|
|
|
15059
15212
|
const summary = posture.join("; ");
|
|
15060
15213
|
if (policy.ok) this.log.info({ posture }, `runners: OS sandbox policy \u2014 ${summary}`);
|
|
15061
15214
|
else this.log.error({ posture }, `runners: ${summary}`);
|
|
15215
|
+
void this.companionStores().refresh();
|
|
15062
15216
|
}
|
|
15063
15217
|
const startupHarnessRefresh = this.refreshHarnessStatuses();
|
|
15064
15218
|
this.trackHeartbeat(startupHarnessRefresh);
|
|
@@ -15508,6 +15662,12 @@ var CompanionSupervisor = class {
|
|
|
15508
15662
|
this.parsedSandboxPolicy ??= parseSandboxPolicy(this.config.runners?.policy);
|
|
15509
15663
|
return this.parsedSandboxPolicy;
|
|
15510
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
|
+
}
|
|
15511
15671
|
runnerSandbox() {
|
|
15512
15672
|
if (this.config.runners?.sandbox !== "os" || this.dockerRunners()) return "none";
|
|
15513
15673
|
this.sandboxUsable ??= osSandboxUsable(this.log);
|
|
@@ -15628,7 +15788,8 @@ var CompanionSupervisor = class {
|
|
|
15628
15788
|
connectorHealth: this.connectorHealth,
|
|
15629
15789
|
locks: this.runnerLocks,
|
|
15630
15790
|
sandbox: this.runnerSandbox(),
|
|
15631
|
-
policy: this.sandboxPolicy()
|
|
15791
|
+
policy: this.sandboxPolicy(),
|
|
15792
|
+
companionStores: () => this.companionStores().stores()
|
|
15632
15793
|
}
|
|
15633
15794
|
} : {}
|
|
15634
15795
|
});
|
package/dist/runtime.js
CHANGED
|
@@ -2279,6 +2279,11 @@ function leaseRefusal(err) {
|
|
|
2279
2279
|
function isWriteFenceRefusal(err) {
|
|
2280
2280
|
return err instanceof ApiError && err.status === 409 && apiErrorCode(err) === "not_running";
|
|
2281
2281
|
}
|
|
2282
|
+
function isTurnEndedRefusal(err) {
|
|
2283
|
+
if (!(err instanceof ApiError) || err.status !== 401) return false;
|
|
2284
|
+
const body = err.body;
|
|
2285
|
+
return typeof body === "object" && body !== null && body.reason === "turn_ended";
|
|
2286
|
+
}
|
|
2282
2287
|
var ERROR_BODY_LOG_CAP = 2e3;
|
|
2283
2288
|
function describeErrorBody(body) {
|
|
2284
2289
|
if (body === void 0 || body === null) return void 0;
|
|
@@ -2469,6 +2474,15 @@ var CabaneApi = class {
|
|
|
2469
2474
|
progressed = true;
|
|
2470
2475
|
} catch (err) {
|
|
2471
2476
|
if (err instanceof ApiError && err.status >= 400 && err.status < 500) {
|
|
2477
|
+
if (entry.kind === "active-run" && isTurnEndedRefusal(err)) {
|
|
2478
|
+
this.opts.log?.debug(
|
|
2479
|
+
{ kind: entry.kind, turnId: entry.turnId, ...apiErrorLogFields(err) },
|
|
2480
|
+
"companion: queued settle refused; the turn had already ended"
|
|
2481
|
+
);
|
|
2482
|
+
outbox.remove(entry.turnId, entry.seq);
|
|
2483
|
+
progressed = true;
|
|
2484
|
+
continue;
|
|
2485
|
+
}
|
|
2472
2486
|
this.opts.log?.warn(
|
|
2473
2487
|
{ kind: entry.kind, turnId: entry.turnId, seq: entry.seq, ...apiErrorLogFields(err) },
|
|
2474
2488
|
"Cabane rejected a saved update; it could not be delivered. This companion may need an update: npm i -g @cabane/companion@latest"
|
|
@@ -3997,8 +4011,8 @@ function readThinking(runtimeOptions) {
|
|
|
3997
4011
|
var CLAUDE_CODE_DEFAULT_MODEL_BARE = "default";
|
|
3998
4012
|
function parseClaudeCodeModel(model) {
|
|
3999
4013
|
if (!model) return null;
|
|
4000
|
-
const
|
|
4001
|
-
const bare =
|
|
4014
|
+
const sep3 = model.indexOf("/");
|
|
4015
|
+
const bare = sep3 === -1 || model.slice(0, sep3) !== "anthropic" ? model : model.slice(sep3 + 1);
|
|
4002
4016
|
return bare === CLAUDE_CODE_DEFAULT_MODEL_BARE ? null : model;
|
|
4003
4017
|
}
|
|
4004
4018
|
|
|
@@ -5349,9 +5363,9 @@ function readOpencodeDialect(runtimeOptions) {
|
|
|
5349
5363
|
|
|
5350
5364
|
// packages/agent-runtime/src/opencode/model.ts
|
|
5351
5365
|
function parseOpencodeModel(model) {
|
|
5352
|
-
const
|
|
5353
|
-
if (
|
|
5354
|
-
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) };
|
|
5355
5369
|
}
|
|
5356
5370
|
|
|
5357
5371
|
// packages/agent-runtime/src/opencode/run-spec.ts
|
|
@@ -5603,10 +5617,10 @@ async function* parseSseStream(body, sessionId, signal) {
|
|
|
5603
5617
|
const { value, done } = await reader.read();
|
|
5604
5618
|
if (done) return;
|
|
5605
5619
|
buf += decoder.decode(value, { stream: true });
|
|
5606
|
-
let
|
|
5607
|
-
while ((
|
|
5608
|
-
const frame = buf.slice(0,
|
|
5609
|
-
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);
|
|
5610
5624
|
const data = frame.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trim()).join("");
|
|
5611
5625
|
if (!data) continue;
|
|
5612
5626
|
let ev;
|
|
@@ -6566,8 +6580,8 @@ function readCodexDialect(runtimeOptions) {
|
|
|
6566
6580
|
// packages/agent-runtime/src/codex/model.ts
|
|
6567
6581
|
var CODEX_DEFAULT_MODEL_BARES = /* @__PURE__ */ new Set(["default"]);
|
|
6568
6582
|
function parseCodexModel(model) {
|
|
6569
|
-
const
|
|
6570
|
-
const bare =
|
|
6583
|
+
const sep3 = model.indexOf("/");
|
|
6584
|
+
const bare = sep3 === -1 || model.slice(0, sep3) !== "openai" ? model : model.slice(sep3 + 1);
|
|
6571
6585
|
return CODEX_DEFAULT_MODEL_BARES.has(bare) ? null : bare;
|
|
6572
6586
|
}
|
|
6573
6587
|
|
|
@@ -8529,6 +8543,9 @@ var runnerSpecSchema = z16.object({
|
|
|
8529
8543
|
// CT1684: the device's sandbox policy, validated by the launcher. Absent
|
|
8530
8544
|
// when the sandbox is off.
|
|
8531
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(),
|
|
8532
8549
|
logLevel: z16.string(),
|
|
8533
8550
|
// The launcher's connector-health observations, by runtime, as of the start.
|
|
8534
8551
|
connectorHealth: z16.record(z16.string(), runtimeHealthSchema),
|
|
@@ -8620,7 +8637,7 @@ function healthSnapshot(store) {
|
|
|
8620
8637
|
}
|
|
8621
8638
|
return out;
|
|
8622
8639
|
}
|
|
8623
|
-
function runnerSpec(opts, launch, duties, runnerId) {
|
|
8640
|
+
function runnerSpec(opts, launch, duties, runnerId, companionStores) {
|
|
8624
8641
|
return {
|
|
8625
8642
|
runnerId,
|
|
8626
8643
|
...opts.deviceId ? { deviceId: opts.deviceId } : {},
|
|
@@ -8645,6 +8662,7 @@ function runnerSpec(opts, launch, duties, runnerId) {
|
|
|
8645
8662
|
...launch.provider.bindAttempt ? { attemptState: true } : {},
|
|
8646
8663
|
...launch.provider.inheritsEnvironment === false ? { secretsFromLauncher: true } : {},
|
|
8647
8664
|
...launch.sandbox === "os" && launch.policy?.ok ? { sandboxPolicy: launch.policy.policy } : {},
|
|
8665
|
+
...launch.sandbox === "os" && companionStores ? { companionStores: [...companionStores] } : {},
|
|
8648
8666
|
logLevel: opts.log.level,
|
|
8649
8667
|
connectorHealth: healthSnapshot(launch.connectorHealth),
|
|
8650
8668
|
sessionWriteNoticeClaimed: duties.sessionWriteNoticeClaimed()
|
|
@@ -8851,6 +8869,7 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
|
8851
8869
|
if (v !== void 0) env[k] = v;
|
|
8852
8870
|
}
|
|
8853
8871
|
}
|
|
8872
|
+
const companionStores = launch.sandbox === "os" ? await launch.companionStores?.() : void 0;
|
|
8854
8873
|
const input = {
|
|
8855
8874
|
baseUrl: launch.runnerBaseUrl ?? opts.baseUrl,
|
|
8856
8875
|
credential: minted.credential,
|
|
@@ -8858,7 +8877,7 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
|
8858
8877
|
dispatch: payload,
|
|
8859
8878
|
turnId,
|
|
8860
8879
|
resumed: handleOpts.resumed,
|
|
8861
|
-
spec: runnerSpec(opts, launch, duties, minted.runnerId)
|
|
8880
|
+
spec: runnerSpec(opts, launch, duties, minted.runnerId, companionStores)
|
|
8862
8881
|
};
|
|
8863
8882
|
Object.assign(env, runnerEnv(input));
|
|
8864
8883
|
const running = launch.provider.exec(handle, launch.command, env, {
|
|
@@ -9984,10 +10003,17 @@ var TurnExecution = class {
|
|
|
9984
10003
|
body
|
|
9985
10004
|
);
|
|
9986
10005
|
} catch (err) {
|
|
9987
|
-
|
|
9988
|
-
|
|
9989
|
-
|
|
9990
|
-
|
|
10006
|
+
if (isTurnEndedRefusal(err)) {
|
|
10007
|
+
turnLog.debug(
|
|
10008
|
+
{ ...apiErrorLogFields(err), turnId },
|
|
10009
|
+
"companion: settle refused; the turn had already ended"
|
|
10010
|
+
);
|
|
10011
|
+
} else {
|
|
10012
|
+
turnLog.warn(
|
|
10013
|
+
{ ...apiErrorLogFields(err), turnId },
|
|
10014
|
+
"Couldn't save the turn's result to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
|
|
10015
|
+
);
|
|
10016
|
+
}
|
|
9991
10017
|
}
|
|
9992
10018
|
const durationMs = Date.now() - startedAt;
|
|
9993
10019
|
this.supervisor.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
|
|
@@ -10795,10 +10821,17 @@ var TurnExecution = class {
|
|
|
10795
10821
|
body
|
|
10796
10822
|
);
|
|
10797
10823
|
} catch (err) {
|
|
10798
|
-
|
|
10799
|
-
|
|
10800
|
-
|
|
10801
|
-
|
|
10824
|
+
if (isTurnEndedRefusal(err)) {
|
|
10825
|
+
turnLog.debug(
|
|
10826
|
+
{ ...apiErrorLogFields(err) },
|
|
10827
|
+
"companion: settle refused; the turn had already ended"
|
|
10828
|
+
);
|
|
10829
|
+
} else {
|
|
10830
|
+
turnLog.warn(
|
|
10831
|
+
{ ...apiErrorLogFields(err) },
|
|
10832
|
+
"Couldn't save the turn's result to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
|
|
10833
|
+
);
|
|
10834
|
+
}
|
|
10802
10835
|
}
|
|
10803
10836
|
this.supervisor.releaseAbort(turnId, abortController);
|
|
10804
10837
|
}
|
|
@@ -12423,6 +12456,7 @@ var DeviceLocks = class {
|
|
|
12423
12456
|
};
|
|
12424
12457
|
|
|
12425
12458
|
// src/runner/harness-sandbox.ts
|
|
12459
|
+
import { execFile as execFile2 } from "child_process";
|
|
12426
12460
|
import { lookup } from "dns/promises";
|
|
12427
12461
|
import {
|
|
12428
12462
|
chmodSync as chmodSync6,
|
|
@@ -12436,9 +12470,111 @@ import {
|
|
|
12436
12470
|
import { createRequire as createRequire2 } from "module";
|
|
12437
12471
|
import { isIP } from "net";
|
|
12438
12472
|
import { homedir as homedir3, networkInterfaces, tmpdir as tmpdir3, userInfo as userInfo2 } from "os";
|
|
12439
|
-
import { dirname as dirname8, join as join18 } from "path";
|
|
12473
|
+
import { dirname as dirname8, join as join18, sep as sep2 } from "path";
|
|
12440
12474
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
12441
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
|
+
}
|
|
12442
12578
|
function osSandboxUsable(log) {
|
|
12443
12579
|
if (!SandboxManager.isSupportedPlatform()) {
|
|
12444
12580
|
log.warn(
|
|
@@ -12971,6 +13107,7 @@ var CompanionSupervisor = class {
|
|
|
12971
13107
|
const summary = posture.join("; ");
|
|
12972
13108
|
if (policy.ok) this.log.info({ posture }, `runners: OS sandbox policy \u2014 ${summary}`);
|
|
12973
13109
|
else this.log.error({ posture }, `runners: ${summary}`);
|
|
13110
|
+
void this.companionStores().refresh();
|
|
12974
13111
|
}
|
|
12975
13112
|
const startupHarnessRefresh = this.refreshHarnessStatuses();
|
|
12976
13113
|
this.trackHeartbeat(startupHarnessRefresh);
|
|
@@ -13420,6 +13557,12 @@ var CompanionSupervisor = class {
|
|
|
13420
13557
|
this.parsedSandboxPolicy ??= parseSandboxPolicy(this.config.runners?.policy);
|
|
13421
13558
|
return this.parsedSandboxPolicy;
|
|
13422
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
|
+
}
|
|
13423
13566
|
runnerSandbox() {
|
|
13424
13567
|
if (this.config.runners?.sandbox !== "os" || this.dockerRunners()) return "none";
|
|
13425
13568
|
this.sandboxUsable ??= osSandboxUsable(this.log);
|
|
@@ -13540,7 +13683,8 @@ var CompanionSupervisor = class {
|
|
|
13540
13683
|
connectorHealth: this.connectorHealth,
|
|
13541
13684
|
locks: this.runnerLocks,
|
|
13542
13685
|
sandbox: this.runnerSandbox(),
|
|
13543
|
-
policy: this.sandboxPolicy()
|
|
13686
|
+
policy: this.sandboxPolicy(),
|
|
13687
|
+
companionStores: () => this.companionStores().stores()
|
|
13544
13688
|
}
|
|
13545
13689
|
} : {}
|
|
13546
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",
|