@cabane/companion 0.6.122 → 0.6.124
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 +140 -29
- package/dist/runtime.js +111 -19
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1334,6 +1334,13 @@ var init_api = __esm({
|
|
|
1334
1334
|
(e) => e.turnId === turnId && e.kind === "message" && e.body?.kind !== "runtime_notice"
|
|
1335
1335
|
);
|
|
1336
1336
|
}
|
|
1337
|
+
// CT1680: the launcher mints the runner credential for one dispatch, on its
|
|
1338
|
+
// PAT and from its device, and hands it to the runner in place of both. A
|
|
1339
|
+
// retry is safe: the server returns the same runner id and retires the
|
|
1340
|
+
// credential the lost response carried.
|
|
1341
|
+
mintRunnerCredential(body) {
|
|
1342
|
+
return this.request("POST", "/api/agent/runner-credentials", body, { retry: true });
|
|
1343
|
+
}
|
|
1337
1344
|
// Read the skip intent recorded by the URL turn-control MCP surface.
|
|
1338
1345
|
// Codex/opencode use this server-side state; settle imports skipped/skipReason
|
|
1339
1346
|
// into the same local skip state used by claude-code's in-process tools.
|
|
@@ -7049,8 +7056,7 @@ function parseRunnerInput(env, stdin) {
|
|
|
7049
7056
|
const credentials = credentialsSchema.parse(JSON.parse(stdin || "{}"));
|
|
7050
7057
|
return {
|
|
7051
7058
|
baseUrl: required(env, RUNNER_ENV.url),
|
|
7052
|
-
|
|
7053
|
-
...credentials.deviceToken ? { deviceToken: credentials.deviceToken } : {},
|
|
7059
|
+
credential: credentials.credential,
|
|
7054
7060
|
dispatch,
|
|
7055
7061
|
turnId: required(env, RUNNER_ENV.turnId),
|
|
7056
7062
|
resumed: env[RUNNER_ENV.resumed] === "1",
|
|
@@ -7067,13 +7073,7 @@ function runnerEnv(input) {
|
|
|
7067
7073
|
};
|
|
7068
7074
|
}
|
|
7069
7075
|
function runnerStdin(input) {
|
|
7070
|
-
return `${JSON.stringify({
|
|
7071
|
-
// Phase 1 (CT1680) replaces the agent PAT with a runner credential bound to
|
|
7072
|
-
// this one dispatch; until then the child holds what the in-process turn
|
|
7073
|
-
// held, on the same machine.
|
|
7074
|
-
agentToken: input.agentToken,
|
|
7075
|
-
...input.deviceToken ? { deviceToken: input.deviceToken } : {}
|
|
7076
|
-
})}
|
|
7076
|
+
return `${JSON.stringify({ credential: input.credential })}
|
|
7077
7077
|
`;
|
|
7078
7078
|
}
|
|
7079
7079
|
function encodeLauncherMessage(message) {
|
|
@@ -7157,7 +7157,10 @@ var init_protocol = __esm({
|
|
|
7157
7157
|
connectorHealth: z14.record(z14.string(), runtimeHealthSchema),
|
|
7158
7158
|
// True when this (conversation, agent) pair was already told its session
|
|
7159
7159
|
// could not be saved: the notice is once per pair, not once per process.
|
|
7160
|
-
sessionWriteNoticeClaimed: z14.boolean()
|
|
7160
|
+
sessionWriteNoticeClaimed: z14.boolean(),
|
|
7161
|
+
// CT1680: the runner's identity, minted with its credential. The start
|
|
7162
|
+
// PATCH carries it so the lease knows which runner holds the span.
|
|
7163
|
+
runnerId: z14.string()
|
|
7161
7164
|
});
|
|
7162
7165
|
dispatchSchema = z14.object({
|
|
7163
7166
|
type: z14.literal("device:dispatch_requested"),
|
|
@@ -7168,8 +7171,7 @@ var init_protocol = __esm({
|
|
|
7168
7171
|
episode: z14.number().int().optional()
|
|
7169
7172
|
});
|
|
7170
7173
|
credentialsSchema = z14.object({
|
|
7171
|
-
|
|
7172
|
-
deviceToken: z14.string().min(1).optional()
|
|
7174
|
+
credential: z14.string().startsWith("cabrn_")
|
|
7173
7175
|
});
|
|
7174
7176
|
}
|
|
7175
7177
|
});
|
|
@@ -7192,8 +7194,9 @@ function healthSnapshot(store) {
|
|
|
7192
7194
|
}
|
|
7193
7195
|
return out;
|
|
7194
7196
|
}
|
|
7195
|
-
function runnerSpec(opts, launch, duties) {
|
|
7197
|
+
function runnerSpec(opts, launch, duties, runnerId) {
|
|
7196
7198
|
return {
|
|
7199
|
+
runnerId,
|
|
7197
7200
|
...opts.deviceId ? { deviceId: opts.deviceId } : {},
|
|
7198
7201
|
bootId,
|
|
7199
7202
|
workspaceId: opts.workspaceId,
|
|
@@ -7274,6 +7277,43 @@ function giveBackLocks(locks, prefix, remote, launch, log) {
|
|
|
7274
7277
|
})();
|
|
7275
7278
|
}
|
|
7276
7279
|
}
|
|
7280
|
+
async function mintForRunner(opts, payload, handleOpts, signal, log) {
|
|
7281
|
+
try {
|
|
7282
|
+
const minted = await opts.api.mintRunnerCredential({
|
|
7283
|
+
conversationId: payload.conversationId,
|
|
7284
|
+
messageId: payload.messageId,
|
|
7285
|
+
...payload.episode !== void 0 ? { episode: payload.episode } : {},
|
|
7286
|
+
// The one turn the credential will operate: this run's.
|
|
7287
|
+
turnId: handleOpts.turnId
|
|
7288
|
+
});
|
|
7289
|
+
return { ok: true, credential: minted.credential, runnerId: minted.runnerId };
|
|
7290
|
+
} catch (err) {
|
|
7291
|
+
if (signal.aborted) return { ok: false, reason: "cancelled" };
|
|
7292
|
+
const status2 = err instanceof ApiError ? err.status : 0;
|
|
7293
|
+
if (status2 === 404) return { ok: false, reason: "runner_credential_not_found" };
|
|
7294
|
+
if (status2 === 409 && apiErrorCode(err) === "dispatch_not_admitted")
|
|
7295
|
+
return { ok: false, reason: "runner_credential_not_admitted" };
|
|
7296
|
+
log.warn(apiErrorLogFields(err), "runner: couldn't get a credential for the turn's runner");
|
|
7297
|
+
const reason = `fetch_failed: ${describeError(err)}`;
|
|
7298
|
+
return { ok: false, reason, errorReason: reason };
|
|
7299
|
+
}
|
|
7300
|
+
}
|
|
7301
|
+
async function settleUnstarted(opts, payload, turnId, errorReason, log) {
|
|
7302
|
+
try {
|
|
7303
|
+
await opts.api.setActiveRun(opts.workspaceId, payload.conversationId, payload.agentId, {
|
|
7304
|
+
activeRunStartedAt: null,
|
|
7305
|
+
turnId,
|
|
7306
|
+
outcome: "failed",
|
|
7307
|
+
...errorReason ? { errorReason: errorReason.slice(0, 200) } : {},
|
|
7308
|
+
readThrough: payload.messageId
|
|
7309
|
+
});
|
|
7310
|
+
} catch (err) {
|
|
7311
|
+
log.warn(
|
|
7312
|
+
{ ...apiErrorLogFields(err), turnId },
|
|
7313
|
+
"Couldn't update the turn's status in Cabane. Check the conversation before trying again."
|
|
7314
|
+
);
|
|
7315
|
+
}
|
|
7316
|
+
}
|
|
7277
7317
|
async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
7278
7318
|
const startedAt = Date.now();
|
|
7279
7319
|
const { turnId } = handleOpts;
|
|
@@ -7297,6 +7337,15 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
|
7297
7337
|
agentId: payload.agentId,
|
|
7298
7338
|
turnId
|
|
7299
7339
|
};
|
|
7340
|
+
const minted = await mintForRunner(opts, payload, handleOpts, controller.signal, log);
|
|
7341
|
+
if (!minted.ok) {
|
|
7342
|
+
controller.signal.removeEventListener("abort", forwardStop);
|
|
7343
|
+
duties.releaseAbort(turnId, controller);
|
|
7344
|
+
const durationMs2 = Date.now() - startedAt;
|
|
7345
|
+
await settleUnstarted(opts, payload, turnId, minted.errorReason, log);
|
|
7346
|
+
duties.notifyEnd({ id: payload.messageId, ok: false, durationMs: durationMs2, reason: minted.reason });
|
|
7347
|
+
return { ok: false, durationMs: durationMs2, reason: minted.reason };
|
|
7348
|
+
}
|
|
7300
7349
|
let result;
|
|
7301
7350
|
let thrown;
|
|
7302
7351
|
let code;
|
|
@@ -7314,12 +7363,11 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
|
7314
7363
|
}
|
|
7315
7364
|
const input = {
|
|
7316
7365
|
baseUrl: opts.baseUrl,
|
|
7317
|
-
|
|
7318
|
-
...launch.deviceToken ? { deviceToken: launch.deviceToken } : {},
|
|
7366
|
+
credential: minted.credential,
|
|
7319
7367
|
dispatch: payload,
|
|
7320
7368
|
turnId,
|
|
7321
7369
|
resumed: handleOpts.resumed,
|
|
7322
|
-
spec: runnerSpec(opts, launch, duties)
|
|
7370
|
+
spec: runnerSpec(opts, launch, duties, minted.runnerId)
|
|
7323
7371
|
};
|
|
7324
7372
|
Object.assign(env, runnerEnv(input));
|
|
7325
7373
|
const running = launch.provider.exec(handle, launch.command, env, {
|
|
@@ -7431,6 +7479,14 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
|
7431
7479
|
"Couldn't update the turn's status in Cabane. Check the conversation before trying again."
|
|
7432
7480
|
);
|
|
7433
7481
|
}
|
|
7482
|
+
await launch.reportRunnerGone?.({
|
|
7483
|
+
workspaceId: opts.workspaceId,
|
|
7484
|
+
conversationId: payload.conversationId,
|
|
7485
|
+
agentId: payload.agentId,
|
|
7486
|
+
messageId: payload.messageId,
|
|
7487
|
+
runnerId: minted.runnerId,
|
|
7488
|
+
reason: "exited"
|
|
7489
|
+
});
|
|
7434
7490
|
if (thrown !== void 0) throw new Error(thrown);
|
|
7435
7491
|
log.warn({ turnId, code }, "runner: exited without reporting a result");
|
|
7436
7492
|
duties.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
|
|
@@ -7442,6 +7498,7 @@ var init_launch = __esm({
|
|
|
7442
7498
|
"use strict";
|
|
7443
7499
|
init_opencode_lock();
|
|
7444
7500
|
init_api_error_shape();
|
|
7501
|
+
init_errors();
|
|
7445
7502
|
init_boot_id();
|
|
7446
7503
|
init_turn_containment();
|
|
7447
7504
|
init_protocol();
|
|
@@ -8769,14 +8826,15 @@ var init_turn_execution = __esm({
|
|
|
8769
8826
|
const reportPreparing = (phase) => {
|
|
8770
8827
|
preparing ??= { id: `prepare:${randomUUID3()}`, seq: this.nextSeq() };
|
|
8771
8828
|
const { id, seq } = preparing;
|
|
8772
|
-
|
|
8829
|
+
const body = {
|
|
8773
8830
|
turnId,
|
|
8774
8831
|
toolUseId: id,
|
|
8775
8832
|
toolName: PREPARING_TOOL_NAME,
|
|
8776
8833
|
summary: "",
|
|
8777
8834
|
phase,
|
|
8778
8835
|
seq
|
|
8779
|
-
}
|
|
8836
|
+
};
|
|
8837
|
+
void this.opts.api.reportActivity(workspaceId, payload.conversationId, payload.agentId, body).catch((err) => {
|
|
8780
8838
|
turnLog.debug(
|
|
8781
8839
|
{ err: err instanceof Error ? err.message : String(err), phase },
|
|
8782
8840
|
"dispatcher: preparing-activity report failed (continuing with the hook)"
|
|
@@ -8852,6 +8910,8 @@ var init_turn_execution = __esm({
|
|
|
8852
8910
|
// CT1679: a runner leases under its launcher's boot id — the one the
|
|
8853
8911
|
// device heartbeats — or the lost-run sweep reads it as a restart.
|
|
8854
8912
|
bootId: this.opts.inRunner?.bootId ?? bootId,
|
|
8913
|
+
// CT1680: the runner holding the span, beside the device's boot id.
|
|
8914
|
+
...this.opts.inRunner ? { runnerId: this.opts.inRunner.runnerId } : {},
|
|
8855
8915
|
// CT1380: the durable record that this turn resumed, and from where.
|
|
8856
8916
|
...this.resumedFromSeq !== void 0 ? { resumedFromSeq: this.resumedFromSeq } : {},
|
|
8857
8917
|
// CT1454: the issuance the context THIS attempt runs on came from. Read
|
|
@@ -9219,7 +9279,10 @@ var init_turn_execution = __esm({
|
|
|
9219
9279
|
workspaceId,
|
|
9220
9280
|
payload.conversationId,
|
|
9221
9281
|
payload.agentId,
|
|
9222
|
-
|
|
9282
|
+
// CT1680: named for its turn, which a runner credential
|
|
9283
|
+
// requires once its start is admitted; the server then binds
|
|
9284
|
+
// the capture to that turn's attempt rather than the pair's.
|
|
9285
|
+
{ agentSessionId: event.state, turnId }
|
|
9223
9286
|
);
|
|
9224
9287
|
} catch (err) {
|
|
9225
9288
|
const status2 = err instanceof ApiError ? err.status : void 0;
|
|
@@ -9577,8 +9640,10 @@ var init_dispatcher = __esm({
|
|
|
9577
9640
|
});
|
|
9578
9641
|
|
|
9579
9642
|
// src/runner/harness-sandbox.ts
|
|
9643
|
+
import { lookup } from "dns/promises";
|
|
9580
9644
|
import { chmodSync as chmodSync5, mkdirSync as mkdirSync9, mkdtempSync, rmSync as rmSync7, writeFileSync as writeFileSync6 } from "fs";
|
|
9581
|
-
import {
|
|
9645
|
+
import { isIP } from "net";
|
|
9646
|
+
import { homedir as homedir3, networkInterfaces, tmpdir as tmpdir2, userInfo as userInfo2 } from "os";
|
|
9582
9647
|
import { join as join15 } from "path";
|
|
9583
9648
|
import { SandboxManager } from "@anthropic-ai/sandbox-runtime";
|
|
9584
9649
|
function accountHome() {
|
|
@@ -9591,19 +9656,36 @@ function accountHome() {
|
|
|
9591
9656
|
function shellQuote(value) {
|
|
9592
9657
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
9593
9658
|
}
|
|
9594
|
-
function
|
|
9659
|
+
function cabaneHostEntries(baseUrl, resolved, ownAddresses2) {
|
|
9595
9660
|
const url = new URL(baseUrl);
|
|
9596
|
-
const loopback = ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname);
|
|
9597
|
-
if (!loopback) return url.hostname;
|
|
9598
9661
|
const port = url.port || (url.protocol === "https:" ? "443" : "80");
|
|
9599
|
-
return `127.0.0.1:${port}
|
|
9662
|
+
if (["127.0.0.1", "localhost", "[::1]"].includes(url.hostname)) return [`127.0.0.1:${port}`];
|
|
9663
|
+
const own = new Set(ownAddresses2);
|
|
9664
|
+
const literals = resolved.filter((address) => own.has(address)).map((address) => address.includes(":") ? `[${address}]:${port}` : `${address}:${port}`);
|
|
9665
|
+
return [url.hostname, ...literals];
|
|
9666
|
+
}
|
|
9667
|
+
async function resolvedAddresses(baseUrl) {
|
|
9668
|
+
const { hostname: hostname5 } = new URL(baseUrl);
|
|
9669
|
+
if (isIP(hostname5)) return [];
|
|
9670
|
+
try {
|
|
9671
|
+
const answers = await lookup(hostname5, { all: true });
|
|
9672
|
+
return answers.map((a) => a.address);
|
|
9673
|
+
} catch {
|
|
9674
|
+
return [];
|
|
9675
|
+
}
|
|
9676
|
+
}
|
|
9677
|
+
function ownAddresses() {
|
|
9678
|
+
return Object.values(networkInterfaces()).flat().flatMap((i) => i ? [i.address] : []);
|
|
9600
9679
|
}
|
|
9601
9680
|
function harnessSandboxConfig(input) {
|
|
9602
9681
|
const claudeDir = input.claudeConfigDir ?? join15(input.home, ".claude");
|
|
9603
9682
|
const codexDir = input.codexHome ?? join15(input.home, ".codex");
|
|
9604
9683
|
return {
|
|
9605
9684
|
network: {
|
|
9606
|
-
allowedDomains: [
|
|
9685
|
+
allowedDomains: [
|
|
9686
|
+
...cabaneHostEntries(input.baseUrl, input.resolved, input.ownAddresses),
|
|
9687
|
+
...MODEL_AND_GIT_HOSTS
|
|
9688
|
+
],
|
|
9607
9689
|
deniedDomains: [],
|
|
9608
9690
|
allowLocalBinding: true
|
|
9609
9691
|
},
|
|
@@ -9656,6 +9738,8 @@ async function prepareHarnessSandbox(input) {
|
|
|
9656
9738
|
const config = harnessSandboxConfig({
|
|
9657
9739
|
cwd: input.cwd,
|
|
9658
9740
|
baseUrl: input.baseUrl,
|
|
9741
|
+
resolved: await resolvedAddresses(input.baseUrl),
|
|
9742
|
+
ownAddresses: ownAddresses(),
|
|
9659
9743
|
home: homedir3(),
|
|
9660
9744
|
accountHome: accountHome(),
|
|
9661
9745
|
tmp: tmpdir2(),
|
|
@@ -10028,8 +10112,7 @@ function runnerDispatcherOptions(input, log, emit, extras = {}) {
|
|
|
10028
10112
|
const outbox = new Outbox(spec.agentId, log);
|
|
10029
10113
|
const api = new CabaneApi({
|
|
10030
10114
|
baseUrl: input.baseUrl,
|
|
10031
|
-
token: input.
|
|
10032
|
-
...input.deviceToken ? { deviceToken: input.deviceToken } : {},
|
|
10115
|
+
token: input.credential,
|
|
10033
10116
|
outbox,
|
|
10034
10117
|
log
|
|
10035
10118
|
});
|
|
@@ -10051,7 +10134,7 @@ function runnerDispatcherOptions(input, log, emit, extras = {}) {
|
|
|
10051
10134
|
workspaceSlug: spec.workspaceSlug,
|
|
10052
10135
|
agentId: spec.agentId,
|
|
10053
10136
|
agentUsername: spec.agentUsername,
|
|
10054
|
-
credential: input.
|
|
10137
|
+
credential: input.credential,
|
|
10055
10138
|
local: spec.local,
|
|
10056
10139
|
runConfig: spec.runConfig,
|
|
10057
10140
|
log,
|
|
@@ -10067,6 +10150,7 @@ function runnerDispatcherOptions(input, log, emit, extras = {}) {
|
|
|
10067
10150
|
connectorHealth,
|
|
10068
10151
|
inRunner: {
|
|
10069
10152
|
bootId: spec.bootId,
|
|
10153
|
+
runnerId: spec.runnerId,
|
|
10070
10154
|
opencodeTransport: (serverUrl) => runnerOpencodeTransport(
|
|
10071
10155
|
serverUrl,
|
|
10072
10156
|
acquireLock,
|
|
@@ -13433,7 +13517,7 @@ var CompanionSupervisor = class {
|
|
|
13433
13517
|
runner: {
|
|
13434
13518
|
provider: this.runnerProvider(),
|
|
13435
13519
|
command: this.runnerCommand ?? selfRunnerCommand(),
|
|
13436
|
-
|
|
13520
|
+
reportRunnerGone: (report) => this.reportRunnerGone(report),
|
|
13437
13521
|
connectorHealth: this.connectorHealth,
|
|
13438
13522
|
locks: this.runnerLocks,
|
|
13439
13523
|
sandbox: this.runnerSandbox()
|
|
@@ -13623,6 +13707,33 @@ var CompanionSupervisor = class {
|
|
|
13623
13707
|
);
|
|
13624
13708
|
return false;
|
|
13625
13709
|
}
|
|
13710
|
+
// CT1680: a runner is gone without having settled its turn. Same bounded
|
|
13711
|
+
// retry as the unrunnable report above, on the device credential, which is
|
|
13712
|
+
// the one the launcher still holds whatever became of the runner.
|
|
13713
|
+
async reportRunnerGone(report) {
|
|
13714
|
+
if (!this.deviceApi) return;
|
|
13715
|
+
let lastErr;
|
|
13716
|
+
for (const wait of this.unrunnableRetryDelaysMs) {
|
|
13717
|
+
if (this.stopped) return;
|
|
13718
|
+
if (wait > 0) await new Promise((r) => setTimeout(r, wait));
|
|
13719
|
+
try {
|
|
13720
|
+
await this.deviceApi.reportDispatchDrop(report);
|
|
13721
|
+
return;
|
|
13722
|
+
} catch (err) {
|
|
13723
|
+
lastErr = err;
|
|
13724
|
+
}
|
|
13725
|
+
}
|
|
13726
|
+
this.log.error(
|
|
13727
|
+
{
|
|
13728
|
+
workspaceId: report.workspaceId,
|
|
13729
|
+
conversationId: report.conversationId,
|
|
13730
|
+
agentId: report.agentId,
|
|
13731
|
+
runnerId: report.runnerId,
|
|
13732
|
+
err: lastErr instanceof Error ? lastErr.message : String(lastErr)
|
|
13733
|
+
},
|
|
13734
|
+
"Couldn't tell Cabane a turn's runner stopped; its credential expires on its own."
|
|
13735
|
+
);
|
|
13736
|
+
}
|
|
13626
13737
|
// Pull-on-miss for the assignment-change race: a dispatch arrived for an agent
|
|
13627
13738
|
// we don't yet run. If it's addressed to THIS device, the assignment just
|
|
13628
13739
|
// changed under us (the poll hasn't caught it) — force a fresh assignments pull
|
package/dist/runtime.js
CHANGED
|
@@ -2516,6 +2516,13 @@ var CabaneApi = class {
|
|
|
2516
2516
|
(e) => e.turnId === turnId && e.kind === "message" && e.body?.kind !== "runtime_notice"
|
|
2517
2517
|
);
|
|
2518
2518
|
}
|
|
2519
|
+
// CT1680: the launcher mints the runner credential for one dispatch, on its
|
|
2520
|
+
// PAT and from its device, and hands it to the runner in place of both. A
|
|
2521
|
+
// retry is safe: the server returns the same runner id and retires the
|
|
2522
|
+
// credential the lost response carried.
|
|
2523
|
+
mintRunnerCredential(body) {
|
|
2524
|
+
return this.request("POST", "/api/agent/runner-credentials", body, { retry: true });
|
|
2525
|
+
}
|
|
2519
2526
|
// Read the skip intent recorded by the URL turn-control MCP surface.
|
|
2520
2527
|
// Codex/opencode use this server-side state; settle imports skipped/skipReason
|
|
2521
2528
|
// into the same local skip state used by claude-code's in-process tools.
|
|
@@ -8243,7 +8250,10 @@ var runnerSpecSchema = z14.object({
|
|
|
8243
8250
|
connectorHealth: z14.record(z14.string(), runtimeHealthSchema),
|
|
8244
8251
|
// True when this (conversation, agent) pair was already told its session
|
|
8245
8252
|
// could not be saved: the notice is once per pair, not once per process.
|
|
8246
|
-
sessionWriteNoticeClaimed: z14.boolean()
|
|
8253
|
+
sessionWriteNoticeClaimed: z14.boolean(),
|
|
8254
|
+
// CT1680: the runner's identity, minted with its credential. The start
|
|
8255
|
+
// PATCH carries it so the lease knows which runner holds the span.
|
|
8256
|
+
runnerId: z14.string()
|
|
8247
8257
|
});
|
|
8248
8258
|
var dispatchSchema = z14.object({
|
|
8249
8259
|
type: z14.literal("device:dispatch_requested"),
|
|
@@ -8254,8 +8264,7 @@ var dispatchSchema = z14.object({
|
|
|
8254
8264
|
episode: z14.number().int().optional()
|
|
8255
8265
|
});
|
|
8256
8266
|
var credentialsSchema = z14.object({
|
|
8257
|
-
|
|
8258
|
-
deviceToken: z14.string().min(1).optional()
|
|
8267
|
+
credential: z14.string().startsWith("cabrn_")
|
|
8259
8268
|
});
|
|
8260
8269
|
function runnerEnv(input) {
|
|
8261
8270
|
return {
|
|
@@ -8267,13 +8276,7 @@ function runnerEnv(input) {
|
|
|
8267
8276
|
};
|
|
8268
8277
|
}
|
|
8269
8278
|
function runnerStdin(input) {
|
|
8270
|
-
return `${JSON.stringify({
|
|
8271
|
-
// Phase 1 (CT1680) replaces the agent PAT with a runner credential bound to
|
|
8272
|
-
// this one dispatch; until then the child holds what the in-process turn
|
|
8273
|
-
// held, on the same machine.
|
|
8274
|
-
agentToken: input.agentToken,
|
|
8275
|
-
...input.deviceToken ? { deviceToken: input.deviceToken } : {}
|
|
8276
|
-
})}
|
|
8279
|
+
return `${JSON.stringify({ credential: input.credential })}
|
|
8277
8280
|
`;
|
|
8278
8281
|
}
|
|
8279
8282
|
function encodeLauncherMessage(message) {
|
|
@@ -8309,8 +8312,9 @@ function healthSnapshot(store) {
|
|
|
8309
8312
|
}
|
|
8310
8313
|
return out;
|
|
8311
8314
|
}
|
|
8312
|
-
function runnerSpec(opts, launch, duties) {
|
|
8315
|
+
function runnerSpec(opts, launch, duties, runnerId) {
|
|
8313
8316
|
return {
|
|
8317
|
+
runnerId,
|
|
8314
8318
|
...opts.deviceId ? { deviceId: opts.deviceId } : {},
|
|
8315
8319
|
bootId,
|
|
8316
8320
|
workspaceId: opts.workspaceId,
|
|
@@ -8399,6 +8403,43 @@ function giveBackLocks(locks, prefix, remote, launch, log) {
|
|
|
8399
8403
|
})();
|
|
8400
8404
|
}
|
|
8401
8405
|
}
|
|
8406
|
+
async function mintForRunner(opts, payload, handleOpts, signal, log) {
|
|
8407
|
+
try {
|
|
8408
|
+
const minted = await opts.api.mintRunnerCredential({
|
|
8409
|
+
conversationId: payload.conversationId,
|
|
8410
|
+
messageId: payload.messageId,
|
|
8411
|
+
...payload.episode !== void 0 ? { episode: payload.episode } : {},
|
|
8412
|
+
// The one turn the credential will operate: this run's.
|
|
8413
|
+
turnId: handleOpts.turnId
|
|
8414
|
+
});
|
|
8415
|
+
return { ok: true, credential: minted.credential, runnerId: minted.runnerId };
|
|
8416
|
+
} catch (err) {
|
|
8417
|
+
if (signal.aborted) return { ok: false, reason: "cancelled" };
|
|
8418
|
+
const status = err instanceof ApiError ? err.status : 0;
|
|
8419
|
+
if (status === 404) return { ok: false, reason: "runner_credential_not_found" };
|
|
8420
|
+
if (status === 409 && apiErrorCode(err) === "dispatch_not_admitted")
|
|
8421
|
+
return { ok: false, reason: "runner_credential_not_admitted" };
|
|
8422
|
+
log.warn(apiErrorLogFields(err), "runner: couldn't get a credential for the turn's runner");
|
|
8423
|
+
const reason = `fetch_failed: ${describeError(err)}`;
|
|
8424
|
+
return { ok: false, reason, errorReason: reason };
|
|
8425
|
+
}
|
|
8426
|
+
}
|
|
8427
|
+
async function settleUnstarted(opts, payload, turnId, errorReason, log) {
|
|
8428
|
+
try {
|
|
8429
|
+
await opts.api.setActiveRun(opts.workspaceId, payload.conversationId, payload.agentId, {
|
|
8430
|
+
activeRunStartedAt: null,
|
|
8431
|
+
turnId,
|
|
8432
|
+
outcome: "failed",
|
|
8433
|
+
...errorReason ? { errorReason: errorReason.slice(0, 200) } : {},
|
|
8434
|
+
readThrough: payload.messageId
|
|
8435
|
+
});
|
|
8436
|
+
} catch (err) {
|
|
8437
|
+
log.warn(
|
|
8438
|
+
{ ...apiErrorLogFields(err), turnId },
|
|
8439
|
+
"Couldn't update the turn's status in Cabane. Check the conversation before trying again."
|
|
8440
|
+
);
|
|
8441
|
+
}
|
|
8442
|
+
}
|
|
8402
8443
|
async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
8403
8444
|
const startedAt = Date.now();
|
|
8404
8445
|
const { turnId } = handleOpts;
|
|
@@ -8422,6 +8463,15 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
|
8422
8463
|
agentId: payload.agentId,
|
|
8423
8464
|
turnId
|
|
8424
8465
|
};
|
|
8466
|
+
const minted = await mintForRunner(opts, payload, handleOpts, controller.signal, log);
|
|
8467
|
+
if (!minted.ok) {
|
|
8468
|
+
controller.signal.removeEventListener("abort", forwardStop);
|
|
8469
|
+
duties.releaseAbort(turnId, controller);
|
|
8470
|
+
const durationMs2 = Date.now() - startedAt;
|
|
8471
|
+
await settleUnstarted(opts, payload, turnId, minted.errorReason, log);
|
|
8472
|
+
duties.notifyEnd({ id: payload.messageId, ok: false, durationMs: durationMs2, reason: minted.reason });
|
|
8473
|
+
return { ok: false, durationMs: durationMs2, reason: minted.reason };
|
|
8474
|
+
}
|
|
8425
8475
|
let result;
|
|
8426
8476
|
let thrown;
|
|
8427
8477
|
let code;
|
|
@@ -8439,12 +8489,11 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
|
8439
8489
|
}
|
|
8440
8490
|
const input = {
|
|
8441
8491
|
baseUrl: opts.baseUrl,
|
|
8442
|
-
|
|
8443
|
-
...launch.deviceToken ? { deviceToken: launch.deviceToken } : {},
|
|
8492
|
+
credential: minted.credential,
|
|
8444
8493
|
dispatch: payload,
|
|
8445
8494
|
turnId,
|
|
8446
8495
|
resumed: handleOpts.resumed,
|
|
8447
|
-
spec: runnerSpec(opts, launch, duties)
|
|
8496
|
+
spec: runnerSpec(opts, launch, duties, minted.runnerId)
|
|
8448
8497
|
};
|
|
8449
8498
|
Object.assign(env, runnerEnv(input));
|
|
8450
8499
|
const running = launch.provider.exec(handle, launch.command, env, {
|
|
@@ -8556,6 +8605,14 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
|
8556
8605
|
"Couldn't update the turn's status in Cabane. Check the conversation before trying again."
|
|
8557
8606
|
);
|
|
8558
8607
|
}
|
|
8608
|
+
await launch.reportRunnerGone?.({
|
|
8609
|
+
workspaceId: opts.workspaceId,
|
|
8610
|
+
conversationId: payload.conversationId,
|
|
8611
|
+
agentId: payload.agentId,
|
|
8612
|
+
messageId: payload.messageId,
|
|
8613
|
+
runnerId: minted.runnerId,
|
|
8614
|
+
reason: "exited"
|
|
8615
|
+
});
|
|
8559
8616
|
if (thrown !== void 0) throw new Error(thrown);
|
|
8560
8617
|
log.warn({ turnId, code }, "runner: exited without reporting a result");
|
|
8561
8618
|
duties.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
|
|
@@ -9783,14 +9840,15 @@ var TurnExecution = class {
|
|
|
9783
9840
|
const reportPreparing = (phase) => {
|
|
9784
9841
|
preparing ??= { id: `prepare:${randomUUID3()}`, seq: this.nextSeq() };
|
|
9785
9842
|
const { id, seq } = preparing;
|
|
9786
|
-
|
|
9843
|
+
const body = {
|
|
9787
9844
|
turnId,
|
|
9788
9845
|
toolUseId: id,
|
|
9789
9846
|
toolName: PREPARING_TOOL_NAME,
|
|
9790
9847
|
summary: "",
|
|
9791
9848
|
phase,
|
|
9792
9849
|
seq
|
|
9793
|
-
}
|
|
9850
|
+
};
|
|
9851
|
+
void this.opts.api.reportActivity(workspaceId, payload.conversationId, payload.agentId, body).catch((err) => {
|
|
9794
9852
|
turnLog.debug(
|
|
9795
9853
|
{ err: err instanceof Error ? err.message : String(err), phase },
|
|
9796
9854
|
"dispatcher: preparing-activity report failed (continuing with the hook)"
|
|
@@ -9866,6 +9924,8 @@ var TurnExecution = class {
|
|
|
9866
9924
|
// CT1679: a runner leases under its launcher's boot id — the one the
|
|
9867
9925
|
// device heartbeats — or the lost-run sweep reads it as a restart.
|
|
9868
9926
|
bootId: this.opts.inRunner?.bootId ?? bootId,
|
|
9927
|
+
// CT1680: the runner holding the span, beside the device's boot id.
|
|
9928
|
+
...this.opts.inRunner ? { runnerId: this.opts.inRunner.runnerId } : {},
|
|
9869
9929
|
// CT1380: the durable record that this turn resumed, and from where.
|
|
9870
9930
|
...this.resumedFromSeq !== void 0 ? { resumedFromSeq: this.resumedFromSeq } : {},
|
|
9871
9931
|
// CT1454: the issuance the context THIS attempt runs on came from. Read
|
|
@@ -10233,7 +10293,10 @@ var TurnExecution = class {
|
|
|
10233
10293
|
workspaceId,
|
|
10234
10294
|
payload.conversationId,
|
|
10235
10295
|
payload.agentId,
|
|
10236
|
-
|
|
10296
|
+
// CT1680: named for its turn, which a runner credential
|
|
10297
|
+
// requires once its start is admitted; the server then binds
|
|
10298
|
+
// the capture to that turn's attempt rather than the pair's.
|
|
10299
|
+
{ agentSessionId: event.state, turnId }
|
|
10237
10300
|
);
|
|
10238
10301
|
} catch (err) {
|
|
10239
10302
|
const status = err instanceof ApiError ? err.status : void 0;
|
|
@@ -10734,8 +10797,10 @@ var DeviceLocks = class {
|
|
|
10734
10797
|
};
|
|
10735
10798
|
|
|
10736
10799
|
// src/runner/harness-sandbox.ts
|
|
10800
|
+
import { lookup } from "dns/promises";
|
|
10737
10801
|
import { chmodSync as chmodSync5, mkdirSync as mkdirSync9, mkdtempSync, rmSync as rmSync7, writeFileSync as writeFileSync6 } from "fs";
|
|
10738
|
-
import {
|
|
10802
|
+
import { isIP } from "net";
|
|
10803
|
+
import { homedir as homedir3, networkInterfaces, tmpdir as tmpdir2, userInfo as userInfo2 } from "os";
|
|
10739
10804
|
import { join as join15 } from "path";
|
|
10740
10805
|
import { SandboxManager } from "@anthropic-ai/sandbox-runtime";
|
|
10741
10806
|
function osSandboxUsable(log) {
|
|
@@ -11731,7 +11796,7 @@ var CompanionSupervisor = class {
|
|
|
11731
11796
|
runner: {
|
|
11732
11797
|
provider: this.runnerProvider(),
|
|
11733
11798
|
command: this.runnerCommand ?? selfRunnerCommand(),
|
|
11734
|
-
|
|
11799
|
+
reportRunnerGone: (report) => this.reportRunnerGone(report),
|
|
11735
11800
|
connectorHealth: this.connectorHealth,
|
|
11736
11801
|
locks: this.runnerLocks,
|
|
11737
11802
|
sandbox: this.runnerSandbox()
|
|
@@ -11921,6 +11986,33 @@ var CompanionSupervisor = class {
|
|
|
11921
11986
|
);
|
|
11922
11987
|
return false;
|
|
11923
11988
|
}
|
|
11989
|
+
// CT1680: a runner is gone without having settled its turn. Same bounded
|
|
11990
|
+
// retry as the unrunnable report above, on the device credential, which is
|
|
11991
|
+
// the one the launcher still holds whatever became of the runner.
|
|
11992
|
+
async reportRunnerGone(report) {
|
|
11993
|
+
if (!this.deviceApi) return;
|
|
11994
|
+
let lastErr;
|
|
11995
|
+
for (const wait of this.unrunnableRetryDelaysMs) {
|
|
11996
|
+
if (this.stopped) return;
|
|
11997
|
+
if (wait > 0) await new Promise((r) => setTimeout(r, wait));
|
|
11998
|
+
try {
|
|
11999
|
+
await this.deviceApi.reportDispatchDrop(report);
|
|
12000
|
+
return;
|
|
12001
|
+
} catch (err) {
|
|
12002
|
+
lastErr = err;
|
|
12003
|
+
}
|
|
12004
|
+
}
|
|
12005
|
+
this.log.error(
|
|
12006
|
+
{
|
|
12007
|
+
workspaceId: report.workspaceId,
|
|
12008
|
+
conversationId: report.conversationId,
|
|
12009
|
+
agentId: report.agentId,
|
|
12010
|
+
runnerId: report.runnerId,
|
|
12011
|
+
err: lastErr instanceof Error ? lastErr.message : String(lastErr)
|
|
12012
|
+
},
|
|
12013
|
+
"Couldn't tell Cabane a turn's runner stopped; its credential expires on its own."
|
|
12014
|
+
);
|
|
12015
|
+
}
|
|
11924
12016
|
// Pull-on-miss for the assignment-change race: a dispatch arrived for an agent
|
|
11925
12017
|
// we don't yet run. If it's addressed to THIS device, the assignment just
|
|
11926
12018
|
// changed under us (the poll hasn't caught it) — force a fresh assignments pull
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cabane/companion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.124",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The Cabane Companion (headless): connect a coding agent on your machine to your Cabane workspace as a responder — drive work against your own codebase, files, and MCP servers without putting any of it in Cabane.",
|
|
6
6
|
"license": "UNLICENSED",
|