@cabane/companion 0.6.105 → 0.6.107
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/dist/cli.js +208 -50
- package/dist/pairing-config.js +9 -1
- package/dist/runtime.js +202 -44
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -93,7 +93,15 @@ var prepareHookSchema = z.object({
|
|
|
93
93
|
//
|
|
94
94
|
// Default OFF, and deliberately: an operator hook that provisions
|
|
95
95
|
// unconditionally must not start running on every turn because it upgraded.
|
|
96
|
-
validateCached: z.boolean().optional()
|
|
96
|
+
validateCached: z.boolean().optional(),
|
|
97
|
+
// Independent operator-owned reporter. Receives sanitized failure metadata
|
|
98
|
+
// on stdin; never re-enters this hook and cannot change the turn outcome.
|
|
99
|
+
failureReporter: z.object({
|
|
100
|
+
command: z.string().min(1),
|
|
101
|
+
args: z.array(z.string()).optional(),
|
|
102
|
+
env: z.record(z.string(), z.string()).optional(),
|
|
103
|
+
timeoutMs: z.number().int().positive().max(3e4).optional()
|
|
104
|
+
}).strict().optional()
|
|
97
105
|
}).strict();
|
|
98
106
|
var DEFAULT_TIMEOUT_MS = 10 * 6e4;
|
|
99
107
|
var prepareResultSchema = z.object({
|
|
@@ -1769,8 +1777,8 @@ function requestEnrollmentCode(baseUrl, opts = {}) {
|
|
|
1769
1777
|
...opts.label ? { label: opts.label } : {}
|
|
1770
1778
|
});
|
|
1771
1779
|
}
|
|
1772
|
-
function deviceLabelFromHostname(
|
|
1773
|
-
const trimmed =
|
|
1780
|
+
function deviceLabelFromHostname(hostname5) {
|
|
1781
|
+
const trimmed = hostname5.trim().replace(/\.local$/i, "");
|
|
1774
1782
|
if (trimmed.length === 0) return void 0;
|
|
1775
1783
|
return trimmed.slice(0, 120);
|
|
1776
1784
|
}
|
|
@@ -2097,7 +2105,7 @@ async function logs(opts = {}) {
|
|
|
2097
2105
|
}
|
|
2098
2106
|
|
|
2099
2107
|
// src/commands/start.ts
|
|
2100
|
-
import { hostname as
|
|
2108
|
+
import { hostname as hostname4 } from "os";
|
|
2101
2109
|
|
|
2102
2110
|
// src/runtime.ts
|
|
2103
2111
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
@@ -2662,7 +2670,7 @@ async function warnAboutHarnessReadiness(cfg, deps = {}) {
|
|
|
2662
2670
|
}
|
|
2663
2671
|
|
|
2664
2672
|
// src/supervisor.ts
|
|
2665
|
-
import { hostname as
|
|
2673
|
+
import { hostname as hostname3 } from "os";
|
|
2666
2674
|
|
|
2667
2675
|
// packages/agent-runtime/src/failure.ts
|
|
2668
2676
|
import { z as z4 } from "zod";
|
|
@@ -7139,6 +7147,67 @@ function isStringRecord2(v) {
|
|
|
7139
7147
|
import {
|
|
7140
7148
|
Codex
|
|
7141
7149
|
} from "@openai/codex-sdk";
|
|
7150
|
+
|
|
7151
|
+
// packages/agent-runtime/src/codex/config-overrides.ts
|
|
7152
|
+
var TOML_BARE_KEY = /^[A-Za-z0-9_-]+$/;
|
|
7153
|
+
function formatKey(key) {
|
|
7154
|
+
return TOML_BARE_KEY.test(key) ? key : JSON.stringify(key);
|
|
7155
|
+
}
|
|
7156
|
+
function isTable(value) {
|
|
7157
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
7158
|
+
}
|
|
7159
|
+
function tomlValue(value, path) {
|
|
7160
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
7161
|
+
if (typeof value === "number") {
|
|
7162
|
+
if (!Number.isFinite(value)) {
|
|
7163
|
+
throw new Error(`Codex config override at ${path} must be a finite number`);
|
|
7164
|
+
}
|
|
7165
|
+
return `${value}`;
|
|
7166
|
+
}
|
|
7167
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
7168
|
+
if (Array.isArray(value)) {
|
|
7169
|
+
return `[${value.map((item, i) => tomlValue(item, `${path}[${i}]`)).join(", ")}]`;
|
|
7170
|
+
}
|
|
7171
|
+
if (isTable(value)) {
|
|
7172
|
+
const parts = [];
|
|
7173
|
+
for (const [key, child] of Object.entries(value)) {
|
|
7174
|
+
if (!key) throw new Error("Codex config override keys must be non-empty strings");
|
|
7175
|
+
if (child === void 0) continue;
|
|
7176
|
+
parts.push(`${formatKey(key)} = ${tomlValue(child, `${path}.${key}`)}`);
|
|
7177
|
+
}
|
|
7178
|
+
return `{${parts.join(", ")}}`;
|
|
7179
|
+
}
|
|
7180
|
+
throw new Error(`Codex config override at ${path} has an unsupported type`);
|
|
7181
|
+
}
|
|
7182
|
+
function walk(table, prefix, out) {
|
|
7183
|
+
const entries = Object.entries(table).filter(([, child]) => child !== void 0);
|
|
7184
|
+
if (entries.length === 0) {
|
|
7185
|
+
if (prefix) out.push(`${prefix}={}`);
|
|
7186
|
+
return;
|
|
7187
|
+
}
|
|
7188
|
+
for (const [key] of entries) {
|
|
7189
|
+
if (!key) throw new Error("Codex config override keys must be non-empty strings");
|
|
7190
|
+
}
|
|
7191
|
+
if (entries.some(([key]) => !TOML_BARE_KEY.test(key))) {
|
|
7192
|
+
if (!prefix) {
|
|
7193
|
+
throw new Error("Codex config override has a non-bare key at the root");
|
|
7194
|
+
}
|
|
7195
|
+
out.push(`${prefix}=${tomlValue(Object.fromEntries(entries), prefix)}`);
|
|
7196
|
+
return;
|
|
7197
|
+
}
|
|
7198
|
+
for (const [key, child] of entries) {
|
|
7199
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
7200
|
+
if (isTable(child)) walk(child, path, out);
|
|
7201
|
+
else out.push(`${path}=${tomlValue(child, path)}`);
|
|
7202
|
+
}
|
|
7203
|
+
}
|
|
7204
|
+
function buildConfigOverrides(config) {
|
|
7205
|
+
const out = [];
|
|
7206
|
+
walk(config, "", out);
|
|
7207
|
+
return out;
|
|
7208
|
+
}
|
|
7209
|
+
|
|
7210
|
+
// packages/agent-runtime/src/codex/transport.ts
|
|
7142
7211
|
function buildSdkThreadOptions(spec) {
|
|
7143
7212
|
return {
|
|
7144
7213
|
...spec.model ? { model: spec.model } : {},
|
|
@@ -7155,11 +7224,17 @@ function createSdkCodexTransport(opts = {}) {
|
|
|
7155
7224
|
const codexOptions = {
|
|
7156
7225
|
...opts.apiKey ? { apiKey: opts.apiKey } : {},
|
|
7157
7226
|
...opts.codexPathOverride ? { codexPathOverride: opts.codexPathOverride } : {},
|
|
7158
|
-
// The `--config` overrides (MCP servers + rmcp flag).
|
|
7159
|
-
//
|
|
7160
|
-
//
|
|
7161
|
-
// `
|
|
7162
|
-
|
|
7227
|
+
// The `--config` overrides (MCP servers + rmcp flag). CT1501: rendered to
|
|
7228
|
+
// finished `--config` strings by US and passed through `configOverrides`,
|
|
7229
|
+
// the SDK's raw passthrough — NOT through `config`, which would flatten
|
|
7230
|
+
// them itself. A user MCP server named `my.server` cannot go in an
|
|
7231
|
+
// override's dotted path at all (the CLI splits the path on `.` and
|
|
7232
|
+
// ignores TOML quoting), so it has to ride inside an inline-table value,
|
|
7233
|
+
// and the SDK's flattener cannot be steered into producing one. This is
|
|
7234
|
+
// also what makes the PUBLISHED companion correct, where the vendored
|
|
7235
|
+
// patch never reached — and why that patch is gone. See
|
|
7236
|
+
// `config-overrides.ts`.
|
|
7237
|
+
configOverrides: buildConfigOverrides(spec.config)
|
|
7163
7238
|
};
|
|
7164
7239
|
const codex = new Codex(codexOptions);
|
|
7165
7240
|
const threadOptions = buildSdkThreadOptions(spec);
|
|
@@ -8537,6 +8612,68 @@ function readOutboxFloor(read, turnId, log) {
|
|
|
8537
8612
|
}
|
|
8538
8613
|
}
|
|
8539
8614
|
|
|
8615
|
+
// src/prepare-failure.ts
|
|
8616
|
+
import { spawn as spawn4 } from "child_process";
|
|
8617
|
+
import { hostname as hostname2 } from "os";
|
|
8618
|
+
function prepareFailureCause(error) {
|
|
8619
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8620
|
+
if (/timed out/i.test(message)) return "prepare hook timed out";
|
|
8621
|
+
if (/MODULE_NOT_FOUND|Cannot find (?:module|package)/.test(message))
|
|
8622
|
+
return "prepare hook module missing; check the configured command and its installation for this device";
|
|
8623
|
+
if (/not valid JSON|JSON must carry|produced no output/.test(message))
|
|
8624
|
+
return "prepare hook returned malformed output; expected a cwd path or JSON with a non-empty cwd";
|
|
8625
|
+
if (/ENOENT/.test(message)) return "prepare hook executable missing (ENOENT)";
|
|
8626
|
+
if (/EACCES/.test(message)) return "prepare hook executable is not accessible (EACCES)";
|
|
8627
|
+
const exit = message.match(/exited with code (\d+)/)?.[1];
|
|
8628
|
+
return exit ? `prepare hook exited with code ${exit}` : "prepare hook failed; inspect the device-local hook log";
|
|
8629
|
+
}
|
|
8630
|
+
async function runPrepareWithReporting(runner, hook, input, context, reportError) {
|
|
8631
|
+
try {
|
|
8632
|
+
return await runner(hook, input);
|
|
8633
|
+
} catch (error) {
|
|
8634
|
+
if (hook.failureReporter) {
|
|
8635
|
+
try {
|
|
8636
|
+
await reportFailure(hook.failureReporter, {
|
|
8637
|
+
workspaceId: input.workspaceId,
|
|
8638
|
+
conversationId: input.conversationId,
|
|
8639
|
+
agentId: input.agentId,
|
|
8640
|
+
agentUsername: input.agentUsername,
|
|
8641
|
+
turnId: context.turnId,
|
|
8642
|
+
deviceId: context.deviceId ?? hostname2(),
|
|
8643
|
+
cause: prepareFailureCause(error)
|
|
8644
|
+
});
|
|
8645
|
+
} catch {
|
|
8646
|
+
reportError();
|
|
8647
|
+
}
|
|
8648
|
+
}
|
|
8649
|
+
throw error;
|
|
8650
|
+
}
|
|
8651
|
+
}
|
|
8652
|
+
function reportFailure(config, body) {
|
|
8653
|
+
return new Promise((resolve2, reject) => {
|
|
8654
|
+
const child = spawn4(config.command, config.args ?? [], {
|
|
8655
|
+
stdio: ["pipe", "ignore", "ignore"],
|
|
8656
|
+
env: { ...process.env, ...config.env }
|
|
8657
|
+
});
|
|
8658
|
+
const timer = setTimeout(() => {
|
|
8659
|
+
child.kill("SIGKILL");
|
|
8660
|
+
reject(new Error("reporter timeout"));
|
|
8661
|
+
}, config.timeoutMs ?? 15e3);
|
|
8662
|
+
child.stdin.on("error", () => {
|
|
8663
|
+
});
|
|
8664
|
+
child.once("error", () => {
|
|
8665
|
+
clearTimeout(timer);
|
|
8666
|
+
reject(new Error("reporter failed to start"));
|
|
8667
|
+
});
|
|
8668
|
+
child.once("close", (code) => {
|
|
8669
|
+
clearTimeout(timer);
|
|
8670
|
+
if (code === 0) resolve2();
|
|
8671
|
+
else reject(new Error("reporter failed"));
|
|
8672
|
+
});
|
|
8673
|
+
child.stdin.end(JSON.stringify(body));
|
|
8674
|
+
});
|
|
8675
|
+
}
|
|
8676
|
+
|
|
8540
8677
|
// src/prepared.ts
|
|
8541
8678
|
import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, rmSync as rmSync5, writeFileSync as writeFileSync6, existsSync as existsSync8 } from "fs";
|
|
8542
8679
|
import { join as join12 } from "path";
|
|
@@ -8767,7 +8904,7 @@ function pruneOld(dir2, retain) {
|
|
|
8767
8904
|
}
|
|
8768
8905
|
|
|
8769
8906
|
// src/turn-containment.ts
|
|
8770
|
-
import { execFile, execFileSync, spawn as
|
|
8907
|
+
import { execFile, execFileSync, spawn as spawn5 } from "child_process";
|
|
8771
8908
|
import { readFileSync as readFileSync8 } from "fs";
|
|
8772
8909
|
import { promisify } from "util";
|
|
8773
8910
|
var execFileAsync = promisify(execFile);
|
|
@@ -8849,7 +8986,7 @@ function createTurnContainment(turnId, deps = {}) {
|
|
|
8849
8986
|
command: options.command
|
|
8850
8987
|
});
|
|
8851
8988
|
return asContained(
|
|
8852
|
-
|
|
8989
|
+
spawn5(
|
|
8853
8990
|
"systemd-run",
|
|
8854
8991
|
[
|
|
8855
8992
|
"--user",
|
|
@@ -8876,7 +9013,7 @@ function createTurnContainment(turnId, deps = {}) {
|
|
|
8876
9013
|
);
|
|
8877
9014
|
}
|
|
8878
9015
|
function spawnInGroup(options) {
|
|
8879
|
-
const child =
|
|
9016
|
+
const child = spawn5(options.command, options.args, {
|
|
8880
9017
|
...options.cwd ? { cwd: options.cwd } : {},
|
|
8881
9018
|
env: options.env,
|
|
8882
9019
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -9578,18 +9715,28 @@ var TurnExecution = class {
|
|
|
9578
9715
|
if (prepareHook.validateCached) {
|
|
9579
9716
|
const runHook = this.opts.prepareHookRunner ?? runPrepareHook;
|
|
9580
9717
|
try {
|
|
9581
|
-
await
|
|
9582
|
-
|
|
9583
|
-
|
|
9584
|
-
|
|
9585
|
-
|
|
9586
|
-
|
|
9587
|
-
|
|
9588
|
-
|
|
9589
|
-
|
|
9590
|
-
|
|
9591
|
-
|
|
9592
|
-
|
|
9718
|
+
await runPrepareWithReporting(
|
|
9719
|
+
runHook,
|
|
9720
|
+
prepareHook,
|
|
9721
|
+
{
|
|
9722
|
+
workspaceId,
|
|
9723
|
+
conversationId: payload.conversationId,
|
|
9724
|
+
agentId: payload.agentId,
|
|
9725
|
+
agentUsername: this.opts.agentUsername,
|
|
9726
|
+
runtime: this.turnContext.runtime,
|
|
9727
|
+
hostAccess: this.turnContext.policy.hostFs,
|
|
9728
|
+
triggerEntryPaths: this.turnContext.conversation.triggerEntryPaths ?? [],
|
|
9729
|
+
title: this.turnContext.conversation.title,
|
|
9730
|
+
messageBody: this.turnContext.message.body,
|
|
9731
|
+
prepared: cached2
|
|
9732
|
+
},
|
|
9733
|
+
{ turnId, deviceId: this.opts.deviceId },
|
|
9734
|
+
() => {
|
|
9735
|
+
turnLog.error(
|
|
9736
|
+
"dispatcher: prepare failure reporter failed; original failure preserved"
|
|
9737
|
+
);
|
|
9738
|
+
}
|
|
9739
|
+
);
|
|
9593
9740
|
} catch (err) {
|
|
9594
9741
|
const reason = err instanceof Error ? err.message : String(err);
|
|
9595
9742
|
turnLog.debug({ err: reason }, "dispatcher: prepare hook rejected a prepared turn");
|
|
@@ -9625,24 +9772,34 @@ var TurnExecution = class {
|
|
|
9625
9772
|
preparingTimer.unref?.();
|
|
9626
9773
|
const runHook = this.opts.prepareHookRunner ?? runPrepareHook;
|
|
9627
9774
|
try {
|
|
9628
|
-
const result = await
|
|
9629
|
-
|
|
9630
|
-
|
|
9631
|
-
|
|
9632
|
-
|
|
9633
|
-
|
|
9634
|
-
|
|
9635
|
-
|
|
9636
|
-
|
|
9637
|
-
|
|
9638
|
-
|
|
9639
|
-
|
|
9640
|
-
|
|
9641
|
-
|
|
9642
|
-
|
|
9643
|
-
|
|
9644
|
-
|
|
9645
|
-
|
|
9775
|
+
const result = await runPrepareWithReporting(
|
|
9776
|
+
runHook,
|
|
9777
|
+
prepareHook,
|
|
9778
|
+
{
|
|
9779
|
+
workspaceId,
|
|
9780
|
+
conversationId: payload.conversationId,
|
|
9781
|
+
agentId: payload.agentId,
|
|
9782
|
+
agentUsername: this.opts.agentUsername,
|
|
9783
|
+
runtime: this.turnContext.runtime,
|
|
9784
|
+
hostAccess: this.turnContext.policy.hostFs,
|
|
9785
|
+
// CT317/CT319: the trigger message's referenced-entry paths — what the
|
|
9786
|
+
// tasker prepare hook keys its per-task env off. Defaults to `[]` for
|
|
9787
|
+
// an older API. The conversation anchor is gone (CT319).
|
|
9788
|
+
triggerEntryPaths: this.turnContext.conversation.triggerEntryPaths ?? [],
|
|
9789
|
+
title: this.turnContext.conversation.title,
|
|
9790
|
+
// CT943: the dispatching message's text — where an `env:` directive
|
|
9791
|
+
// rides. The server has always sent the trigger body on the turn
|
|
9792
|
+
// context (for the live feed); this is the first thing to read it as
|
|
9793
|
+
// an INPUT, so a dispatch can ask for its environment in words.
|
|
9794
|
+
messageBody: this.turnContext.message.body
|
|
9795
|
+
},
|
|
9796
|
+
{ turnId, deviceId: this.opts.deviceId },
|
|
9797
|
+
() => {
|
|
9798
|
+
turnLog.error(
|
|
9799
|
+
"dispatcher: prepare failure reporter failed; original failure preserved"
|
|
9800
|
+
);
|
|
9801
|
+
}
|
|
9802
|
+
);
|
|
9646
9803
|
clearTimeout(preparingTimer);
|
|
9647
9804
|
if (preparingStarted) reportPreparing("done");
|
|
9648
9805
|
writePrepared(workspaceId, payload.conversationId, payload.agentId, result);
|
|
@@ -10803,7 +10960,7 @@ var CompanionSupervisor = class {
|
|
|
10803
10960
|
async start() {
|
|
10804
10961
|
this.log.info(
|
|
10805
10962
|
{ protocolVersion: TURN_PROTOCOL_VERSION, version: COMPANION_VERSION },
|
|
10806
|
-
`Companion ${COMPANION_VERSION} starting on ${this.config.deviceLabel ??
|
|
10963
|
+
`Companion ${COMPANION_VERSION} starting on ${this.config.deviceLabel ?? hostname3()}`
|
|
10807
10964
|
);
|
|
10808
10965
|
const startupHarnessRefresh = this.refreshHarnessStatuses();
|
|
10809
10966
|
this.trackHeartbeat(startupHarnessRefresh);
|
|
@@ -10822,7 +10979,7 @@ var CompanionSupervisor = class {
|
|
|
10822
10979
|
token: this.config.deviceToken,
|
|
10823
10980
|
log: this.log,
|
|
10824
10981
|
lastEventId: null,
|
|
10825
|
-
onOpen: () => this.log.info(`Connected to Cabane as ${this.hub.statusJson().device_label ??
|
|
10982
|
+
onOpen: () => this.log.info(`Connected to Cabane as ${this.hub.statusJson().device_label ?? hostname3()}`),
|
|
10826
10983
|
onMessage: async (ev) => {
|
|
10827
10984
|
if (ev.event === "assignments_changed") {
|
|
10828
10985
|
if (this.refreshing && this.inFlightRefresh) await this.inFlightRefresh;
|
|
@@ -11238,6 +11395,7 @@ var CompanionSupervisor = class {
|
|
|
11238
11395
|
});
|
|
11239
11396
|
if (this.dispatcherFactory) return this.dispatcherFactory({ ...ctx, local });
|
|
11240
11397
|
return new Dispatcher({
|
|
11398
|
+
deviceId: this.deviceId ?? void 0,
|
|
11241
11399
|
api: ctx.api,
|
|
11242
11400
|
baseUrl: ctx.baseUrl,
|
|
11243
11401
|
workspaceId: ctx.workspaceId,
|
|
@@ -11885,9 +12043,9 @@ async function waitForBoundedHeartbeat(heartbeat) {
|
|
|
11885
12043
|
}
|
|
11886
12044
|
function defaultReexec() {
|
|
11887
12045
|
clearRuntimeState();
|
|
11888
|
-
void import("child_process").then(({ spawn:
|
|
12046
|
+
void import("child_process").then(({ spawn: spawn7 }) => {
|
|
11889
12047
|
try {
|
|
11890
|
-
const child =
|
|
12048
|
+
const child = spawn7(process.execPath, process.argv.slice(1), {
|
|
11891
12049
|
stdio: "inherit",
|
|
11892
12050
|
detached: false
|
|
11893
12051
|
});
|
|
@@ -12142,7 +12300,7 @@ async function closeSurfaces(control, dashboard) {
|
|
|
12142
12300
|
}
|
|
12143
12301
|
|
|
12144
12302
|
// src/commands/daemon.ts
|
|
12145
|
-
import { spawn as
|
|
12303
|
+
import { spawn as spawn6 } from "child_process";
|
|
12146
12304
|
import { closeSync as closeSync3, mkdirSync as mkdirSync12, openSync as openSync3 } from "fs";
|
|
12147
12305
|
|
|
12148
12306
|
// src/cli-entry.ts
|
|
@@ -12254,7 +12412,7 @@ function defaultSpawnDetached(args) {
|
|
|
12254
12412
|
mkdirSync12(cabaneDir(), { recursive: true });
|
|
12255
12413
|
const logFd = openSync3(companionLogPath(), "a");
|
|
12256
12414
|
try {
|
|
12257
|
-
return
|
|
12415
|
+
return spawn6(process.execPath, [cliPath, ...args], {
|
|
12258
12416
|
detached: true,
|
|
12259
12417
|
stdio: ["ignore", logFd, logFd],
|
|
12260
12418
|
env: { ...process.env, CABANE_COMPANION_DAEMON: "1" }
|
|
@@ -12422,7 +12580,7 @@ function prePairFoundPhrase(harness) {
|
|
|
12422
12580
|
}
|
|
12423
12581
|
async function pairHere(opts, interactive) {
|
|
12424
12582
|
const baseUrl = resolvePairBaseUrl(opts.server);
|
|
12425
|
-
const label = deviceLabelFromHostname(
|
|
12583
|
+
const label = deviceLabelFromHostname(hostname4());
|
|
12426
12584
|
const aborter = new AbortController();
|
|
12427
12585
|
const onSigint = () => aborter.abort();
|
|
12428
12586
|
process.on("SIGINT", onSigint);
|
package/dist/pairing-config.js
CHANGED
|
@@ -76,7 +76,15 @@ var prepareHookSchema = z.object({
|
|
|
76
76
|
//
|
|
77
77
|
// Default OFF, and deliberately: an operator hook that provisions
|
|
78
78
|
// unconditionally must not start running on every turn because it upgraded.
|
|
79
|
-
validateCached: z.boolean().optional()
|
|
79
|
+
validateCached: z.boolean().optional(),
|
|
80
|
+
// Independent operator-owned reporter. Receives sanitized failure metadata
|
|
81
|
+
// on stdin; never re-enters this hook and cannot change the turn outcome.
|
|
82
|
+
failureReporter: z.object({
|
|
83
|
+
command: z.string().min(1),
|
|
84
|
+
args: z.array(z.string()).optional(),
|
|
85
|
+
env: z.record(z.string(), z.string()).optional(),
|
|
86
|
+
timeoutMs: z.number().int().positive().max(3e4).optional()
|
|
87
|
+
}).strict().optional()
|
|
80
88
|
}).strict();
|
|
81
89
|
var DEFAULT_TIMEOUT_MS = 10 * 6e4;
|
|
82
90
|
var prepareResultSchema = z.object({
|
package/dist/runtime.js
CHANGED
|
@@ -85,7 +85,15 @@ var prepareHookSchema = z.object({
|
|
|
85
85
|
//
|
|
86
86
|
// Default OFF, and deliberately: an operator hook that provisions
|
|
87
87
|
// unconditionally must not start running on every turn because it upgraded.
|
|
88
|
-
validateCached: z.boolean().optional()
|
|
88
|
+
validateCached: z.boolean().optional(),
|
|
89
|
+
// Independent operator-owned reporter. Receives sanitized failure metadata
|
|
90
|
+
// on stdin; never re-enters this hook and cannot change the turn outcome.
|
|
91
|
+
failureReporter: z.object({
|
|
92
|
+
command: z.string().min(1),
|
|
93
|
+
args: z.array(z.string()).optional(),
|
|
94
|
+
env: z.record(z.string(), z.string()).optional(),
|
|
95
|
+
timeoutMs: z.number().int().positive().max(3e4).optional()
|
|
96
|
+
}).strict().optional()
|
|
89
97
|
}).strict();
|
|
90
98
|
var DEFAULT_TIMEOUT_MS = 10 * 6e4;
|
|
91
99
|
var prepareResultSchema = z.object({
|
|
@@ -1996,7 +2004,7 @@ async function verifyRuntime(state, requestImpl = controlRequest) {
|
|
|
1996
2004
|
}
|
|
1997
2005
|
|
|
1998
2006
|
// src/supervisor.ts
|
|
1999
|
-
import { hostname } from "os";
|
|
2007
|
+
import { hostname as hostname2 } from "os";
|
|
2000
2008
|
|
|
2001
2009
|
// packages/agent-runtime/src/failure.ts
|
|
2002
2010
|
import { z as z3 } from "zod";
|
|
@@ -6552,6 +6560,67 @@ function isStringRecord2(v) {
|
|
|
6552
6560
|
import {
|
|
6553
6561
|
Codex
|
|
6554
6562
|
} from "@openai/codex-sdk";
|
|
6563
|
+
|
|
6564
|
+
// packages/agent-runtime/src/codex/config-overrides.ts
|
|
6565
|
+
var TOML_BARE_KEY = /^[A-Za-z0-9_-]+$/;
|
|
6566
|
+
function formatKey(key) {
|
|
6567
|
+
return TOML_BARE_KEY.test(key) ? key : JSON.stringify(key);
|
|
6568
|
+
}
|
|
6569
|
+
function isTable(value) {
|
|
6570
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
6571
|
+
}
|
|
6572
|
+
function tomlValue(value, path) {
|
|
6573
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
6574
|
+
if (typeof value === "number") {
|
|
6575
|
+
if (!Number.isFinite(value)) {
|
|
6576
|
+
throw new Error(`Codex config override at ${path} must be a finite number`);
|
|
6577
|
+
}
|
|
6578
|
+
return `${value}`;
|
|
6579
|
+
}
|
|
6580
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
6581
|
+
if (Array.isArray(value)) {
|
|
6582
|
+
return `[${value.map((item, i) => tomlValue(item, `${path}[${i}]`)).join(", ")}]`;
|
|
6583
|
+
}
|
|
6584
|
+
if (isTable(value)) {
|
|
6585
|
+
const parts = [];
|
|
6586
|
+
for (const [key, child] of Object.entries(value)) {
|
|
6587
|
+
if (!key) throw new Error("Codex config override keys must be non-empty strings");
|
|
6588
|
+
if (child === void 0) continue;
|
|
6589
|
+
parts.push(`${formatKey(key)} = ${tomlValue(child, `${path}.${key}`)}`);
|
|
6590
|
+
}
|
|
6591
|
+
return `{${parts.join(", ")}}`;
|
|
6592
|
+
}
|
|
6593
|
+
throw new Error(`Codex config override at ${path} has an unsupported type`);
|
|
6594
|
+
}
|
|
6595
|
+
function walk(table, prefix, out) {
|
|
6596
|
+
const entries = Object.entries(table).filter(([, child]) => child !== void 0);
|
|
6597
|
+
if (entries.length === 0) {
|
|
6598
|
+
if (prefix) out.push(`${prefix}={}`);
|
|
6599
|
+
return;
|
|
6600
|
+
}
|
|
6601
|
+
for (const [key] of entries) {
|
|
6602
|
+
if (!key) throw new Error("Codex config override keys must be non-empty strings");
|
|
6603
|
+
}
|
|
6604
|
+
if (entries.some(([key]) => !TOML_BARE_KEY.test(key))) {
|
|
6605
|
+
if (!prefix) {
|
|
6606
|
+
throw new Error("Codex config override has a non-bare key at the root");
|
|
6607
|
+
}
|
|
6608
|
+
out.push(`${prefix}=${tomlValue(Object.fromEntries(entries), prefix)}`);
|
|
6609
|
+
return;
|
|
6610
|
+
}
|
|
6611
|
+
for (const [key, child] of entries) {
|
|
6612
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
6613
|
+
if (isTable(child)) walk(child, path, out);
|
|
6614
|
+
else out.push(`${path}=${tomlValue(child, path)}`);
|
|
6615
|
+
}
|
|
6616
|
+
}
|
|
6617
|
+
function buildConfigOverrides(config) {
|
|
6618
|
+
const out = [];
|
|
6619
|
+
walk(config, "", out);
|
|
6620
|
+
return out;
|
|
6621
|
+
}
|
|
6622
|
+
|
|
6623
|
+
// packages/agent-runtime/src/codex/transport.ts
|
|
6555
6624
|
function buildSdkThreadOptions(spec) {
|
|
6556
6625
|
return {
|
|
6557
6626
|
...spec.model ? { model: spec.model } : {},
|
|
@@ -6568,11 +6637,17 @@ function createSdkCodexTransport(opts = {}) {
|
|
|
6568
6637
|
const codexOptions = {
|
|
6569
6638
|
...opts.apiKey ? { apiKey: opts.apiKey } : {},
|
|
6570
6639
|
...opts.codexPathOverride ? { codexPathOverride: opts.codexPathOverride } : {},
|
|
6571
|
-
// The `--config` overrides (MCP servers + rmcp flag).
|
|
6572
|
-
//
|
|
6573
|
-
//
|
|
6574
|
-
// `
|
|
6575
|
-
|
|
6640
|
+
// The `--config` overrides (MCP servers + rmcp flag). CT1501: rendered to
|
|
6641
|
+
// finished `--config` strings by US and passed through `configOverrides`,
|
|
6642
|
+
// the SDK's raw passthrough — NOT through `config`, which would flatten
|
|
6643
|
+
// them itself. A user MCP server named `my.server` cannot go in an
|
|
6644
|
+
// override's dotted path at all (the CLI splits the path on `.` and
|
|
6645
|
+
// ignores TOML quoting), so it has to ride inside an inline-table value,
|
|
6646
|
+
// and the SDK's flattener cannot be steered into producing one. This is
|
|
6647
|
+
// also what makes the PUBLISHED companion correct, where the vendored
|
|
6648
|
+
// patch never reached — and why that patch is gone. See
|
|
6649
|
+
// `config-overrides.ts`.
|
|
6650
|
+
configOverrides: buildConfigOverrides(spec.config)
|
|
6576
6651
|
};
|
|
6577
6652
|
const codex = new Codex(codexOptions);
|
|
6578
6653
|
const threadOptions = buildSdkThreadOptions(spec);
|
|
@@ -7960,6 +8035,68 @@ function readOutboxFloor(read, turnId, log) {
|
|
|
7960
8035
|
}
|
|
7961
8036
|
}
|
|
7962
8037
|
|
|
8038
|
+
// src/prepare-failure.ts
|
|
8039
|
+
import { spawn as spawn4 } from "child_process";
|
|
8040
|
+
import { hostname } from "os";
|
|
8041
|
+
function prepareFailureCause(error) {
|
|
8042
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8043
|
+
if (/timed out/i.test(message)) return "prepare hook timed out";
|
|
8044
|
+
if (/MODULE_NOT_FOUND|Cannot find (?:module|package)/.test(message))
|
|
8045
|
+
return "prepare hook module missing; check the configured command and its installation for this device";
|
|
8046
|
+
if (/not valid JSON|JSON must carry|produced no output/.test(message))
|
|
8047
|
+
return "prepare hook returned malformed output; expected a cwd path or JSON with a non-empty cwd";
|
|
8048
|
+
if (/ENOENT/.test(message)) return "prepare hook executable missing (ENOENT)";
|
|
8049
|
+
if (/EACCES/.test(message)) return "prepare hook executable is not accessible (EACCES)";
|
|
8050
|
+
const exit = message.match(/exited with code (\d+)/)?.[1];
|
|
8051
|
+
return exit ? `prepare hook exited with code ${exit}` : "prepare hook failed; inspect the device-local hook log";
|
|
8052
|
+
}
|
|
8053
|
+
async function runPrepareWithReporting(runner, hook, input, context, reportError) {
|
|
8054
|
+
try {
|
|
8055
|
+
return await runner(hook, input);
|
|
8056
|
+
} catch (error) {
|
|
8057
|
+
if (hook.failureReporter) {
|
|
8058
|
+
try {
|
|
8059
|
+
await reportFailure(hook.failureReporter, {
|
|
8060
|
+
workspaceId: input.workspaceId,
|
|
8061
|
+
conversationId: input.conversationId,
|
|
8062
|
+
agentId: input.agentId,
|
|
8063
|
+
agentUsername: input.agentUsername,
|
|
8064
|
+
turnId: context.turnId,
|
|
8065
|
+
deviceId: context.deviceId ?? hostname(),
|
|
8066
|
+
cause: prepareFailureCause(error)
|
|
8067
|
+
});
|
|
8068
|
+
} catch {
|
|
8069
|
+
reportError();
|
|
8070
|
+
}
|
|
8071
|
+
}
|
|
8072
|
+
throw error;
|
|
8073
|
+
}
|
|
8074
|
+
}
|
|
8075
|
+
function reportFailure(config, body) {
|
|
8076
|
+
return new Promise((resolve2, reject) => {
|
|
8077
|
+
const child = spawn4(config.command, config.args ?? [], {
|
|
8078
|
+
stdio: ["pipe", "ignore", "ignore"],
|
|
8079
|
+
env: { ...process.env, ...config.env }
|
|
8080
|
+
});
|
|
8081
|
+
const timer = setTimeout(() => {
|
|
8082
|
+
child.kill("SIGKILL");
|
|
8083
|
+
reject(new Error("reporter timeout"));
|
|
8084
|
+
}, config.timeoutMs ?? 15e3);
|
|
8085
|
+
child.stdin.on("error", () => {
|
|
8086
|
+
});
|
|
8087
|
+
child.once("error", () => {
|
|
8088
|
+
clearTimeout(timer);
|
|
8089
|
+
reject(new Error("reporter failed to start"));
|
|
8090
|
+
});
|
|
8091
|
+
child.once("close", (code) => {
|
|
8092
|
+
clearTimeout(timer);
|
|
8093
|
+
if (code === 0) resolve2();
|
|
8094
|
+
else reject(new Error("reporter failed"));
|
|
8095
|
+
});
|
|
8096
|
+
child.stdin.end(JSON.stringify(body));
|
|
8097
|
+
});
|
|
8098
|
+
}
|
|
8099
|
+
|
|
7963
8100
|
// src/prepared.ts
|
|
7964
8101
|
import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, rmSync as rmSync5, writeFileSync as writeFileSync6, existsSync as existsSync8 } from "fs";
|
|
7965
8102
|
import { join as join12 } from "path";
|
|
@@ -8190,7 +8327,7 @@ function pruneOld(dir2, retain) {
|
|
|
8190
8327
|
}
|
|
8191
8328
|
|
|
8192
8329
|
// src/turn-containment.ts
|
|
8193
|
-
import { execFile, execFileSync, spawn as
|
|
8330
|
+
import { execFile, execFileSync, spawn as spawn5 } from "child_process";
|
|
8194
8331
|
import { readFileSync as readFileSync8 } from "fs";
|
|
8195
8332
|
import { promisify } from "util";
|
|
8196
8333
|
var execFileAsync = promisify(execFile);
|
|
@@ -8272,7 +8409,7 @@ function createTurnContainment(turnId, deps = {}) {
|
|
|
8272
8409
|
command: options.command
|
|
8273
8410
|
});
|
|
8274
8411
|
return asContained(
|
|
8275
|
-
|
|
8412
|
+
spawn5(
|
|
8276
8413
|
"systemd-run",
|
|
8277
8414
|
[
|
|
8278
8415
|
"--user",
|
|
@@ -8299,7 +8436,7 @@ function createTurnContainment(turnId, deps = {}) {
|
|
|
8299
8436
|
);
|
|
8300
8437
|
}
|
|
8301
8438
|
function spawnInGroup(options) {
|
|
8302
|
-
const child =
|
|
8439
|
+
const child = spawn5(options.command, options.args, {
|
|
8303
8440
|
...options.cwd ? { cwd: options.cwd } : {},
|
|
8304
8441
|
env: options.env,
|
|
8305
8442
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -9001,18 +9138,28 @@ var TurnExecution = class {
|
|
|
9001
9138
|
if (prepareHook.validateCached) {
|
|
9002
9139
|
const runHook = this.opts.prepareHookRunner ?? runPrepareHook;
|
|
9003
9140
|
try {
|
|
9004
|
-
await
|
|
9005
|
-
|
|
9006
|
-
|
|
9007
|
-
|
|
9008
|
-
|
|
9009
|
-
|
|
9010
|
-
|
|
9011
|
-
|
|
9012
|
-
|
|
9013
|
-
|
|
9014
|
-
|
|
9015
|
-
|
|
9141
|
+
await runPrepareWithReporting(
|
|
9142
|
+
runHook,
|
|
9143
|
+
prepareHook,
|
|
9144
|
+
{
|
|
9145
|
+
workspaceId,
|
|
9146
|
+
conversationId: payload.conversationId,
|
|
9147
|
+
agentId: payload.agentId,
|
|
9148
|
+
agentUsername: this.opts.agentUsername,
|
|
9149
|
+
runtime: this.turnContext.runtime,
|
|
9150
|
+
hostAccess: this.turnContext.policy.hostFs,
|
|
9151
|
+
triggerEntryPaths: this.turnContext.conversation.triggerEntryPaths ?? [],
|
|
9152
|
+
title: this.turnContext.conversation.title,
|
|
9153
|
+
messageBody: this.turnContext.message.body,
|
|
9154
|
+
prepared: cached2
|
|
9155
|
+
},
|
|
9156
|
+
{ turnId, deviceId: this.opts.deviceId },
|
|
9157
|
+
() => {
|
|
9158
|
+
turnLog.error(
|
|
9159
|
+
"dispatcher: prepare failure reporter failed; original failure preserved"
|
|
9160
|
+
);
|
|
9161
|
+
}
|
|
9162
|
+
);
|
|
9016
9163
|
} catch (err) {
|
|
9017
9164
|
const reason = err instanceof Error ? err.message : String(err);
|
|
9018
9165
|
turnLog.debug({ err: reason }, "dispatcher: prepare hook rejected a prepared turn");
|
|
@@ -9048,24 +9195,34 @@ var TurnExecution = class {
|
|
|
9048
9195
|
preparingTimer.unref?.();
|
|
9049
9196
|
const runHook = this.opts.prepareHookRunner ?? runPrepareHook;
|
|
9050
9197
|
try {
|
|
9051
|
-
const result = await
|
|
9052
|
-
|
|
9053
|
-
|
|
9054
|
-
|
|
9055
|
-
|
|
9056
|
-
|
|
9057
|
-
|
|
9058
|
-
|
|
9059
|
-
|
|
9060
|
-
|
|
9061
|
-
|
|
9062
|
-
|
|
9063
|
-
|
|
9064
|
-
|
|
9065
|
-
|
|
9066
|
-
|
|
9067
|
-
|
|
9068
|
-
|
|
9198
|
+
const result = await runPrepareWithReporting(
|
|
9199
|
+
runHook,
|
|
9200
|
+
prepareHook,
|
|
9201
|
+
{
|
|
9202
|
+
workspaceId,
|
|
9203
|
+
conversationId: payload.conversationId,
|
|
9204
|
+
agentId: payload.agentId,
|
|
9205
|
+
agentUsername: this.opts.agentUsername,
|
|
9206
|
+
runtime: this.turnContext.runtime,
|
|
9207
|
+
hostAccess: this.turnContext.policy.hostFs,
|
|
9208
|
+
// CT317/CT319: the trigger message's referenced-entry paths — what the
|
|
9209
|
+
// tasker prepare hook keys its per-task env off. Defaults to `[]` for
|
|
9210
|
+
// an older API. The conversation anchor is gone (CT319).
|
|
9211
|
+
triggerEntryPaths: this.turnContext.conversation.triggerEntryPaths ?? [],
|
|
9212
|
+
title: this.turnContext.conversation.title,
|
|
9213
|
+
// CT943: the dispatching message's text — where an `env:` directive
|
|
9214
|
+
// rides. The server has always sent the trigger body on the turn
|
|
9215
|
+
// context (for the live feed); this is the first thing to read it as
|
|
9216
|
+
// an INPUT, so a dispatch can ask for its environment in words.
|
|
9217
|
+
messageBody: this.turnContext.message.body
|
|
9218
|
+
},
|
|
9219
|
+
{ turnId, deviceId: this.opts.deviceId },
|
|
9220
|
+
() => {
|
|
9221
|
+
turnLog.error(
|
|
9222
|
+
"dispatcher: prepare failure reporter failed; original failure preserved"
|
|
9223
|
+
);
|
|
9224
|
+
}
|
|
9225
|
+
);
|
|
9069
9226
|
clearTimeout(preparingTimer);
|
|
9070
9227
|
if (preparingStarted) reportPreparing("done");
|
|
9071
9228
|
writePrepared(workspaceId, payload.conversationId, payload.agentId, result);
|
|
@@ -10226,7 +10383,7 @@ var CompanionSupervisor = class {
|
|
|
10226
10383
|
async start() {
|
|
10227
10384
|
this.log.info(
|
|
10228
10385
|
{ protocolVersion: TURN_PROTOCOL_VERSION, version: COMPANION_VERSION },
|
|
10229
|
-
`Companion ${COMPANION_VERSION} starting on ${this.config.deviceLabel ??
|
|
10386
|
+
`Companion ${COMPANION_VERSION} starting on ${this.config.deviceLabel ?? hostname2()}`
|
|
10230
10387
|
);
|
|
10231
10388
|
const startupHarnessRefresh = this.refreshHarnessStatuses();
|
|
10232
10389
|
this.trackHeartbeat(startupHarnessRefresh);
|
|
@@ -10245,7 +10402,7 @@ var CompanionSupervisor = class {
|
|
|
10245
10402
|
token: this.config.deviceToken,
|
|
10246
10403
|
log: this.log,
|
|
10247
10404
|
lastEventId: null,
|
|
10248
|
-
onOpen: () => this.log.info(`Connected to Cabane as ${this.hub.statusJson().device_label ??
|
|
10405
|
+
onOpen: () => this.log.info(`Connected to Cabane as ${this.hub.statusJson().device_label ?? hostname2()}`),
|
|
10249
10406
|
onMessage: async (ev) => {
|
|
10250
10407
|
if (ev.event === "assignments_changed") {
|
|
10251
10408
|
if (this.refreshing && this.inFlightRefresh) await this.inFlightRefresh;
|
|
@@ -10661,6 +10818,7 @@ var CompanionSupervisor = class {
|
|
|
10661
10818
|
});
|
|
10662
10819
|
if (this.dispatcherFactory) return this.dispatcherFactory({ ...ctx, local });
|
|
10663
10820
|
return new Dispatcher({
|
|
10821
|
+
deviceId: this.deviceId ?? void 0,
|
|
10664
10822
|
api: ctx.api,
|
|
10665
10823
|
baseUrl: ctx.baseUrl,
|
|
10666
10824
|
workspaceId: ctx.workspaceId,
|
|
@@ -11308,9 +11466,9 @@ async function waitForBoundedHeartbeat(heartbeat) {
|
|
|
11308
11466
|
}
|
|
11309
11467
|
function defaultReexec() {
|
|
11310
11468
|
clearRuntimeState();
|
|
11311
|
-
void import("child_process").then(({ spawn:
|
|
11469
|
+
void import("child_process").then(({ spawn: spawn6 }) => {
|
|
11312
11470
|
try {
|
|
11313
|
-
const child =
|
|
11471
|
+
const child = spawn6(process.execPath, process.argv.slice(1), {
|
|
11314
11472
|
stdio: "inherit",
|
|
11315
11473
|
detached: false
|
|
11316
11474
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cabane/companion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.107",
|
|
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",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"prepublishOnly": "pnpm build"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@anthropic-ai/claude-agent-sdk": "0.3.
|
|
33
|
-
"@openai/codex-sdk": "0.
|
|
32
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.270",
|
|
33
|
+
"@openai/codex-sdk": "0.154.0",
|
|
34
34
|
"@hono/node-server": "1.19.14",
|
|
35
35
|
"@inquirer/prompts": "7.10.1",
|
|
36
36
|
"commander": "12.1.0",
|