@cabane/companion 0.6.123 → 0.6.125
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 +2142 -420
- package/dist/pairing-config.js +32 -2
- package/dist/runtime.js +1767 -302
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -543,8 +543,38 @@ var init_config = __esm({
|
|
|
543
543
|
// configuration in its own right rather than a degraded one.
|
|
544
544
|
runners: z2.object({
|
|
545
545
|
enabled: z2.boolean().optional(),
|
|
546
|
-
provider: z2.
|
|
547
|
-
sandbox: z2.enum(["os", "none"]).optional()
|
|
546
|
+
provider: z2.enum(["local-process", "docker"]).optional(),
|
|
547
|
+
sandbox: z2.enum(["os", "none"]).optional(),
|
|
548
|
+
// CT1681: the `docker` provider — a container per conversation and agent,
|
|
549
|
+
// from one image, on a network whose only way out is an allow-listed proxy.
|
|
550
|
+
docker: z2.object({
|
|
551
|
+
// The base image a container runs. Default: the one this companion
|
|
552
|
+
// builds from its own Dockerfile on first use. One named here must
|
|
553
|
+
// have that one's layout (the `runner` user, `cabane-companion` on
|
|
554
|
+
// PATH reading `CABANE_COMPANION_ROOT`).
|
|
555
|
+
image: z2.string().min(1).optional(),
|
|
556
|
+
// The companion install mounted into every container. Default, on
|
|
557
|
+
// Linux: this companion's own. Needed for one run from a checkout
|
|
558
|
+
// (a `pnpm --filter @cabane/companion deploy --prod <dir>`).
|
|
559
|
+
companionDir: z2.string().min(1).optional(),
|
|
560
|
+
// Hosts a turn may reach besides the Cabane API, the model providers
|
|
561
|
+
// and the git host. `*.example.com` matches any subdomain.
|
|
562
|
+
egressAllow: z2.array(z2.string().min(1)).optional(),
|
|
563
|
+
// Keep a turn's container between turns and start it again for the
|
|
564
|
+
// next one. Off by default: the container goes after every turn and
|
|
565
|
+
// the runtime's session files are restored into the next.
|
|
566
|
+
keepContainers: z2.boolean().optional(),
|
|
567
|
+
// The Cabane URL as a container reaches it, for a device whose own
|
|
568
|
+
// `baseUrl` is a loopback address the container can't see.
|
|
569
|
+
apiUrl: z2.string().url().refine(isAllowedBaseUrl).optional(),
|
|
570
|
+
// Env set on each turn's runner, `${NAME}` resolved from secrets.json
|
|
571
|
+
// at exec: the runtime's own auth, which the image never carries.
|
|
572
|
+
env: z2.record(z2.string(), z2.string()).optional(),
|
|
573
|
+
// A git credential helper run on this device to answer the
|
|
574
|
+
// container's git for an allowed host, e.g. `!gh auth git-credential`.
|
|
575
|
+
// Absent: git's own configured helpers on this device.
|
|
576
|
+
gitCredentialHelper: z2.string().min(1).optional()
|
|
577
|
+
}).strict().optional()
|
|
548
578
|
}).strict().optional()
|
|
549
579
|
});
|
|
550
580
|
}
|
|
@@ -2543,8 +2573,8 @@ var init_policy = __esm({
|
|
|
2543
2573
|
// packages/agent-runtime/src/claude-code/model.ts
|
|
2544
2574
|
function parseClaudeCodeModel(model) {
|
|
2545
2575
|
if (!model) return null;
|
|
2546
|
-
const
|
|
2547
|
-
const bare =
|
|
2576
|
+
const sep2 = model.indexOf("/");
|
|
2577
|
+
const bare = sep2 === -1 || model.slice(0, sep2) !== "anthropic" ? model : model.slice(sep2 + 1);
|
|
2548
2578
|
return bare === CLAUDE_CODE_DEFAULT_MODEL_BARE ? null : model;
|
|
2549
2579
|
}
|
|
2550
2580
|
var CLAUDE_CODE_DEFAULT_MODEL_BARE;
|
|
@@ -3991,9 +4021,9 @@ var init_policy2 = __esm({
|
|
|
3991
4021
|
|
|
3992
4022
|
// packages/agent-runtime/src/opencode/model.ts
|
|
3993
4023
|
function parseOpencodeModel(model) {
|
|
3994
|
-
const
|
|
3995
|
-
if (
|
|
3996
|
-
return { providerID: model.slice(0,
|
|
4024
|
+
const sep2 = model.indexOf("/");
|
|
4025
|
+
if (sep2 === -1) return { providerID: "", modelID: model };
|
|
4026
|
+
return { providerID: model.slice(0, sep2), modelID: model.slice(sep2 + 1) };
|
|
3997
4027
|
}
|
|
3998
4028
|
var init_model2 = __esm({
|
|
3999
4029
|
"packages/agent-runtime/src/opencode/model.ts"() {
|
|
@@ -4262,10 +4292,10 @@ async function* parseSseStream(body, sessionId, signal) {
|
|
|
4262
4292
|
const { value, done } = await reader.read();
|
|
4263
4293
|
if (done) return;
|
|
4264
4294
|
buf += decoder.decode(value, { stream: true });
|
|
4265
|
-
let
|
|
4266
|
-
while ((
|
|
4267
|
-
const frame = buf.slice(0,
|
|
4268
|
-
buf = buf.slice(
|
|
4295
|
+
let sep2;
|
|
4296
|
+
while ((sep2 = buf.indexOf("\n\n")) !== -1) {
|
|
4297
|
+
const frame = buf.slice(0, sep2);
|
|
4298
|
+
buf = buf.slice(sep2 + 2);
|
|
4269
4299
|
const data = frame.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trim()).join("");
|
|
4270
4300
|
if (!data) continue;
|
|
4271
4301
|
let ev;
|
|
@@ -5291,8 +5321,8 @@ var init_policy3 = __esm({
|
|
|
5291
5321
|
|
|
5292
5322
|
// packages/agent-runtime/src/codex/model.ts
|
|
5293
5323
|
function parseCodexModel(model) {
|
|
5294
|
-
const
|
|
5295
|
-
const bare =
|
|
5324
|
+
const sep2 = model.indexOf("/");
|
|
5325
|
+
const bare = sep2 === -1 || model.slice(0, sep2) !== "openai" ? model : model.slice(sep2 + 1);
|
|
5296
5326
|
return CODEX_DEFAULT_MODEL_BARES.has(bare) ? null : bare;
|
|
5297
5327
|
}
|
|
5298
5328
|
var CODEX_DEFAULT_MODEL_BARES;
|
|
@@ -6990,8 +7020,107 @@ var init_turn_containment = __esm({
|
|
|
6990
7020
|
}
|
|
6991
7021
|
});
|
|
6992
7022
|
|
|
6993
|
-
// src/
|
|
7023
|
+
// src/secrets.ts
|
|
7024
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
|
|
7025
|
+
import { join as join10 } from "path";
|
|
6994
7026
|
import { z as z13 } from "zod";
|
|
7027
|
+
function secretsPath() {
|
|
7028
|
+
return join10(cabaneDir(), "secrets.json");
|
|
7029
|
+
}
|
|
7030
|
+
function loadSecretStore() {
|
|
7031
|
+
const path = secretsPath();
|
|
7032
|
+
if (!existsSync7(path)) return makeStore({});
|
|
7033
|
+
let raw;
|
|
7034
|
+
try {
|
|
7035
|
+
raw = readFileSync6(path, "utf8");
|
|
7036
|
+
} catch (err) {
|
|
7037
|
+
throw new ConfigError(
|
|
7038
|
+
`couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
7039
|
+
);
|
|
7040
|
+
}
|
|
7041
|
+
if (raw.trim().length === 0) return makeStore({});
|
|
7042
|
+
let parsed;
|
|
7043
|
+
try {
|
|
7044
|
+
parsed = JSON.parse(raw);
|
|
7045
|
+
} catch (err) {
|
|
7046
|
+
throw new ConfigError(
|
|
7047
|
+
`${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}. It must be a flat object of "NAME": "value" secret pairs.`
|
|
7048
|
+
);
|
|
7049
|
+
}
|
|
7050
|
+
const result = secretStoreSchema.safeParse(parsed);
|
|
7051
|
+
if (!result.success) {
|
|
7052
|
+
throw new ConfigError(
|
|
7053
|
+
`${path} must be a flat object mapping secret names to string values (e.g. { "GITHUB_TOKEN": "ghp_\u2026" }).`
|
|
7054
|
+
);
|
|
7055
|
+
}
|
|
7056
|
+
return makeStore(result.data);
|
|
7057
|
+
}
|
|
7058
|
+
function loadSecretStoreTolerant(onWarn) {
|
|
7059
|
+
try {
|
|
7060
|
+
return loadSecretStore();
|
|
7061
|
+
} catch (err) {
|
|
7062
|
+
onWarn?.(err instanceof Error ? err.message : String(err));
|
|
7063
|
+
return makeStore({});
|
|
7064
|
+
}
|
|
7065
|
+
}
|
|
7066
|
+
function makeStore(map) {
|
|
7067
|
+
return {
|
|
7068
|
+
get: (name) => map[name],
|
|
7069
|
+
names: () => Object.keys(map).sort()
|
|
7070
|
+
};
|
|
7071
|
+
}
|
|
7072
|
+
function requiredSecretNames(mcpServers) {
|
|
7073
|
+
const names = /* @__PURE__ */ new Set();
|
|
7074
|
+
for (const server of Object.values(mcpServers ?? {})) {
|
|
7075
|
+
const bags = [
|
|
7076
|
+
"env" in server ? server.env : void 0,
|
|
7077
|
+
"headers" in server ? server.headers : void 0
|
|
7078
|
+
];
|
|
7079
|
+
for (const bag of bags) {
|
|
7080
|
+
for (const value of Object.values(bag ?? {})) {
|
|
7081
|
+
for (const [, name] of value.matchAll(PLACEHOLDER_RE)) names.add(name);
|
|
7082
|
+
}
|
|
7083
|
+
}
|
|
7084
|
+
}
|
|
7085
|
+
return [...names].sort();
|
|
7086
|
+
}
|
|
7087
|
+
function resolveMcpSecrets(mcpServers, store) {
|
|
7088
|
+
const missing = /* @__PURE__ */ new Set();
|
|
7089
|
+
const interpolate = (value) => value.replace(PLACEHOLDER_RE, (_match, name) => {
|
|
7090
|
+
const resolved = store.get(name);
|
|
7091
|
+
if (resolved === void 0) {
|
|
7092
|
+
missing.add(name);
|
|
7093
|
+
return _match;
|
|
7094
|
+
}
|
|
7095
|
+
return resolved;
|
|
7096
|
+
});
|
|
7097
|
+
const mapBag = (bag) => {
|
|
7098
|
+
const out = {};
|
|
7099
|
+
for (const [k, v] of Object.entries(bag)) out[k] = interpolate(v);
|
|
7100
|
+
return out;
|
|
7101
|
+
};
|
|
7102
|
+
const servers = {};
|
|
7103
|
+
for (const [name, server] of Object.entries(mcpServers)) {
|
|
7104
|
+
const next = { ...server };
|
|
7105
|
+
if ("env" in next && next.env) next.env = mapBag(next.env);
|
|
7106
|
+
if ("headers" in next && next.headers) next.headers = mapBag(next.headers);
|
|
7107
|
+
servers[name] = next;
|
|
7108
|
+
}
|
|
7109
|
+
return { servers, missing: [...missing].sort() };
|
|
7110
|
+
}
|
|
7111
|
+
var PLACEHOLDER_RE, secretStoreSchema;
|
|
7112
|
+
var init_secrets = __esm({
|
|
7113
|
+
"src/secrets.ts"() {
|
|
7114
|
+
"use strict";
|
|
7115
|
+
init_config();
|
|
7116
|
+
init_errors();
|
|
7117
|
+
PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
7118
|
+
secretStoreSchema = z13.record(z13.string(), z13.string());
|
|
7119
|
+
}
|
|
7120
|
+
});
|
|
7121
|
+
|
|
7122
|
+
// src/run-config.ts
|
|
7123
|
+
import { z as z14 } from "zod";
|
|
6995
7124
|
function parseRunConfig(raw, onWarn) {
|
|
6996
7125
|
const result = runConfigSchema.safeParse(raw ?? {});
|
|
6997
7126
|
if (result.success) return result.data;
|
|
@@ -7004,38 +7133,38 @@ var mcpStdioServerSchema, mcpHttpServerSchema, mcpSseServerSchema, mcpServerDefS
|
|
|
7004
7133
|
var init_run_config = __esm({
|
|
7005
7134
|
"src/run-config.ts"() {
|
|
7006
7135
|
"use strict";
|
|
7007
|
-
mcpStdioServerSchema =
|
|
7008
|
-
type:
|
|
7009
|
-
command:
|
|
7010
|
-
args:
|
|
7011
|
-
env:
|
|
7136
|
+
mcpStdioServerSchema = z14.object({
|
|
7137
|
+
type: z14.literal("stdio").optional(),
|
|
7138
|
+
command: z14.string().min(1),
|
|
7139
|
+
args: z14.array(z14.string()).optional(),
|
|
7140
|
+
env: z14.record(z14.string(), z14.string()).optional()
|
|
7012
7141
|
});
|
|
7013
|
-
mcpHttpServerSchema =
|
|
7014
|
-
type:
|
|
7015
|
-
url:
|
|
7016
|
-
headers:
|
|
7142
|
+
mcpHttpServerSchema = z14.object({
|
|
7143
|
+
type: z14.literal("http"),
|
|
7144
|
+
url: z14.string().url(),
|
|
7145
|
+
headers: z14.record(z14.string(), z14.string()).optional()
|
|
7017
7146
|
});
|
|
7018
|
-
mcpSseServerSchema =
|
|
7019
|
-
type:
|
|
7020
|
-
url:
|
|
7021
|
-
headers:
|
|
7147
|
+
mcpSseServerSchema = z14.object({
|
|
7148
|
+
type: z14.literal("sse"),
|
|
7149
|
+
url: z14.string().url(),
|
|
7150
|
+
headers: z14.record(z14.string(), z14.string()).optional()
|
|
7022
7151
|
});
|
|
7023
|
-
mcpServerDefSchema =
|
|
7024
|
-
thinkingConfigSchema =
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
|
|
7152
|
+
mcpServerDefSchema = z14.union([mcpHttpServerSchema, mcpSseServerSchema, mcpStdioServerSchema]);
|
|
7153
|
+
thinkingConfigSchema = z14.discriminatedUnion("type", [
|
|
7154
|
+
z14.object({ type: z14.literal("adaptive") }),
|
|
7155
|
+
z14.object({ type: z14.literal("enabled"), budgetTokens: z14.number().int().positive().optional() }),
|
|
7156
|
+
z14.object({ type: z14.literal("disabled") })
|
|
7028
7157
|
]);
|
|
7029
|
-
effortSchema =
|
|
7030
|
-
runConfigSchema =
|
|
7158
|
+
effortSchema = z14.enum(["low", "medium", "high", "xhigh", "max"]);
|
|
7159
|
+
runConfigSchema = z14.object({
|
|
7031
7160
|
// CT788: the host-access binary replaced the `assistant`/`coding`/`custom` mode
|
|
7032
7161
|
// trio + its custom tool lists — `true` grants the host filesystem/shell, absent
|
|
7033
7162
|
// is the locked surface. Kept in lockstep with `@cabane/shared`'s
|
|
7034
7163
|
// `agentRunConfigSchema` (independent zod, forward-compatible: unknown keys are
|
|
7035
7164
|
// stripped, so an older companion riding a newer server never rejects the config).
|
|
7036
|
-
hostAccess:
|
|
7037
|
-
mcpServers:
|
|
7038
|
-
model:
|
|
7165
|
+
hostAccess: z14.boolean().optional(),
|
|
7166
|
+
mcpServers: z14.record(z14.string(), mcpServerDefSchema).optional(),
|
|
7167
|
+
model: z14.string().min(1).optional(),
|
|
7039
7168
|
thinking: thinkingConfigSchema.optional(),
|
|
7040
7169
|
effort: effortSchema.optional()
|
|
7041
7170
|
});
|
|
@@ -7044,7 +7173,7 @@ var init_run_config = __esm({
|
|
|
7044
7173
|
});
|
|
7045
7174
|
|
|
7046
7175
|
// src/runner/protocol.ts
|
|
7047
|
-
import { z as
|
|
7176
|
+
import { z as z15 } from "zod";
|
|
7048
7177
|
function required(env, name) {
|
|
7049
7178
|
const value = env[name];
|
|
7050
7179
|
if (!value) throw new Error(`runner: ${name} is not set`);
|
|
@@ -7122,56 +7251,64 @@ var init_protocol = __esm({
|
|
|
7122
7251
|
// Everything else the turn is constructed from, as JSON: see `RunnerSpec`.
|
|
7123
7252
|
spec: "CABANE_RUNNER_SPEC"
|
|
7124
7253
|
};
|
|
7125
|
-
runtimeHealthSchema =
|
|
7126
|
-
quotaState:
|
|
7127
|
-
limitedUntil:
|
|
7128
|
-
lastError:
|
|
7254
|
+
runtimeHealthSchema = z15.object({
|
|
7255
|
+
quotaState: z15.enum(["ok", "limited", "auth_expired", "unknown"]),
|
|
7256
|
+
limitedUntil: z15.string().nullable(),
|
|
7257
|
+
lastError: z15.string().nullable()
|
|
7129
7258
|
});
|
|
7130
|
-
runnerSpecSchema =
|
|
7131
|
-
deviceId:
|
|
7259
|
+
runnerSpecSchema = z15.object({
|
|
7260
|
+
deviceId: z15.string().optional(),
|
|
7132
7261
|
// The launcher's boot id: the one its heartbeat carries, so a runner's lease
|
|
7133
7262
|
// matches the device that holds it.
|
|
7134
|
-
bootId:
|
|
7135
|
-
workspaceId:
|
|
7136
|
-
workspaceSlug:
|
|
7137
|
-
agentId:
|
|
7138
|
-
agentUsername:
|
|
7139
|
-
agentName:
|
|
7263
|
+
bootId: z15.string(),
|
|
7264
|
+
workspaceId: z15.string(),
|
|
7265
|
+
workspaceSlug: z15.string(),
|
|
7266
|
+
agentId: z15.string(),
|
|
7267
|
+
agentUsername: z15.string(),
|
|
7268
|
+
agentName: z15.string(),
|
|
7140
7269
|
local: localAgentConfigSchema,
|
|
7141
7270
|
runConfig: runConfigSchema,
|
|
7142
|
-
opencodeServerUrl:
|
|
7143
|
-
claudeCodeAvailable:
|
|
7144
|
-
codexAvailable:
|
|
7145
|
-
idleTimeoutMs:
|
|
7146
|
-
totalTimeoutMs:
|
|
7147
|
-
leaseRenewalMs:
|
|
7148
|
-
preparingRowDelayMs:
|
|
7271
|
+
opencodeServerUrl: z15.string().optional(),
|
|
7272
|
+
claudeCodeAvailable: z15.boolean(),
|
|
7273
|
+
codexAvailable: z15.boolean(),
|
|
7274
|
+
idleTimeoutMs: z15.number().int().positive().optional(),
|
|
7275
|
+
totalTimeoutMs: z15.number().int().positive().optional(),
|
|
7276
|
+
leaseRenewalMs: z15.number().int().positive().optional(),
|
|
7277
|
+
preparingRowDelayMs: z15.number().int().nonnegative().optional(),
|
|
7149
7278
|
// Whether the turn writes a transcript. The directory is the runner's own
|
|
7150
7279
|
// (`transcriptsDir()`), so `CABANE_RUNNER_STATE_DIR` moves it with the outbox.
|
|
7151
|
-
transcripts:
|
|
7280
|
+
transcripts: z15.boolean(),
|
|
7152
7281
|
// Whether the harness starts under the OS sandbox. The launcher sends 'os'
|
|
7153
7282
|
// only when this host can make one.
|
|
7154
|
-
sandbox:
|
|
7155
|
-
logLevel:
|
|
7283
|
+
sandbox: z15.enum(["os", "none"]),
|
|
7284
|
+
logLevel: z15.string(),
|
|
7156
7285
|
// The launcher's connector-health observations, by runtime, as of the start.
|
|
7157
|
-
connectorHealth:
|
|
7286
|
+
connectorHealth: z15.record(z15.string(), runtimeHealthSchema),
|
|
7158
7287
|
// True when this (conversation, agent) pair was already told its session
|
|
7159
7288
|
// could not be saved: the notice is once per pair, not once per process.
|
|
7160
|
-
sessionWriteNoticeClaimed:
|
|
7289
|
+
sessionWriteNoticeClaimed: z15.boolean(),
|
|
7161
7290
|
// CT1680: the runner's identity, minted with its credential. The start
|
|
7162
7291
|
// PATCH carries it so the lease knows which runner holds the span.
|
|
7163
|
-
runnerId:
|
|
7292
|
+
runnerId: z15.string(),
|
|
7293
|
+
// CT1681: the launcher keeps the runtime's session files beside the attempt
|
|
7294
|
+
// and restores them into this runner's home, so the runner names the attempt
|
|
7295
|
+
// each context was composed for and waits for the launcher to say it's in
|
|
7296
|
+
// place. Absent: nothing to wait for (a runner on the launcher's own disk).
|
|
7297
|
+
attemptState: z15.boolean().optional(),
|
|
7298
|
+
// CT1681: MCP `${NAME}` secrets come from the launcher, by name, instead of
|
|
7299
|
+
// this machine's secrets.json. Absent: the runner reads the device's file.
|
|
7300
|
+
secretsFromLauncher: z15.boolean().optional()
|
|
7164
7301
|
});
|
|
7165
|
-
dispatchSchema =
|
|
7166
|
-
type:
|
|
7167
|
-
agentId:
|
|
7168
|
-
deviceId:
|
|
7169
|
-
conversationId:
|
|
7170
|
-
messageId:
|
|
7171
|
-
episode:
|
|
7302
|
+
dispatchSchema = z15.object({
|
|
7303
|
+
type: z15.literal("device:dispatch_requested"),
|
|
7304
|
+
agentId: z15.string(),
|
|
7305
|
+
deviceId: z15.string(),
|
|
7306
|
+
conversationId: z15.string(),
|
|
7307
|
+
messageId: z15.string(),
|
|
7308
|
+
episode: z15.number().int().optional()
|
|
7172
7309
|
});
|
|
7173
|
-
credentialsSchema =
|
|
7174
|
-
credential:
|
|
7310
|
+
credentialsSchema = z15.object({
|
|
7311
|
+
credential: z15.string().startsWith("cabrn_")
|
|
7175
7312
|
});
|
|
7176
7313
|
}
|
|
7177
7314
|
});
|
|
@@ -7183,6 +7320,11 @@ function selfRunnerCommand() {
|
|
|
7183
7320
|
if (!script) throw new Error("runners: cannot tell which script started this companion");
|
|
7184
7321
|
return [process.execPath, ...process.execArgv, script, "run-turn"];
|
|
7185
7322
|
}
|
|
7323
|
+
function defaultSecretStore(log) {
|
|
7324
|
+
return loadSecretStoreTolerant(
|
|
7325
|
+
(m) => log.warn({ err: m }, "Could not read the secrets file. Check ~/.cabane/secrets.json.")
|
|
7326
|
+
);
|
|
7327
|
+
}
|
|
7186
7328
|
function healthSnapshot(store) {
|
|
7187
7329
|
const out = {};
|
|
7188
7330
|
for (const r of store?.reports() ?? []) {
|
|
@@ -7216,6 +7358,8 @@ function runnerSpec(opts, launch, duties, runnerId) {
|
|
|
7216
7358
|
...opts.preparingRowDelayMs !== void 0 ? { preparingRowDelayMs: opts.preparingRowDelayMs } : {},
|
|
7217
7359
|
transcripts: opts.transcriptDir !== void 0,
|
|
7218
7360
|
sandbox: launch.sandbox ?? "none",
|
|
7361
|
+
...launch.provider.bindAttempt ? { attemptState: true } : {},
|
|
7362
|
+
...launch.provider.inheritsEnvironment === false ? { secretsFromLauncher: true } : {},
|
|
7219
7363
|
logLevel: opts.log.level,
|
|
7220
7364
|
connectorHealth: healthSnapshot(launch.connectorHealth),
|
|
7221
7365
|
sessionWriteNoticeClaimed: duties.sessionWriteNoticeClaimed()
|
|
@@ -7355,14 +7499,40 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
|
7355
7499
|
const remote = [];
|
|
7356
7500
|
const lockOwner = (id) => `${turnId}#${id}`;
|
|
7357
7501
|
const locks = launch.locks;
|
|
7502
|
+
let leasedAttemptId = null;
|
|
7358
7503
|
try {
|
|
7359
7504
|
handle = await launch.provider.acquire(attempt);
|
|
7505
|
+
} catch (err) {
|
|
7506
|
+
const reason2 = `runner_unavailable: ${describeError(err)}`;
|
|
7507
|
+
log.warn(
|
|
7508
|
+
{ turnId, provider: launch.provider.name, err: describeError(err) },
|
|
7509
|
+
"runner: no place to run the turn"
|
|
7510
|
+
);
|
|
7511
|
+
controller.signal.removeEventListener("abort", forwardStop);
|
|
7512
|
+
duties.releaseAbort(turnId, controller);
|
|
7513
|
+
await settleUnstarted(opts, payload, turnId, reason2, log);
|
|
7514
|
+
await launch.reportRunnerGone?.({
|
|
7515
|
+
workspaceId: opts.workspaceId,
|
|
7516
|
+
conversationId: payload.conversationId,
|
|
7517
|
+
agentId: payload.agentId,
|
|
7518
|
+
messageId: payload.messageId,
|
|
7519
|
+
runnerId: minted.runnerId,
|
|
7520
|
+
reason: "provider_failed"
|
|
7521
|
+
});
|
|
7522
|
+
const durationMs2 = Date.now() - startedAt;
|
|
7523
|
+
duties.notifyEnd({ id: payload.messageId, ok: false, durationMs: durationMs2, reason: reason2 });
|
|
7524
|
+
return { ok: false, durationMs: durationMs2, reason: reason2 };
|
|
7525
|
+
}
|
|
7526
|
+
try {
|
|
7527
|
+
const place = handle;
|
|
7360
7528
|
const env = {};
|
|
7361
|
-
|
|
7362
|
-
|
|
7529
|
+
if (launch.provider.inheritsEnvironment !== false) {
|
|
7530
|
+
for (const [k, v] of Object.entries(launch.env ?? process.env)) {
|
|
7531
|
+
if (v !== void 0) env[k] = v;
|
|
7532
|
+
}
|
|
7363
7533
|
}
|
|
7364
7534
|
const input = {
|
|
7365
|
-
baseUrl: opts.baseUrl,
|
|
7535
|
+
baseUrl: launch.runnerBaseUrl ?? opts.baseUrl,
|
|
7366
7536
|
credential: minted.credential,
|
|
7367
7537
|
dispatch: payload,
|
|
7368
7538
|
turnId,
|
|
@@ -7440,6 +7610,38 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
|
7440
7610
|
case "remote_session":
|
|
7441
7611
|
remote.push({ serverUrl: event.serverUrl, sessionId: event.sessionId });
|
|
7442
7612
|
return;
|
|
7613
|
+
case "attempt": {
|
|
7614
|
+
const bind = launch.provider.bindAttempt?.(place, event.attemptId) ?? Promise.resolve();
|
|
7615
|
+
void bind.then(
|
|
7616
|
+
() => running.stdin.write(encodeLauncherMessage({ cabaneLauncher: "bound", id: event.id })),
|
|
7617
|
+
(err) => {
|
|
7618
|
+
const refused = describeError(err);
|
|
7619
|
+
log.warn(
|
|
7620
|
+
{ attemptId: event.attemptId, err: refused },
|
|
7621
|
+
"runner: couldn't make the place ready for the turn's attempt; not running it there"
|
|
7622
|
+
);
|
|
7623
|
+
running.stdin.write(
|
|
7624
|
+
encodeLauncherMessage({ cabaneLauncher: "bound", id: event.id, refused })
|
|
7625
|
+
);
|
|
7626
|
+
}
|
|
7627
|
+
);
|
|
7628
|
+
return;
|
|
7629
|
+
}
|
|
7630
|
+
case "leased":
|
|
7631
|
+
leasedAttemptId = event.attemptId;
|
|
7632
|
+
return;
|
|
7633
|
+
case "secrets": {
|
|
7634
|
+
const store = (launch.secretStore ?? defaultSecretStore)(log);
|
|
7635
|
+
const values = {};
|
|
7636
|
+
for (const name of event.names) {
|
|
7637
|
+
const value = store.get(name);
|
|
7638
|
+
if (value !== void 0) values[name] = value;
|
|
7639
|
+
}
|
|
7640
|
+
running.stdin.write(
|
|
7641
|
+
encodeLauncherMessage({ cabaneLauncher: "secrets", id: event.id, values })
|
|
7642
|
+
);
|
|
7643
|
+
return;
|
|
7644
|
+
}
|
|
7443
7645
|
}
|
|
7444
7646
|
});
|
|
7445
7647
|
const stderrDone = lines(running.stderr, (line) => relayRunnerLog(line, opts.log));
|
|
@@ -7450,7 +7652,14 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
|
7450
7652
|
child?.stdin.end();
|
|
7451
7653
|
controller.signal.removeEventListener("abort", forwardStop);
|
|
7452
7654
|
duties.releaseAbort(turnId, controller);
|
|
7453
|
-
if (handle)
|
|
7655
|
+
if (handle) {
|
|
7656
|
+
await launch.provider.collect?.(handle, { attemptId: leasedAttemptId }).catch(
|
|
7657
|
+
(err) => log.warn({ err: describeError(err) }, "runner: couldn't collect what the runner left")
|
|
7658
|
+
);
|
|
7659
|
+
await launch.provider.release(handle, { destroy: false }).catch(
|
|
7660
|
+
(err) => log.warn({ err: describeError(err) }, "runner: couldn't give its place back")
|
|
7661
|
+
);
|
|
7662
|
+
}
|
|
7454
7663
|
}
|
|
7455
7664
|
if (result) return result;
|
|
7456
7665
|
const durationMs = Date.now() - startedAt;
|
|
@@ -7501,6 +7710,7 @@ var init_launch = __esm({
|
|
|
7501
7710
|
init_errors();
|
|
7502
7711
|
init_boot_id();
|
|
7503
7712
|
init_turn_containment();
|
|
7713
|
+
init_secrets();
|
|
7504
7714
|
init_protocol();
|
|
7505
7715
|
PINO_LEVELS = {
|
|
7506
7716
|
10: "trace",
|
|
@@ -7515,7 +7725,7 @@ var init_launch = __esm({
|
|
|
7515
7725
|
|
|
7516
7726
|
// src/turn-control-tools.ts
|
|
7517
7727
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
7518
|
-
import { z as
|
|
7728
|
+
import { z as z16 } from "zod";
|
|
7519
7729
|
function createSkipState() {
|
|
7520
7730
|
return { skipped: false, reason: null };
|
|
7521
7731
|
}
|
|
@@ -7566,7 +7776,7 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
7566
7776
|
SKIP_TURN_TOOL,
|
|
7567
7777
|
TURN_CONTROL_TOOL_DESCRIPTIONS.skip_turn,
|
|
7568
7778
|
{
|
|
7569
|
-
reason:
|
|
7779
|
+
reason: z16.string().min(1).max(MAX_SKIP_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.skip_turn.reason)
|
|
7570
7780
|
},
|
|
7571
7781
|
async (args) => {
|
|
7572
7782
|
skipState.skipped = true;
|
|
@@ -7582,42 +7792,42 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
7582
7792
|
ASK_TOOL,
|
|
7583
7793
|
TURN_CONTROL_TOOL_DESCRIPTIONS.ask,
|
|
7584
7794
|
{
|
|
7585
|
-
targetUserId:
|
|
7586
|
-
question:
|
|
7587
|
-
headline:
|
|
7588
|
-
options:
|
|
7589
|
-
|
|
7590
|
-
|
|
7591
|
-
|
|
7592
|
-
label:
|
|
7593
|
-
move:
|
|
7594
|
-
task:
|
|
7595
|
-
status:
|
|
7795
|
+
targetUserId: z16.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.targetUserId),
|
|
7796
|
+
question: z16.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.question),
|
|
7797
|
+
headline: z16.string().min(1).max(MAX_ASK_HEADLINE_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.headline),
|
|
7798
|
+
options: z16.array(
|
|
7799
|
+
z16.union([
|
|
7800
|
+
z16.string().min(1).max(MAX_ASK_OPTION_BYTES),
|
|
7801
|
+
z16.object({
|
|
7802
|
+
label: z16.string().min(1).max(MAX_ASK_OPTION_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.label"]),
|
|
7803
|
+
move: z16.object({
|
|
7804
|
+
task: z16.string().min(1).max(200).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.move.task"]),
|
|
7805
|
+
status: z16.enum(TASK_STAGES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.move.status"])
|
|
7596
7806
|
}).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.move"])
|
|
7597
7807
|
})
|
|
7598
7808
|
])
|
|
7599
7809
|
).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.options),
|
|
7600
|
-
questions:
|
|
7601
|
-
|
|
7602
|
-
headline:
|
|
7603
|
-
body:
|
|
7604
|
-
options:
|
|
7605
|
-
|
|
7606
|
-
|
|
7607
|
-
|
|
7608
|
-
label:
|
|
7609
|
-
move:
|
|
7610
|
-
task:
|
|
7611
|
-
status:
|
|
7810
|
+
questions: z16.array(
|
|
7811
|
+
z16.object({
|
|
7812
|
+
headline: z16.string().min(1).max(MAX_ASK_HEADLINE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.headline"]),
|
|
7813
|
+
body: z16.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.body"]),
|
|
7814
|
+
options: z16.array(
|
|
7815
|
+
z16.union([
|
|
7816
|
+
z16.string().min(1).max(MAX_ASK_OPTION_BYTES),
|
|
7817
|
+
z16.object({
|
|
7818
|
+
label: z16.string().min(1).max(MAX_ASK_OPTION_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.label"]),
|
|
7819
|
+
move: z16.object({
|
|
7820
|
+
task: z16.string().min(1).max(200).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.move.task"]),
|
|
7821
|
+
status: z16.enum(TASK_STAGES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.move.status"])
|
|
7612
7822
|
}).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.move"])
|
|
7613
7823
|
})
|
|
7614
7824
|
])
|
|
7615
7825
|
).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options"])
|
|
7616
7826
|
})
|
|
7617
7827
|
).min(1).max(MAX_ASK_ITEMS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.questions),
|
|
7618
|
-
replaces:
|
|
7619
|
-
askId:
|
|
7620
|
-
reason:
|
|
7828
|
+
replaces: z16.object({
|
|
7829
|
+
askId: z16.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.askId"]),
|
|
7830
|
+
reason: z16.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.reason"])
|
|
7621
7831
|
}).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.replaces)
|
|
7622
7832
|
},
|
|
7623
7833
|
async (args) => {
|
|
@@ -7658,9 +7868,9 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
7658
7868
|
WAKE_ME_TOOL,
|
|
7659
7869
|
TURN_CONTROL_TOOL_DESCRIPTIONS.wake_me,
|
|
7660
7870
|
{
|
|
7661
|
-
afterSeconds:
|
|
7662
|
-
at:
|
|
7663
|
-
note:
|
|
7871
|
+
afterSeconds: z16.number().int().positive().optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.afterSeconds),
|
|
7872
|
+
at: z16.string().datetime({ offset: true }).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.at),
|
|
7873
|
+
note: z16.string().min(1).max(MAX_WAKE_NOTE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.note)
|
|
7664
7874
|
},
|
|
7665
7875
|
async (args) => {
|
|
7666
7876
|
const hasAfter = args.afterSeconds !== void 0;
|
|
@@ -7694,8 +7904,8 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
7694
7904
|
WITHDRAW_ASK_TOOL,
|
|
7695
7905
|
TURN_CONTROL_TOOL_DESCRIPTIONS.withdraw_ask,
|
|
7696
7906
|
{
|
|
7697
|
-
askId:
|
|
7698
|
-
reason:
|
|
7907
|
+
askId: z16.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.askId),
|
|
7908
|
+
reason: z16.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.reason)
|
|
7699
7909
|
},
|
|
7700
7910
|
async (args) => performAct(acts, {
|
|
7701
7911
|
kind: "withdraw_ask",
|
|
@@ -7790,10 +8000,10 @@ var init_build_options = __esm({
|
|
|
7790
8000
|
// src/codex-instructions.ts
|
|
7791
8001
|
import { mkdtemp, rm, writeFile } from "fs/promises";
|
|
7792
8002
|
import { tmpdir } from "os";
|
|
7793
|
-
import { join as
|
|
8003
|
+
import { join as join11 } from "path";
|
|
7794
8004
|
async function writeCodexInstructionsFile(contents) {
|
|
7795
|
-
const dir2 = await mkdtemp(
|
|
7796
|
-
const path =
|
|
8005
|
+
const dir2 = await mkdtemp(join11(tmpdir(), PREFIX));
|
|
8006
|
+
const path = join11(dir2, "instructions.md");
|
|
7797
8007
|
await writeFile(path, contents, { encoding: "utf8", mode: 384 });
|
|
7798
8008
|
return {
|
|
7799
8009
|
path,
|
|
@@ -7917,22 +8127,22 @@ var init_prepare_failure = __esm({
|
|
|
7917
8127
|
});
|
|
7918
8128
|
|
|
7919
8129
|
// src/prepared.ts
|
|
7920
|
-
import { mkdirSync as mkdirSync7, readFileSync as
|
|
7921
|
-
import { join as
|
|
8130
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync7, rmSync as rmSync5, writeFileSync as writeFileSync5, existsSync as existsSync8 } from "fs";
|
|
8131
|
+
import { join as join12 } from "path";
|
|
7922
8132
|
function dirFor(workspaceId) {
|
|
7923
|
-
return
|
|
8133
|
+
return join12(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
|
|
7924
8134
|
}
|
|
7925
8135
|
function conversationDir(workspaceId, conversationId) {
|
|
7926
|
-
return
|
|
8136
|
+
return join12(dirFor(workspaceId), encodeURIComponent(conversationId));
|
|
7927
8137
|
}
|
|
7928
8138
|
function pathFor2(workspaceId, conversationId, agentId) {
|
|
7929
|
-
return
|
|
8139
|
+
return join12(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
|
|
7930
8140
|
}
|
|
7931
8141
|
function readPrepared(workspaceId, conversationId, agentId) {
|
|
7932
8142
|
const path = pathFor2(workspaceId, conversationId, agentId);
|
|
7933
|
-
if (!
|
|
8143
|
+
if (!existsSync8(path)) return null;
|
|
7934
8144
|
try {
|
|
7935
|
-
const parsed = JSON.parse(
|
|
8145
|
+
const parsed = JSON.parse(readFileSync7(path, "utf8"));
|
|
7936
8146
|
if (parsed && typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
|
|
7937
8147
|
return {
|
|
7938
8148
|
cwd: parsed.cwd,
|
|
@@ -7962,105 +8172,6 @@ var init_prepared = __esm({
|
|
|
7962
8172
|
}
|
|
7963
8173
|
});
|
|
7964
8174
|
|
|
7965
|
-
// src/secrets.ts
|
|
7966
|
-
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
|
|
7967
|
-
import { join as join12 } from "path";
|
|
7968
|
-
import { z as z16 } from "zod";
|
|
7969
|
-
function secretsPath() {
|
|
7970
|
-
return join12(cabaneDir(), "secrets.json");
|
|
7971
|
-
}
|
|
7972
|
-
function loadSecretStore() {
|
|
7973
|
-
const path = secretsPath();
|
|
7974
|
-
if (!existsSync8(path)) return makeStore({});
|
|
7975
|
-
let raw;
|
|
7976
|
-
try {
|
|
7977
|
-
raw = readFileSync7(path, "utf8");
|
|
7978
|
-
} catch (err) {
|
|
7979
|
-
throw new ConfigError(
|
|
7980
|
-
`couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
7981
|
-
);
|
|
7982
|
-
}
|
|
7983
|
-
if (raw.trim().length === 0) return makeStore({});
|
|
7984
|
-
let parsed;
|
|
7985
|
-
try {
|
|
7986
|
-
parsed = JSON.parse(raw);
|
|
7987
|
-
} catch (err) {
|
|
7988
|
-
throw new ConfigError(
|
|
7989
|
-
`${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}. It must be a flat object of "NAME": "value" secret pairs.`
|
|
7990
|
-
);
|
|
7991
|
-
}
|
|
7992
|
-
const result = secretStoreSchema.safeParse(parsed);
|
|
7993
|
-
if (!result.success) {
|
|
7994
|
-
throw new ConfigError(
|
|
7995
|
-
`${path} must be a flat object mapping secret names to string values (e.g. { "GITHUB_TOKEN": "ghp_\u2026" }).`
|
|
7996
|
-
);
|
|
7997
|
-
}
|
|
7998
|
-
return makeStore(result.data);
|
|
7999
|
-
}
|
|
8000
|
-
function loadSecretStoreTolerant(onWarn) {
|
|
8001
|
-
try {
|
|
8002
|
-
return loadSecretStore();
|
|
8003
|
-
} catch (err) {
|
|
8004
|
-
onWarn?.(err instanceof Error ? err.message : String(err));
|
|
8005
|
-
return makeStore({});
|
|
8006
|
-
}
|
|
8007
|
-
}
|
|
8008
|
-
function makeStore(map) {
|
|
8009
|
-
return {
|
|
8010
|
-
get: (name) => map[name],
|
|
8011
|
-
names: () => Object.keys(map).sort()
|
|
8012
|
-
};
|
|
8013
|
-
}
|
|
8014
|
-
function requiredSecretNames(mcpServers) {
|
|
8015
|
-
const names = /* @__PURE__ */ new Set();
|
|
8016
|
-
for (const server of Object.values(mcpServers ?? {})) {
|
|
8017
|
-
const bags = [
|
|
8018
|
-
"env" in server ? server.env : void 0,
|
|
8019
|
-
"headers" in server ? server.headers : void 0
|
|
8020
|
-
];
|
|
8021
|
-
for (const bag of bags) {
|
|
8022
|
-
for (const value of Object.values(bag ?? {})) {
|
|
8023
|
-
for (const [, name] of value.matchAll(PLACEHOLDER_RE)) names.add(name);
|
|
8024
|
-
}
|
|
8025
|
-
}
|
|
8026
|
-
}
|
|
8027
|
-
return [...names].sort();
|
|
8028
|
-
}
|
|
8029
|
-
function resolveMcpSecrets(mcpServers, store) {
|
|
8030
|
-
const missing = /* @__PURE__ */ new Set();
|
|
8031
|
-
const interpolate = (value) => value.replace(PLACEHOLDER_RE, (_match, name) => {
|
|
8032
|
-
const resolved = store.get(name);
|
|
8033
|
-
if (resolved === void 0) {
|
|
8034
|
-
missing.add(name);
|
|
8035
|
-
return _match;
|
|
8036
|
-
}
|
|
8037
|
-
return resolved;
|
|
8038
|
-
});
|
|
8039
|
-
const mapBag = (bag) => {
|
|
8040
|
-
const out = {};
|
|
8041
|
-
for (const [k, v] of Object.entries(bag)) out[k] = interpolate(v);
|
|
8042
|
-
return out;
|
|
8043
|
-
};
|
|
8044
|
-
const servers = {};
|
|
8045
|
-
for (const [name, server] of Object.entries(mcpServers)) {
|
|
8046
|
-
const next = { ...server };
|
|
8047
|
-
if ("env" in next && next.env) next.env = mapBag(next.env);
|
|
8048
|
-
if ("headers" in next && next.headers) next.headers = mapBag(next.headers);
|
|
8049
|
-
servers[name] = next;
|
|
8050
|
-
}
|
|
8051
|
-
return { servers, missing: [...missing].sort() };
|
|
8052
|
-
}
|
|
8053
|
-
var PLACEHOLDER_RE, secretStoreSchema;
|
|
8054
|
-
var init_secrets = __esm({
|
|
8055
|
-
"src/secrets.ts"() {
|
|
8056
|
-
"use strict";
|
|
8057
|
-
init_config();
|
|
8058
|
-
init_errors();
|
|
8059
|
-
PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
8060
|
-
secretStoreSchema = z16.record(z16.string(), z16.string());
|
|
8061
|
-
}
|
|
8062
|
-
});
|
|
8063
|
-
|
|
8064
8175
|
// src/transcript-writer.ts
|
|
8065
8176
|
import { appendFileSync, chmodSync as chmodSync4, copyFileSync, mkdirSync as mkdirSync8, readdirSync, rmSync as rmSync6 } from "fs";
|
|
8066
8177
|
import { basename, dirname as dirname6, join as join13 } from "path";
|
|
@@ -8727,6 +8838,12 @@ var init_turn_execution = __esm({
|
|
|
8727
8838
|
this.resumedSpoke = turnContext.turnSpoke === true || outboxSpoke;
|
|
8728
8839
|
if (this.resumedSpoke) turnLog.debug({ turnId }, "dispatcher: resumed span already spoke");
|
|
8729
8840
|
}
|
|
8841
|
+
try {
|
|
8842
|
+
await this.opts.inRunner?.onContext?.(turnContext.attemptId ?? null);
|
|
8843
|
+
} catch (err) {
|
|
8844
|
+
const reason = `runner_unavailable: ${err instanceof Error ? err.message : String(err)}`;
|
|
8845
|
+
throw this.concluded(reason, reason);
|
|
8846
|
+
}
|
|
8730
8847
|
}
|
|
8731
8848
|
gateTrigger() {
|
|
8732
8849
|
const { payload, turnLog } = this;
|
|
@@ -8744,7 +8861,8 @@ var init_turn_execution = __esm({
|
|
|
8744
8861
|
}
|
|
8745
8862
|
async resolveSecrets() {
|
|
8746
8863
|
const { payload, workspaceId, turnLog } = this;
|
|
8747
|
-
const
|
|
8864
|
+
const fromLauncher = this.opts.inRunner?.secrets;
|
|
8865
|
+
const secretStore = this.opts.secretStore ?? (fromLauncher ? await fromLauncher(requiredSecretNames(this.turnContext.mcpServers)) : null) ?? loadSecretStoreTolerant(
|
|
8748
8866
|
(m) => turnLog.warn({ err: m }, "Could not read the secrets file. Check ~/.cabane/secrets.json.")
|
|
8749
8867
|
);
|
|
8750
8868
|
const { servers: resolvedMcpServers, missing } = resolveMcpSecrets(
|
|
@@ -8904,42 +9022,48 @@ var init_turn_execution = __esm({
|
|
|
8904
9022
|
const { payload, workspaceId, turnId, turnLog } = this;
|
|
8905
9023
|
const abortController = this.abortController;
|
|
8906
9024
|
this.supervisor.registerAbort(turnId, abortController);
|
|
9025
|
+
let started;
|
|
8907
9026
|
try {
|
|
8908
|
-
await this.opts.api.setActiveRun(
|
|
8909
|
-
|
|
8910
|
-
|
|
8911
|
-
|
|
8912
|
-
|
|
8913
|
-
|
|
8914
|
-
|
|
8915
|
-
|
|
8916
|
-
|
|
8917
|
-
|
|
8918
|
-
|
|
8919
|
-
|
|
8920
|
-
|
|
8921
|
-
|
|
8922
|
-
|
|
8923
|
-
|
|
8924
|
-
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
8938
|
-
|
|
8939
|
-
|
|
8940
|
-
|
|
8941
|
-
|
|
8942
|
-
|
|
9027
|
+
started = await this.opts.api.setActiveRun(
|
|
9028
|
+
workspaceId,
|
|
9029
|
+
payload.conversationId,
|
|
9030
|
+
payload.agentId,
|
|
9031
|
+
{
|
|
9032
|
+
activeRunStartedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9033
|
+
// CT1679: a runner leases under its launcher's boot id — the one the
|
|
9034
|
+
// device heartbeats — or the lost-run sweep reads it as a restart.
|
|
9035
|
+
bootId: this.opts.inRunner?.bootId ?? bootId,
|
|
9036
|
+
// CT1680: the runner holding the span, beside the device's boot id.
|
|
9037
|
+
...this.opts.inRunner ? { runnerId: this.opts.inRunner.runnerId } : {},
|
|
9038
|
+
// CT1380: the durable record that this turn resumed, and from where.
|
|
9039
|
+
...this.resumedFromSeq !== void 0 ? { resumedFromSeq: this.resumedFromSeq } : {},
|
|
9040
|
+
// CT1454: the issuance the context THIS attempt runs on came from. Read
|
|
9041
|
+
// off `this.turnContext`, which is the fetch that composed this request —
|
|
9042
|
+
// so a retransmitted start echoes the same id and reuses the same record,
|
|
9043
|
+
// and a resumed attempt echoes its own, newer one. The post-admission
|
|
9044
|
+
// fresh-session recompose (`freshRequest`) deliberately does not touch it:
|
|
9045
|
+
// it adopts only the prompt, never the run-config, so its issuance must
|
|
9046
|
+
// not become this attempt's evidence.
|
|
9047
|
+
...this.turnContext.issuanceId ? { issuanceId: this.turnContext.issuanceId } : {},
|
|
9048
|
+
// CT33: hand the server this turn's id so the new-run chokepoint's
|
|
9049
|
+
// `closeAbandonedTurns` sweep excludes it. The prepare hook may have
|
|
9050
|
+
// already emitted a "preparing" activity row for this turn above (it
|
|
9051
|
+
// runs before this flip), which would otherwise make the sweep mistake
|
|
9052
|
+
// this live turn for an abandoned one and close it with a `stopped`.
|
|
9053
|
+
turnId,
|
|
9054
|
+
// The newest message this turn's read covered: the lease records it,
|
|
9055
|
+
// and settle moves the cursor to it. Omitted when the server sent
|
|
9056
|
+
// none (an older API), so the cursor stays where it was.
|
|
9057
|
+
...this.turnContext.readThrough !== void 0 ? { readThrough: this.turnContext.readThrough } : {},
|
|
9058
|
+
...this.turnContext.readRevision !== void 0 ? { readRevision: this.turnContext.readRevision } : {},
|
|
9059
|
+
// Rooms: the attempt this request's session belongs to (null = fresh).
|
|
9060
|
+
...this.turnContext.attemptId !== void 0 ? { attemptId: this.turnContext.attemptId } : {},
|
|
9061
|
+
// CT1543: the frame this start acts on. The turn-context fetch already
|
|
9062
|
+
// checked it, but that was before an assembly this turn awaited — the
|
|
9063
|
+
// lease checks it again where the stamp is actually taken.
|
|
9064
|
+
...payload.episode !== void 0 ? { dispatchMessageId: payload.messageId, dispatchEpisode: payload.episode } : {}
|
|
9065
|
+
}
|
|
9066
|
+
);
|
|
8943
9067
|
} catch (err) {
|
|
8944
9068
|
if (leaseRefusal(err) === "dispatch_context_stale" && !this.recomposedForLease) {
|
|
8945
9069
|
this.recomposedForLease = true;
|
|
@@ -8964,6 +9088,7 @@ var init_turn_execution = __esm({
|
|
|
8964
9088
|
throw this.concluded(reason, reason);
|
|
8965
9089
|
}
|
|
8966
9090
|
this.admitted = true;
|
|
9091
|
+
if (started?.attemptId) this.opts.inRunner?.onLeased?.(started.attemptId);
|
|
8967
9092
|
}
|
|
8968
9093
|
buildRequest() {
|
|
8969
9094
|
const { payload, workspaceId } = this;
|
|
@@ -9633,16 +9758,261 @@ var init_dispatcher = __esm({
|
|
|
9633
9758
|
} catch {
|
|
9634
9759
|
}
|
|
9635
9760
|
}
|
|
9636
|
-
return true;
|
|
9637
|
-
}
|
|
9638
|
-
};
|
|
9761
|
+
return true;
|
|
9762
|
+
}
|
|
9763
|
+
};
|
|
9764
|
+
}
|
|
9765
|
+
});
|
|
9766
|
+
|
|
9767
|
+
// src/runner/egress-proxy.ts
|
|
9768
|
+
var egress_proxy_exports = {};
|
|
9769
|
+
__export(egress_proxy_exports, {
|
|
9770
|
+
connectTarget: () => connectTarget,
|
|
9771
|
+
createEgressProxy: () => createEgressProxy,
|
|
9772
|
+
egressAllowed: () => egressAllowed
|
|
9773
|
+
});
|
|
9774
|
+
import { createServer as createServer2, request as httpRequest } from "http";
|
|
9775
|
+
import { connect as connect3 } from "net";
|
|
9776
|
+
function egressAllowed(host, allow, port, defaultPort = 443) {
|
|
9777
|
+
const h = host.toLowerCase().replace(/\.$/, "");
|
|
9778
|
+
if (!h) return false;
|
|
9779
|
+
const p = port ?? defaultPort;
|
|
9780
|
+
return allow.some((entry) => {
|
|
9781
|
+
const raw = entry.toLowerCase().trim();
|
|
9782
|
+
const m = /^(.*):(\d{1,5})$/.exec(raw);
|
|
9783
|
+
const name = m ? m[1] : raw;
|
|
9784
|
+
const wanted = m ? Number(m[2]) : defaultPort;
|
|
9785
|
+
if (p !== wanted) return false;
|
|
9786
|
+
if (name.startsWith("*.")) return h.endsWith(name.slice(1));
|
|
9787
|
+
return h === name;
|
|
9788
|
+
});
|
|
9789
|
+
}
|
|
9790
|
+
function connectTarget(target) {
|
|
9791
|
+
const m = /^\[?([^\]]+?)\]?:(\d{1,5})$/.exec(target);
|
|
9792
|
+
if (!m) return null;
|
|
9793
|
+
const port = Number(m[2]);
|
|
9794
|
+
if (!m[1] || port < 1 || port > 65535) return null;
|
|
9795
|
+
return { host: m[1], port };
|
|
9796
|
+
}
|
|
9797
|
+
function createEgressProxy(opts) {
|
|
9798
|
+
const { allow, log } = opts;
|
|
9799
|
+
const refuse = (host) => `cabane: ${host} is not on this device's egress allow-list (runners.docker.egressAllow)
|
|
9800
|
+
`;
|
|
9801
|
+
const server = createServer2((req, res) => {
|
|
9802
|
+
req.on("error", () => res.destroy());
|
|
9803
|
+
res.on("error", () => req.destroy());
|
|
9804
|
+
let url;
|
|
9805
|
+
try {
|
|
9806
|
+
url = new URL(req.url ?? "");
|
|
9807
|
+
} catch {
|
|
9808
|
+
res.writeHead(400).end("cabane: this proxy forwards absolute http:// URLs only\n");
|
|
9809
|
+
return;
|
|
9810
|
+
}
|
|
9811
|
+
if (url.protocol !== "http:" || !egressAllowed(url.hostname, allow, url.port ? Number(url.port) : 80, 80)) {
|
|
9812
|
+
log?.warn({ host: url.hostname }, "egress: refused");
|
|
9813
|
+
res.writeHead(403, { "content-type": "text/plain" }).end(refuse(url.hostname));
|
|
9814
|
+
return;
|
|
9815
|
+
}
|
|
9816
|
+
const upstream = httpRequest(
|
|
9817
|
+
{
|
|
9818
|
+
host: url.hostname,
|
|
9819
|
+
port: url.port || 80,
|
|
9820
|
+
method: req.method,
|
|
9821
|
+
path: `${url.pathname}${url.search}`,
|
|
9822
|
+
headers: req.headers
|
|
9823
|
+
},
|
|
9824
|
+
(up) => {
|
|
9825
|
+
res.writeHead(up.statusCode ?? 502, up.headers);
|
|
9826
|
+
up.pipe(res);
|
|
9827
|
+
}
|
|
9828
|
+
);
|
|
9829
|
+
upstream.on("error", () => {
|
|
9830
|
+
if (!res.headersSent) res.writeHead(502);
|
|
9831
|
+
res.end();
|
|
9832
|
+
});
|
|
9833
|
+
req.pipe(upstream);
|
|
9834
|
+
});
|
|
9835
|
+
server.on("clientError", (_err, socket) => socket.destroy());
|
|
9836
|
+
server.on("connect", (req, socket, head) => {
|
|
9837
|
+
socket.on("error", () => socket.destroy());
|
|
9838
|
+
const target = connectTarget(req.url ?? "");
|
|
9839
|
+
if (!target || !egressAllowed(target.host, allow, target.port)) {
|
|
9840
|
+
const host = target?.host ?? String(req.url);
|
|
9841
|
+
log?.warn({ host }, "egress: refused");
|
|
9842
|
+
const body = refuse(host);
|
|
9843
|
+
socket.end(
|
|
9844
|
+
`HTTP/1.1 403 Forbidden\r
|
|
9845
|
+
content-type: text/plain\r
|
|
9846
|
+
content-length: ${Buffer.byteLength(body)}\r
|
|
9847
|
+
\r
|
|
9848
|
+
${body}`
|
|
9849
|
+
);
|
|
9850
|
+
return;
|
|
9851
|
+
}
|
|
9852
|
+
const upstream = connect3(target.port, target.host, () => {
|
|
9853
|
+
socket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
9854
|
+
if (head.length) upstream.write(head);
|
|
9855
|
+
upstream.pipe(socket);
|
|
9856
|
+
socket.pipe(upstream);
|
|
9857
|
+
});
|
|
9858
|
+
const close = () => {
|
|
9859
|
+
upstream.destroy();
|
|
9860
|
+
socket.destroy();
|
|
9861
|
+
};
|
|
9862
|
+
upstream.on("error", () => {
|
|
9863
|
+
if (!socket.destroyed) socket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
|
|
9864
|
+
close();
|
|
9865
|
+
});
|
|
9866
|
+
socket.on("error", close);
|
|
9867
|
+
socket.on("close", () => upstream.destroy());
|
|
9868
|
+
});
|
|
9869
|
+
return server;
|
|
9870
|
+
}
|
|
9871
|
+
var init_egress_proxy = __esm({
|
|
9872
|
+
"src/runner/egress-proxy.ts"() {
|
|
9873
|
+
"use strict";
|
|
9874
|
+
}
|
|
9875
|
+
});
|
|
9876
|
+
|
|
9877
|
+
// src/runner/git-credential.ts
|
|
9878
|
+
var git_credential_exports = {};
|
|
9879
|
+
__export(git_credential_exports, {
|
|
9880
|
+
askGitCredential: () => askGitCredential,
|
|
9881
|
+
deviceGitFill: () => deviceGitFill,
|
|
9882
|
+
parseCredential: () => parseCredential,
|
|
9883
|
+
serveGitCredentials: () => serveGitCredentials
|
|
9884
|
+
});
|
|
9885
|
+
import { spawn as spawn8 } from "child_process";
|
|
9886
|
+
import { chmodSync as chmodSync5, rmSync as rmSync7 } from "fs";
|
|
9887
|
+
import { connect as connect4, createServer as createServer3 } from "net";
|
|
9888
|
+
function parseCredential(text) {
|
|
9889
|
+
const out = {};
|
|
9890
|
+
for (const line of text.split("\n")) {
|
|
9891
|
+
const eq = line.indexOf("=");
|
|
9892
|
+
if (eq <= 0) continue;
|
|
9893
|
+
out[line.slice(0, eq)] = line.slice(eq + 1);
|
|
9894
|
+
}
|
|
9895
|
+
return out;
|
|
9896
|
+
}
|
|
9897
|
+
function formatCredential(fields) {
|
|
9898
|
+
return Object.entries(fields).map(([k, v]) => `${k}=${v}`).join("\n") + "\n\n";
|
|
9899
|
+
}
|
|
9900
|
+
function deviceGitFill(helper, timeoutMs = 15e3) {
|
|
9901
|
+
return (request) => new Promise((resolve2) => {
|
|
9902
|
+
const args = [
|
|
9903
|
+
...helper ? ["-c", "credential.helper=", "-c", `credential.helper=${helper}`] : [],
|
|
9904
|
+
"credential",
|
|
9905
|
+
"fill"
|
|
9906
|
+
];
|
|
9907
|
+
const child = spawn8("git", args, {
|
|
9908
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
9909
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_ASKPASS: "", SSH_ASKPASS: "" }
|
|
9910
|
+
});
|
|
9911
|
+
let out = "";
|
|
9912
|
+
const timer = setTimeout(() => child.kill("SIGKILL"), timeoutMs);
|
|
9913
|
+
child.stdout.on("data", (chunk) => out += String(chunk));
|
|
9914
|
+
child.once("error", () => resolve2(null));
|
|
9915
|
+
child.once("close", (code) => {
|
|
9916
|
+
clearTimeout(timer);
|
|
9917
|
+
resolve2(code === 0 ? out : null);
|
|
9918
|
+
});
|
|
9919
|
+
child.stdin.end(formatCredential(request));
|
|
9920
|
+
});
|
|
9921
|
+
}
|
|
9922
|
+
async function serveGitCredentials(opts) {
|
|
9923
|
+
const { socketPath, hosts, fill, log } = opts;
|
|
9924
|
+
rmSync7(socketPath, { force: true });
|
|
9925
|
+
const server = createServer3({ allowHalfOpen: true }, (socket) => {
|
|
9926
|
+
let input = "";
|
|
9927
|
+
socket.setEncoding("utf8");
|
|
9928
|
+
socket.on("data", (chunk) => input += chunk);
|
|
9929
|
+
socket.on("error", () => socket.destroy());
|
|
9930
|
+
socket.on("end", () => {
|
|
9931
|
+
void (async () => {
|
|
9932
|
+
const [op = "", ...rest] = input.split("\n");
|
|
9933
|
+
const request = parseCredential(rest.join("\n"));
|
|
9934
|
+
if (op !== "get" || request.protocol !== "https" || !request.host) {
|
|
9935
|
+
socket.end();
|
|
9936
|
+
return;
|
|
9937
|
+
}
|
|
9938
|
+
if (!egressAllowed(request.host.replace(/:\d+$/, ""), hosts)) {
|
|
9939
|
+
log?.warn(
|
|
9940
|
+
{ host: request.host },
|
|
9941
|
+
"runner: git asked for a credential for a host it may not use"
|
|
9942
|
+
);
|
|
9943
|
+
socket.end();
|
|
9944
|
+
return;
|
|
9945
|
+
}
|
|
9946
|
+
const answer = await fill({
|
|
9947
|
+
protocol: "https",
|
|
9948
|
+
host: request.host,
|
|
9949
|
+
...request.path ? { path: request.path } : {},
|
|
9950
|
+
...request.username ? { username: request.username } : {}
|
|
9951
|
+
});
|
|
9952
|
+
if (!answer) {
|
|
9953
|
+
log?.warn(
|
|
9954
|
+
{ host: request.host },
|
|
9955
|
+
"runner: this device's git has no credential for the host"
|
|
9956
|
+
);
|
|
9957
|
+
socket.end();
|
|
9958
|
+
return;
|
|
9959
|
+
}
|
|
9960
|
+
const filled = parseCredential(answer);
|
|
9961
|
+
socket.end(
|
|
9962
|
+
formatCredential({
|
|
9963
|
+
protocol: "https",
|
|
9964
|
+
host: request.host,
|
|
9965
|
+
...filled.username ? { username: filled.username } : {},
|
|
9966
|
+
...filled.password ? { password: filled.password } : {}
|
|
9967
|
+
})
|
|
9968
|
+
);
|
|
9969
|
+
})();
|
|
9970
|
+
});
|
|
9971
|
+
});
|
|
9972
|
+
await new Promise((resolve2, reject) => {
|
|
9973
|
+
server.once("error", reject);
|
|
9974
|
+
server.listen(socketPath, () => {
|
|
9975
|
+
server.off("error", reject);
|
|
9976
|
+
resolve2();
|
|
9977
|
+
});
|
|
9978
|
+
});
|
|
9979
|
+
chmodSync5(socketPath, 438);
|
|
9980
|
+
return {
|
|
9981
|
+
close: () => new Promise((resolve2) => {
|
|
9982
|
+
server.close(() => resolve2());
|
|
9983
|
+
rmSync7(socketPath, { force: true });
|
|
9984
|
+
})
|
|
9985
|
+
};
|
|
9986
|
+
}
|
|
9987
|
+
function askGitCredential(socketPath, op, request, timeoutMs = 2e4) {
|
|
9988
|
+
return new Promise((resolve2) => {
|
|
9989
|
+
const socket = connect4(socketPath);
|
|
9990
|
+
let out = "";
|
|
9991
|
+
const timer = setTimeout(() => socket.destroy(), timeoutMs);
|
|
9992
|
+
socket.setEncoding("utf8");
|
|
9993
|
+
socket.on("data", (chunk) => out += chunk);
|
|
9994
|
+
socket.on("error", () => resolve2(""));
|
|
9995
|
+
socket.on("close", () => {
|
|
9996
|
+
clearTimeout(timer);
|
|
9997
|
+
resolve2(out);
|
|
9998
|
+
});
|
|
9999
|
+
socket.end(`${op}
|
|
10000
|
+
${request}`);
|
|
10001
|
+
});
|
|
10002
|
+
}
|
|
10003
|
+
var init_git_credential = __esm({
|
|
10004
|
+
"src/runner/git-credential.ts"() {
|
|
10005
|
+
"use strict";
|
|
10006
|
+
init_egress_proxy();
|
|
9639
10007
|
}
|
|
9640
10008
|
});
|
|
9641
10009
|
|
|
9642
10010
|
// src/runner/harness-sandbox.ts
|
|
9643
|
-
import {
|
|
9644
|
-
import {
|
|
9645
|
-
import {
|
|
10011
|
+
import { lookup } from "dns/promises";
|
|
10012
|
+
import { chmodSync as chmodSync6, mkdirSync as mkdirSync11, mkdtempSync, rmSync as rmSync10, writeFileSync as writeFileSync7 } from "fs";
|
|
10013
|
+
import { isIP } from "net";
|
|
10014
|
+
import { homedir as homedir3, networkInterfaces, tmpdir as tmpdir3, userInfo as userInfo2 } from "os";
|
|
10015
|
+
import { join as join18 } from "path";
|
|
9646
10016
|
import { SandboxManager } from "@anthropic-ai/sandbox-runtime";
|
|
9647
10017
|
function accountHome() {
|
|
9648
10018
|
try {
|
|
@@ -9654,19 +10024,36 @@ function accountHome() {
|
|
|
9654
10024
|
function shellQuote(value) {
|
|
9655
10025
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
9656
10026
|
}
|
|
9657
|
-
function
|
|
10027
|
+
function cabaneHostEntries(baseUrl, resolved, ownAddresses2) {
|
|
9658
10028
|
const url = new URL(baseUrl);
|
|
9659
|
-
const loopback = ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname);
|
|
9660
|
-
if (!loopback) return url.hostname;
|
|
9661
10029
|
const port = url.port || (url.protocol === "https:" ? "443" : "80");
|
|
9662
|
-
return `127.0.0.1:${port}
|
|
10030
|
+
if (["127.0.0.1", "localhost", "[::1]"].includes(url.hostname)) return [`127.0.0.1:${port}`];
|
|
10031
|
+
const own = new Set(ownAddresses2);
|
|
10032
|
+
const literals = resolved.filter((address) => own.has(address)).map((address) => address.includes(":") ? `[${address}]:${port}` : `${address}:${port}`);
|
|
10033
|
+
return [url.hostname, ...literals];
|
|
10034
|
+
}
|
|
10035
|
+
async function resolvedAddresses(baseUrl) {
|
|
10036
|
+
const { hostname: hostname5 } = new URL(baseUrl);
|
|
10037
|
+
if (isIP(hostname5)) return [];
|
|
10038
|
+
try {
|
|
10039
|
+
const answers = await lookup(hostname5, { all: true });
|
|
10040
|
+
return answers.map((a) => a.address);
|
|
10041
|
+
} catch {
|
|
10042
|
+
return [];
|
|
10043
|
+
}
|
|
10044
|
+
}
|
|
10045
|
+
function ownAddresses() {
|
|
10046
|
+
return Object.values(networkInterfaces()).flat().flatMap((i) => i ? [i.address] : []);
|
|
9663
10047
|
}
|
|
9664
10048
|
function harnessSandboxConfig(input) {
|
|
9665
|
-
const claudeDir = input.claudeConfigDir ??
|
|
9666
|
-
const codexDir = input.codexHome ??
|
|
10049
|
+
const claudeDir = input.claudeConfigDir ?? join18(input.home, ".claude");
|
|
10050
|
+
const codexDir = input.codexHome ?? join18(input.home, ".codex");
|
|
9667
10051
|
return {
|
|
9668
10052
|
network: {
|
|
9669
|
-
allowedDomains: [
|
|
10053
|
+
allowedDomains: [
|
|
10054
|
+
...cabaneHostEntries(input.baseUrl, input.resolved, input.ownAddresses),
|
|
10055
|
+
...MODEL_AND_GIT_HOSTS
|
|
10056
|
+
],
|
|
9670
10057
|
deniedDomains: [],
|
|
9671
10058
|
allowLocalBinding: true
|
|
9672
10059
|
},
|
|
@@ -9676,9 +10063,9 @@ function harnessSandboxConfig(input) {
|
|
|
9676
10063
|
denyRead: [
|
|
9677
10064
|
input.cabaneDir,
|
|
9678
10065
|
...[.../* @__PURE__ */ new Set([input.home, input.accountHome ?? input.home])].flatMap((home) => [
|
|
9679
|
-
|
|
9680
|
-
|
|
9681
|
-
|
|
10066
|
+
join18(home, ".ssh"),
|
|
10067
|
+
join18(home, ".aws"),
|
|
10068
|
+
join18(home, ".gnupg")
|
|
9682
10069
|
])
|
|
9683
10070
|
],
|
|
9684
10071
|
// The checkout, scratch, and the harnesses' own session state — a turn
|
|
@@ -9688,7 +10075,7 @@ function harnessSandboxConfig(input) {
|
|
|
9688
10075
|
...input.cwd ? [input.cwd] : [],
|
|
9689
10076
|
input.tmp,
|
|
9690
10077
|
claudeDir,
|
|
9691
|
-
|
|
10078
|
+
join18(input.home, ".claude.json"),
|
|
9692
10079
|
codexDir
|
|
9693
10080
|
],
|
|
9694
10081
|
denyWrite: []
|
|
@@ -9719,9 +10106,11 @@ async function prepareHarnessSandbox(input) {
|
|
|
9719
10106
|
const config = harnessSandboxConfig({
|
|
9720
10107
|
cwd: input.cwd,
|
|
9721
10108
|
baseUrl: input.baseUrl,
|
|
10109
|
+
resolved: await resolvedAddresses(input.baseUrl),
|
|
10110
|
+
ownAddresses: ownAddresses(),
|
|
9722
10111
|
home: homedir3(),
|
|
9723
10112
|
accountHome: accountHome(),
|
|
9724
|
-
tmp:
|
|
10113
|
+
tmp: tmpdir3(),
|
|
9725
10114
|
cabaneDir: cabaneDir(),
|
|
9726
10115
|
claudeConfigDir: process.env.CLAUDE_CONFIG_DIR,
|
|
9727
10116
|
codexHome: process.env.CODEX_HOME
|
|
@@ -9747,10 +10136,10 @@ async function prepareHarnessSandbox(input) {
|
|
|
9747
10136
|
});
|
|
9748
10137
|
return null;
|
|
9749
10138
|
}
|
|
9750
|
-
|
|
9751
|
-
const dir2 = mkdtempSync(
|
|
9752
|
-
const script =
|
|
9753
|
-
|
|
10139
|
+
mkdirSync11(join18(cabaneDir(), "sandbox"), { recursive: true });
|
|
10140
|
+
const dir2 = mkdtempSync(join18(cabaneDir(), "sandbox", "turn-"));
|
|
10141
|
+
const script = join18(dir2, "exec-sandboxed");
|
|
10142
|
+
writeFileSync7(
|
|
9754
10143
|
script,
|
|
9755
10144
|
[
|
|
9756
10145
|
"#!/bin/bash",
|
|
@@ -9760,20 +10149,20 @@ async function prepareHarnessSandbox(input) {
|
|
|
9760
10149
|
""
|
|
9761
10150
|
].join("\n")
|
|
9762
10151
|
);
|
|
9763
|
-
|
|
10152
|
+
chmodSync6(script, 448);
|
|
9764
10153
|
let shims = 0;
|
|
9765
10154
|
return {
|
|
9766
10155
|
wrap: (command, args) => ({ command: script, args: [command, ...args] }),
|
|
9767
10156
|
executableFor: (real) => {
|
|
9768
|
-
const shim =
|
|
9769
|
-
|
|
10157
|
+
const shim = join18(dir2, `exec-${++shims}`);
|
|
10158
|
+
writeFileSync7(shim, `#!/bin/bash
|
|
9770
10159
|
exec ${shellQuote(script)} ${shellQuote(real)} "$@"
|
|
9771
10160
|
`);
|
|
9772
|
-
|
|
10161
|
+
chmodSync6(shim, 448);
|
|
9773
10162
|
return shim;
|
|
9774
10163
|
},
|
|
9775
10164
|
dispose: async () => {
|
|
9776
|
-
|
|
10165
|
+
rmSync10(dir2, { recursive: true, force: true });
|
|
9777
10166
|
await SandboxManager.reset();
|
|
9778
10167
|
}
|
|
9779
10168
|
};
|
|
@@ -9801,15 +10190,15 @@ var init_harness_sandbox = __esm({
|
|
|
9801
10190
|
|
|
9802
10191
|
// src/outbox.ts
|
|
9803
10192
|
import {
|
|
9804
|
-
existsSync as
|
|
9805
|
-
mkdirSync as
|
|
9806
|
-
readdirSync as
|
|
9807
|
-
readFileSync as
|
|
9808
|
-
renameSync as
|
|
9809
|
-
rmSync as
|
|
9810
|
-
writeFileSync as
|
|
10193
|
+
existsSync as existsSync14,
|
|
10194
|
+
mkdirSync as mkdirSync12,
|
|
10195
|
+
readdirSync as readdirSync5,
|
|
10196
|
+
readFileSync as readFileSync10,
|
|
10197
|
+
renameSync as renameSync6,
|
|
10198
|
+
rmSync as rmSync11,
|
|
10199
|
+
writeFileSync as writeFileSync8
|
|
9811
10200
|
} from "fs";
|
|
9812
|
-
import { join as
|
|
10201
|
+
import { join as join19 } from "path";
|
|
9813
10202
|
var MAX_ENTRIES, MAX_AGE_MS, Outbox;
|
|
9814
10203
|
var init_outbox = __esm({
|
|
9815
10204
|
"src/outbox.ts"() {
|
|
@@ -9827,25 +10216,25 @@ var init_outbox = __esm({
|
|
|
9827
10216
|
// Resolved lazily (per call, like cursor.ts) so tests that swap HOME between
|
|
9828
10217
|
// cases route writes at the right tmpdir.
|
|
9829
10218
|
dir() {
|
|
9830
|
-
return
|
|
10219
|
+
return join19(runnerStateDir(), "outbox", encodeURIComponent(this.workspaceId));
|
|
9831
10220
|
}
|
|
9832
10221
|
fileFor(turnId, seq) {
|
|
9833
|
-
return
|
|
10222
|
+
return join19(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
|
|
9834
10223
|
}
|
|
9835
10224
|
// Persist a commit for later draining. Atomic (temp file + rename) so a
|
|
9836
10225
|
// concurrent `list()` never reads a half-written entry, then enforces the
|
|
9837
10226
|
// per-workspace bounds.
|
|
9838
10227
|
persist(entry) {
|
|
9839
10228
|
const dir2 = this.dir();
|
|
9840
|
-
|
|
10229
|
+
mkdirSync12(dir2, { recursive: true });
|
|
9841
10230
|
const target = this.fileFor(entry.turnId, entry.seq);
|
|
9842
10231
|
const tmp = `${target}.${process.pid}.tmp`;
|
|
9843
10232
|
try {
|
|
9844
|
-
|
|
9845
|
-
|
|
10233
|
+
writeFileSync8(tmp, JSON.stringify(entry) + "\n", "utf8");
|
|
10234
|
+
renameSync6(tmp, target);
|
|
9846
10235
|
} catch (err) {
|
|
9847
10236
|
try {
|
|
9848
|
-
|
|
10237
|
+
rmSync11(tmp, { force: true });
|
|
9849
10238
|
} catch {
|
|
9850
10239
|
}
|
|
9851
10240
|
this.log?.warn(
|
|
@@ -9862,19 +10251,19 @@ var init_outbox = __esm({
|
|
|
9862
10251
|
// wedging the drain.
|
|
9863
10252
|
list() {
|
|
9864
10253
|
const dir2 = this.dir();
|
|
9865
|
-
if (!
|
|
10254
|
+
if (!existsSync14(dir2)) return [];
|
|
9866
10255
|
let names;
|
|
9867
10256
|
try {
|
|
9868
|
-
names =
|
|
10257
|
+
names = readdirSync5(dir2);
|
|
9869
10258
|
} catch {
|
|
9870
10259
|
return [];
|
|
9871
10260
|
}
|
|
9872
10261
|
const entries = [];
|
|
9873
10262
|
for (const name of names) {
|
|
9874
10263
|
if (!name.endsWith(".json")) continue;
|
|
9875
|
-
const full =
|
|
10264
|
+
const full = join19(dir2, name);
|
|
9876
10265
|
try {
|
|
9877
|
-
const parsed = JSON.parse(
|
|
10266
|
+
const parsed = JSON.parse(readFileSync10(full, "utf8"));
|
|
9878
10267
|
if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
|
|
9879
10268
|
entries.push(parsed);
|
|
9880
10269
|
} else {
|
|
@@ -9908,8 +10297,8 @@ var init_outbox = __esm({
|
|
|
9908
10297
|
// different: it is positive evidence that nothing was ever queued here.
|
|
9909
10298
|
maxSeqForTurn(turnId) {
|
|
9910
10299
|
const dir2 = this.dir();
|
|
9911
|
-
if (!
|
|
9912
|
-
const names =
|
|
10300
|
+
if (!existsSync14(dir2)) return 0;
|
|
10301
|
+
const names = readdirSync5(dir2);
|
|
9913
10302
|
const prefix = `${encodeURIComponent(turnId)}__`;
|
|
9914
10303
|
let max = 0;
|
|
9915
10304
|
for (const name of names) {
|
|
@@ -9922,15 +10311,15 @@ var init_outbox = __esm({
|
|
|
9922
10311
|
// Remove a delivered (or terminally-discarded) entry. No-op if already gone.
|
|
9923
10312
|
remove(turnId, seq) {
|
|
9924
10313
|
try {
|
|
9925
|
-
|
|
10314
|
+
rmSync11(this.fileFor(turnId, seq), { force: true });
|
|
9926
10315
|
} catch {
|
|
9927
10316
|
}
|
|
9928
10317
|
}
|
|
9929
10318
|
size() {
|
|
9930
10319
|
const dir2 = this.dir();
|
|
9931
|
-
if (!
|
|
10320
|
+
if (!existsSync14(dir2)) return 0;
|
|
9932
10321
|
try {
|
|
9933
|
-
return
|
|
10322
|
+
return readdirSync5(dir2).filter((n) => n.endsWith(".json")).length;
|
|
9934
10323
|
} catch {
|
|
9935
10324
|
return 0;
|
|
9936
10325
|
}
|
|
@@ -9941,7 +10330,7 @@ var init_outbox = __esm({
|
|
|
9941
10330
|
"A saved update is unreadable and couldn't be delivered. Check the conversation for missing replies."
|
|
9942
10331
|
);
|
|
9943
10332
|
try {
|
|
9944
|
-
|
|
10333
|
+
rmSync11(full, { force: true });
|
|
9945
10334
|
} catch {
|
|
9946
10335
|
}
|
|
9947
10336
|
}
|
|
@@ -9977,9 +10366,42 @@ var init_outbox = __esm({
|
|
|
9977
10366
|
}
|
|
9978
10367
|
});
|
|
9979
10368
|
|
|
10369
|
+
// src/version.ts
|
|
10370
|
+
import { createRequire as createRequire2 } from "module";
|
|
10371
|
+
var pkg, COMPANION_VERSION;
|
|
10372
|
+
var init_version2 = __esm({
|
|
10373
|
+
"src/version.ts"() {
|
|
10374
|
+
"use strict";
|
|
10375
|
+
pkg = createRequire2(import.meta.url)("../package.json");
|
|
10376
|
+
COMPANION_VERSION = pkg.version;
|
|
10377
|
+
}
|
|
10378
|
+
});
|
|
10379
|
+
|
|
10380
|
+
// src/runner/image-manifest.ts
|
|
10381
|
+
var image_manifest_exports = {};
|
|
10382
|
+
__export(image_manifest_exports, {
|
|
10383
|
+
runnerManifest: () => runnerManifest
|
|
10384
|
+
});
|
|
10385
|
+
function runnerManifest() {
|
|
10386
|
+
return {
|
|
10387
|
+
version: COMPANION_VERSION,
|
|
10388
|
+
bundled: {
|
|
10389
|
+
"claude-code": resolveBundledBinary("claude-code").status,
|
|
10390
|
+
codex: resolveBundledBinary("codex").status
|
|
10391
|
+
}
|
|
10392
|
+
};
|
|
10393
|
+
}
|
|
10394
|
+
var init_image_manifest = __esm({
|
|
10395
|
+
"src/runner/image-manifest.ts"() {
|
|
10396
|
+
"use strict";
|
|
10397
|
+
init_bundled_binaries();
|
|
10398
|
+
init_version2();
|
|
10399
|
+
}
|
|
10400
|
+
});
|
|
10401
|
+
|
|
9980
10402
|
// src/runner/exec-shim.ts
|
|
9981
|
-
import { chmodSync as
|
|
9982
|
-
import { join as
|
|
10403
|
+
import { chmodSync as chmodSync7, mkdirSync as mkdirSync15, mkdtempSync as mkdtempSync2, rmSync as rmSync13, writeFileSync as writeFileSync10 } from "fs";
|
|
10404
|
+
import { join as join22 } from "path";
|
|
9983
10405
|
function shellQuote2(value) {
|
|
9984
10406
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
9985
10407
|
}
|
|
@@ -9989,18 +10411,18 @@ function createShimDir() {
|
|
|
9989
10411
|
return {
|
|
9990
10412
|
write(argv) {
|
|
9991
10413
|
if (!dir2) {
|
|
9992
|
-
|
|
9993
|
-
dir2 = mkdtempSync2(
|
|
10414
|
+
mkdirSync15(join22(cabaneDir(), "runner"), { recursive: true });
|
|
10415
|
+
dir2 = mkdtempSync2(join22(cabaneDir(), "runner", "turn-"));
|
|
9994
10416
|
}
|
|
9995
|
-
const path =
|
|
9996
|
-
|
|
10417
|
+
const path = join22(dir2, `exec-${++count}`);
|
|
10418
|
+
writeFileSync10(path, `#!/bin/sh
|
|
9997
10419
|
exec ${argv.map(shellQuote2).join(" ")} "$@"
|
|
9998
10420
|
`);
|
|
9999
|
-
|
|
10421
|
+
chmodSync7(path, 448);
|
|
10000
10422
|
return path;
|
|
10001
10423
|
},
|
|
10002
10424
|
dispose() {
|
|
10003
|
-
if (dir2)
|
|
10425
|
+
if (dir2) rmSync13(dir2, { recursive: true, force: true });
|
|
10004
10426
|
dir2 = null;
|
|
10005
10427
|
}
|
|
10006
10428
|
};
|
|
@@ -10016,6 +10438,7 @@ var init_exec_shim = __esm({
|
|
|
10016
10438
|
import { createInterface as createInterface3 } from "readline";
|
|
10017
10439
|
function openLauncherChannel(stdin, emit, onLauncherGone) {
|
|
10018
10440
|
const waiting = /* @__PURE__ */ new Map();
|
|
10441
|
+
const replies = /* @__PURE__ */ new Map();
|
|
10019
10442
|
let next = 0;
|
|
10020
10443
|
let resolveFirst;
|
|
10021
10444
|
let gotFirst = false;
|
|
@@ -10029,11 +10452,26 @@ function openLauncherChannel(stdin, emit, onLauncherGone) {
|
|
|
10029
10452
|
}
|
|
10030
10453
|
const message = decodeLauncherMessage(line);
|
|
10031
10454
|
if (message?.cabaneLauncher === "granted") waiting.get(message.id)?.();
|
|
10455
|
+
else if (message) replies.get(message.id)?.(message);
|
|
10032
10456
|
});
|
|
10457
|
+
let gone = false;
|
|
10033
10458
|
lines2.once("close", () => {
|
|
10459
|
+
gone = true;
|
|
10034
10460
|
if (!gotFirst) resolveFirst("");
|
|
10461
|
+
for (const reply2 of [...replies.values()]) reply2(null);
|
|
10035
10462
|
onLauncherGone();
|
|
10036
10463
|
});
|
|
10464
|
+
const ask = (event) => {
|
|
10465
|
+
if (gone) return Promise.resolve(null);
|
|
10466
|
+
const id = String(++next);
|
|
10467
|
+
return new Promise((resolve2) => {
|
|
10468
|
+
replies.set(id, (message) => {
|
|
10469
|
+
replies.delete(id);
|
|
10470
|
+
resolve2(message);
|
|
10471
|
+
});
|
|
10472
|
+
emit(event(id));
|
|
10473
|
+
});
|
|
10474
|
+
};
|
|
10037
10475
|
return {
|
|
10038
10476
|
first,
|
|
10039
10477
|
acquire(key, signal) {
|
|
@@ -10059,6 +10497,14 @@ function openLauncherChannel(stdin, emit, onLauncherGone) {
|
|
|
10059
10497
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
10060
10498
|
emit({ cabaneRunner: "lock", id, key });
|
|
10061
10499
|
});
|
|
10500
|
+
},
|
|
10501
|
+
async bindAttempt(attemptId) {
|
|
10502
|
+
const reply2 = await ask((id) => ({ cabaneRunner: "attempt", id, attemptId }));
|
|
10503
|
+
if (reply2?.cabaneLauncher === "bound" && reply2.refused) throw new Error(reply2.refused);
|
|
10504
|
+
},
|
|
10505
|
+
async secrets(names) {
|
|
10506
|
+
const reply2 = await ask((id) => ({ cabaneRunner: "secrets", id, names: [...names] }));
|
|
10507
|
+
return reply2?.cabaneLauncher === "secrets" ? reply2.values : {};
|
|
10062
10508
|
}
|
|
10063
10509
|
};
|
|
10064
10510
|
}
|
|
@@ -10072,6 +10518,7 @@ var init_launcher_channel = __esm({
|
|
|
10072
10518
|
// src/runner/run-turn.ts
|
|
10073
10519
|
var run_turn_exports = {};
|
|
10074
10520
|
__export(run_turn_exports, {
|
|
10521
|
+
drainBeforeExit: () => drainBeforeExit,
|
|
10075
10522
|
runTurn: () => runTurn,
|
|
10076
10523
|
runnerDispatcherOptions: () => runnerDispatcherOptions,
|
|
10077
10524
|
runnerLogger: () => runnerLogger,
|
|
@@ -10079,6 +10526,19 @@ __export(run_turn_exports, {
|
|
|
10079
10526
|
});
|
|
10080
10527
|
import { spawnSync } from "child_process";
|
|
10081
10528
|
import pino2 from "pino";
|
|
10529
|
+
async function drainBeforeExit(outbox, api, graceMs = OUTBOX_GRACE_MS, log) {
|
|
10530
|
+
if (outbox.size() === 0) return;
|
|
10531
|
+
const deadline = Date.now() + graceMs;
|
|
10532
|
+
while (Date.now() < deadline) {
|
|
10533
|
+
const { remaining } = await api.drainOutbox().catch(() => ({ remaining: outbox.size() }));
|
|
10534
|
+
if (remaining === 0) return;
|
|
10535
|
+
await new Promise((resolve2) => setTimeout(resolve2, Math.min(OUTBOX_RETRY_MS, graceMs)));
|
|
10536
|
+
}
|
|
10537
|
+
log?.warn(
|
|
10538
|
+
{ remaining: outbox.size() },
|
|
10539
|
+
"runner: exiting with updates Cabane hasn't taken yet; the companion delivers them"
|
|
10540
|
+
);
|
|
10541
|
+
}
|
|
10082
10542
|
function runnerLogger(input, stderr) {
|
|
10083
10543
|
return pino2({ level: input.spec.logLevel }, stderr).child({ agentName: input.spec.agentName });
|
|
10084
10544
|
}
|
|
@@ -10095,6 +10555,8 @@ function runnerDispatcherOptions(input, log, emit, extras = {}) {
|
|
|
10095
10555
|
outbox,
|
|
10096
10556
|
log
|
|
10097
10557
|
});
|
|
10558
|
+
extras.onOutbox?.(outbox, api);
|
|
10559
|
+
const { bindAttempt, secrets } = extras;
|
|
10098
10560
|
const health = new Map(Object.entries(spec.connectorHealth));
|
|
10099
10561
|
const connectorHealth = {
|
|
10100
10562
|
lookup: (runtime) => health.get(runtime),
|
|
@@ -10136,6 +10598,11 @@ function runnerDispatcherOptions(input, log, emit, extras = {}) {
|
|
|
10136
10598
|
(sessionId) => emit({ cabaneRunner: "remote_session", serverUrl, sessionId })
|
|
10137
10599
|
),
|
|
10138
10600
|
...extras.onProcessGroup ? { onProcessGroup: extras.onProcessGroup } : {},
|
|
10601
|
+
...spec.attemptState && bindAttempt ? {
|
|
10602
|
+
onContext: (attemptId) => bindAttempt(attemptId),
|
|
10603
|
+
onLeased: (attemptId) => emit({ cabaneRunner: "leased", attemptId })
|
|
10604
|
+
} : {},
|
|
10605
|
+
...spec.secretsFromLauncher && secrets ? { secrets: async (names) => makeStore(await secrets(names)) } : {},
|
|
10139
10606
|
codexExecutable: (path, containment) => {
|
|
10140
10607
|
const prefix = containment.scopePrefix(process.env);
|
|
10141
10608
|
return prefix ? shims.write([...prefix, path]) : path;
|
|
@@ -10238,6 +10705,7 @@ async function runTurn(env, deps) {
|
|
|
10238
10705
|
notifyStart: (info) => emit({ cabaneRunner: "start", ...info }),
|
|
10239
10706
|
notifyEnd: (info) => emit({ cabaneRunner: "end", ...info })
|
|
10240
10707
|
};
|
|
10708
|
+
let outbox;
|
|
10241
10709
|
const opts = runnerDispatcherOptions(input, log, emit, {
|
|
10242
10710
|
...deps.queryFn ? { queryFn: deps.queryFn } : {},
|
|
10243
10711
|
onSandbox: (s) => sandbox = s,
|
|
@@ -10246,7 +10714,10 @@ async function runTurn(env, deps) {
|
|
|
10246
10714
|
onProcessGroup: (pgid) => {
|
|
10247
10715
|
groups.push(pgid);
|
|
10248
10716
|
emit({ cabaneRunner: "process_group", pgid });
|
|
10249
|
-
}
|
|
10717
|
+
},
|
|
10718
|
+
bindAttempt: (attemptId) => channel.bindAttempt(attemptId),
|
|
10719
|
+
secrets: (names) => channel.secrets(names),
|
|
10720
|
+
onOutbox: (o, a) => outbox = { outbox: o, api: a }
|
|
10250
10721
|
});
|
|
10251
10722
|
const result = await new TurnExecution(opts, supervisor, input.dispatch, {
|
|
10252
10723
|
turnId: input.turnId,
|
|
@@ -10255,6 +10726,7 @@ async function runTurn(env, deps) {
|
|
|
10255
10726
|
shims.dispose();
|
|
10256
10727
|
await sandbox?.dispose();
|
|
10257
10728
|
});
|
|
10729
|
+
if (outbox) await drainBeforeExit(outbox.outbox, outbox.api, deps.outboxGraceMs, log);
|
|
10258
10730
|
emit({ cabaneRunner: "result", result });
|
|
10259
10731
|
return { code: result.ok ? 0 : 1, result };
|
|
10260
10732
|
} catch (err) {
|
|
@@ -10264,12 +10736,13 @@ async function runTurn(env, deps) {
|
|
|
10264
10736
|
deps.signals.off("SIGTERM", onTerm);
|
|
10265
10737
|
}
|
|
10266
10738
|
}
|
|
10267
|
-
var LAUNCHER_GONE_GRACE_MS;
|
|
10739
|
+
var OUTBOX_GRACE_MS, OUTBOX_RETRY_MS, LAUNCHER_GONE_GRACE_MS;
|
|
10268
10740
|
var init_run_turn = __esm({
|
|
10269
10741
|
"src/runner/run-turn.ts"() {
|
|
10270
10742
|
"use strict";
|
|
10271
10743
|
init_api();
|
|
10272
10744
|
init_outbox();
|
|
10745
|
+
init_secrets();
|
|
10273
10746
|
init_exec_shim();
|
|
10274
10747
|
init_harness_sandbox();
|
|
10275
10748
|
init_launcher_channel();
|
|
@@ -10277,6 +10750,8 @@ var init_run_turn = __esm({
|
|
|
10277
10750
|
init_transcript_writer();
|
|
10278
10751
|
init_turn_execution();
|
|
10279
10752
|
init_protocol();
|
|
10753
|
+
OUTBOX_GRACE_MS = 3e4;
|
|
10754
|
+
OUTBOX_RETRY_MS = 1e3;
|
|
10280
10755
|
LAUNCHER_GONE_GRACE_MS = 3e3;
|
|
10281
10756
|
}
|
|
10282
10757
|
});
|
|
@@ -10580,7 +11055,8 @@ function buildCompanionManifest(opts) {
|
|
|
10580
11055
|
return {
|
|
10581
11056
|
runtimes,
|
|
10582
11057
|
capabilities: { ...DEVICE_MANIFEST.capabilities },
|
|
10583
|
-
runtimeIssues: opts.runtimeIssues ?? []
|
|
11058
|
+
runtimeIssues: opts.runtimeIssues ?? [],
|
|
11059
|
+
...opts.runnerProvider ? { runner: { provider: opts.runnerProvider } } : {}
|
|
10584
11060
|
};
|
|
10585
11061
|
}
|
|
10586
11062
|
function runtimeIssuesFor(gates) {
|
|
@@ -11410,7 +11886,7 @@ async function pair(opts = {}) {
|
|
|
11410
11886
|
}
|
|
11411
11887
|
|
|
11412
11888
|
// src/cli.ts
|
|
11413
|
-
import { readFileSync as
|
|
11889
|
+
import { readFileSync as readFileSync13 } from "fs";
|
|
11414
11890
|
|
|
11415
11891
|
// src/commands/logs.ts
|
|
11416
11892
|
import { once } from "events";
|
|
@@ -11616,7 +12092,7 @@ import { hostname as hostname4 } from "os";
|
|
|
11616
12092
|
|
|
11617
12093
|
// src/runtime.ts
|
|
11618
12094
|
init_config();
|
|
11619
|
-
import { randomUUID as
|
|
12095
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
11620
12096
|
|
|
11621
12097
|
// src/dashboard/server.ts
|
|
11622
12098
|
init_errors();
|
|
@@ -12256,7 +12732,7 @@ function turnFailureCopy(reason, runtime) {
|
|
|
12256
12732
|
init_api_error_shape();
|
|
12257
12733
|
init_boot_id();
|
|
12258
12734
|
init_api();
|
|
12259
|
-
import { randomUUID as
|
|
12735
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
12260
12736
|
|
|
12261
12737
|
// src/device-api.ts
|
|
12262
12738
|
init_errors();
|
|
@@ -12528,56 +13004,976 @@ var ConnectorHealthStore = class {
|
|
|
12528
13004
|
lastError: h.lastError
|
|
12529
13005
|
}));
|
|
12530
13006
|
}
|
|
12531
|
-
};
|
|
12532
|
-
|
|
12533
|
-
// src/supervisor.ts
|
|
12534
|
-
init_dispatcher();
|
|
13007
|
+
};
|
|
13008
|
+
|
|
13009
|
+
// src/supervisor.ts
|
|
13010
|
+
init_dispatcher();
|
|
13011
|
+
|
|
13012
|
+
// src/providers/local-process.ts
|
|
13013
|
+
import { spawn as spawn6 } from "child_process";
|
|
13014
|
+
import { constants } from "os";
|
|
13015
|
+
var DEFAULT_KILL_GRACE_MS = 3e4;
|
|
13016
|
+
var DEFAULT_REAP_GRACE_MS = 3e3;
|
|
13017
|
+
function exitCode(code, signal) {
|
|
13018
|
+
if (code !== null) return code;
|
|
13019
|
+
const signum = signal ? constants.signals[signal] : void 0;
|
|
13020
|
+
return signum !== void 0 ? 128 + signum : 1;
|
|
13021
|
+
}
|
|
13022
|
+
function createLocalProcessProvider(opts = {}) {
|
|
13023
|
+
const killGraceMs = opts.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
|
|
13024
|
+
const spawnFn = opts.spawnFn ?? spawn6;
|
|
13025
|
+
const reapGraceMs = opts.reapGraceMs ?? DEFAULT_REAP_GRACE_MS;
|
|
13026
|
+
const killFn = opts.killFn ?? ((pid, signal) => process.kill(pid, signal));
|
|
13027
|
+
const groupAlive = (pgid) => {
|
|
13028
|
+
try {
|
|
13029
|
+
killFn(-pgid, 0);
|
|
13030
|
+
return true;
|
|
13031
|
+
} catch {
|
|
13032
|
+
return false;
|
|
13033
|
+
}
|
|
13034
|
+
};
|
|
13035
|
+
const signalGroup = (pgid, signal) => {
|
|
13036
|
+
try {
|
|
13037
|
+
killFn(-pgid, signal);
|
|
13038
|
+
} catch {
|
|
13039
|
+
}
|
|
13040
|
+
};
|
|
13041
|
+
return {
|
|
13042
|
+
name: "local-process",
|
|
13043
|
+
async acquire(attempt) {
|
|
13044
|
+
return { provider: "local-process", id: attempt.turnId };
|
|
13045
|
+
},
|
|
13046
|
+
exec(_handle, argv, env, { signal, stdin }) {
|
|
13047
|
+
const [command, ...args] = argv;
|
|
13048
|
+
if (!command) throw new Error("local-process: exec needs a command");
|
|
13049
|
+
const child = spawnFn(command, args, {
|
|
13050
|
+
env: { ...env },
|
|
13051
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
13052
|
+
detached: process.platform !== "win32"
|
|
13053
|
+
});
|
|
13054
|
+
child.stdin?.on("error", () => {
|
|
13055
|
+
});
|
|
13056
|
+
if (stdin) child.stdin?.write(stdin);
|
|
13057
|
+
const exit = new Promise((resolve2) => {
|
|
13058
|
+
let settled = false;
|
|
13059
|
+
const done = (code) => {
|
|
13060
|
+
if (settled) return;
|
|
13061
|
+
settled = true;
|
|
13062
|
+
signal.removeEventListener("abort", stop2);
|
|
13063
|
+
if (forceTimer) clearTimeout(forceTimer);
|
|
13064
|
+
resolve2(code);
|
|
13065
|
+
};
|
|
13066
|
+
let forceTimer;
|
|
13067
|
+
const stop2 = () => {
|
|
13068
|
+
child.kill("SIGTERM");
|
|
13069
|
+
forceTimer = setTimeout(() => child.kill("SIGKILL"), killGraceMs);
|
|
13070
|
+
forceTimer.unref?.();
|
|
13071
|
+
};
|
|
13072
|
+
child.once("error", () => done(1));
|
|
13073
|
+
child.once("close", (code, sig) => done(exitCode(code, sig)));
|
|
13074
|
+
if (signal.aborted) stop2();
|
|
13075
|
+
else signal.addEventListener("abort", stop2, { once: true });
|
|
13076
|
+
});
|
|
13077
|
+
if (!child.stdout || !child.stderr || !child.stdin)
|
|
13078
|
+
throw new Error("local-process: runner stdio missing");
|
|
13079
|
+
const pid = child.pid;
|
|
13080
|
+
return {
|
|
13081
|
+
exit,
|
|
13082
|
+
...pid !== void 0 ? { pid } : {},
|
|
13083
|
+
stdout: child.stdout,
|
|
13084
|
+
stderr: child.stderr,
|
|
13085
|
+
stdin: child.stdin,
|
|
13086
|
+
async reap(groups) {
|
|
13087
|
+
if (process.platform === "win32") return;
|
|
13088
|
+
const targets = [.../* @__PURE__ */ new Set([...pid !== void 0 ? [pid] : [], ...groups])].filter(
|
|
13089
|
+
groupAlive
|
|
13090
|
+
);
|
|
13091
|
+
if (!targets.length) return;
|
|
13092
|
+
for (const pgid of targets) signalGroup(pgid, "SIGTERM");
|
|
13093
|
+
await new Promise((resolve2) => setTimeout(resolve2, reapGraceMs));
|
|
13094
|
+
for (const pgid of targets.filter(groupAlive)) signalGroup(pgid, "SIGKILL");
|
|
13095
|
+
}
|
|
13096
|
+
};
|
|
13097
|
+
},
|
|
13098
|
+
async release() {
|
|
13099
|
+
}
|
|
13100
|
+
};
|
|
13101
|
+
}
|
|
13102
|
+
|
|
13103
|
+
// src/providers/docker.ts
|
|
13104
|
+
import { createHash as createHash4, randomUUID as randomUUID5 } from "crypto";
|
|
13105
|
+
import { constants as constants2, tmpdir as tmpdir2 } from "os";
|
|
13106
|
+
import {
|
|
13107
|
+
createReadStream,
|
|
13108
|
+
createWriteStream as createWriteStream2,
|
|
13109
|
+
existsSync as existsSync13,
|
|
13110
|
+
mkdirSync as mkdirSync10,
|
|
13111
|
+
readdirSync as readdirSync4,
|
|
13112
|
+
readFileSync as readFileSync9,
|
|
13113
|
+
renameSync as renameSync5,
|
|
13114
|
+
rmSync as rmSync9,
|
|
13115
|
+
statSync as statSync3,
|
|
13116
|
+
writeFileSync as writeFileSync6
|
|
13117
|
+
} from "fs";
|
|
13118
|
+
import { join as join17 } from "path";
|
|
13119
|
+
import { Writable } from "stream";
|
|
13120
|
+
|
|
13121
|
+
// src/providers/docker-cli.ts
|
|
13122
|
+
import { spawn as spawn7 } from "child_process";
|
|
13123
|
+
function cliEnv(extra) {
|
|
13124
|
+
const keep = [
|
|
13125
|
+
"PATH",
|
|
13126
|
+
"HOME",
|
|
13127
|
+
"DOCKER_HOST",
|
|
13128
|
+
"DOCKER_CONFIG",
|
|
13129
|
+
"DOCKER_CONTEXT",
|
|
13130
|
+
"DOCKER_CERT_PATH"
|
|
13131
|
+
];
|
|
13132
|
+
const env = {};
|
|
13133
|
+
for (const k of keep) if (process.env[k] !== void 0) env[k] = process.env[k];
|
|
13134
|
+
return { ...env, ...extra };
|
|
13135
|
+
}
|
|
13136
|
+
function dockerCli(bin = "docker") {
|
|
13137
|
+
return {
|
|
13138
|
+
run(args, opts = {}) {
|
|
13139
|
+
return new Promise((resolve2) => {
|
|
13140
|
+
let child;
|
|
13141
|
+
try {
|
|
13142
|
+
child = spawn7(bin, [...args], {
|
|
13143
|
+
env: cliEnv(opts.env),
|
|
13144
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
13145
|
+
});
|
|
13146
|
+
} catch (err) {
|
|
13147
|
+
resolve2({ code: 127, stdout: "", stderr: String(err) });
|
|
13148
|
+
return;
|
|
13149
|
+
}
|
|
13150
|
+
let stdout = "";
|
|
13151
|
+
let stderr = "";
|
|
13152
|
+
const timer = opts.timeoutMs ? setTimeout(() => child.kill("SIGKILL"), opts.timeoutMs) : void 0;
|
|
13153
|
+
if (opts.stdout) child.stdout?.pipe(opts.stdout);
|
|
13154
|
+
else child.stdout?.on("data", (chunk) => stdout += String(chunk));
|
|
13155
|
+
child.stderr?.on("data", (chunk) => stderr += String(chunk));
|
|
13156
|
+
child.stdin?.on("error", () => {
|
|
13157
|
+
});
|
|
13158
|
+
child.once("error", (err) => {
|
|
13159
|
+
if (timer) clearTimeout(timer);
|
|
13160
|
+
resolve2({ code: 127, stdout, stderr: stderr || err.message });
|
|
13161
|
+
});
|
|
13162
|
+
child.once("close", (code, signal) => {
|
|
13163
|
+
if (timer) clearTimeout(timer);
|
|
13164
|
+
resolve2({ code: code ?? (signal ? 137 : 1), stdout, stderr });
|
|
13165
|
+
});
|
|
13166
|
+
const input = opts.input;
|
|
13167
|
+
if (input === void 0) child.stdin?.end();
|
|
13168
|
+
else if (typeof input === "string" || Buffer.isBuffer(input)) child.stdin?.end(input);
|
|
13169
|
+
else input.pipe(child.stdin);
|
|
13170
|
+
});
|
|
13171
|
+
},
|
|
13172
|
+
spawn(args, env) {
|
|
13173
|
+
return spawn7(bin, [...args], { env: cliEnv(env), stdio: ["pipe", "pipe", "pipe"] });
|
|
13174
|
+
}
|
|
13175
|
+
};
|
|
13176
|
+
}
|
|
13177
|
+
|
|
13178
|
+
// src/providers/docker.ts
|
|
13179
|
+
init_git_credential();
|
|
13180
|
+
|
|
13181
|
+
// src/runner/collected-outbox.ts
|
|
13182
|
+
import {
|
|
13183
|
+
copyFileSync as copyFileSync2,
|
|
13184
|
+
existsSync as existsSync11,
|
|
13185
|
+
mkdirSync as mkdirSync9,
|
|
13186
|
+
readdirSync as readdirSync3,
|
|
13187
|
+
readFileSync as readFileSync8,
|
|
13188
|
+
renameSync as renameSync4,
|
|
13189
|
+
rmSync as rmSync8
|
|
13190
|
+
} from "fs";
|
|
13191
|
+
import { join as join15 } from "path";
|
|
13192
|
+
function routes(t) {
|
|
13193
|
+
const conv = `/api/workspaces/${t.workspaceId}/conversations/${t.conversationId}`;
|
|
13194
|
+
const agent = `${conv}/participants/agents/${t.agentId}`;
|
|
13195
|
+
return {
|
|
13196
|
+
message: { method: "POST", path: `${conv}/messages` },
|
|
13197
|
+
activity: { method: "POST", path: `${agent}/activity` },
|
|
13198
|
+
thinking: { method: "POST", path: `${agent}/thinking` },
|
|
13199
|
+
text: { method: "POST", path: `${agent}/text` },
|
|
13200
|
+
"active-run": { method: "PATCH", path: `${agent}/active-run` }
|
|
13201
|
+
};
|
|
13202
|
+
}
|
|
13203
|
+
function admitCollectedEntry(raw, turn) {
|
|
13204
|
+
if (!raw || typeof raw !== "object") return null;
|
|
13205
|
+
const entry = raw;
|
|
13206
|
+
const kind = entry.kind;
|
|
13207
|
+
if (!kind || !(kind in routes(turn))) return null;
|
|
13208
|
+
const route = routes(turn)[kind];
|
|
13209
|
+
if (entry.method !== route.method || entry.path !== route.path) return null;
|
|
13210
|
+
if (typeof entry.seq !== "number" || typeof entry.enqueuedAt !== "number") return null;
|
|
13211
|
+
const body = entry.body;
|
|
13212
|
+
if (!body || typeof body !== "object" || typeof body.turnId !== "string") return null;
|
|
13213
|
+
if (!turn.turnIds.includes(body.turnId)) return null;
|
|
13214
|
+
if (kind === "active-run") {
|
|
13215
|
+
if (body.activeRunStartedAt !== null) return null;
|
|
13216
|
+
if (entry.turnId !== `activerun:${turn.conversationId}:${turn.agentId}`) return null;
|
|
13217
|
+
} else if (entry.turnId !== body.turnId) {
|
|
13218
|
+
return null;
|
|
13219
|
+
}
|
|
13220
|
+
return entry;
|
|
13221
|
+
}
|
|
13222
|
+
var atomicMove = (from, to) => {
|
|
13223
|
+
try {
|
|
13224
|
+
renameSync4(from, to);
|
|
13225
|
+
} catch {
|
|
13226
|
+
const partial = `${to}.partial`;
|
|
13227
|
+
copyFileSync2(from, partial);
|
|
13228
|
+
renameSync4(partial, to);
|
|
13229
|
+
rmSync8(from, { force: true });
|
|
13230
|
+
}
|
|
13231
|
+
};
|
|
13232
|
+
function publishStaged(from, to, move = atomicMove) {
|
|
13233
|
+
if (!existsSync11(from)) return { published: 0, complete: true };
|
|
13234
|
+
const staged = readdirSync3(from).filter((n) => n.endsWith(".json")).map((name) => {
|
|
13235
|
+
let entry = {};
|
|
13236
|
+
try {
|
|
13237
|
+
entry = JSON.parse(readFileSync8(join15(from, name), "utf8"));
|
|
13238
|
+
} catch {
|
|
13239
|
+
}
|
|
13240
|
+
return { name, entry };
|
|
13241
|
+
});
|
|
13242
|
+
const settle = (e) => e.kind === "active-run" ? 1 : 0;
|
|
13243
|
+
staged.sort(
|
|
13244
|
+
(a, b) => settle(a.entry) - settle(b.entry) || (a.entry.enqueuedAt ?? 0) - (b.entry.enqueuedAt ?? 0) || String(a.entry.turnId).localeCompare(String(b.entry.turnId)) || (a.entry.seq ?? 0) - (b.entry.seq ?? 0)
|
|
13245
|
+
);
|
|
13246
|
+
if (staged.length) mkdirSync9(to, { recursive: true });
|
|
13247
|
+
let published = 0;
|
|
13248
|
+
for (const { name } of staged) {
|
|
13249
|
+
try {
|
|
13250
|
+
move(join15(from, name), join15(to, name));
|
|
13251
|
+
} catch {
|
|
13252
|
+
return { published, complete: false };
|
|
13253
|
+
}
|
|
13254
|
+
published++;
|
|
13255
|
+
}
|
|
13256
|
+
rmSync8(from, { recursive: true, force: true });
|
|
13257
|
+
return { published, complete: true };
|
|
13258
|
+
}
|
|
13259
|
+
|
|
13260
|
+
// src/providers/runner-base.ts
|
|
13261
|
+
import { createHash as createHash3 } from "crypto";
|
|
13262
|
+
import { existsSync as existsSync12, realpathSync as realpathSync2 } from "fs";
|
|
13263
|
+
import { dirname as dirname7, join as join16, sep } from "path";
|
|
13264
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
13265
|
+
var COMPANION_MOUNT = "/opt/cabane/companion";
|
|
13266
|
+
var BASE_DOCKERFILE = `FROM node:22-slim
|
|
13267
|
+
RUN apt-get update \\
|
|
13268
|
+
&& apt-get install -y --no-install-recommends git curl ca-certificates procps \\
|
|
13269
|
+
&& rm -rf /var/lib/apt/lists/* \\
|
|
13270
|
+
&& corepack enable
|
|
13271
|
+
RUN usermod -l runner -d /home/runner -m node \\
|
|
13272
|
+
&& groupmod -n runner node \\
|
|
13273
|
+
&& mkdir -p /work /var/lib/cabane-runner /run/cabane ${COMPANION_MOUNT} \\
|
|
13274
|
+
&& chown runner:runner /work /var/lib/cabane-runner
|
|
13275
|
+
RUN mkdir -p /home/runner/.codex /opt/cabane \\
|
|
13276
|
+
&& printf '[features]\\nuse_legacy_landlock = true\\n' > /opt/cabane/codex-config.toml \\
|
|
13277
|
+
&& cp /opt/cabane/codex-config.toml /home/runner/.codex/config.toml \\
|
|
13278
|
+
&& chown -R runner:runner /home/runner/.codex
|
|
13279
|
+
RUN printf '#!/bin/sh\\nexec node "\${CABANE_COMPANION_ROOT:-${COMPANION_MOUNT}}/dist/cli.js" "$@"\\n' > /usr/local/bin/cabane-companion \\
|
|
13280
|
+
&& chmod 755 /usr/local/bin/cabane-companion
|
|
13281
|
+
USER runner
|
|
13282
|
+
WORKDIR /home/runner
|
|
13283
|
+
CMD ["sleep", "infinity"]
|
|
13284
|
+
`;
|
|
13285
|
+
function baseImageTag(dockerfile = BASE_DOCKERFILE) {
|
|
13286
|
+
return `cabane-runner-base:${createHash3("sha256").update(dockerfile).digest("hex").slice(0, 12)}`;
|
|
13287
|
+
}
|
|
13288
|
+
async function ensureBaseImage(docker, image, log) {
|
|
13289
|
+
if ((await docker.run(["image", "inspect", image])).code === 0) return true;
|
|
13290
|
+
if (image !== baseImageTag()) {
|
|
13291
|
+
log.error({ image }, "runners: the configured runner image isn't on this engine");
|
|
13292
|
+
return false;
|
|
13293
|
+
}
|
|
13294
|
+
log.info({ image }, "runners: building the runner base image (once per base)");
|
|
13295
|
+
const built = await docker.run(["build", "-t", image, "-"], {
|
|
13296
|
+
input: BASE_DOCKERFILE,
|
|
13297
|
+
timeoutMs: 20 * 6e4
|
|
13298
|
+
});
|
|
13299
|
+
if (built.code !== 0) {
|
|
13300
|
+
log.error(
|
|
13301
|
+
{ image, err: built.stderr.trim().slice(-2e3) },
|
|
13302
|
+
"runners: couldn't build the runner base image; no turn can run until it is"
|
|
13303
|
+
);
|
|
13304
|
+
return false;
|
|
13305
|
+
}
|
|
13306
|
+
return true;
|
|
13307
|
+
}
|
|
13308
|
+
function ownInstall(moduleFile = fileURLToPath3(import.meta.url)) {
|
|
13309
|
+
let dir2 = dirname7(moduleFile);
|
|
13310
|
+
for (let i = 0; i < 4; i++) {
|
|
13311
|
+
if (existsSync12(join16(dir2, "dist", "cli.js")) && existsSync12(join16(dir2, "package.json"))) break;
|
|
13312
|
+
dir2 = dirname7(dir2);
|
|
13313
|
+
}
|
|
13314
|
+
if (!existsSync12(join16(dir2, "dist", "cli.js"))) return null;
|
|
13315
|
+
const sdk = join16(dir2, "node_modules", "@anthropic-ai", "claude-agent-sdk");
|
|
13316
|
+
if (!existsSync12(sdk)) return null;
|
|
13317
|
+
const root = realpathSync2(dir2);
|
|
13318
|
+
return realpathSync2(sdk).startsWith(root + sep) ? root : null;
|
|
13319
|
+
}
|
|
13320
|
+
function companionSource(input) {
|
|
13321
|
+
if (input.configured) {
|
|
13322
|
+
return { kind: "dir", path: input.configured, key: `dir:${input.configured}` };
|
|
13323
|
+
}
|
|
13324
|
+
if (input.platform === "linux") {
|
|
13325
|
+
if (input.own)
|
|
13326
|
+
return { kind: "dir", path: input.own, key: `dir:${input.own}:${input.version}` };
|
|
13327
|
+
return {
|
|
13328
|
+
error: "this companion runs from a checkout, which a container can't mount on its own; build one with `pnpm --filter @cabane/companion deploy --prod <dir>` and set runners.docker.companionDir"
|
|
13329
|
+
};
|
|
13330
|
+
}
|
|
13331
|
+
if (input.version === "0.0.0") {
|
|
13332
|
+
return {
|
|
13333
|
+
error: "an unreleased companion can't be installed into a container from npm; set runners.docker.companionDir to a Linux install of it"
|
|
13334
|
+
};
|
|
13335
|
+
}
|
|
13336
|
+
return {
|
|
13337
|
+
kind: "volume",
|
|
13338
|
+
volume: `cabane-companion-${input.version}`,
|
|
13339
|
+
version: input.version,
|
|
13340
|
+
key: `volume:${input.version}`
|
|
13341
|
+
};
|
|
13342
|
+
}
|
|
13343
|
+
function companionRoot(source) {
|
|
13344
|
+
return source.kind === "dir" ? COMPANION_MOUNT : `${COMPANION_MOUNT}/lib/node_modules/@cabane/companion`;
|
|
13345
|
+
}
|
|
13346
|
+
function companionMountArgs(source) {
|
|
13347
|
+
const mount = source.kind === "dir" ? `type=bind,src=${source.path},dst=${COMPANION_MOUNT},readonly` : `type=volume,src=${source.volume},dst=${COMPANION_MOUNT},readonly`;
|
|
13348
|
+
return ["--mount", mount, "-e", `CABANE_COMPANION_ROOT=${companionRoot(source)}`];
|
|
13349
|
+
}
|
|
13350
|
+
var INSTALL_MARKER = `${COMPANION_MOUNT}/.cabane-install-complete`;
|
|
13351
|
+
function volumeInstallScript(version, install = `npm install -g --prefix ${COMPANION_MOUNT} @cabane/companion@${version}`) {
|
|
13352
|
+
const root = `${COMPANION_MOUNT}/lib/node_modules/@cabane/companion`;
|
|
13353
|
+
return [
|
|
13354
|
+
`find ${COMPANION_MOUNT} -mindepth 1 -delete`,
|
|
13355
|
+
install,
|
|
13356
|
+
// Installed is not the same as runnable: the package must start and say
|
|
13357
|
+
// what it runs before the volume counts as done.
|
|
13358
|
+
`node ${root}/dist/cli.js run-turn --manifest > /dev/null`,
|
|
13359
|
+
`printf %s ${version} > ${INSTALL_MARKER}`
|
|
13360
|
+
].join(" && ");
|
|
13361
|
+
}
|
|
13362
|
+
var volumeMount = (source) => `type=volume,src=${source.volume},dst=${COMPANION_MOUNT}`;
|
|
13363
|
+
async function ensureCompanionVolume(docker, image, source, log, script = volumeInstallScript) {
|
|
13364
|
+
if (source.kind !== "volume") return true;
|
|
13365
|
+
const done = await docker.run([
|
|
13366
|
+
"run",
|
|
13367
|
+
"--rm",
|
|
13368
|
+
"--network",
|
|
13369
|
+
"none",
|
|
13370
|
+
"--mount",
|
|
13371
|
+
volumeMount(source),
|
|
13372
|
+
image,
|
|
13373
|
+
"sh",
|
|
13374
|
+
"-c",
|
|
13375
|
+
`[ "$(cat ${INSTALL_MARKER} 2>/dev/null)" = "$1" ]`,
|
|
13376
|
+
"sh",
|
|
13377
|
+
source.version
|
|
13378
|
+
]);
|
|
13379
|
+
if (done.code === 0) return true;
|
|
13380
|
+
log.info(
|
|
13381
|
+
{ version: source.version },
|
|
13382
|
+
"runners: installing this companion version for its containers"
|
|
13383
|
+
);
|
|
13384
|
+
const installed = await docker.run(
|
|
13385
|
+
[
|
|
13386
|
+
"run",
|
|
13387
|
+
"--rm",
|
|
13388
|
+
"-u",
|
|
13389
|
+
"root",
|
|
13390
|
+
"--mount",
|
|
13391
|
+
volumeMount(source),
|
|
13392
|
+
image,
|
|
13393
|
+
"sh",
|
|
13394
|
+
"-c",
|
|
13395
|
+
script(source.version)
|
|
13396
|
+
],
|
|
13397
|
+
{ timeoutMs: 20 * 6e4 }
|
|
13398
|
+
);
|
|
13399
|
+
if (installed.code !== 0) {
|
|
13400
|
+
log.error(
|
|
13401
|
+
{ version: source.version, err: installed.stderr.trim().slice(-2e3) },
|
|
13402
|
+
"runners: couldn't install the companion for its containers; the next start tries again"
|
|
13403
|
+
);
|
|
13404
|
+
return false;
|
|
13405
|
+
}
|
|
13406
|
+
return true;
|
|
13407
|
+
}
|
|
13408
|
+
async function invalidateCompanionVolume(docker, image, source) {
|
|
13409
|
+
if (source.kind !== "volume") return;
|
|
13410
|
+
await docker.run([
|
|
13411
|
+
"run",
|
|
13412
|
+
"--rm",
|
|
13413
|
+
"-u",
|
|
13414
|
+
"root",
|
|
13415
|
+
"--network",
|
|
13416
|
+
"none",
|
|
13417
|
+
"--mount",
|
|
13418
|
+
volumeMount(source),
|
|
13419
|
+
image,
|
|
13420
|
+
"rm",
|
|
13421
|
+
"-f",
|
|
13422
|
+
INSTALL_MARKER
|
|
13423
|
+
]);
|
|
13424
|
+
}
|
|
12535
13425
|
|
|
12536
|
-
// src/providers/
|
|
12537
|
-
|
|
12538
|
-
|
|
12539
|
-
var
|
|
12540
|
-
var
|
|
12541
|
-
|
|
12542
|
-
|
|
12543
|
-
|
|
12544
|
-
|
|
13426
|
+
// src/providers/docker.ts
|
|
13427
|
+
var CONTAINER_HOME = "/home/runner";
|
|
13428
|
+
var CONTAINER_WORK = "/work";
|
|
13429
|
+
var CONTAINER_STATE = "/var/lib/cabane-runner";
|
|
13430
|
+
var CONTAINER_SOCKETS = "/run/cabane";
|
|
13431
|
+
var PROXY_ALIAS = "cabane-egress";
|
|
13432
|
+
var PROXY_PORT = 3128;
|
|
13433
|
+
var DEFAULT_KILL_GRACE_MS2 = 3e4;
|
|
13434
|
+
var REAP_GRACE_S = 3;
|
|
13435
|
+
var DEFAULT_SESSION_CAP_BYTES = 512 * 1024 * 1024;
|
|
13436
|
+
var COLLECT_CAP_BYTES = 128 * 1024 * 1024;
|
|
13437
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
13438
|
+
function containerName(attempt) {
|
|
13439
|
+
return `cabane-runner-${attempt.conversationId}-${attempt.agentId}`;
|
|
13440
|
+
}
|
|
13441
|
+
function sessionArchivePath(deviceDir, attemptId) {
|
|
13442
|
+
if (!UUID_RE.test(attemptId)) throw new Error(`docker: not an attempt id: ${attemptId}`);
|
|
13443
|
+
return join17(deviceDir, "attempts", attemptId, "session.tar");
|
|
13444
|
+
}
|
|
13445
|
+
function pruneOtherAttempts(deviceDir, pair2, keep) {
|
|
13446
|
+
const root = join17(deviceDir, "attempts");
|
|
13447
|
+
if (!existsSync13(root)) return;
|
|
13448
|
+
for (const id of readdirSync4(root)) {
|
|
13449
|
+
if (id === keep || !UUID_RE.test(id)) continue;
|
|
13450
|
+
let pairOf = "";
|
|
13451
|
+
try {
|
|
13452
|
+
pairOf = readFileSync9(join17(root, id, "pair"), "utf8");
|
|
13453
|
+
} catch {
|
|
13454
|
+
continue;
|
|
13455
|
+
}
|
|
13456
|
+
if (pairOf === `${pair2.conversationId}/${pair2.agentId}`) {
|
|
13457
|
+
rmSync9(join17(root, id), { recursive: true, force: true });
|
|
13458
|
+
}
|
|
13459
|
+
}
|
|
12545
13460
|
}
|
|
12546
|
-
|
|
12547
|
-
|
|
12548
|
-
|
|
12549
|
-
|
|
12550
|
-
|
|
12551
|
-
|
|
13461
|
+
var CappedFile = class extends Writable {
|
|
13462
|
+
constructor(path, cap) {
|
|
13463
|
+
super();
|
|
13464
|
+
this.path = path;
|
|
13465
|
+
this.cap = cap;
|
|
13466
|
+
this.out = createWriteStream2(path);
|
|
13467
|
+
}
|
|
13468
|
+
path;
|
|
13469
|
+
cap;
|
|
13470
|
+
bytes = 0;
|
|
13471
|
+
over = false;
|
|
13472
|
+
out;
|
|
13473
|
+
_write(chunk, _enc, done) {
|
|
13474
|
+
this.bytes += chunk.length;
|
|
13475
|
+
if (this.bytes > this.cap) {
|
|
13476
|
+
this.over = true;
|
|
13477
|
+
done();
|
|
13478
|
+
return;
|
|
13479
|
+
}
|
|
13480
|
+
this.out.write(chunk, done);
|
|
13481
|
+
}
|
|
13482
|
+
_final(done) {
|
|
13483
|
+
this.out.end(done);
|
|
13484
|
+
}
|
|
13485
|
+
};
|
|
13486
|
+
var DUMP_SCRIPT = `
|
|
13487
|
+
const fs = require('fs'), path = require('path');
|
|
13488
|
+
const dir = process.argv[1], cap = Number(process.argv[2]);
|
|
13489
|
+
let names;
|
|
13490
|
+
try { names = fs.readdirSync(dir); }
|
|
13491
|
+
catch (e) {
|
|
13492
|
+
if (e.code === 'ENOENT') { process.stdout.write(JSON.stringify({ complete: true, files: [] })); process.exit(0); }
|
|
13493
|
+
process.stderr.write(String(e.message)); process.exit(2);
|
|
13494
|
+
}
|
|
13495
|
+
let total = 0, complete = true; const files = [];
|
|
13496
|
+
for (const name of names) {
|
|
13497
|
+
const p = path.join(dir, name);
|
|
13498
|
+
let st;
|
|
13499
|
+
try { st = fs.lstatSync(p); } catch (e) { process.stderr.write(String(e.message)); process.exit(2); }
|
|
13500
|
+
if (!st.isFile()) continue;
|
|
13501
|
+
// Always at least one file per read, so one over the cap is still collected.
|
|
13502
|
+
if (files.length && total + st.size > cap) { complete = false; continue; }
|
|
13503
|
+
let data;
|
|
13504
|
+
try { data = fs.readFileSync(p); } catch (e) { process.stderr.write(String(e.message)); process.exit(2); }
|
|
13505
|
+
total += st.size;
|
|
13506
|
+
files.push({ name, data: data.toString('base64') });
|
|
13507
|
+
}
|
|
13508
|
+
process.stdout.write(JSON.stringify({ complete, files }));
|
|
13509
|
+
`;
|
|
13510
|
+
var SAFE_NAME_RE = /^[\w.%@+-]+$/;
|
|
13511
|
+
function createDockerProvider(opts) {
|
|
13512
|
+
const docker = opts.docker ?? dockerCli();
|
|
13513
|
+
const log = opts.log;
|
|
13514
|
+
const killGraceMs = opts.killGraceMs ?? DEFAULT_KILL_GRACE_MS2;
|
|
13515
|
+
const sessionCap = opts.sessionCapBytes ?? DEFAULT_SESSION_CAP_BYTES;
|
|
13516
|
+
const collectCap = opts.collectCapBytes ?? COLLECT_CAP_BYTES;
|
|
13517
|
+
const network = `cabane-runners-${opts.deviceKey}`;
|
|
13518
|
+
const companionLabel = createHash4("sha256").update(opts.companion.key).digest("hex").slice(0, 16);
|
|
13519
|
+
const proxy = `cabane-egress-${opts.deviceKey}`;
|
|
13520
|
+
const held = /* @__PURE__ */ new Map();
|
|
13521
|
+
const queues = /* @__PURE__ */ new Map();
|
|
13522
|
+
const must = async (args, what) => {
|
|
13523
|
+
const r = await docker.run(args);
|
|
13524
|
+
if (r.code !== 0)
|
|
13525
|
+
throw new Error(`docker: ${what} failed: ${r.stderr.trim() || `exit ${r.code}`}`);
|
|
13526
|
+
return r.stdout;
|
|
13527
|
+
};
|
|
13528
|
+
const execIn = (name, args, extra = {}) => docker.run(
|
|
13529
|
+
["exec", ...extra.input !== void 0 ? ["-i"] : [], "-u", "runner", name, ...args],
|
|
13530
|
+
extra
|
|
13531
|
+
);
|
|
13532
|
+
const proxyListening = async () => {
|
|
13533
|
+
for (let i = 0; i < 40; i++) {
|
|
13534
|
+
const r = await docker.run([
|
|
13535
|
+
"exec",
|
|
13536
|
+
proxy,
|
|
13537
|
+
"curl",
|
|
13538
|
+
"-s",
|
|
13539
|
+
"-o",
|
|
13540
|
+
"/dev/null",
|
|
13541
|
+
"--max-time",
|
|
13542
|
+
"1",
|
|
13543
|
+
`http://127.0.0.1:${PROXY_PORT}/`
|
|
13544
|
+
]);
|
|
13545
|
+
if (r.code === 0) return;
|
|
13546
|
+
await new Promise((resolve2) => setTimeout(resolve2, 250));
|
|
13547
|
+
}
|
|
13548
|
+
throw new Error("docker: the egress proxy never started listening");
|
|
13549
|
+
};
|
|
13550
|
+
let networkReady = null;
|
|
13551
|
+
const proxyKey = async () => {
|
|
13552
|
+
const id = (await docker.run(["image", "inspect", "-f", "{{.Id}}", opts.image])).stdout.trim();
|
|
13553
|
+
return createHash4("sha256").update(JSON.stringify([id || opts.image, opts.companion.key, [...opts.egressAllow].sort()])).digest("hex").slice(0, 16);
|
|
13554
|
+
};
|
|
13555
|
+
const ensureNetwork = () => {
|
|
13556
|
+
networkReady ??= (async () => {
|
|
13557
|
+
const allowKey = await proxyKey();
|
|
13558
|
+
if ((await docker.run(["network", "inspect", network])).code !== 0) {
|
|
13559
|
+
await must(
|
|
13560
|
+
[
|
|
13561
|
+
"network",
|
|
13562
|
+
"create",
|
|
13563
|
+
// No route off the host, and no address of the host's on the
|
|
13564
|
+
// bridge: the host's own ports are out of reach too. The proxy is
|
|
13565
|
+
// the only other thing on the network.
|
|
13566
|
+
"--internal",
|
|
13567
|
+
"-o",
|
|
13568
|
+
"com.docker.network.bridge.inhibit_ipv4=true",
|
|
13569
|
+
"--label",
|
|
13570
|
+
`cabane.device=${opts.deviceKey}`,
|
|
13571
|
+
network
|
|
13572
|
+
],
|
|
13573
|
+
"creating the runners network"
|
|
13574
|
+
);
|
|
13575
|
+
}
|
|
13576
|
+
const seen = await docker.run([
|
|
13577
|
+
"inspect",
|
|
13578
|
+
"-f",
|
|
13579
|
+
'{{index .Config.Labels "cabane.allow"}} {{.State.Running}}',
|
|
13580
|
+
proxy
|
|
13581
|
+
]);
|
|
13582
|
+
const [key, running] = seen.stdout.trim().split(" ");
|
|
13583
|
+
if (seen.code === 0 && key === allowKey) {
|
|
13584
|
+
if (running !== "true") {
|
|
13585
|
+
await must(["start", proxy], "starting the egress proxy");
|
|
13586
|
+
await proxyListening();
|
|
13587
|
+
}
|
|
13588
|
+
return;
|
|
13589
|
+
}
|
|
13590
|
+
if (seen.code === 0) await docker.run(["rm", "-f", proxy]);
|
|
13591
|
+
await must(
|
|
13592
|
+
[
|
|
13593
|
+
"run",
|
|
13594
|
+
"-d",
|
|
13595
|
+
"--name",
|
|
13596
|
+
proxy,
|
|
13597
|
+
"--label",
|
|
13598
|
+
`cabane.allow=${allowKey}`,
|
|
13599
|
+
"--label",
|
|
13600
|
+
`cabane.device=${opts.deviceKey}`,
|
|
13601
|
+
"--restart",
|
|
13602
|
+
"unless-stopped",
|
|
13603
|
+
"--cap-drop",
|
|
13604
|
+
"ALL",
|
|
13605
|
+
"--security-opt",
|
|
13606
|
+
"no-new-privileges",
|
|
13607
|
+
"-e",
|
|
13608
|
+
`CABANE_EGRESS_ALLOW=${JSON.stringify(opts.egressAllow)}`,
|
|
13609
|
+
...companionMountArgs(opts.companion),
|
|
13610
|
+
opts.image,
|
|
13611
|
+
"cabane-companion",
|
|
13612
|
+
"egress-proxy",
|
|
13613
|
+
"--port",
|
|
13614
|
+
String(PROXY_PORT)
|
|
13615
|
+
],
|
|
13616
|
+
"starting the egress proxy"
|
|
13617
|
+
);
|
|
13618
|
+
await must(
|
|
13619
|
+
["network", "connect", "--alias", PROXY_ALIAS, network, proxy],
|
|
13620
|
+
"connecting the proxy"
|
|
13621
|
+
);
|
|
13622
|
+
await proxyListening();
|
|
13623
|
+
log.info({ network, proxy, allow: opts.egressAllow }, "runners: egress proxy ready");
|
|
13624
|
+
})().catch((err) => {
|
|
13625
|
+
networkReady = null;
|
|
13626
|
+
throw err;
|
|
13627
|
+
});
|
|
13628
|
+
return networkReady;
|
|
13629
|
+
};
|
|
13630
|
+
const socketRoot = opts.socketRoot ?? join17(
|
|
13631
|
+
process.env.XDG_RUNTIME_DIR && existsSync13(process.env.XDG_RUNTIME_DIR) ? process.env.XDG_RUNTIME_DIR : tmpdir2(),
|
|
13632
|
+
`cabane-runners-${opts.deviceKey}`
|
|
13633
|
+
);
|
|
13634
|
+
const socketDir = (name) => join17(socketRoot, createHash4("sha256").update(name).digest("hex").slice(0, 12));
|
|
13635
|
+
const create = async (name, attempt) => {
|
|
13636
|
+
mkdirSync10(socketRoot, { recursive: true, mode: 448 });
|
|
13637
|
+
mkdirSync10(socketDir(name), { recursive: true, mode: 457 });
|
|
13638
|
+
const mounts = (opts.mounts ?? []).filter((m) => m.source.startsWith("/") && existsSync13(m.source)).flatMap((m) => ["--mount", `type=bind,src=${m.source},dst=${m.target},readonly`]);
|
|
13639
|
+
await must(
|
|
13640
|
+
[
|
|
13641
|
+
"create",
|
|
13642
|
+
"--name",
|
|
13643
|
+
name,
|
|
13644
|
+
"--init",
|
|
13645
|
+
"--hostname",
|
|
13646
|
+
"cabane-runner",
|
|
13647
|
+
"--label",
|
|
13648
|
+
"cabane.runner=1",
|
|
13649
|
+
"--label",
|
|
13650
|
+
`cabane.device=${opts.deviceKey}`,
|
|
13651
|
+
"--label",
|
|
13652
|
+
`cabane.workspace=${attempt.workspaceId}`,
|
|
13653
|
+
"--label",
|
|
13654
|
+
`cabane.conversation=${attempt.conversationId}`,
|
|
13655
|
+
"--label",
|
|
13656
|
+
`cabane.agent=${attempt.agentId}`,
|
|
13657
|
+
"--label",
|
|
13658
|
+
`cabane.companion=${companionLabel}`,
|
|
13659
|
+
"--network",
|
|
13660
|
+
network,
|
|
13661
|
+
"--mount",
|
|
13662
|
+
`type=volume,src=${name}-home,dst=${CONTAINER_HOME}`,
|
|
13663
|
+
"--mount",
|
|
13664
|
+
`type=volume,src=${name}-work,dst=${CONTAINER_WORK}`,
|
|
13665
|
+
"--mount",
|
|
13666
|
+
`type=bind,src=${socketDir(name)},dst=${CONTAINER_SOCKETS},readonly`,
|
|
13667
|
+
...mounts,
|
|
13668
|
+
...companionMountArgs(opts.companion),
|
|
13669
|
+
"--cap-drop",
|
|
13670
|
+
"ALL",
|
|
13671
|
+
"--security-opt",
|
|
13672
|
+
"no-new-privileges",
|
|
13673
|
+
"--pids-limit",
|
|
13674
|
+
"4096",
|
|
13675
|
+
opts.image,
|
|
13676
|
+
"sleep",
|
|
13677
|
+
"infinity"
|
|
13678
|
+
],
|
|
13679
|
+
"creating the container"
|
|
13680
|
+
);
|
|
13681
|
+
};
|
|
13682
|
+
const destroy = async (name) => {
|
|
13683
|
+
await docker.run(["rm", "-f", "-v", name]);
|
|
13684
|
+
await docker.run(["volume", "rm", "-f", `${name}-home`, `${name}-work`]);
|
|
13685
|
+
rmSync9(socketDir(name), { recursive: true, force: true });
|
|
13686
|
+
};
|
|
13687
|
+
const containerEnv = () => {
|
|
13688
|
+
const proxyUrl = `http://${PROXY_ALIAS}:${PROXY_PORT}`;
|
|
13689
|
+
const git = [
|
|
13690
|
+
["credential.helper", ""],
|
|
13691
|
+
// The helper speaks a socket, not the network: no proxy, and none of
|
|
13692
|
+
// Node's warning about it in git's output.
|
|
13693
|
+
["credential.helper", "!NODE_USE_ENV_PROXY= cabane-companion git-credential"],
|
|
13694
|
+
...opts.gitHosts.flatMap((h) => [
|
|
13695
|
+
[`url.https://${h}/.insteadOf`, `git@${h}:`],
|
|
13696
|
+
[`url.https://${h}/.insteadOf`, `ssh://git@${h}/`]
|
|
13697
|
+
])
|
|
13698
|
+
];
|
|
13699
|
+
return {
|
|
13700
|
+
HOME: CONTAINER_HOME,
|
|
13701
|
+
CABANE_COMPANION_HOME: CONTAINER_STATE,
|
|
13702
|
+
CABANE_RUNNER_STATE_DIR: CONTAINER_STATE,
|
|
13703
|
+
HTTPS_PROXY: proxyUrl,
|
|
13704
|
+
HTTP_PROXY: proxyUrl,
|
|
13705
|
+
https_proxy: proxyUrl,
|
|
13706
|
+
http_proxy: proxyUrl,
|
|
13707
|
+
NO_PROXY: "localhost,127.0.0.1,::1",
|
|
13708
|
+
no_proxy: "localhost,127.0.0.1,::1",
|
|
13709
|
+
// Node's own fetch — the runner's wire — honours the proxy only when asked.
|
|
13710
|
+
NODE_USE_ENV_PROXY: "1",
|
|
13711
|
+
CABANE_GIT_CREDENTIAL_SOCKET: `${CONTAINER_SOCKETS}/git.sock`,
|
|
13712
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
13713
|
+
GIT_CONFIG_COUNT: String(git.length),
|
|
13714
|
+
...Object.fromEntries(
|
|
13715
|
+
git.flatMap(([k, v], i) => [
|
|
13716
|
+
[`GIT_CONFIG_KEY_${i}`, k],
|
|
13717
|
+
[`GIT_CONFIG_VALUE_${i}`, v]
|
|
13718
|
+
])
|
|
13719
|
+
)
|
|
13720
|
+
};
|
|
13721
|
+
};
|
|
13722
|
+
const heldFor = (handle) => {
|
|
13723
|
+
const h = held.get(handle.id);
|
|
13724
|
+
if (!h) throw new Error(`docker: ${handle.id} is not held`);
|
|
13725
|
+
return h;
|
|
13726
|
+
};
|
|
13727
|
+
const writeMarker = (name, attemptId) => execIn(name, [
|
|
13728
|
+
"sh",
|
|
13729
|
+
"-c",
|
|
13730
|
+
`mkdir -p ${CONTAINER_STATE} && printf %s "$1" > ${CONTAINER_STATE}/attempt`,
|
|
13731
|
+
"sh",
|
|
13732
|
+
attemptId
|
|
13733
|
+
]);
|
|
13734
|
+
const readDir = async (name, from) => {
|
|
13735
|
+
const r = await execIn(name, [
|
|
13736
|
+
"sh",
|
|
13737
|
+
"-c",
|
|
13738
|
+
'[ -e "$1" ] && chmod -R u+rwX "$1"; exec node -e "$2" "$1" "$3"',
|
|
13739
|
+
"sh",
|
|
13740
|
+
from,
|
|
13741
|
+
DUMP_SCRIPT,
|
|
13742
|
+
String(collectCap)
|
|
13743
|
+
]);
|
|
13744
|
+
if (r.code !== 0) {
|
|
13745
|
+
log.warn(
|
|
13746
|
+
{ container: name, dir: from, err: r.stderr.trim() },
|
|
13747
|
+
"runners: couldn't read what the runner left"
|
|
13748
|
+
);
|
|
13749
|
+
return null;
|
|
13750
|
+
}
|
|
12552
13751
|
try {
|
|
12553
|
-
|
|
12554
|
-
return
|
|
13752
|
+
const dump = JSON.parse(r.stdout);
|
|
13753
|
+
if (typeof dump.complete !== "boolean" || !Array.isArray(dump.files)) return null;
|
|
13754
|
+
return {
|
|
13755
|
+
complete: dump.complete,
|
|
13756
|
+
files: dump.files.filter(
|
|
13757
|
+
(f) => typeof f.name === "string" && typeof f.data === "string"
|
|
13758
|
+
).map((f) => ({ name: f.name, data: Buffer.from(f.data, "base64") }))
|
|
13759
|
+
};
|
|
12555
13760
|
} catch {
|
|
12556
|
-
return
|
|
13761
|
+
return null;
|
|
12557
13762
|
}
|
|
12558
13763
|
};
|
|
12559
|
-
const
|
|
13764
|
+
const removeIn = (name, from, names) => names.length ? execIn(name, ["sh", "-c", 'cd "$1" && shift && rm -f -- "$@"', "sh", from, ...names]) : Promise.resolve(null);
|
|
13765
|
+
const pendingPath = (name) => join17(opts.deviceDir, "runners-pending", `${name}.json`);
|
|
13766
|
+
const readPending = (name) => {
|
|
12560
13767
|
try {
|
|
12561
|
-
|
|
13768
|
+
const ids = JSON.parse(readFileSync9(pendingPath(name), "utf8"));
|
|
13769
|
+
return Array.isArray(ids) ? ids.filter((id) => typeof id === "string") : [];
|
|
12562
13770
|
} catch {
|
|
13771
|
+
return [];
|
|
12563
13772
|
}
|
|
12564
13773
|
};
|
|
12565
|
-
|
|
12566
|
-
|
|
13774
|
+
const writePending = (name, ids) => {
|
|
13775
|
+
if (!ids.length) {
|
|
13776
|
+
rmSync9(pendingPath(name), { force: true });
|
|
13777
|
+
return;
|
|
13778
|
+
}
|
|
13779
|
+
mkdirSync10(join17(opts.deviceDir, "runners-pending"), { recursive: true });
|
|
13780
|
+
writeFileSync6(pendingPath(name), JSON.stringify([...new Set(ids)]));
|
|
13781
|
+
};
|
|
13782
|
+
const keepSession = async (name, attempt, attemptId) => {
|
|
13783
|
+
const archive = sessionArchivePath(opts.deviceDir, attemptId);
|
|
13784
|
+
mkdirSync10(join17(archive, ".."), { recursive: true });
|
|
13785
|
+
const partial = `${archive}.${process.pid}.partial`;
|
|
13786
|
+
const file = new CappedFile(partial, sessionCap);
|
|
13787
|
+
const r = await execIn(
|
|
13788
|
+
name,
|
|
13789
|
+
[
|
|
13790
|
+
"sh",
|
|
13791
|
+
"-c",
|
|
13792
|
+
'cd "$HOME" && set -- && for p in .claude/projects .codex; do [ -e "$p" ] && set -- "$@" "$p"; done; [ $# -gt 0 ] || exit 3; exec tar -cf - --exclude=.codex/auth.json "$@"'
|
|
13793
|
+
],
|
|
13794
|
+
{ stdout: file }
|
|
13795
|
+
);
|
|
13796
|
+
await new Promise(
|
|
13797
|
+
(resolve2) => file.writableFinished ? resolve2() : file.once("finish", resolve2)
|
|
13798
|
+
);
|
|
13799
|
+
if (r.code === 0 && !file.over) {
|
|
13800
|
+
renameSync5(partial, archive);
|
|
13801
|
+
writeFileSync6(join17(archive, "..", "pair"), `${attempt.conversationId}/${attempt.agentId}`);
|
|
13802
|
+
pruneOtherAttempts(opts.deviceDir, attempt, attemptId);
|
|
13803
|
+
await writeMarker(name, attemptId);
|
|
13804
|
+
log.debug({ container: name, attemptId, bytes: file.bytes }, "runners: session files kept");
|
|
13805
|
+
return;
|
|
13806
|
+
}
|
|
13807
|
+
rmSync9(partial, { force: true });
|
|
13808
|
+
if (r.code === 3 && !file.over) return;
|
|
13809
|
+
dropArchive(attemptId);
|
|
13810
|
+
log.warn(
|
|
13811
|
+
file.over ? { container: name, attemptId, capBytes: sessionCap } : { container: name, attemptId, err: r.stderr.trim() },
|
|
13812
|
+
file.over ? "runners: the session files are over the size cap and weren't kept; the next new container starts the session over" : "runners: couldn't keep the session files; the next new container starts the session over"
|
|
13813
|
+
);
|
|
13814
|
+
};
|
|
13815
|
+
const stagingDir = (name) => join17(opts.deviceDir, "runners-pending", `${name}.outbox`);
|
|
13816
|
+
const ensureRunning = async (name) => {
|
|
13817
|
+
const seen = await docker.run(["inspect", "-f", "{{.State.Running}}", name]);
|
|
13818
|
+
if (seen.code !== 0) return false;
|
|
13819
|
+
if (seen.stdout.trim() === "true") return true;
|
|
13820
|
+
return (await docker.run(["start", name])).code === 0;
|
|
13821
|
+
};
|
|
13822
|
+
const dropArchive = (attemptId) => {
|
|
13823
|
+
rmSync9(join17(sessionArchivePath(opts.deviceDir, attemptId), ".."), {
|
|
13824
|
+
recursive: true,
|
|
13825
|
+
force: true
|
|
13826
|
+
});
|
|
13827
|
+
};
|
|
13828
|
+
const RETIRE_SCRIPT = [
|
|
13829
|
+
`chmod -R u+rwX /work ${CONTAINER_HOME}`,
|
|
13830
|
+
"find /work -mindepth 1 -delete",
|
|
13831
|
+
`find ${CONTAINER_HOME} -mindepth 1 -delete`,
|
|
13832
|
+
`cp -a /etc/skel/. ${CONTAINER_HOME}/`,
|
|
13833
|
+
`mkdir -p ${CONTAINER_HOME}/.codex`,
|
|
13834
|
+
`cp /opt/cabane/codex-config.toml ${CONTAINER_HOME}/.codex/config.toml`,
|
|
13835
|
+
`rm -rf ${CONTAINER_STATE}/prepared`
|
|
13836
|
+
].join(" && ");
|
|
13837
|
+
const provider = {
|
|
13838
|
+
name: "docker",
|
|
13839
|
+
inheritsEnvironment: false,
|
|
12567
13840
|
async acquire(attempt) {
|
|
12568
|
-
|
|
12569
|
-
|
|
12570
|
-
|
|
12571
|
-
const
|
|
12572
|
-
|
|
12573
|
-
|
|
12574
|
-
|
|
12575
|
-
|
|
12576
|
-
detached: process.platform !== "win32"
|
|
13841
|
+
const name = containerName(attempt);
|
|
13842
|
+
const before = queues.get(name) ?? Promise.resolve();
|
|
13843
|
+
let release;
|
|
13844
|
+
const mine = new Promise((resolve2) => release = resolve2);
|
|
13845
|
+
const tail = before.then(() => mine);
|
|
13846
|
+
queues.set(name, tail);
|
|
13847
|
+
void tail.then(() => {
|
|
13848
|
+
if (queues.get(name) === tail) queues.delete(name);
|
|
12577
13849
|
});
|
|
13850
|
+
await before;
|
|
13851
|
+
try {
|
|
13852
|
+
await ensureNetwork();
|
|
13853
|
+
mkdirSync10(socketRoot, { recursive: true, mode: 448 });
|
|
13854
|
+
mkdirSync10(socketDir(name), { recursive: true, mode: 457 });
|
|
13855
|
+
let seen = await docker.run([
|
|
13856
|
+
"inspect",
|
|
13857
|
+
"-f",
|
|
13858
|
+
'{{.State.Running}} {{index .Config.Labels "cabane.companion"}}',
|
|
13859
|
+
name
|
|
13860
|
+
]);
|
|
13861
|
+
if (seen.code === 0 && seen.stdout.trim().split(" ")[1] !== companionLabel) {
|
|
13862
|
+
held.set(name, { attempt, release: () => {
|
|
13863
|
+
}, collected: false });
|
|
13864
|
+
let collected = false;
|
|
13865
|
+
try {
|
|
13866
|
+
await provider.collect({ provider: "docker", id: name }, { attemptId: null });
|
|
13867
|
+
collected = held.get(name)?.collected === true;
|
|
13868
|
+
} finally {
|
|
13869
|
+
held.delete(name);
|
|
13870
|
+
}
|
|
13871
|
+
if (!collected)
|
|
13872
|
+
throw new Error(
|
|
13873
|
+
"docker: this conversation's container runs another companion and what it holds couldn't be collected"
|
|
13874
|
+
);
|
|
13875
|
+
await destroy(name);
|
|
13876
|
+
log.info(
|
|
13877
|
+
{ container: name },
|
|
13878
|
+
"runners: replaced a container made with another companion"
|
|
13879
|
+
);
|
|
13880
|
+
seen = { code: 1, stdout: "", stderr: "" };
|
|
13881
|
+
}
|
|
13882
|
+
if (seen.code !== 0) {
|
|
13883
|
+
await create(name, attempt);
|
|
13884
|
+
log.debug({ container: name }, "runners: container created");
|
|
13885
|
+
}
|
|
13886
|
+
if (seen.stdout.trim().split(" ")[0] !== "true")
|
|
13887
|
+
await must(["start", name], "starting the container");
|
|
13888
|
+
const git = await serveGitCredentials({
|
|
13889
|
+
socketPath: join17(socketDir(name), "git.sock"),
|
|
13890
|
+
hosts: opts.gitHosts,
|
|
13891
|
+
fill: opts.gitFill,
|
|
13892
|
+
log
|
|
13893
|
+
});
|
|
13894
|
+
held.set(name, { attempt, release, git, collected: false });
|
|
13895
|
+
return { provider: "docker", id: name };
|
|
13896
|
+
} catch (err) {
|
|
13897
|
+
const keep = opts.keepContainers || readPending(name).length > 0;
|
|
13898
|
+
await (keep ? docker.run(["stop", "-t", "1", name]) : destroy(name)).catch(() => {
|
|
13899
|
+
});
|
|
13900
|
+
release();
|
|
13901
|
+
throw err;
|
|
13902
|
+
}
|
|
13903
|
+
},
|
|
13904
|
+
async bindAttempt(handle, attemptId) {
|
|
13905
|
+
const name = handle.id;
|
|
13906
|
+
const marker = await execIn(name, ["cat", `${CONTAINER_STATE}/attempt`]);
|
|
13907
|
+
const bound = marker.code === 0 ? marker.stdout.trim() : "";
|
|
13908
|
+
if (bound === (attemptId ?? "")) return;
|
|
13909
|
+
const { attempt } = heldFor(handle);
|
|
13910
|
+
pruneOtherAttempts(opts.deviceDir, attempt, attemptId ?? "");
|
|
13911
|
+
if (bound) {
|
|
13912
|
+
const wiped = await execIn(name, ["sh", "-c", RETIRE_SCRIPT]);
|
|
13913
|
+
if (wiped.code !== 0)
|
|
13914
|
+
throw new Error(`docker: couldn't clear the retired attempt: ${wiped.stderr.trim()}`);
|
|
13915
|
+
log.info(
|
|
13916
|
+
{ container: name, retired: bound, attemptId },
|
|
13917
|
+
"runners: retired attempt cleared"
|
|
13918
|
+
);
|
|
13919
|
+
}
|
|
13920
|
+
if (attemptId && UUID_RE.test(attemptId)) {
|
|
13921
|
+
const archive = sessionArchivePath(opts.deviceDir, attemptId);
|
|
13922
|
+
if (existsSync13(archive)) {
|
|
13923
|
+
const r = await execIn(name, ["tar", "-x", "-C", CONTAINER_HOME, "-f", "-"], {
|
|
13924
|
+
input: createReadStream(archive)
|
|
13925
|
+
});
|
|
13926
|
+
if (r.code !== 0) {
|
|
13927
|
+
log.warn(
|
|
13928
|
+
{ container: name, attemptId, err: r.stderr.trim() },
|
|
13929
|
+
"runners: couldn't restore the session files; the runtime will start its session over"
|
|
13930
|
+
);
|
|
13931
|
+
} else {
|
|
13932
|
+
log.info(
|
|
13933
|
+
{ container: name, attemptId, bytes: statSync3(archive).size },
|
|
13934
|
+
"runners: session files restored"
|
|
13935
|
+
);
|
|
13936
|
+
}
|
|
13937
|
+
} else {
|
|
13938
|
+
log.info(
|
|
13939
|
+
{ container: name, attemptId },
|
|
13940
|
+
"runners: no session files kept for this attempt on this device"
|
|
13941
|
+
);
|
|
13942
|
+
}
|
|
13943
|
+
}
|
|
13944
|
+
await writeMarker(name, attemptId ?? "");
|
|
13945
|
+
},
|
|
13946
|
+
exec(handle, argv, env, { signal, stdin }) {
|
|
13947
|
+
const name = handle.id;
|
|
13948
|
+
const pidFile = `/tmp/cabane-runner-${randomUUID5()}.pid`;
|
|
13949
|
+
const passed = { ...env, ...opts.runtimeEnv?.() ?? {} };
|
|
13950
|
+
const literal = containerEnv();
|
|
13951
|
+
const args = [
|
|
13952
|
+
"exec",
|
|
13953
|
+
"-i",
|
|
13954
|
+
"-u",
|
|
13955
|
+
"runner",
|
|
13956
|
+
"-w",
|
|
13957
|
+
CONTAINER_HOME,
|
|
13958
|
+
...Object.entries(literal).flatMap(([k, v]) => ["-e", `${k}=${v}`]),
|
|
13959
|
+
...Object.keys(passed).flatMap((k) => ["-e", k]),
|
|
13960
|
+
name,
|
|
13961
|
+
"sh",
|
|
13962
|
+
"-c",
|
|
13963
|
+
'echo $$ > "$0" && exec "$@"',
|
|
13964
|
+
pidFile,
|
|
13965
|
+
...argv
|
|
13966
|
+
];
|
|
13967
|
+
const child = docker.spawn(args, passed);
|
|
12578
13968
|
child.stdin?.on("error", () => {
|
|
12579
13969
|
});
|
|
12580
13970
|
if (stdin) child.stdin?.write(stdin);
|
|
13971
|
+
let forceTimer;
|
|
13972
|
+
const stop2 = () => {
|
|
13973
|
+
void execIn(name, ["sh", "-c", 'kill -TERM "$(cat "$1")"', "sh", pidFile]);
|
|
13974
|
+
forceTimer = setTimeout(() => void docker.run(["kill", name]), killGraceMs);
|
|
13975
|
+
forceTimer.unref?.();
|
|
13976
|
+
};
|
|
12581
13977
|
const exit = new Promise((resolve2) => {
|
|
12582
13978
|
let settled = false;
|
|
12583
13979
|
const done = (code) => {
|
|
@@ -12587,43 +13983,253 @@ function createLocalProcessProvider(opts = {}) {
|
|
|
12587
13983
|
if (forceTimer) clearTimeout(forceTimer);
|
|
12588
13984
|
resolve2(code);
|
|
12589
13985
|
};
|
|
12590
|
-
let forceTimer;
|
|
12591
|
-
const stop2 = () => {
|
|
12592
|
-
child.kill("SIGTERM");
|
|
12593
|
-
forceTimer = setTimeout(() => child.kill("SIGKILL"), killGraceMs);
|
|
12594
|
-
forceTimer.unref?.();
|
|
12595
|
-
};
|
|
12596
13986
|
child.once("error", () => done(1));
|
|
12597
|
-
child.once("close", (code, sig) =>
|
|
13987
|
+
child.once("close", (code, sig) => {
|
|
13988
|
+
const signum = sig ? constants2.signals[sig] : void 0;
|
|
13989
|
+
done(code ?? (signum !== void 0 ? 128 + signum : 1));
|
|
13990
|
+
});
|
|
12598
13991
|
if (signal.aborted) stop2();
|
|
12599
13992
|
else signal.addEventListener("abort", stop2, { once: true });
|
|
12600
13993
|
});
|
|
12601
13994
|
if (!child.stdout || !child.stderr || !child.stdin)
|
|
12602
|
-
throw new Error("
|
|
12603
|
-
const pid = child.pid;
|
|
13995
|
+
throw new Error("docker: runner stdio missing");
|
|
12604
13996
|
return {
|
|
12605
13997
|
exit,
|
|
12606
|
-
...pid !== void 0 ? { pid } : {},
|
|
13998
|
+
...child.pid !== void 0 ? { pid: child.pid } : {},
|
|
12607
13999
|
stdout: child.stdout,
|
|
12608
14000
|
stderr: child.stderr,
|
|
12609
14001
|
stdin: child.stdin,
|
|
12610
|
-
|
|
12611
|
-
|
|
12612
|
-
|
|
12613
|
-
|
|
12614
|
-
|
|
12615
|
-
|
|
12616
|
-
|
|
12617
|
-
|
|
12618
|
-
for (const pgid of targets.filter(groupAlive)) signalGroup(pgid, "SIGKILL");
|
|
14002
|
+
// The runner is the only thing this user runs in the container, so
|
|
14003
|
+
// everything it left is everything the user has left.
|
|
14004
|
+
async reap() {
|
|
14005
|
+
await execIn(name, [
|
|
14006
|
+
"sh",
|
|
14007
|
+
"-c",
|
|
14008
|
+
`kill -TERM -1 2>/dev/null; sleep ${REAP_GRACE_S}; kill -KILL -1 2>/dev/null; true`
|
|
14009
|
+
]);
|
|
12619
14010
|
}
|
|
12620
14011
|
};
|
|
12621
14012
|
},
|
|
12622
|
-
async
|
|
14013
|
+
async collect(handle, { attemptId }) {
|
|
14014
|
+
const name = handle.id;
|
|
14015
|
+
const held2 = heldFor(handle);
|
|
14016
|
+
const { attempt } = held2;
|
|
14017
|
+
const turnIds = [.../* @__PURE__ */ new Set([attempt.turnId, ...readPending(name)])];
|
|
14018
|
+
const running = await ensureRunning(name);
|
|
14019
|
+
if (attemptId && UUID_RE.test(attemptId)) {
|
|
14020
|
+
if (!running) {
|
|
14021
|
+
dropArchive(attemptId);
|
|
14022
|
+
log.warn(
|
|
14023
|
+
{ container: name, attemptId },
|
|
14024
|
+
"runners: couldn't reach the container to keep the session files; the next turn starts the session over"
|
|
14025
|
+
);
|
|
14026
|
+
} else {
|
|
14027
|
+
await keepSession(name, attempt, attemptId);
|
|
14028
|
+
}
|
|
14029
|
+
}
|
|
14030
|
+
if (!running) {
|
|
14031
|
+
writePending(name, turnIds);
|
|
14032
|
+
return;
|
|
14033
|
+
}
|
|
14034
|
+
const transcripts = await readDir(name, `${CONTAINER_STATE}/transcripts`);
|
|
14035
|
+
if (transcripts) {
|
|
14036
|
+
const kept = transcripts.files.filter((f) => SAFE_NAME_RE.test(f.name));
|
|
14037
|
+
if (kept.length) mkdirSync10(join17(opts.hostStateDir, "transcripts"), { recursive: true });
|
|
14038
|
+
for (const f of kept) writeFileSync6(join17(opts.hostStateDir, "transcripts", f.name), f.data);
|
|
14039
|
+
await removeIn(
|
|
14040
|
+
name,
|
|
14041
|
+
`${CONTAINER_STATE}/transcripts`,
|
|
14042
|
+
kept.map((f) => f.name)
|
|
14043
|
+
);
|
|
14044
|
+
}
|
|
14045
|
+
const outboxRel = join17("outbox", encodeURIComponent(attempt.agentId));
|
|
14046
|
+
const outbox = await readDir(name, `${CONTAINER_STATE}/${outboxRel}`);
|
|
14047
|
+
let unpublished = false;
|
|
14048
|
+
if (outbox) {
|
|
14049
|
+
const scope = {
|
|
14050
|
+
workspaceId: attempt.workspaceId,
|
|
14051
|
+
conversationId: attempt.conversationId,
|
|
14052
|
+
agentId: attempt.agentId,
|
|
14053
|
+
turnIds
|
|
14054
|
+
};
|
|
14055
|
+
let queued = 0;
|
|
14056
|
+
let refused = 0;
|
|
14057
|
+
for (const f of outbox.files) {
|
|
14058
|
+
let entry = null;
|
|
14059
|
+
try {
|
|
14060
|
+
entry = admitCollectedEntry(JSON.parse(f.data.toString("utf8")), scope);
|
|
14061
|
+
} catch {
|
|
14062
|
+
entry = null;
|
|
14063
|
+
}
|
|
14064
|
+
if (!entry) {
|
|
14065
|
+
refused++;
|
|
14066
|
+
continue;
|
|
14067
|
+
}
|
|
14068
|
+
const staged = stagingDir(name);
|
|
14069
|
+
mkdirSync10(staged, { recursive: true });
|
|
14070
|
+
writeFileSync6(
|
|
14071
|
+
join17(staged, `${encodeURIComponent(entry.turnId)}__${entry.seq}.json`),
|
|
14072
|
+
`${JSON.stringify(entry)}
|
|
14073
|
+
`
|
|
14074
|
+
);
|
|
14075
|
+
}
|
|
14076
|
+
await removeIn(
|
|
14077
|
+
name,
|
|
14078
|
+
`${CONTAINER_STATE}/${outboxRel}`,
|
|
14079
|
+
outbox.files.map((f) => f.name)
|
|
14080
|
+
);
|
|
14081
|
+
if (outbox.complete) {
|
|
14082
|
+
const published = publishStaged(
|
|
14083
|
+
stagingDir(name),
|
|
14084
|
+
join17(opts.hostStateDir, outboxRel),
|
|
14085
|
+
opts.moveFile
|
|
14086
|
+
);
|
|
14087
|
+
queued = published.published;
|
|
14088
|
+
if (!published.complete) {
|
|
14089
|
+
unpublished = true;
|
|
14090
|
+
log.warn(
|
|
14091
|
+
{ container: name },
|
|
14092
|
+
"runners: couldn't hand all of the runner's outbox to the companion; the rest waits for the next collection"
|
|
14093
|
+
);
|
|
14094
|
+
}
|
|
14095
|
+
}
|
|
14096
|
+
if (refused) {
|
|
14097
|
+
log.warn(
|
|
14098
|
+
{ container: name, refused },
|
|
14099
|
+
"runners: refused outbox entries a runner's own credential couldn't have sent"
|
|
14100
|
+
);
|
|
14101
|
+
}
|
|
14102
|
+
if (queued) {
|
|
14103
|
+
log.info(
|
|
14104
|
+
{ container: name, queued },
|
|
14105
|
+
"runners: delivering what the runner left in its outbox"
|
|
14106
|
+
);
|
|
14107
|
+
opts.onOutboxCollected?.(attempt.agentId);
|
|
14108
|
+
}
|
|
14109
|
+
}
|
|
14110
|
+
held2.collected = !!transcripts?.complete && !!outbox?.complete && !unpublished;
|
|
14111
|
+
writePending(name, held2.collected ? [] : turnIds);
|
|
14112
|
+
},
|
|
14113
|
+
async release(handle, { destroy: over }) {
|
|
14114
|
+
const h = held.get(handle.id);
|
|
14115
|
+
held.delete(handle.id);
|
|
14116
|
+
try {
|
|
14117
|
+
await h?.git?.close();
|
|
14118
|
+
if (h && !h.collected) {
|
|
14119
|
+
log.warn(
|
|
14120
|
+
{ container: handle.id },
|
|
14121
|
+
"runners: kept the container: what its runner left couldn't be collected yet"
|
|
14122
|
+
);
|
|
14123
|
+
await docker.run(["stop", "-t", "10", handle.id]);
|
|
14124
|
+
} else if (over || !opts.keepContainers) await destroy(handle.id);
|
|
14125
|
+
else await docker.run(["stop", "-t", "10", handle.id]);
|
|
14126
|
+
} finally {
|
|
14127
|
+
h?.release();
|
|
14128
|
+
}
|
|
12623
14129
|
}
|
|
12624
14130
|
};
|
|
14131
|
+
return provider;
|
|
12625
14132
|
}
|
|
12626
14133
|
|
|
14134
|
+
// src/providers/docker-setup.ts
|
|
14135
|
+
import { createHash as createHash5 } from "crypto";
|
|
14136
|
+
var MODEL_HOSTS = [
|
|
14137
|
+
"api.anthropic.com",
|
|
14138
|
+
"api.openai.com",
|
|
14139
|
+
"chatgpt.com",
|
|
14140
|
+
"auth.openai.com"
|
|
14141
|
+
];
|
|
14142
|
+
var GIT_HOSTS = ["github.com"];
|
|
14143
|
+
function dockerRunnerSettings(config) {
|
|
14144
|
+
const docker = config.runners?.docker ?? {};
|
|
14145
|
+
const apiUrl = docker.apiUrl ?? config.baseUrl;
|
|
14146
|
+
const apiHost = new URL(apiUrl).host;
|
|
14147
|
+
const egressAllow = [
|
|
14148
|
+
.../* @__PURE__ */ new Set([apiHost, ...MODEL_HOSTS, ...GIT_HOSTS, ...docker.egressAllow ?? []])
|
|
14149
|
+
];
|
|
14150
|
+
return {
|
|
14151
|
+
image: docker.image ?? baseImageTag(),
|
|
14152
|
+
keepContainers: docker.keepContainers === true,
|
|
14153
|
+
apiUrl,
|
|
14154
|
+
egressAllow,
|
|
14155
|
+
gitHosts: [...GIT_HOSTS]
|
|
14156
|
+
};
|
|
14157
|
+
}
|
|
14158
|
+
function loopbackApi(apiUrl) {
|
|
14159
|
+
const host = new URL(apiUrl).hostname;
|
|
14160
|
+
return host === "localhost" || host === "::1" || host === "[::1]" || host.startsWith("127.");
|
|
14161
|
+
}
|
|
14162
|
+
function deviceKey(config) {
|
|
14163
|
+
const seed = config.deviceId ?? config.baseUrl;
|
|
14164
|
+
return createHash5("sha256").update(seed).digest("hex").slice(0, 12);
|
|
14165
|
+
}
|
|
14166
|
+
function resolveRuntimeEnv(env, store, log) {
|
|
14167
|
+
const out = {};
|
|
14168
|
+
for (const [name, template] of Object.entries(env ?? {})) {
|
|
14169
|
+
const missing = [];
|
|
14170
|
+
const value = template.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_m, key) => {
|
|
14171
|
+
const v = store.get(key);
|
|
14172
|
+
if (v === void 0) missing.push(key);
|
|
14173
|
+
return v ?? "";
|
|
14174
|
+
});
|
|
14175
|
+
if (missing.length) {
|
|
14176
|
+
log?.warn({ name, missing }, "runners: a runner env value names secrets this device lacks");
|
|
14177
|
+
continue;
|
|
14178
|
+
}
|
|
14179
|
+
out[name] = value;
|
|
14180
|
+
}
|
|
14181
|
+
return out;
|
|
14182
|
+
}
|
|
14183
|
+
async function probeRunner(docker, image, source, log) {
|
|
14184
|
+
const r = await docker.run(
|
|
14185
|
+
[
|
|
14186
|
+
"run",
|
|
14187
|
+
"--rm",
|
|
14188
|
+
"--network",
|
|
14189
|
+
"none",
|
|
14190
|
+
...companionMountArgs(source),
|
|
14191
|
+
image,
|
|
14192
|
+
"cabane-companion",
|
|
14193
|
+
"run-turn",
|
|
14194
|
+
"--manifest"
|
|
14195
|
+
],
|
|
14196
|
+
{ timeoutMs: 6e4 }
|
|
14197
|
+
);
|
|
14198
|
+
try {
|
|
14199
|
+
const manifest = JSON.parse(r.stdout.trim());
|
|
14200
|
+
log.info(
|
|
14201
|
+
{ image, version: manifest.version, bundled: manifest.bundled },
|
|
14202
|
+
"runners: runner ready"
|
|
14203
|
+
);
|
|
14204
|
+
return manifest;
|
|
14205
|
+
} catch {
|
|
14206
|
+
log.error(
|
|
14207
|
+
{ image, err: r.stderr.trim().slice(-2e3) },
|
|
14208
|
+
"runners: the companion didn't start in a runner container"
|
|
14209
|
+
);
|
|
14210
|
+
return null;
|
|
14211
|
+
}
|
|
14212
|
+
}
|
|
14213
|
+
function imageResolution(manifest, image, runtime) {
|
|
14214
|
+
const status2 = manifest?.bundled[runtime] ?? "platform_package_missing";
|
|
14215
|
+
return {
|
|
14216
|
+
runtime,
|
|
14217
|
+
status: status2,
|
|
14218
|
+
path: status2 === "present" ? `${image} (bundled)` : null,
|
|
14219
|
+
packageName: null,
|
|
14220
|
+
platform: "linux",
|
|
14221
|
+
arch: "image"
|
|
14222
|
+
};
|
|
14223
|
+
}
|
|
14224
|
+
function containerHookPath(hostPath) {
|
|
14225
|
+
const name = hostPath.split("/").pop() || "hook";
|
|
14226
|
+
const tag = createHash5("sha256").update(hostPath).digest("hex").slice(0, 8);
|
|
14227
|
+
return `/opt/cabane/hooks/${tag}-${name}`;
|
|
14228
|
+
}
|
|
14229
|
+
|
|
14230
|
+
// src/supervisor.ts
|
|
14231
|
+
init_git_credential();
|
|
14232
|
+
|
|
12627
14233
|
// src/runner/device-locks.ts
|
|
12628
14234
|
var DeviceLocks = class {
|
|
12629
14235
|
holders = /* @__PURE__ */ new Map();
|
|
@@ -12875,13 +14481,7 @@ function sleep4(ms) {
|
|
|
12875
14481
|
|
|
12876
14482
|
// src/supervisor.ts
|
|
12877
14483
|
init_transcript_writer();
|
|
12878
|
-
|
|
12879
|
-
// src/version.ts
|
|
12880
|
-
import { createRequire as createRequire2 } from "module";
|
|
12881
|
-
var pkg = createRequire2(import.meta.url)("../package.json");
|
|
12882
|
-
var COMPANION_VERSION = pkg.version;
|
|
12883
|
-
|
|
12884
|
-
// src/supervisor.ts
|
|
14484
|
+
init_version2();
|
|
12885
14485
|
init_src();
|
|
12886
14486
|
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
12887
14487
|
var SHUTDOWN_HEARTBEAT_WAIT_MS = 1e3;
|
|
@@ -12982,8 +14582,27 @@ var CompanionSupervisor = class {
|
|
|
12982
14582
|
if (opts.runnerCommand) this.runnerCommand = opts.runnerCommand;
|
|
12983
14583
|
if (opts.runnerProvider) this.providerForRunners = opts.runnerProvider;
|
|
12984
14584
|
this.unrunnableRetryDelaysMs = opts.unrunnableRetryDelaysMs ?? DEFAULT_UNRUNNABLE_RETRY_DELAYS_MS;
|
|
14585
|
+
if (this.dockerRunners()) {
|
|
14586
|
+
const { image } = dockerRunnerSettings(this.config);
|
|
14587
|
+
this.resolveBundled = (runtime) => imageResolution(this.runnerImage, image, runtime);
|
|
14588
|
+
}
|
|
12985
14589
|
this.latchBootBundledState();
|
|
12986
14590
|
}
|
|
14591
|
+
// CT1681: what a runner container said it can run, probed once at start;
|
|
14592
|
+
// null until then, and when no container could be started.
|
|
14593
|
+
runnerImage = null;
|
|
14594
|
+
// CT1681: the companion every container mounts — this one's own install.
|
|
14595
|
+
runnerCompanion() {
|
|
14596
|
+
return companionSource({
|
|
14597
|
+
configured: this.config.runners?.docker?.companionDir,
|
|
14598
|
+
platform: process.platform,
|
|
14599
|
+
version: COMPANION_VERSION,
|
|
14600
|
+
own: ownInstall()
|
|
14601
|
+
});
|
|
14602
|
+
}
|
|
14603
|
+
dockerRunners() {
|
|
14604
|
+
return runnersEnabled(this.config) && this.config.runners?.provider === "docker";
|
|
14605
|
+
}
|
|
12987
14606
|
// Stand up the data plane: pair check, initial assignments pull, then the
|
|
12988
14607
|
// heartbeat + poll loops. A companion with no device token (logged out) does
|
|
12989
14608
|
// nothing but say so.
|
|
@@ -12992,6 +14611,24 @@ var CompanionSupervisor = class {
|
|
|
12992
14611
|
{ protocolVersion: TURN_PROTOCOL_VERSION, version: COMPANION_VERSION },
|
|
12993
14612
|
`Companion ${COMPANION_VERSION} starting on ${this.config.deviceLabel ?? hostname3()}`
|
|
12994
14613
|
);
|
|
14614
|
+
if (this.dockerRunners() && !this.providerForRunners) {
|
|
14615
|
+
const { image, apiUrl } = dockerRunnerSettings(this.config);
|
|
14616
|
+
if (loopbackApi(apiUrl)) {
|
|
14617
|
+
this.log.error(
|
|
14618
|
+
{ apiUrl },
|
|
14619
|
+
"runners: a container can't reach this machine's loopback; set runners.docker.apiUrl to an address it can"
|
|
14620
|
+
);
|
|
14621
|
+
}
|
|
14622
|
+
const source = this.runnerCompanion();
|
|
14623
|
+
const docker = dockerCli();
|
|
14624
|
+
if ("error" in source) {
|
|
14625
|
+
this.log.error({ reason: source.error }, "runners: no companion to put in a container");
|
|
14626
|
+
} else if (await ensureBaseImage(docker, image, this.log) && await ensureCompanionVolume(docker, image, source, this.log)) {
|
|
14627
|
+
this.runnerImage = await probeRunner(docker, image, source, this.log);
|
|
14628
|
+
if (!this.runnerImage) await invalidateCompanionVolume(docker, image, source);
|
|
14629
|
+
}
|
|
14630
|
+
this.latchBootBundledState();
|
|
14631
|
+
}
|
|
12995
14632
|
const startupHarnessRefresh = this.refreshHarnessStatuses();
|
|
12996
14633
|
this.trackHeartbeat(startupHarnessRefresh);
|
|
12997
14634
|
if (!this.config.deviceToken) {
|
|
@@ -13099,7 +14736,9 @@ var CompanionSupervisor = class {
|
|
|
13099
14736
|
runtimeIssues: this.runtimeIssues(),
|
|
13100
14737
|
// CT571/CT586: each runtime's `version` from the latest harness probe
|
|
13101
14738
|
// (fail-soft to null). Informational only — the server matches on name.
|
|
13102
|
-
versions: this.harnessVersions
|
|
14739
|
+
versions: this.harnessVersions,
|
|
14740
|
+
// CT1681: shown on the device page.
|
|
14741
|
+
...runnersEnabled(this.config) ? { runnerProvider: this.config.runners?.provider ?? "local-process" } : {}
|
|
13103
14742
|
}),
|
|
13104
14743
|
// CT566: echo the last classified credential state per runtime, when the
|
|
13105
14744
|
// companion has seen any. Omitted (undefined) until the first observed
|
|
@@ -13432,21 +15071,71 @@ var CompanionSupervisor = class {
|
|
|
13432
15071
|
// can't make the sandbox says so once and runs its turns unwrapped.
|
|
13433
15072
|
sandboxUsable;
|
|
13434
15073
|
runnerSandbox() {
|
|
13435
|
-
if (this.config.runners?.sandbox !== "os") return "none";
|
|
15074
|
+
if (this.config.runners?.sandbox !== "os" || this.dockerRunners()) return "none";
|
|
13436
15075
|
this.sandboxUsable ??= osSandboxUsable(this.log);
|
|
13437
15076
|
return this.sandboxUsable ? "os" : "none";
|
|
13438
15077
|
}
|
|
13439
15078
|
// CT1679: one provider for the device, built on first use.
|
|
13440
15079
|
runnerProvider() {
|
|
13441
|
-
this.providerForRunners ??= createLocalProcessProvider();
|
|
15080
|
+
this.providerForRunners ??= this.dockerRunners() ? this.createDockerRunners() : createLocalProcessProvider();
|
|
13442
15081
|
return this.providerForRunners;
|
|
13443
15082
|
}
|
|
15083
|
+
createDockerRunners() {
|
|
15084
|
+
const settings = dockerRunnerSettings(this.config);
|
|
15085
|
+
const docker = this.config.runners?.docker ?? {};
|
|
15086
|
+
const hooks = [
|
|
15087
|
+
this.config.prepareHook?.command,
|
|
15088
|
+
...Object.values(this.config.agents ?? {}).map((a) => a.prepareHook?.command)
|
|
15089
|
+
].filter((c) => typeof c === "string");
|
|
15090
|
+
const source = this.runnerCompanion();
|
|
15091
|
+
return createDockerProvider({
|
|
15092
|
+
image: settings.image,
|
|
15093
|
+
// Without a companion nothing is advertised, so nothing is sent; a turn
|
|
15094
|
+
// that arrives anyway fails at the mount and says so.
|
|
15095
|
+
companion: "error" in source ? { kind: "dir", path: "/nonexistent", key: "none" } : source,
|
|
15096
|
+
deviceKey: deviceKey(this.config),
|
|
15097
|
+
keepContainers: settings.keepContainers,
|
|
15098
|
+
egressAllow: settings.egressAllow,
|
|
15099
|
+
gitHosts: settings.gitHosts,
|
|
15100
|
+
gitFill: deviceGitFill(docker.gitCredentialHelper),
|
|
15101
|
+
runtimeEnv: () => resolveRuntimeEnv(
|
|
15102
|
+
docker.env,
|
|
15103
|
+
loadSecretStoreTolerant(
|
|
15104
|
+
(m) => this.log.warn(
|
|
15105
|
+
{ err: m },
|
|
15106
|
+
"Could not read the secrets file. Check ~/.cabane/secrets.json."
|
|
15107
|
+
)
|
|
15108
|
+
),
|
|
15109
|
+
this.log
|
|
15110
|
+
),
|
|
15111
|
+
mounts: [...new Set(hooks)].map((source2) => ({ source: source2, target: containerHookPath(source2) })),
|
|
15112
|
+
deviceDir: cabaneDir(),
|
|
15113
|
+
hostStateDir: runnerStateDir(),
|
|
15114
|
+
onOutboxCollected: (agentId) => {
|
|
15115
|
+
for (const wr of this.workspaces.values()) wr.agents.get(agentId)?.kickDrain();
|
|
15116
|
+
},
|
|
15117
|
+
log: this.log
|
|
15118
|
+
});
|
|
15119
|
+
}
|
|
15120
|
+
// CT1681: the runner command for this device's provider: the image's own
|
|
15121
|
+
// companion in a container, this process's otherwise.
|
|
15122
|
+
runnerCommandFor() {
|
|
15123
|
+
if (this.runnerCommand) return this.runnerCommand;
|
|
15124
|
+
return this.dockerRunners() ? ["cabane-companion", "run-turn"] : selfRunnerCommand();
|
|
15125
|
+
}
|
|
13444
15126
|
buildDispatcher(ctx) {
|
|
13445
|
-
const
|
|
15127
|
+
const hostLocal = localAgentConfig(this.config, {
|
|
13446
15128
|
agentId: ctx.agentId,
|
|
13447
15129
|
agentUsername: ctx.agentUsername,
|
|
13448
15130
|
workspaceSlug: ctx.workspaceSlug
|
|
13449
15131
|
});
|
|
15132
|
+
const local = this.dockerRunners() && hostLocal.prepareHook ? {
|
|
15133
|
+
...hostLocal,
|
|
15134
|
+
prepareHook: {
|
|
15135
|
+
...hostLocal.prepareHook,
|
|
15136
|
+
command: containerHookPath(hostLocal.prepareHook.command)
|
|
15137
|
+
}
|
|
15138
|
+
} : hostLocal;
|
|
13450
15139
|
if (this.dispatcherFactory) return this.dispatcherFactory({ ...ctx, local });
|
|
13451
15140
|
return new Dispatcher({
|
|
13452
15141
|
deviceId: this.deviceId ?? void 0,
|
|
@@ -13495,7 +15184,8 @@ var CompanionSupervisor = class {
|
|
|
13495
15184
|
...runnersEnabled(this.config) ? {
|
|
13496
15185
|
runner: {
|
|
13497
15186
|
provider: this.runnerProvider(),
|
|
13498
|
-
command: this.
|
|
15187
|
+
command: this.runnerCommandFor(),
|
|
15188
|
+
...this.dockerRunners() ? { runnerBaseUrl: dockerRunnerSettings(this.config).apiUrl } : {},
|
|
13499
15189
|
reportRunnerGone: (report) => this.reportRunnerGone(report),
|
|
13500
15190
|
connectorHealth: this.connectorHealth,
|
|
13501
15191
|
locks: this.runnerLocks,
|
|
@@ -13819,7 +15509,7 @@ var CompanionSupervisor = class {
|
|
|
13819
15509
|
);
|
|
13820
15510
|
}
|
|
13821
15511
|
const resumedTurnId = recoveredTurnId ?? (dispatchId ? turnIdForEvent(workspaceId, dispatchId) : null);
|
|
13822
|
-
const turnId = resumedTurnId ??
|
|
15512
|
+
const turnId = resumedTurnId ?? randomUUID6();
|
|
13823
15513
|
if (dispatchId) {
|
|
13824
15514
|
const liveTurnIds = rememberTurnId(workspaceId, dispatchId, turnId);
|
|
13825
15515
|
if (liveTurnIds > TURN_ID_OVERFLOW_WARN) {
|
|
@@ -14124,9 +15814,9 @@ async function waitForBoundedHeartbeat(heartbeat) {
|
|
|
14124
15814
|
}
|
|
14125
15815
|
function defaultReexec() {
|
|
14126
15816
|
clearRuntimeState();
|
|
14127
|
-
void import("child_process").then(({ spawn:
|
|
15817
|
+
void import("child_process").then(({ spawn: spawn10 }) => {
|
|
14128
15818
|
try {
|
|
14129
|
-
const child =
|
|
15819
|
+
const child = spawn10(process.execPath, process.argv.slice(1), {
|
|
14130
15820
|
stdio: "inherit",
|
|
14131
15821
|
detached: false
|
|
14132
15822
|
});
|
|
@@ -14138,6 +15828,9 @@ function defaultReexec() {
|
|
|
14138
15828
|
});
|
|
14139
15829
|
}
|
|
14140
15830
|
|
|
15831
|
+
// src/runtime.ts
|
|
15832
|
+
init_version2();
|
|
15833
|
+
|
|
14141
15834
|
// src/safety.ts
|
|
14142
15835
|
var RECOVERABLE_CODES = /* @__PURE__ */ new Set([
|
|
14143
15836
|
"EPIPE",
|
|
@@ -14197,22 +15890,22 @@ function handleUncaught(log, err, origin) {
|
|
|
14197
15890
|
|
|
14198
15891
|
// src/crash-marker.ts
|
|
14199
15892
|
init_config();
|
|
14200
|
-
import { existsSync as
|
|
14201
|
-
import { join as
|
|
15893
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync13, readFileSync as readFileSync11, rmSync as rmSync12, writeFileSync as writeFileSync9 } from "fs";
|
|
15894
|
+
import { join as join20 } from "path";
|
|
14202
15895
|
function crashMarkerPath() {
|
|
14203
|
-
return
|
|
15896
|
+
return join20(cabaneDir(), "last-error.json");
|
|
14204
15897
|
}
|
|
14205
15898
|
function recordCrash(rec2) {
|
|
14206
15899
|
try {
|
|
14207
|
-
|
|
14208
|
-
|
|
15900
|
+
mkdirSync13(cabaneDir(), { recursive: true });
|
|
15901
|
+
writeFileSync9(crashMarkerPath(), JSON.stringify(rec2, null, 2) + "\n");
|
|
14209
15902
|
} catch {
|
|
14210
15903
|
}
|
|
14211
15904
|
}
|
|
14212
15905
|
function clearCrash() {
|
|
14213
15906
|
try {
|
|
14214
15907
|
const path = crashMarkerPath();
|
|
14215
|
-
if (
|
|
15908
|
+
if (existsSync15(path)) rmSync12(path, { force: true });
|
|
14216
15909
|
} catch {
|
|
14217
15910
|
}
|
|
14218
15911
|
}
|
|
@@ -14255,7 +15948,7 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
14255
15948
|
opencodeServerUrl: cfg.opencode?.serverUrl,
|
|
14256
15949
|
codex: isCodexEnabled(cfg)
|
|
14257
15950
|
});
|
|
14258
|
-
const instanceId =
|
|
15951
|
+
const instanceId = randomUUID7();
|
|
14259
15952
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
14260
15953
|
const pre = readLiveRuntimeState();
|
|
14261
15954
|
if (pre && await verifyRuntime(pre) === "stale") clearRuntimeState();
|
|
@@ -14385,16 +16078,16 @@ async function closeSurfaces(control, dashboard) {
|
|
|
14385
16078
|
init_term();
|
|
14386
16079
|
|
|
14387
16080
|
// src/commands/daemon.ts
|
|
14388
|
-
import { spawn as
|
|
14389
|
-
import { closeSync as closeSync3, mkdirSync as
|
|
16081
|
+
import { spawn as spawn9 } from "child_process";
|
|
16082
|
+
import { closeSync as closeSync3, mkdirSync as mkdirSync14, openSync as openSync3 } from "fs";
|
|
14390
16083
|
|
|
14391
16084
|
// src/cli-entry.ts
|
|
14392
|
-
import { existsSync as
|
|
14393
|
-
import { fileURLToPath as
|
|
16085
|
+
import { existsSync as existsSync16 } from "fs";
|
|
16086
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
14394
16087
|
var RELATIVE_CANDIDATES = ["./cli.js", "../dist/cli.js", "../cli.js"];
|
|
14395
16088
|
function companionCliEntry(deps = {}) {
|
|
14396
|
-
const exists = deps.exists ??
|
|
14397
|
-
const candidates = deps.candidates ?? RELATIVE_CANDIDATES.map((rel) =>
|
|
16089
|
+
const exists = deps.exists ?? existsSync16;
|
|
16090
|
+
const candidates = deps.candidates ?? RELATIVE_CANDIDATES.map((rel) => fileURLToPath4(new URL(rel, import.meta.url)));
|
|
14398
16091
|
for (const candidate of candidates) {
|
|
14399
16092
|
if (exists(candidate)) return candidate;
|
|
14400
16093
|
}
|
|
@@ -14495,10 +16188,10 @@ Stop: cabane-companion stop
|
|
|
14495
16188
|
}
|
|
14496
16189
|
function defaultSpawnDetached(args) {
|
|
14497
16190
|
const cliPath = companionCliEntry();
|
|
14498
|
-
|
|
16191
|
+
mkdirSync14(cabaneDir(), { recursive: true });
|
|
14499
16192
|
const logFd = openSync3(companionLogPath(), "a");
|
|
14500
16193
|
try {
|
|
14501
|
-
return
|
|
16194
|
+
return spawn9(process.execPath, [cliPath, ...args], {
|
|
14502
16195
|
detached: true,
|
|
14503
16196
|
stdio: ["ignore", logFd, logFd],
|
|
14504
16197
|
env: { ...process.env, CABANE_COMPANION_DAEMON: "1" }
|
|
@@ -14787,7 +16480,7 @@ async function runAttached(runtime) {
|
|
|
14787
16480
|
|
|
14788
16481
|
// src/commands/status.ts
|
|
14789
16482
|
init_config();
|
|
14790
|
-
import { readdirSync as
|
|
16483
|
+
import { readdirSync as readdirSync6 } from "fs";
|
|
14791
16484
|
init_secrets();
|
|
14792
16485
|
init_transcript_writer();
|
|
14793
16486
|
async function status() {
|
|
@@ -14849,7 +16542,7 @@ function transcriptsLine() {
|
|
|
14849
16542
|
let count = 0;
|
|
14850
16543
|
let newest;
|
|
14851
16544
|
try {
|
|
14852
|
-
const files =
|
|
16545
|
+
const files = readdirSync6(dir2).filter((f) => f.endsWith(".jsonl")).sort();
|
|
14853
16546
|
count = files.length;
|
|
14854
16547
|
newest = files[files.length - 1];
|
|
14855
16548
|
} catch {
|
|
@@ -14930,8 +16623,8 @@ function isAlive(kill, pid) {
|
|
|
14930
16623
|
init_errors();
|
|
14931
16624
|
init_src();
|
|
14932
16625
|
init_transcript_writer();
|
|
14933
|
-
import { existsSync as
|
|
14934
|
-
import { isAbsolute, join as
|
|
16626
|
+
import { existsSync as existsSync17, readFileSync as readFileSync12, readdirSync as readdirSync7 } from "fs";
|
|
16627
|
+
import { isAbsolute, join as join21 } from "path";
|
|
14935
16628
|
async function transcript(opts = {}) {
|
|
14936
16629
|
const dir2 = transcriptsDir();
|
|
14937
16630
|
if (opts.follow) {
|
|
@@ -14948,7 +16641,7 @@ async function transcript(opts = {}) {
|
|
|
14948
16641
|
process.stdout.write(emptyMessage(dir2));
|
|
14949
16642
|
return;
|
|
14950
16643
|
}
|
|
14951
|
-
process.stdout.write(renderFile(
|
|
16644
|
+
process.stdout.write(renderFile(join21(dir2, newest)) + "\n");
|
|
14952
16645
|
return;
|
|
14953
16646
|
}
|
|
14954
16647
|
printList(dir2);
|
|
@@ -15031,7 +16724,7 @@ function isComplete(content) {
|
|
|
15031
16724
|
async function followTranscripts(dir2) {
|
|
15032
16725
|
const follower = new TranscriptFollower({
|
|
15033
16726
|
listFiles: () => listFiles(dir2),
|
|
15034
|
-
read: (f) =>
|
|
16727
|
+
read: (f) => readFileSync12(join21(dir2, f), "utf8"),
|
|
15035
16728
|
write: (s) => process.stdout.write(s),
|
|
15036
16729
|
// CSI: cursor up `n` lines, then erase from cursor to end of screen.
|
|
15037
16730
|
clearLines: (n) => process.stdout.write(`\x1B[${n}A\x1B[0J`),
|
|
@@ -15056,7 +16749,7 @@ async function followTranscripts(dir2) {
|
|
|
15056
16749
|
}
|
|
15057
16750
|
function listFiles(dir2) {
|
|
15058
16751
|
try {
|
|
15059
|
-
return
|
|
16752
|
+
return readdirSync7(dir2).filter((f) => f.endsWith(".jsonl")).sort().reverse();
|
|
15060
16753
|
} catch {
|
|
15061
16754
|
return [];
|
|
15062
16755
|
}
|
|
@@ -15071,7 +16764,7 @@ function printList(dir2) {
|
|
|
15071
16764
|
|
|
15072
16765
|
`);
|
|
15073
16766
|
for (const f of files.slice(0, 20)) {
|
|
15074
|
-
const { meta, outcome } = peek(
|
|
16767
|
+
const { meta, outcome } = peek(join21(dir2, f));
|
|
15075
16768
|
const when = fmtTime(rec(meta)?.ts);
|
|
15076
16769
|
const ws = str2(rec(meta)?.workspaceSlug);
|
|
15077
16770
|
const o = rec(outcome);
|
|
@@ -15092,7 +16785,7 @@ function peek(path) {
|
|
|
15092
16785
|
let meta;
|
|
15093
16786
|
let outcome;
|
|
15094
16787
|
try {
|
|
15095
|
-
for (const line of
|
|
16788
|
+
for (const line of readFileSync12(path, "utf8").split("\n")) {
|
|
15096
16789
|
if (!line.trim()) continue;
|
|
15097
16790
|
const o = safeParse(line);
|
|
15098
16791
|
const t = str2(rec(o)?.type);
|
|
@@ -15105,13 +16798,13 @@ function peek(path) {
|
|
|
15105
16798
|
}
|
|
15106
16799
|
function resolveTarget(dir2, target) {
|
|
15107
16800
|
if (isAbsolute(target) || target.includes("/")) {
|
|
15108
|
-
if (
|
|
16801
|
+
if (existsSync17(target)) return target;
|
|
15109
16802
|
throw new CompanionError(`no transcript at ${target}.`);
|
|
15110
16803
|
}
|
|
15111
|
-
const exact =
|
|
15112
|
-
if (
|
|
16804
|
+
const exact = join21(dir2, target);
|
|
16805
|
+
if (existsSync17(exact)) return exact;
|
|
15113
16806
|
const matches = listFiles(dir2).filter((f) => f.includes(target));
|
|
15114
|
-
if (matches.length === 1) return
|
|
16807
|
+
if (matches.length === 1) return join21(dir2, matches[0]);
|
|
15115
16808
|
if (matches.length === 0) {
|
|
15116
16809
|
throw new CompanionError(
|
|
15117
16810
|
`no transcript matching "${target}" in ${dir2}. Run \`cabane-companion transcript\` to list them.`
|
|
@@ -15125,7 +16818,7 @@ function resolveTarget(dir2, target) {
|
|
|
15125
16818
|
function renderFile(path) {
|
|
15126
16819
|
let content;
|
|
15127
16820
|
try {
|
|
15128
|
-
content =
|
|
16821
|
+
content = readFileSync12(path, "utf8");
|
|
15129
16822
|
} catch (err) {
|
|
15130
16823
|
throw new CompanionError(
|
|
15131
16824
|
`couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -15274,6 +16967,7 @@ They are written per dispatch while \`cabane-companion start\` is running.
|
|
|
15274
16967
|
|
|
15275
16968
|
// src/cli.ts
|
|
15276
16969
|
init_errors();
|
|
16970
|
+
init_version2();
|
|
15277
16971
|
var program = new Command();
|
|
15278
16972
|
program.name("cabane-companion").description(
|
|
15279
16973
|
"Connect a coding agent on your machine to your Cabane workspace as a responder \u2014 reply to Cabane messages while staying a full local AI client (Claude Code or OpenCode)."
|
|
@@ -15286,7 +16980,7 @@ program.command("pair").description(
|
|
|
15286
16980
|
});
|
|
15287
16981
|
});
|
|
15288
16982
|
program.command("write-paired-config", { hidden: true }).description("persist an already-completed device enrollment payload from stdin.").action(() => {
|
|
15289
|
-
writeCompletedPairing(
|
|
16983
|
+
writeCompletedPairing(readFileSync13(0, "utf8"));
|
|
15290
16984
|
});
|
|
15291
16985
|
program.command("start").description(
|
|
15292
16986
|
"pair this device if needed, connect a coding agent on it, and run in the background."
|
|
@@ -15343,7 +17037,13 @@ program.command("logout").description(
|
|
|
15343
17037
|
...opts.purge ? { purge: true } : {}
|
|
15344
17038
|
});
|
|
15345
17039
|
});
|
|
15346
|
-
program.command("run-turn", { hidden: true }).action(async () => {
|
|
17040
|
+
program.command("run-turn", { hidden: true }).option("--manifest", "print what this install can run, as JSON, and exit").action(async (opts) => {
|
|
17041
|
+
if (opts.manifest) {
|
|
17042
|
+
const { runnerManifest: runnerManifest2 } = await Promise.resolve().then(() => (init_image_manifest(), image_manifest_exports));
|
|
17043
|
+
process.stdout.write(`${JSON.stringify(runnerManifest2())}
|
|
17044
|
+
`);
|
|
17045
|
+
return;
|
|
17046
|
+
}
|
|
15347
17047
|
const { runTurn: runTurn2 } = await Promise.resolve().then(() => (init_run_turn(), run_turn_exports));
|
|
15348
17048
|
const { code } = await runTurn2(process.env, {
|
|
15349
17049
|
stdin: process.stdin,
|
|
@@ -15355,6 +17055,28 @@ program.command("run-turn", { hidden: true }).action(async () => {
|
|
|
15355
17055
|
await new Promise((resolve2) => process.stderr.write("", () => resolve2()));
|
|
15356
17056
|
process.exit(code);
|
|
15357
17057
|
});
|
|
17058
|
+
program.command("egress-proxy", { hidden: true }).requiredOption("--port <port>", "port to listen on").action(async (opts) => {
|
|
17059
|
+
const { createEgressProxy: createEgressProxy2 } = await Promise.resolve().then(() => (init_egress_proxy(), egress_proxy_exports));
|
|
17060
|
+
const allow = JSON.parse(process.env.CABANE_EGRESS_ALLOW ?? "[]");
|
|
17061
|
+
const log = (await import("pino")).default({ level: "info" }, process.stderr);
|
|
17062
|
+
createEgressProxy2({ allow, log }).listen(
|
|
17063
|
+
Number(opts.port),
|
|
17064
|
+
"0.0.0.0",
|
|
17065
|
+
() => log.info({ allow }, "egress proxy listening")
|
|
17066
|
+
);
|
|
17067
|
+
});
|
|
17068
|
+
program.command("git-credential <operation>", { hidden: true }).action(async (operation) => {
|
|
17069
|
+
const socket = process.env.CABANE_GIT_CREDENTIAL_SOCKET;
|
|
17070
|
+
if (!socket) return;
|
|
17071
|
+
const { askGitCredential: askGitCredential2 } = await Promise.resolve().then(() => (init_git_credential(), git_credential_exports));
|
|
17072
|
+
const request = await new Promise((resolve2) => {
|
|
17073
|
+
let input = "";
|
|
17074
|
+
process.stdin.setEncoding("utf8");
|
|
17075
|
+
process.stdin.on("data", (chunk) => input += chunk);
|
|
17076
|
+
process.stdin.on("end", () => resolve2(input));
|
|
17077
|
+
});
|
|
17078
|
+
process.stdout.write(await askGitCredential2(socket, operation, request));
|
|
17079
|
+
});
|
|
15358
17080
|
program.parseAsync(process.argv).catch((err) => {
|
|
15359
17081
|
if (err instanceof CompanionError) {
|
|
15360
17082
|
process.stderr.write(`error: ${err.message}
|