@cabane/companion 0.6.215 → 0.6.217
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/dist/cli.js +448 -2670
- package/dist/credential-broker.js +653 -0
- package/dist/launch.js +7 -4
- package/dist/pairing-config.js +7 -4
- package/dist/runtime.js +770 -2955
- package/package.json +11 -4
- package/dist/static/app.js +0 -545
- package/dist/static/index.html +0 -122
- package/dist/static/styles.css +0 -531
package/dist/runtime.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/runtime.ts
|
|
2
2
|
import { randomUUID as randomUUID7 } from "crypto";
|
|
3
|
-
import { fileURLToPath as
|
|
3
|
+
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
4
4
|
|
|
5
5
|
// src/cabane-command.ts
|
|
6
6
|
import { accessSync, constants, realpathSync as realpathSync2 } from "fs";
|
|
@@ -254,9 +254,9 @@ var companionConfigSchema = z2.object({
|
|
|
254
254
|
// silent roster: an agent nobody listed gets no hook and no error, and starts
|
|
255
255
|
// in the operator's `$HOME`.
|
|
256
256
|
prepareHook: prepareHookSchema.optional(),
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
//
|
|
257
|
+
// The CLI ignores both dashboard settings. Desktop uses `dashboardPort`;
|
|
258
|
+
// `autoOpen` remains parseable for older config files but no longer controls
|
|
259
|
+
// startup. `logLevel` is pino's level, live-editable.
|
|
260
260
|
dashboardPort: z2.number().int().min(1).max(65535).optional(),
|
|
261
261
|
autoOpen: z2.boolean().optional(),
|
|
262
262
|
logLevel: z2.enum(["warn", "info", "debug"]).optional(),
|
|
@@ -294,7 +294,10 @@ var companionConfigSchema = z2.object({
|
|
|
294
294
|
// credential and secrets on its stdin (`runner/launch.ts`, `wrapped`). The README lists
|
|
295
295
|
// what a wrapper can rely on.
|
|
296
296
|
runner: z2.object({
|
|
297
|
-
command: z2.array(z2.string().min(1)).min(1)
|
|
297
|
+
command: z2.array(z2.string().min(1)).min(1),
|
|
298
|
+
// The external wrapper supplies only broker placeholders and removes
|
|
299
|
+
// credential mounts. Do not hand provider logins to that runner on stdin.
|
|
300
|
+
brokeredCredentials: z2.boolean().optional()
|
|
298
301
|
}).strict().optional()
|
|
299
302
|
});
|
|
300
303
|
function workRoot(cfg) {
|
|
@@ -456,31 +459,199 @@ function requireConfig() {
|
|
|
456
459
|
return cfg;
|
|
457
460
|
}
|
|
458
461
|
|
|
459
|
-
// src/
|
|
460
|
-
import {
|
|
461
|
-
import {
|
|
462
|
-
import {
|
|
463
|
-
import {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
462
|
+
// src/control-socket.ts
|
|
463
|
+
import { createHash } from "crypto";
|
|
464
|
+
import { chmodSync as chmodSync2, existsSync as existsSync4, rmSync as rmSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
465
|
+
import { createServer, connect } from "net";
|
|
466
|
+
import { join as join5 } from "path";
|
|
467
|
+
var CONTROL_TIMEOUT_MS = 1e3;
|
|
468
|
+
var MAX_UNIX_SOCKET_PATH_BYTES = 103;
|
|
469
|
+
function controlSocketPath() {
|
|
470
|
+
const dir2 = cabaneDir();
|
|
471
|
+
const key = createHash("sha256").update(dir2).digest("hex").slice(0, 16);
|
|
472
|
+
if (process.platform === "win32") {
|
|
473
|
+
return `\\\\.\\pipe\\cabane-companion-${key}`;
|
|
474
|
+
}
|
|
475
|
+
const besideRuntime = join5(dir2, "companion.sock");
|
|
476
|
+
if (Buffer.byteLength(besideRuntime) <= MAX_UNIX_SOCKET_PATH_BYTES) return besideRuntime;
|
|
477
|
+
const filename = `cabane-companion-${key}.sock`;
|
|
478
|
+
const xdgPath = process.env.XDG_RUNTIME_DIR && existsSync4(process.env.XDG_RUNTIME_DIR) ? join5(process.env.XDG_RUNTIME_DIR, filename) : null;
|
|
479
|
+
return xdgPath && Buffer.byteLength(xdgPath) <= MAX_UNIX_SOCKET_PATH_BYTES ? xdgPath : join5("/tmp", filename);
|
|
480
|
+
}
|
|
481
|
+
async function startControlServer(handlers) {
|
|
482
|
+
const path = controlSocketPath();
|
|
483
|
+
mkdirSync2(cabaneDir(), { recursive: true });
|
|
484
|
+
if (process.platform !== "win32" && existsSync4(path)) {
|
|
485
|
+
const alive = await ping(path);
|
|
486
|
+
if (alive) throw new Error(`another companion is already listening on ${path}`);
|
|
487
|
+
rmSync2(path, { force: true });
|
|
488
|
+
}
|
|
489
|
+
const server = createServer((socket) => {
|
|
490
|
+
void serveConnection(socket, handlers);
|
|
491
|
+
});
|
|
492
|
+
server.unref();
|
|
493
|
+
await new Promise((resolve, reject) => {
|
|
494
|
+
server.once("error", reject);
|
|
495
|
+
server.listen(path, () => {
|
|
496
|
+
server.removeListener("error", reject);
|
|
497
|
+
resolve();
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
if (process.platform !== "win32") {
|
|
501
|
+
try {
|
|
502
|
+
chmodSync2(path, 384);
|
|
503
|
+
} catch (err) {
|
|
504
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
505
|
+
try {
|
|
506
|
+
rmSync2(path, { force: true });
|
|
507
|
+
} catch {
|
|
508
|
+
}
|
|
509
|
+
throw err;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
server.on("error", () => {
|
|
513
|
+
});
|
|
514
|
+
return {
|
|
515
|
+
path,
|
|
516
|
+
close: () => new Promise((resolve) => {
|
|
517
|
+
server.close(() => {
|
|
518
|
+
if (process.platform !== "win32") rmSync2(path, { force: true });
|
|
519
|
+
resolve();
|
|
520
|
+
});
|
|
521
|
+
})
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
async function serveConnection(socket, handlers) {
|
|
525
|
+
socket.on("error", () => socket.destroy());
|
|
526
|
+
const line = await readLine(socket, CONTROL_TIMEOUT_MS * 5);
|
|
527
|
+
if (line === null) {
|
|
528
|
+
socket.destroy();
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
let req;
|
|
532
|
+
try {
|
|
533
|
+
req = JSON.parse(line);
|
|
534
|
+
} catch {
|
|
535
|
+
reply(socket, { error: "malformed request" });
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
try {
|
|
539
|
+
if (req.cmd === "status") {
|
|
540
|
+
reply(socket, handlers.status());
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
if (req.cmd === "detect") {
|
|
544
|
+
const result = { detection: await handlers.detect() };
|
|
545
|
+
reply(socket, result);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
if (req.cmd === "allow") {
|
|
549
|
+
const result = { detection: await handlers.allow(req.runtime) };
|
|
550
|
+
reply(socket, result);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (req.cmd === "update") {
|
|
554
|
+
reply(socket, await handlers.update(req.auto));
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
if (req.cmd === "stop") {
|
|
558
|
+
reply(socket, { ok: true });
|
|
559
|
+
setTimeout(() => handlers.stop(), 50).unref?.();
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
reply(socket, { error: `unknown command "${String(req.cmd)}"` });
|
|
563
|
+
} catch (err) {
|
|
564
|
+
reply(socket, { error: err instanceof Error ? err.message : String(err) });
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
function reply(socket, body) {
|
|
568
|
+
try {
|
|
569
|
+
socket.end(`${JSON.stringify(body)}
|
|
570
|
+
`);
|
|
571
|
+
} catch {
|
|
572
|
+
socket.destroy();
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
async function controlRequest(path, req, timeoutMs = CONTROL_TIMEOUT_MS) {
|
|
576
|
+
const socket = connect(path);
|
|
577
|
+
try {
|
|
578
|
+
await new Promise((resolve, reject) => {
|
|
579
|
+
const timer = setTimeout(() => reject(new ControlTimeout()), timeoutMs);
|
|
580
|
+
timer.unref?.();
|
|
581
|
+
socket.once("connect", () => {
|
|
582
|
+
clearTimeout(timer);
|
|
583
|
+
resolve();
|
|
584
|
+
});
|
|
585
|
+
socket.once("error", (err) => {
|
|
586
|
+
clearTimeout(timer);
|
|
587
|
+
reject(err);
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
socket.write(`${JSON.stringify(req)}
|
|
591
|
+
`);
|
|
592
|
+
const line = await readLine(socket, timeoutMs);
|
|
593
|
+
if (line === null) throw new ControlTimeout();
|
|
594
|
+
return JSON.parse(line);
|
|
595
|
+
} finally {
|
|
596
|
+
socket.destroy();
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
var ControlTimeout = class extends Error {
|
|
600
|
+
constructor() {
|
|
601
|
+
super("the companion did not answer its control socket in time");
|
|
602
|
+
this.name = "ControlTimeout";
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
function isNotListening(err) {
|
|
606
|
+
const code = err?.code;
|
|
607
|
+
return code === "ENOENT" || code === "ECONNREFUSED";
|
|
608
|
+
}
|
|
609
|
+
function readLine(socket, timeoutMs) {
|
|
610
|
+
return new Promise((resolve) => {
|
|
611
|
+
let buf = "";
|
|
612
|
+
let settled = false;
|
|
613
|
+
const done = (v) => {
|
|
614
|
+
if (settled) return;
|
|
615
|
+
settled = true;
|
|
616
|
+
clearTimeout(timer);
|
|
617
|
+
socket.removeListener("data", onData);
|
|
618
|
+
resolve(v);
|
|
619
|
+
};
|
|
620
|
+
const timer = setTimeout(() => done(null), timeoutMs);
|
|
621
|
+
timer.unref?.();
|
|
622
|
+
const onData = (chunk) => {
|
|
623
|
+
buf += chunk.toString("utf8");
|
|
624
|
+
const nl = buf.indexOf("\n");
|
|
625
|
+
if (nl >= 0) done(buf.slice(0, nl));
|
|
626
|
+
else if (buf.length > 1e6) done(null);
|
|
627
|
+
};
|
|
628
|
+
socket.on("data", onData);
|
|
629
|
+
socket.once("close", () => done(null));
|
|
630
|
+
socket.once("error", () => done(null));
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
async function ping(path) {
|
|
634
|
+
try {
|
|
635
|
+
await controlRequest(path, { cmd: "status" });
|
|
636
|
+
return true;
|
|
637
|
+
} catch (err) {
|
|
638
|
+
return !isNotListening(err);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
470
641
|
|
|
471
642
|
// src/logger.ts
|
|
472
643
|
import {
|
|
473
644
|
createWriteStream,
|
|
474
|
-
mkdirSync as
|
|
645
|
+
mkdirSync as mkdirSync3,
|
|
475
646
|
openSync,
|
|
476
647
|
renameSync as renameSync2,
|
|
477
648
|
statSync
|
|
478
649
|
} from "fs";
|
|
479
|
-
import { dirname as dirname4, join as
|
|
650
|
+
import { dirname as dirname4, join as join6 } from "path";
|
|
480
651
|
import { stripVTControlCharacters } from "util";
|
|
481
652
|
import pino from "pino";
|
|
482
653
|
function companionLogPath() {
|
|
483
|
-
return
|
|
654
|
+
return join6(cabaneDir(), "companion.log");
|
|
484
655
|
}
|
|
485
656
|
function humanText(value) {
|
|
486
657
|
return stripVTControlCharacters(String(value ?? "")).replace(
|
|
@@ -588,7 +759,7 @@ function configureLogger(log, config) {
|
|
|
588
759
|
}
|
|
589
760
|
function createLogger(destinations = {}, options = {}) {
|
|
590
761
|
const path = companionLogPath();
|
|
591
|
-
if (!destinations.file)
|
|
762
|
+
if (!destinations.file) mkdirSync3(dirname4(path), { recursive: true });
|
|
592
763
|
const file = destinations.file ?? new RotatingLog(path);
|
|
593
764
|
const state = { format: options.logFormat ?? "human" };
|
|
594
765
|
const streams = [];
|
|
@@ -634,671 +805,46 @@ function getLogger() {
|
|
|
634
805
|
return cached;
|
|
635
806
|
}
|
|
636
807
|
|
|
637
|
-
// src/
|
|
638
|
-
import {
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
...r.reply ? { reply: r.reply } : {}
|
|
650
|
-
};
|
|
651
|
-
}
|
|
652
|
-
var MAX_DISPATCHES = 50;
|
|
653
|
-
function today() {
|
|
654
|
-
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
655
|
-
}
|
|
656
|
-
var CompanionStateHub = class {
|
|
657
|
-
constructor(opts) {
|
|
658
|
-
this.opts = opts;
|
|
659
|
-
this.emitter.setMaxListeners(0);
|
|
808
|
+
// src/bundled-binaries.ts
|
|
809
|
+
import { realpathSync as realpathSync3, statSync as statSync2 } from "fs";
|
|
810
|
+
import { createRequire } from "module";
|
|
811
|
+
import { dirname as dirname5, join as join7 } from "path";
|
|
812
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
813
|
+
function nodeResolveSelf(specifier) {
|
|
814
|
+
const here = fileURLToPath3(import.meta.url);
|
|
815
|
+
if (typeof import.meta.resolve === "function") {
|
|
816
|
+
try {
|
|
817
|
+
return fileURLToPath3(import.meta.resolve(specifier));
|
|
818
|
+
} catch {
|
|
819
|
+
}
|
|
660
820
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
dispatches = [];
|
|
665
|
-
dashboardUrl = null;
|
|
666
|
-
deviceId = null;
|
|
667
|
-
deviceLabel = null;
|
|
668
|
-
deviceError = null;
|
|
669
|
-
// The latest harness snapshot the supervisor probed, or null before the
|
|
670
|
-
// first probe (see CompanionStatusJson.harnesses).
|
|
671
|
-
harnesses = null;
|
|
672
|
-
// ---- subscription (SSE) ----
|
|
673
|
-
on(listener) {
|
|
674
|
-
this.emitter.on("event", listener);
|
|
675
|
-
return () => this.emitter.off("event", listener);
|
|
821
|
+
try {
|
|
822
|
+
return createRequire(here).resolve(specifier);
|
|
823
|
+
} catch {
|
|
676
824
|
}
|
|
677
|
-
|
|
678
|
-
|
|
825
|
+
return walkNodeModules(specifier, here);
|
|
826
|
+
}
|
|
827
|
+
function walkNodeModules(pkg2, fromPath) {
|
|
828
|
+
let dir2 = dirname5(fromPath);
|
|
829
|
+
for (; ; ) {
|
|
830
|
+
const manifest = join7(dir2, "node_modules", pkg2, "package.json");
|
|
831
|
+
if (nodeIsFile(manifest)) {
|
|
832
|
+
try {
|
|
833
|
+
return realpathSync3(manifest);
|
|
834
|
+
} catch {
|
|
835
|
+
return manifest;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
const parent = dirname5(dir2);
|
|
839
|
+
if (parent === dir2) return null;
|
|
840
|
+
dir2 = parent;
|
|
679
841
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
setDeviceError(message) {
|
|
687
|
-
if (this.deviceError === message) return;
|
|
688
|
-
this.deviceError = message;
|
|
689
|
-
this.emitStatus();
|
|
690
|
-
}
|
|
691
|
-
clearDeviceError() {
|
|
692
|
-
if (this.deviceError === null) return;
|
|
693
|
-
this.deviceError = null;
|
|
694
|
-
this.emitStatus();
|
|
695
|
-
}
|
|
696
|
-
// ---- harnesses ----
|
|
697
|
-
// Replace the harness snapshot and push a status change (so the SSE feed flips
|
|
698
|
-
// the Companion's Harnesses surface live). The supervisor calls this on each
|
|
699
|
-
// heartbeat-cadence probe and on an on-demand recheck.
|
|
700
|
-
setHarnesses(harnesses) {
|
|
701
|
-
this.harnesses = harnesses;
|
|
702
|
-
this.emitStatus();
|
|
703
|
-
}
|
|
704
|
-
// ---- workspace lifecycle ----
|
|
705
|
-
registerWorkspace(ws) {
|
|
706
|
-
const existing = this.workspaces.get(ws.workspaceId);
|
|
707
|
-
if (existing) {
|
|
708
|
-
existing.slug = ws.slug;
|
|
709
|
-
existing.name = ws.name;
|
|
710
|
-
return;
|
|
711
|
-
}
|
|
712
|
-
this.workspaces.set(ws.workspaceId, {
|
|
713
|
-
workspaceId: ws.workspaceId,
|
|
714
|
-
slug: ws.slug,
|
|
715
|
-
name: ws.name,
|
|
716
|
-
connected: false,
|
|
717
|
-
authFailed: false,
|
|
718
|
-
lastEventAt: null,
|
|
719
|
-
dispatchCountToday: 0,
|
|
720
|
-
countDate: today(),
|
|
721
|
-
agents: /* @__PURE__ */ new Map()
|
|
722
|
-
});
|
|
723
|
-
this.emitStatus();
|
|
724
|
-
}
|
|
725
|
-
removeWorkspace(workspaceId) {
|
|
726
|
-
const ws = this.workspaces.get(workspaceId);
|
|
727
|
-
if (!ws) return;
|
|
728
|
-
this.workspaces.delete(workspaceId);
|
|
729
|
-
this.emit("workspace:disconnected", { slug: ws.slug });
|
|
730
|
-
this.emitStatus();
|
|
731
|
-
}
|
|
732
|
-
setConnected(workspaceId, connected) {
|
|
733
|
-
const ws = this.workspaces.get(workspaceId);
|
|
734
|
-
if (!ws || ws.connected === connected) return;
|
|
735
|
-
ws.connected = connected;
|
|
736
|
-
if (connected) ws.authFailed = false;
|
|
737
|
-
this.emitStatus();
|
|
738
|
-
}
|
|
739
|
-
setAuthFailed(workspaceId) {
|
|
740
|
-
const ws = this.workspaces.get(workspaceId);
|
|
741
|
-
if (!ws) return;
|
|
742
|
-
if (ws.authFailed && !ws.connected) return;
|
|
743
|
-
ws.authFailed = true;
|
|
744
|
-
ws.connected = false;
|
|
745
|
-
this.emitStatus();
|
|
746
|
-
}
|
|
747
|
-
recordEvent(workspaceId) {
|
|
748
|
-
const ws = this.workspaces.get(workspaceId);
|
|
749
|
-
if (!ws) return;
|
|
750
|
-
ws.lastEventAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
751
|
-
}
|
|
752
|
-
// ---- agents ----
|
|
753
|
-
// Upsert one assigned agent's observable state (called on each reconcile).
|
|
754
|
-
setAgent(workspaceId, agent) {
|
|
755
|
-
const ws = this.workspaces.get(workspaceId);
|
|
756
|
-
if (!ws) return;
|
|
757
|
-
ws.agents.set(agent.agentId, agent);
|
|
758
|
-
this.emitStatus();
|
|
759
|
-
}
|
|
760
|
-
removeAgent(workspaceId, agentId) {
|
|
761
|
-
const ws = this.workspaces.get(workspaceId);
|
|
762
|
-
if (!ws) return;
|
|
763
|
-
if (ws.agents.delete(agentId)) this.emitStatus();
|
|
764
|
-
}
|
|
765
|
-
// ---- dispatch feed ----
|
|
766
|
-
// A DispatchObserver bound to one (workspace, agent), handed to that agent's
|
|
767
|
-
// Dispatcher by the supervisor.
|
|
768
|
-
observerFor(workspaceId, agentId, slug) {
|
|
769
|
-
const username = () => this.workspaces.get(workspaceId)?.agents.get(agentId)?.username ?? "";
|
|
770
|
-
return {
|
|
771
|
-
onStart: (info) => this.dispatchStarted(workspaceId, slug, username(), info),
|
|
772
|
-
onEnd: (info) => this.dispatchEnded(slug, username(), info)
|
|
773
|
-
};
|
|
774
|
-
}
|
|
775
|
-
bumpCount(workspaceId) {
|
|
776
|
-
const ws = this.workspaces.get(workspaceId);
|
|
777
|
-
if (!ws) return;
|
|
778
|
-
const day = today();
|
|
779
|
-
if (ws.countDate !== day) {
|
|
780
|
-
ws.countDate = day;
|
|
781
|
-
ws.dispatchCountToday = 0;
|
|
782
|
-
}
|
|
783
|
-
ws.dispatchCountToday += 1;
|
|
784
|
-
}
|
|
785
|
-
dispatchStarted(workspaceId, slug, agentUsername, info) {
|
|
786
|
-
this.bumpCount(workspaceId);
|
|
787
|
-
const record = {
|
|
788
|
-
id: info.id,
|
|
789
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
790
|
-
workspaceSlug: slug,
|
|
791
|
-
...agentUsername ? { agentUsername } : {},
|
|
792
|
-
message: info.message,
|
|
793
|
-
status: "running"
|
|
794
|
-
};
|
|
795
|
-
this.upsertDispatch(record);
|
|
796
|
-
this.emit("dispatch:started", dispatchToWire(record));
|
|
797
|
-
this.emitStatus();
|
|
798
|
-
}
|
|
799
|
-
dispatchEnded(slug, agentUsername, info) {
|
|
800
|
-
const existing = this.dispatches.find((d) => d.id === info.id);
|
|
801
|
-
const record = {
|
|
802
|
-
id: info.id,
|
|
803
|
-
timestamp: existing?.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
804
|
-
workspaceSlug: existing?.workspaceSlug ?? slug,
|
|
805
|
-
...existing?.agentUsername ?? agentUsername ? { agentUsername: existing?.agentUsername ?? agentUsername } : {},
|
|
806
|
-
message: existing?.message ?? "",
|
|
807
|
-
status: info.ok ? "replied" : "error",
|
|
808
|
-
durationMs: info.durationMs,
|
|
809
|
-
...info.reason ? { error: info.reason } : {},
|
|
810
|
-
...info.reply ? { reply: info.reply } : {}
|
|
811
|
-
};
|
|
812
|
-
this.upsertDispatch(record);
|
|
813
|
-
this.emit(info.ok ? "dispatch:completed" : "dispatch:error", dispatchToWire(record));
|
|
814
|
-
}
|
|
815
|
-
upsertDispatch(record) {
|
|
816
|
-
const idx = this.dispatches.findIndex((d) => d.id === record.id);
|
|
817
|
-
if (idx >= 0) {
|
|
818
|
-
this.dispatches[idx] = record;
|
|
819
|
-
return;
|
|
820
|
-
}
|
|
821
|
-
this.dispatches.unshift(record);
|
|
822
|
-
if (this.dispatches.length > MAX_DISPATCHES) this.dispatches.length = MAX_DISPATCHES;
|
|
823
|
-
}
|
|
824
|
-
// ---- snapshots for the HTTP routes ----
|
|
825
|
-
setDashboardUrl(url) {
|
|
826
|
-
this.dashboardUrl = url;
|
|
827
|
-
}
|
|
828
|
-
recentDispatches(limit = 20) {
|
|
829
|
-
return this.dispatches.slice(0, Math.max(0, limit));
|
|
830
|
-
}
|
|
831
|
-
statusJson() {
|
|
832
|
-
const list = [...this.workspaces.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
833
|
-
const lastOverall = list.reduce((acc, ws) => {
|
|
834
|
-
if (!ws.lastEventAt) return acc;
|
|
835
|
-
if (!acc || ws.lastEventAt > acc) return ws.lastEventAt;
|
|
836
|
-
return acc;
|
|
837
|
-
}, null);
|
|
838
|
-
return {
|
|
839
|
-
cabane_url: this.opts.baseUrl,
|
|
840
|
-
device_id: this.deviceId,
|
|
841
|
-
device_label: this.deviceLabel,
|
|
842
|
-
device_error: this.deviceError,
|
|
843
|
-
connected: list.some((ws) => ws.connected),
|
|
844
|
-
workspaces: list.map((ws) => ({
|
|
845
|
-
slug: ws.slug,
|
|
846
|
-
name: ws.name,
|
|
847
|
-
last_event_at: ws.lastEventAt,
|
|
848
|
-
dispatch_count_today: ws.dispatchCountToday,
|
|
849
|
-
connected: ws.connected,
|
|
850
|
-
auth_failed: ws.authFailed,
|
|
851
|
-
agents: [...ws.agents.values()].sort((a, b) => a.username.localeCompare(b.username)).map((a) => ({
|
|
852
|
-
agent_id: a.agentId,
|
|
853
|
-
username: a.username,
|
|
854
|
-
display_name: a.displayName,
|
|
855
|
-
has_credential: a.hasCredential,
|
|
856
|
-
missing_secrets: a.missingSecrets
|
|
857
|
-
}))
|
|
858
|
-
})),
|
|
859
|
-
last_event_at_overall: lastOverall,
|
|
860
|
-
dashboard_url: this.dashboardUrl,
|
|
861
|
-
companion_version: this.opts.companionVersion,
|
|
862
|
-
instance_id: this.opts.instanceId ?? null,
|
|
863
|
-
harnesses: this.harnesses
|
|
864
|
-
};
|
|
865
|
-
}
|
|
866
|
-
emitStatus() {
|
|
867
|
-
this.emit("status:changed", this.statusJson());
|
|
868
|
-
}
|
|
869
|
-
};
|
|
870
|
-
|
|
871
|
-
// src/dashboard/routes.ts
|
|
872
|
-
var CONTENT_TYPES = {
|
|
873
|
-
".html": "text/html; charset=utf-8",
|
|
874
|
-
".css": "text/css; charset=utf-8",
|
|
875
|
-
".js": "text/javascript; charset=utf-8",
|
|
876
|
-
".svg": "image/svg+xml",
|
|
877
|
-
".ico": "image/x-icon"
|
|
878
|
-
};
|
|
879
|
-
function registerRoutes(app, deps) {
|
|
880
|
-
const { supervisor, hub, staticDir } = deps;
|
|
881
|
-
app.get("/", async (c) => {
|
|
882
|
-
const html = await readFile(join6(staticDir, "index.html"), "utf8");
|
|
883
|
-
return c.html(html);
|
|
884
|
-
});
|
|
885
|
-
app.get("/static/:file", async (c) => {
|
|
886
|
-
const file = c.req.param("file");
|
|
887
|
-
const safe = normalize(file).replace(/^(\.\.[/\\])+/, "");
|
|
888
|
-
const full = join6(staticDir, safe);
|
|
889
|
-
if (!full.startsWith(staticDir) || !existsSync4(full)) return c.notFound();
|
|
890
|
-
const body = await readFile(full);
|
|
891
|
-
const type = CONTENT_TYPES[extname(full).toLowerCase()] ?? "application/octet-stream";
|
|
892
|
-
c.header("content-type", type);
|
|
893
|
-
return c.body(body);
|
|
894
|
-
});
|
|
895
|
-
app.get("/api/status", (c) => c.json(hub.statusJson()));
|
|
896
|
-
app.get("/api/dispatches", (c) => {
|
|
897
|
-
const limit = clampLimit(c.req.query("limit"), 20);
|
|
898
|
-
return c.json({ dispatches: hub.recentDispatches(limit).map(dispatchToWire) });
|
|
899
|
-
});
|
|
900
|
-
app.get("/api/logs", async (c) => {
|
|
901
|
-
const lines2 = clampLimit(c.req.query("lines"), 200, 1e3);
|
|
902
|
-
return c.json({ lines: tailFile(companionLogPath(), lines2) });
|
|
903
|
-
});
|
|
904
|
-
app.post("/api/settings", async (c) => {
|
|
905
|
-
const body = await readJson(c);
|
|
906
|
-
const patch = {};
|
|
907
|
-
if (body.logLevel === "warn" || body.logLevel === "info" || body.logLevel === "debug") {
|
|
908
|
-
patch.logLevel = body.logLevel;
|
|
909
|
-
}
|
|
910
|
-
if (typeof body.autoOpen === "boolean") patch.autoOpen = body.autoOpen;
|
|
911
|
-
supervisor.applySettings(patch);
|
|
912
|
-
return c.json({ ok: true, status: hub.statusJson() });
|
|
913
|
-
});
|
|
914
|
-
app.post("/api/control", async (c) => {
|
|
915
|
-
const body = await readJson(c);
|
|
916
|
-
const action = body.action;
|
|
917
|
-
if (action === "stop") {
|
|
918
|
-
setTimeout(() => void supervisor.requestStop(), 50);
|
|
919
|
-
return c.json({ ok: true, action: "stop" });
|
|
920
|
-
}
|
|
921
|
-
if (action === "restart") {
|
|
922
|
-
setTimeout(() => supervisor.requestRestart(), 50);
|
|
923
|
-
return c.json({ ok: true, action: "restart" });
|
|
924
|
-
}
|
|
925
|
-
if (action === "reload-config") {
|
|
926
|
-
supervisor.reloadConfig();
|
|
927
|
-
return c.json({ ok: true, action: "reload-config" });
|
|
928
|
-
}
|
|
929
|
-
if (action === "refresh") {
|
|
930
|
-
supervisor.refresh();
|
|
931
|
-
return c.json({ ok: true, action: "refresh" });
|
|
932
|
-
}
|
|
933
|
-
return c.json({ error: `unknown action "${String(action)}"` }, 400);
|
|
934
|
-
});
|
|
935
|
-
app.post("/api/harnesses/recheck", async (c) => {
|
|
936
|
-
await supervisor.redetect();
|
|
937
|
-
return c.json({ ok: true, status: hub.statusJson() });
|
|
938
|
-
});
|
|
939
|
-
app.get(
|
|
940
|
-
"/api/events",
|
|
941
|
-
(c) => streamSSE(c, async (stream) => {
|
|
942
|
-
await stream.writeSSE({ event: "status:changed", data: JSON.stringify(hub.statusJson()) });
|
|
943
|
-
const queue = [];
|
|
944
|
-
let wake = null;
|
|
945
|
-
const unsubscribe = hub.on((ev) => {
|
|
946
|
-
queue.push(ev);
|
|
947
|
-
wake?.();
|
|
948
|
-
});
|
|
949
|
-
stream.onAbort(() => {
|
|
950
|
-
unsubscribe();
|
|
951
|
-
wake?.();
|
|
952
|
-
});
|
|
953
|
-
while (!stream.aborted) {
|
|
954
|
-
if (queue.length === 0) {
|
|
955
|
-
await Promise.race([
|
|
956
|
-
new Promise((resolve) => {
|
|
957
|
-
wake = resolve;
|
|
958
|
-
}),
|
|
959
|
-
stream.sleep(25e3)
|
|
960
|
-
]);
|
|
961
|
-
wake = null;
|
|
962
|
-
}
|
|
963
|
-
if (stream.aborted) break;
|
|
964
|
-
if (queue.length === 0) {
|
|
965
|
-
await stream.writeSSE({ event: "ping", data: "" });
|
|
966
|
-
continue;
|
|
967
|
-
}
|
|
968
|
-
const ev = queue.shift();
|
|
969
|
-
await stream.writeSSE({ event: ev.type, data: JSON.stringify(ev.data) });
|
|
970
|
-
}
|
|
971
|
-
unsubscribe();
|
|
972
|
-
})
|
|
973
|
-
);
|
|
974
|
-
}
|
|
975
|
-
async function readJson(c) {
|
|
976
|
-
try {
|
|
977
|
-
const parsed = await c.req.json();
|
|
978
|
-
return parsed && typeof parsed === "object" ? parsed : {};
|
|
979
|
-
} catch {
|
|
980
|
-
return {};
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
function clampLimit(raw, fallback, max = 200) {
|
|
984
|
-
if (!raw) return fallback;
|
|
985
|
-
const n = Number(raw);
|
|
986
|
-
if (!Number.isFinite(n) || n <= 0) return fallback;
|
|
987
|
-
return Math.min(Math.floor(n), max);
|
|
988
|
-
}
|
|
989
|
-
function tailFile(path, lines2) {
|
|
990
|
-
if (!existsSync4(path)) return [];
|
|
991
|
-
const MAX_BYTES = 256 * 1024;
|
|
992
|
-
let fd;
|
|
993
|
-
try {
|
|
994
|
-
fd = openSync2(path, "r");
|
|
995
|
-
const size = fstatSync(fd).size;
|
|
996
|
-
const start = Math.max(0, size - MAX_BYTES);
|
|
997
|
-
const len = size - start;
|
|
998
|
-
if (len <= 0) return [];
|
|
999
|
-
const buf = Buffer.alloc(len);
|
|
1000
|
-
readSync(fd, buf, 0, len, start);
|
|
1001
|
-
const text = buf.toString("utf8");
|
|
1002
|
-
const all = text.split("\n");
|
|
1003
|
-
if (start > 0 && all.length > 0) all.shift();
|
|
1004
|
-
return all.filter((l) => l.length > 0).slice(-lines2);
|
|
1005
|
-
} catch {
|
|
1006
|
-
return [];
|
|
1007
|
-
} finally {
|
|
1008
|
-
if (fd !== void 0) closeSync(fd);
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
// src/dashboard/server.ts
|
|
1013
|
-
var DEFAULT_PORT = 7474;
|
|
1014
|
-
var PORT_FALLBACK_SPAN = 10;
|
|
1015
|
-
function buildDashboardApp(deps) {
|
|
1016
|
-
const app = new Hono();
|
|
1017
|
-
app.onError((err, c) => {
|
|
1018
|
-
if (err instanceof ApiError) {
|
|
1019
|
-
const status = err.status >= 400 && err.status < 600 ? err.status : 502;
|
|
1020
|
-
return c.json({ error: err.message }, status);
|
|
1021
|
-
}
|
|
1022
|
-
if (err instanceof CompanionError) {
|
|
1023
|
-
return c.json({ error: err.message }, 400);
|
|
1024
|
-
}
|
|
1025
|
-
return c.json({ error: err instanceof Error ? err.message : "internal error" }, 500);
|
|
1026
|
-
});
|
|
1027
|
-
registerRoutes(app, { ...deps, staticDir: resolveStaticDir() });
|
|
1028
|
-
return app;
|
|
1029
|
-
}
|
|
1030
|
-
async function startDashboard(opts) {
|
|
1031
|
-
const app = buildDashboardApp({ supervisor: opts.supervisor, hub: opts.hub });
|
|
1032
|
-
const preferred = opts.port ?? DEFAULT_PORT;
|
|
1033
|
-
let lastErr;
|
|
1034
|
-
for (let port = preferred; port < preferred + PORT_FALLBACK_SPAN; port++) {
|
|
1035
|
-
try {
|
|
1036
|
-
const server = await listen(app, port);
|
|
1037
|
-
const url = `http://127.0.0.1:${port}`;
|
|
1038
|
-
return {
|
|
1039
|
-
url,
|
|
1040
|
-
port,
|
|
1041
|
-
close: () => new Promise((resolve) => {
|
|
1042
|
-
server.close(() => resolve());
|
|
1043
|
-
server.closeAllConnections?.();
|
|
1044
|
-
})
|
|
1045
|
-
};
|
|
1046
|
-
} catch (err) {
|
|
1047
|
-
if (isAddrInUse(err)) {
|
|
1048
|
-
lastErr = err;
|
|
1049
|
-
continue;
|
|
1050
|
-
}
|
|
1051
|
-
throw err;
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
throw new CompanionError(
|
|
1055
|
-
`couldn't bind the dashboard to any port in ${preferred}\u2013${preferred + PORT_FALLBACK_SPAN - 1} (all in use). Free one up or pass --port. (last error: ${lastErr instanceof Error ? lastErr.message : String(lastErr)})`
|
|
1056
|
-
);
|
|
1057
|
-
}
|
|
1058
|
-
function listen(app, port) {
|
|
1059
|
-
return new Promise((resolve, reject) => {
|
|
1060
|
-
let settled = false;
|
|
1061
|
-
const server = serve({ fetch: app.fetch, hostname: "127.0.0.1", port }, () => {
|
|
1062
|
-
if (!settled) {
|
|
1063
|
-
settled = true;
|
|
1064
|
-
resolve(server);
|
|
1065
|
-
}
|
|
1066
|
-
});
|
|
1067
|
-
server.on("error", (err) => {
|
|
1068
|
-
if (!settled) {
|
|
1069
|
-
settled = true;
|
|
1070
|
-
reject(err);
|
|
1071
|
-
}
|
|
1072
|
-
});
|
|
1073
|
-
});
|
|
1074
|
-
}
|
|
1075
|
-
function isAddrInUse(err) {
|
|
1076
|
-
return Boolean(err && typeof err === "object" && "code" in err && err.code === "EADDRINUSE");
|
|
1077
|
-
}
|
|
1078
|
-
function resolveStaticDir() {
|
|
1079
|
-
return join7(dirname5(fileURLToPath3(import.meta.url)), "static");
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
// src/control-socket.ts
|
|
1083
|
-
import { createHash } from "crypto";
|
|
1084
|
-
import { chmodSync as chmodSync2, existsSync as existsSync5, rmSync as rmSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
1085
|
-
import { createServer, connect } from "net";
|
|
1086
|
-
import { join as join8 } from "path";
|
|
1087
|
-
var CONTROL_TIMEOUT_MS = 1e3;
|
|
1088
|
-
var MAX_UNIX_SOCKET_PATH_BYTES = 103;
|
|
1089
|
-
function controlSocketPath() {
|
|
1090
|
-
const dir2 = cabaneDir();
|
|
1091
|
-
const key = createHash("sha256").update(dir2).digest("hex").slice(0, 16);
|
|
1092
|
-
if (process.platform === "win32") {
|
|
1093
|
-
return `\\\\.\\pipe\\cabane-companion-${key}`;
|
|
1094
|
-
}
|
|
1095
|
-
const besideRuntime = join8(dir2, "companion.sock");
|
|
1096
|
-
if (Buffer.byteLength(besideRuntime) <= MAX_UNIX_SOCKET_PATH_BYTES) return besideRuntime;
|
|
1097
|
-
const filename = `cabane-companion-${key}.sock`;
|
|
1098
|
-
const xdgPath = process.env.XDG_RUNTIME_DIR && existsSync5(process.env.XDG_RUNTIME_DIR) ? join8(process.env.XDG_RUNTIME_DIR, filename) : null;
|
|
1099
|
-
return xdgPath && Buffer.byteLength(xdgPath) <= MAX_UNIX_SOCKET_PATH_BYTES ? xdgPath : join8("/tmp", filename);
|
|
1100
|
-
}
|
|
1101
|
-
async function startControlServer(handlers) {
|
|
1102
|
-
const path = controlSocketPath();
|
|
1103
|
-
mkdirSync3(cabaneDir(), { recursive: true });
|
|
1104
|
-
if (process.platform !== "win32" && existsSync5(path)) {
|
|
1105
|
-
const alive = await ping(path);
|
|
1106
|
-
if (alive) throw new Error(`another companion is already listening on ${path}`);
|
|
1107
|
-
rmSync2(path, { force: true });
|
|
1108
|
-
}
|
|
1109
|
-
const server = createServer((socket) => {
|
|
1110
|
-
void serveConnection(socket, handlers);
|
|
1111
|
-
});
|
|
1112
|
-
server.unref();
|
|
1113
|
-
await new Promise((resolve, reject) => {
|
|
1114
|
-
server.once("error", reject);
|
|
1115
|
-
server.listen(path, () => {
|
|
1116
|
-
server.removeListener("error", reject);
|
|
1117
|
-
resolve();
|
|
1118
|
-
});
|
|
1119
|
-
});
|
|
1120
|
-
if (process.platform !== "win32") {
|
|
1121
|
-
try {
|
|
1122
|
-
chmodSync2(path, 384);
|
|
1123
|
-
} catch (err) {
|
|
1124
|
-
await new Promise((resolve) => server.close(() => resolve()));
|
|
1125
|
-
try {
|
|
1126
|
-
rmSync2(path, { force: true });
|
|
1127
|
-
} catch {
|
|
1128
|
-
}
|
|
1129
|
-
throw err;
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
server.on("error", () => {
|
|
1133
|
-
});
|
|
1134
|
-
return {
|
|
1135
|
-
path,
|
|
1136
|
-
close: () => new Promise((resolve) => {
|
|
1137
|
-
server.close(() => {
|
|
1138
|
-
if (process.platform !== "win32") rmSync2(path, { force: true });
|
|
1139
|
-
resolve();
|
|
1140
|
-
});
|
|
1141
|
-
})
|
|
1142
|
-
};
|
|
1143
|
-
}
|
|
1144
|
-
async function serveConnection(socket, handlers) {
|
|
1145
|
-
socket.on("error", () => socket.destroy());
|
|
1146
|
-
const line = await readLine(socket, CONTROL_TIMEOUT_MS * 5);
|
|
1147
|
-
if (line === null) {
|
|
1148
|
-
socket.destroy();
|
|
1149
|
-
return;
|
|
1150
|
-
}
|
|
1151
|
-
let req;
|
|
1152
|
-
try {
|
|
1153
|
-
req = JSON.parse(line);
|
|
1154
|
-
} catch {
|
|
1155
|
-
reply(socket, { error: "malformed request" });
|
|
1156
|
-
return;
|
|
1157
|
-
}
|
|
1158
|
-
try {
|
|
1159
|
-
if (req.cmd === "status") {
|
|
1160
|
-
reply(socket, handlers.status());
|
|
1161
|
-
return;
|
|
1162
|
-
}
|
|
1163
|
-
if (req.cmd === "detect") {
|
|
1164
|
-
const result = { detection: await handlers.detect() };
|
|
1165
|
-
reply(socket, result);
|
|
1166
|
-
return;
|
|
1167
|
-
}
|
|
1168
|
-
if (req.cmd === "allow") {
|
|
1169
|
-
const result = { detection: await handlers.allow(req.runtime) };
|
|
1170
|
-
reply(socket, result);
|
|
1171
|
-
return;
|
|
1172
|
-
}
|
|
1173
|
-
if (req.cmd === "update") {
|
|
1174
|
-
reply(socket, await handlers.update(req.auto));
|
|
1175
|
-
return;
|
|
1176
|
-
}
|
|
1177
|
-
if (req.cmd === "stop") {
|
|
1178
|
-
reply(socket, { ok: true });
|
|
1179
|
-
setTimeout(() => handlers.stop(), 50).unref?.();
|
|
1180
|
-
return;
|
|
1181
|
-
}
|
|
1182
|
-
reply(socket, { error: `unknown command "${String(req.cmd)}"` });
|
|
1183
|
-
} catch (err) {
|
|
1184
|
-
reply(socket, { error: err instanceof Error ? err.message : String(err) });
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
function reply(socket, body) {
|
|
1188
|
-
try {
|
|
1189
|
-
socket.end(`${JSON.stringify(body)}
|
|
1190
|
-
`);
|
|
1191
|
-
} catch {
|
|
1192
|
-
socket.destroy();
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
1195
|
-
async function controlRequest(path, req, timeoutMs = CONTROL_TIMEOUT_MS) {
|
|
1196
|
-
const socket = connect(path);
|
|
1197
|
-
try {
|
|
1198
|
-
await new Promise((resolve, reject) => {
|
|
1199
|
-
const timer = setTimeout(() => reject(new ControlTimeout()), timeoutMs);
|
|
1200
|
-
timer.unref?.();
|
|
1201
|
-
socket.once("connect", () => {
|
|
1202
|
-
clearTimeout(timer);
|
|
1203
|
-
resolve();
|
|
1204
|
-
});
|
|
1205
|
-
socket.once("error", (err) => {
|
|
1206
|
-
clearTimeout(timer);
|
|
1207
|
-
reject(err);
|
|
1208
|
-
});
|
|
1209
|
-
});
|
|
1210
|
-
socket.write(`${JSON.stringify(req)}
|
|
1211
|
-
`);
|
|
1212
|
-
const line = await readLine(socket, timeoutMs);
|
|
1213
|
-
if (line === null) throw new ControlTimeout();
|
|
1214
|
-
return JSON.parse(line);
|
|
1215
|
-
} finally {
|
|
1216
|
-
socket.destroy();
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
var ControlTimeout = class extends Error {
|
|
1220
|
-
constructor() {
|
|
1221
|
-
super("the companion did not answer its control socket in time");
|
|
1222
|
-
this.name = "ControlTimeout";
|
|
1223
|
-
}
|
|
1224
|
-
};
|
|
1225
|
-
function isNotListening(err) {
|
|
1226
|
-
const code = err?.code;
|
|
1227
|
-
return code === "ENOENT" || code === "ECONNREFUSED";
|
|
1228
|
-
}
|
|
1229
|
-
function readLine(socket, timeoutMs) {
|
|
1230
|
-
return new Promise((resolve) => {
|
|
1231
|
-
let buf = "";
|
|
1232
|
-
let settled = false;
|
|
1233
|
-
const done = (v) => {
|
|
1234
|
-
if (settled) return;
|
|
1235
|
-
settled = true;
|
|
1236
|
-
clearTimeout(timer);
|
|
1237
|
-
socket.removeListener("data", onData);
|
|
1238
|
-
resolve(v);
|
|
1239
|
-
};
|
|
1240
|
-
const timer = setTimeout(() => done(null), timeoutMs);
|
|
1241
|
-
timer.unref?.();
|
|
1242
|
-
const onData = (chunk) => {
|
|
1243
|
-
buf += chunk.toString("utf8");
|
|
1244
|
-
const nl = buf.indexOf("\n");
|
|
1245
|
-
if (nl >= 0) done(buf.slice(0, nl));
|
|
1246
|
-
else if (buf.length > 1e6) done(null);
|
|
1247
|
-
};
|
|
1248
|
-
socket.on("data", onData);
|
|
1249
|
-
socket.once("close", () => done(null));
|
|
1250
|
-
socket.once("error", () => done(null));
|
|
1251
|
-
});
|
|
1252
|
-
}
|
|
1253
|
-
async function ping(path) {
|
|
1254
|
-
try {
|
|
1255
|
-
await controlRequest(path, { cmd: "status" });
|
|
1256
|
-
return true;
|
|
1257
|
-
} catch (err) {
|
|
1258
|
-
return !isNotListening(err);
|
|
1259
|
-
}
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
// src/bundled-binaries.ts
|
|
1263
|
-
import { realpathSync as realpathSync3, statSync as statSync2 } from "fs";
|
|
1264
|
-
import { createRequire } from "module";
|
|
1265
|
-
import { dirname as dirname6, join as join9 } from "path";
|
|
1266
|
-
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
1267
|
-
function nodeResolveSelf(specifier) {
|
|
1268
|
-
const here = fileURLToPath4(import.meta.url);
|
|
1269
|
-
if (typeof import.meta.resolve === "function") {
|
|
1270
|
-
try {
|
|
1271
|
-
return fileURLToPath4(import.meta.resolve(specifier));
|
|
1272
|
-
} catch {
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
try {
|
|
1276
|
-
return createRequire(here).resolve(specifier);
|
|
1277
|
-
} catch {
|
|
1278
|
-
}
|
|
1279
|
-
return walkNodeModules(specifier, here);
|
|
1280
|
-
}
|
|
1281
|
-
function walkNodeModules(pkg2, fromPath) {
|
|
1282
|
-
let dir2 = dirname6(fromPath);
|
|
1283
|
-
for (; ; ) {
|
|
1284
|
-
const manifest = join9(dir2, "node_modules", pkg2, "package.json");
|
|
1285
|
-
if (nodeIsFile(manifest)) {
|
|
1286
|
-
try {
|
|
1287
|
-
return realpathSync3(manifest);
|
|
1288
|
-
} catch {
|
|
1289
|
-
return manifest;
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
const parent = dirname6(dir2);
|
|
1293
|
-
if (parent === dir2) return null;
|
|
1294
|
-
dir2 = parent;
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
function nodeResolve(specifier, fromPath) {
|
|
1298
|
-
try {
|
|
1299
|
-
return createRequire(fromPath).resolve(specifier);
|
|
1300
|
-
} catch {
|
|
1301
|
-
return null;
|
|
842
|
+
}
|
|
843
|
+
function nodeResolve(specifier, fromPath) {
|
|
844
|
+
try {
|
|
845
|
+
return createRequire(fromPath).resolve(specifier);
|
|
846
|
+
} catch {
|
|
847
|
+
return null;
|
|
1302
848
|
}
|
|
1303
849
|
}
|
|
1304
850
|
function nodeIsFile(path) {
|
|
@@ -1351,7 +897,7 @@ function resolveClaude(env) {
|
|
|
1351
897
|
const manifest = env.resolve(`${pkg2}/package.json`, sdkEntry);
|
|
1352
898
|
if (!manifest) continue;
|
|
1353
899
|
sawPackage ??= pkg2;
|
|
1354
|
-
const exe =
|
|
900
|
+
const exe = join7(dirname5(manifest), env.platform === "win32" ? "claude.exe" : "claude");
|
|
1355
901
|
if (env.isFile(exe)) {
|
|
1356
902
|
return { ...base, packageName: pkg2, status: "present", path: exe };
|
|
1357
903
|
}
|
|
@@ -1402,13 +948,13 @@ function resolveCodex(env) {
|
|
|
1402
948
|
if (!cliManifest) return { ...base, status: "platform_package_missing", path: null };
|
|
1403
949
|
const platformManifest = env.resolve(`${pkg2}/package.json`, cliManifest);
|
|
1404
950
|
if (!platformManifest) return { ...base, status: "platform_package_missing", path: null };
|
|
1405
|
-
const packageRoot =
|
|
951
|
+
const packageRoot = join7(dirname5(platformManifest), "vendor", triple);
|
|
1406
952
|
const exeName = env.platform === "win32" ? "codex.exe" : "codex";
|
|
1407
|
-
const current =
|
|
1408
|
-
if (env.isFile(current) && env.isFile(
|
|
953
|
+
const current = join7(packageRoot, "bin", exeName);
|
|
954
|
+
if (env.isFile(current) && env.isFile(join7(packageRoot, "codex-package.json"))) {
|
|
1409
955
|
return { ...base, status: "present", path: current };
|
|
1410
956
|
}
|
|
1411
|
-
const legacy =
|
|
957
|
+
const legacy = join7(packageRoot, "codex", exeName);
|
|
1412
958
|
if (env.isFile(legacy)) return { ...base, status: "present", path: legacy };
|
|
1413
959
|
return { ...base, status: "executable_missing", path: null };
|
|
1414
960
|
}
|
|
@@ -1473,18 +1019,18 @@ function warnAboutHarnessReadiness(cfg, deps = {}) {
|
|
|
1473
1019
|
|
|
1474
1020
|
// src/runtime-file.ts
|
|
1475
1021
|
import {
|
|
1476
|
-
existsSync as
|
|
1022
|
+
existsSync as existsSync5,
|
|
1477
1023
|
readFileSync as readFileSync3,
|
|
1478
1024
|
rmSync as rmSync3,
|
|
1479
1025
|
writeFileSync as writeFileSync2,
|
|
1480
1026
|
mkdirSync as mkdirSync4,
|
|
1481
|
-
openSync as
|
|
1482
|
-
closeSync
|
|
1027
|
+
openSync as openSync2,
|
|
1028
|
+
closeSync
|
|
1483
1029
|
} from "fs";
|
|
1484
|
-
import { join as
|
|
1030
|
+
import { join as join8 } from "path";
|
|
1485
1031
|
var PROBE_TIMEOUT_MS = 1e3;
|
|
1486
1032
|
function runtimePath() {
|
|
1487
|
-
return
|
|
1033
|
+
return join8(cabaneDir(), "runtime.json");
|
|
1488
1034
|
}
|
|
1489
1035
|
function serialize(state) {
|
|
1490
1036
|
return JSON.stringify(state, null, 2) + "\n";
|
|
@@ -1500,24 +1046,24 @@ function acquireRuntimeState(state) {
|
|
|
1500
1046
|
mkdirSync4(cabaneDir(), { recursive: true });
|
|
1501
1047
|
let fd;
|
|
1502
1048
|
try {
|
|
1503
|
-
fd =
|
|
1049
|
+
fd = openSync2(runtimePath(), "wx");
|
|
1504
1050
|
} catch {
|
|
1505
1051
|
return { acquired: false, existing: readLiveRuntimeState() };
|
|
1506
1052
|
}
|
|
1507
1053
|
try {
|
|
1508
1054
|
writeFileSync2(fd, serialize(state), "utf8");
|
|
1509
1055
|
} finally {
|
|
1510
|
-
|
|
1056
|
+
closeSync(fd);
|
|
1511
1057
|
}
|
|
1512
1058
|
return { acquired: true };
|
|
1513
1059
|
}
|
|
1514
1060
|
function clearRuntimeState() {
|
|
1515
1061
|
const path = runtimePath();
|
|
1516
|
-
if (
|
|
1062
|
+
if (existsSync5(path)) rmSync3(path, { force: true });
|
|
1517
1063
|
}
|
|
1518
1064
|
function clearRuntimeStateIfOurs(instanceId) {
|
|
1519
1065
|
const path = runtimePath();
|
|
1520
|
-
if (!
|
|
1066
|
+
if (!existsSync5(path)) return;
|
|
1521
1067
|
try {
|
|
1522
1068
|
const parsed = JSON.parse(readFileSync3(path, "utf8"));
|
|
1523
1069
|
if (parsed.instanceId && parsed.instanceId !== instanceId) return;
|
|
@@ -1529,7 +1075,7 @@ function readLiveRuntimeState({
|
|
|
1529
1075
|
sweep = true
|
|
1530
1076
|
} = {}) {
|
|
1531
1077
|
const path = runtimePath();
|
|
1532
|
-
if (!
|
|
1078
|
+
if (!existsSync5(path)) return null;
|
|
1533
1079
|
let parsed;
|
|
1534
1080
|
try {
|
|
1535
1081
|
parsed = JSON.parse(readFileSync3(path, "utf8"));
|
|
@@ -1543,23 +1089,257 @@ function readLiveRuntimeState({
|
|
|
1543
1089
|
if (sweep) clearRuntimeState();
|
|
1544
1090
|
return null;
|
|
1545
1091
|
}
|
|
1546
|
-
return parsed;
|
|
1547
|
-
}
|
|
1548
|
-
async function verifyRuntime(state, requestImpl = controlRequest) {
|
|
1549
|
-
if (!state.instanceId || !state.socket) return "unknown";
|
|
1550
|
-
let body;
|
|
1551
|
-
try {
|
|
1552
|
-
body = await requestImpl(
|
|
1553
|
-
state.socket,
|
|
1554
|
-
{ cmd: "status" },
|
|
1555
|
-
PROBE_TIMEOUT_MS
|
|
1556
|
-
);
|
|
1557
|
-
} catch (err) {
|
|
1558
|
-
return isNotListening(err) ? "stale" : "unknown";
|
|
1092
|
+
return parsed;
|
|
1093
|
+
}
|
|
1094
|
+
async function verifyRuntime(state, requestImpl = controlRequest) {
|
|
1095
|
+
if (!state.instanceId || !state.socket) return "unknown";
|
|
1096
|
+
let body;
|
|
1097
|
+
try {
|
|
1098
|
+
body = await requestImpl(
|
|
1099
|
+
state.socket,
|
|
1100
|
+
{ cmd: "status" },
|
|
1101
|
+
PROBE_TIMEOUT_MS
|
|
1102
|
+
);
|
|
1103
|
+
} catch (err) {
|
|
1104
|
+
return isNotListening(err) ? "stale" : "unknown";
|
|
1105
|
+
}
|
|
1106
|
+
if (typeof body?.instance_id !== "string") return "unknown";
|
|
1107
|
+
return body.instance_id === state.instanceId ? "ours" : "stale";
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// src/state.ts
|
|
1111
|
+
import { EventEmitter } from "events";
|
|
1112
|
+
function dispatchToWire(r) {
|
|
1113
|
+
return {
|
|
1114
|
+
id: r.id,
|
|
1115
|
+
timestamp: r.timestamp,
|
|
1116
|
+
workspace_slug: r.workspaceSlug,
|
|
1117
|
+
...r.agentUsername ? { agent_username: r.agentUsername } : {},
|
|
1118
|
+
message: r.message,
|
|
1119
|
+
status: r.status,
|
|
1120
|
+
...r.durationMs !== void 0 ? { duration_ms: r.durationMs } : {},
|
|
1121
|
+
...r.error ? { error: r.error } : {},
|
|
1122
|
+
...r.reply ? { reply: r.reply } : {}
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
var MAX_DISPATCHES = 50;
|
|
1126
|
+
function today() {
|
|
1127
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1128
|
+
}
|
|
1129
|
+
var CompanionStateHub = class {
|
|
1130
|
+
constructor(opts) {
|
|
1131
|
+
this.opts = opts;
|
|
1132
|
+
this.emitter.setMaxListeners(0);
|
|
1133
|
+
}
|
|
1134
|
+
opts;
|
|
1135
|
+
emitter = new EventEmitter();
|
|
1136
|
+
workspaces = /* @__PURE__ */ new Map();
|
|
1137
|
+
dispatches = [];
|
|
1138
|
+
dashboardUrl = null;
|
|
1139
|
+
deviceId = null;
|
|
1140
|
+
deviceLabel = null;
|
|
1141
|
+
deviceError = null;
|
|
1142
|
+
// The latest harness snapshot the supervisor probed, or null before the
|
|
1143
|
+
// first probe (see CompanionStatusJson.harnesses).
|
|
1144
|
+
harnesses = null;
|
|
1145
|
+
// ---- subscription (SSE) ----
|
|
1146
|
+
on(listener) {
|
|
1147
|
+
this.emitter.on("event", listener);
|
|
1148
|
+
return () => this.emitter.off("event", listener);
|
|
1149
|
+
}
|
|
1150
|
+
emit(type, data) {
|
|
1151
|
+
this.emitter.emit("event", { type, data });
|
|
1152
|
+
}
|
|
1153
|
+
// ---- device ----
|
|
1154
|
+
setDevice(info) {
|
|
1155
|
+
if (info.deviceId !== void 0) this.deviceId = info.deviceId;
|
|
1156
|
+
if (info.deviceLabel !== void 0) this.deviceLabel = info.deviceLabel;
|
|
1157
|
+
this.emitStatus();
|
|
1158
|
+
}
|
|
1159
|
+
setDeviceError(message) {
|
|
1160
|
+
if (this.deviceError === message) return;
|
|
1161
|
+
this.deviceError = message;
|
|
1162
|
+
this.emitStatus();
|
|
1163
|
+
}
|
|
1164
|
+
clearDeviceError() {
|
|
1165
|
+
if (this.deviceError === null) return;
|
|
1166
|
+
this.deviceError = null;
|
|
1167
|
+
this.emitStatus();
|
|
1168
|
+
}
|
|
1169
|
+
// ---- harnesses ----
|
|
1170
|
+
// Replace the harness snapshot and push a status change (so the SSE feed flips
|
|
1171
|
+
// the Companion's Harnesses surface live). The supervisor calls this on each
|
|
1172
|
+
// heartbeat-cadence probe and on an on-demand recheck.
|
|
1173
|
+
setHarnesses(harnesses) {
|
|
1174
|
+
this.harnesses = harnesses;
|
|
1175
|
+
this.emitStatus();
|
|
1176
|
+
}
|
|
1177
|
+
// ---- workspace lifecycle ----
|
|
1178
|
+
registerWorkspace(ws) {
|
|
1179
|
+
const existing = this.workspaces.get(ws.workspaceId);
|
|
1180
|
+
if (existing) {
|
|
1181
|
+
existing.slug = ws.slug;
|
|
1182
|
+
existing.name = ws.name;
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
this.workspaces.set(ws.workspaceId, {
|
|
1186
|
+
workspaceId: ws.workspaceId,
|
|
1187
|
+
slug: ws.slug,
|
|
1188
|
+
name: ws.name,
|
|
1189
|
+
connected: false,
|
|
1190
|
+
authFailed: false,
|
|
1191
|
+
lastEventAt: null,
|
|
1192
|
+
dispatchCountToday: 0,
|
|
1193
|
+
countDate: today(),
|
|
1194
|
+
agents: /* @__PURE__ */ new Map()
|
|
1195
|
+
});
|
|
1196
|
+
this.emitStatus();
|
|
1197
|
+
}
|
|
1198
|
+
removeWorkspace(workspaceId) {
|
|
1199
|
+
const ws = this.workspaces.get(workspaceId);
|
|
1200
|
+
if (!ws) return;
|
|
1201
|
+
this.workspaces.delete(workspaceId);
|
|
1202
|
+
this.emit("workspace:disconnected", { slug: ws.slug });
|
|
1203
|
+
this.emitStatus();
|
|
1204
|
+
}
|
|
1205
|
+
setConnected(workspaceId, connected) {
|
|
1206
|
+
const ws = this.workspaces.get(workspaceId);
|
|
1207
|
+
if (!ws || ws.connected === connected) return;
|
|
1208
|
+
ws.connected = connected;
|
|
1209
|
+
if (connected) ws.authFailed = false;
|
|
1210
|
+
this.emitStatus();
|
|
1211
|
+
}
|
|
1212
|
+
setAuthFailed(workspaceId) {
|
|
1213
|
+
const ws = this.workspaces.get(workspaceId);
|
|
1214
|
+
if (!ws) return;
|
|
1215
|
+
if (ws.authFailed && !ws.connected) return;
|
|
1216
|
+
ws.authFailed = true;
|
|
1217
|
+
ws.connected = false;
|
|
1218
|
+
this.emitStatus();
|
|
1219
|
+
}
|
|
1220
|
+
recordEvent(workspaceId) {
|
|
1221
|
+
const ws = this.workspaces.get(workspaceId);
|
|
1222
|
+
if (!ws) return;
|
|
1223
|
+
ws.lastEventAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1224
|
+
}
|
|
1225
|
+
// ---- agents ----
|
|
1226
|
+
// Upsert one assigned agent's observable state (called on each reconcile).
|
|
1227
|
+
setAgent(workspaceId, agent) {
|
|
1228
|
+
const ws = this.workspaces.get(workspaceId);
|
|
1229
|
+
if (!ws) return;
|
|
1230
|
+
ws.agents.set(agent.agentId, agent);
|
|
1231
|
+
this.emitStatus();
|
|
1232
|
+
}
|
|
1233
|
+
removeAgent(workspaceId, agentId) {
|
|
1234
|
+
const ws = this.workspaces.get(workspaceId);
|
|
1235
|
+
if (!ws) return;
|
|
1236
|
+
if (ws.agents.delete(agentId)) this.emitStatus();
|
|
1237
|
+
}
|
|
1238
|
+
// ---- dispatch feed ----
|
|
1239
|
+
// A DispatchObserver bound to one (workspace, agent), handed to that agent's
|
|
1240
|
+
// Dispatcher by the supervisor.
|
|
1241
|
+
observerFor(workspaceId, agentId, slug) {
|
|
1242
|
+
const username = () => this.workspaces.get(workspaceId)?.agents.get(agentId)?.username ?? "";
|
|
1243
|
+
return {
|
|
1244
|
+
onStart: (info) => this.dispatchStarted(workspaceId, slug, username(), info),
|
|
1245
|
+
onEnd: (info) => this.dispatchEnded(slug, username(), info)
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
bumpCount(workspaceId) {
|
|
1249
|
+
const ws = this.workspaces.get(workspaceId);
|
|
1250
|
+
if (!ws) return;
|
|
1251
|
+
const day = today();
|
|
1252
|
+
if (ws.countDate !== day) {
|
|
1253
|
+
ws.countDate = day;
|
|
1254
|
+
ws.dispatchCountToday = 0;
|
|
1255
|
+
}
|
|
1256
|
+
ws.dispatchCountToday += 1;
|
|
1257
|
+
}
|
|
1258
|
+
dispatchStarted(workspaceId, slug, agentUsername, info) {
|
|
1259
|
+
this.bumpCount(workspaceId);
|
|
1260
|
+
const record = {
|
|
1261
|
+
id: info.id,
|
|
1262
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1263
|
+
workspaceSlug: slug,
|
|
1264
|
+
...agentUsername ? { agentUsername } : {},
|
|
1265
|
+
message: info.message,
|
|
1266
|
+
status: "running"
|
|
1267
|
+
};
|
|
1268
|
+
this.upsertDispatch(record);
|
|
1269
|
+
this.emit("dispatch:started", dispatchToWire(record));
|
|
1270
|
+
this.emitStatus();
|
|
1271
|
+
}
|
|
1272
|
+
dispatchEnded(slug, agentUsername, info) {
|
|
1273
|
+
const existing = this.dispatches.find((d) => d.id === info.id);
|
|
1274
|
+
const record = {
|
|
1275
|
+
id: info.id,
|
|
1276
|
+
timestamp: existing?.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1277
|
+
workspaceSlug: existing?.workspaceSlug ?? slug,
|
|
1278
|
+
...existing?.agentUsername ?? agentUsername ? { agentUsername: existing?.agentUsername ?? agentUsername } : {},
|
|
1279
|
+
message: existing?.message ?? "",
|
|
1280
|
+
status: info.ok ? "replied" : "error",
|
|
1281
|
+
durationMs: info.durationMs,
|
|
1282
|
+
...info.reason ? { error: info.reason } : {},
|
|
1283
|
+
...info.reply ? { reply: info.reply } : {}
|
|
1284
|
+
};
|
|
1285
|
+
this.upsertDispatch(record);
|
|
1286
|
+
this.emit(info.ok ? "dispatch:completed" : "dispatch:error", dispatchToWire(record));
|
|
1287
|
+
}
|
|
1288
|
+
upsertDispatch(record) {
|
|
1289
|
+
const idx = this.dispatches.findIndex((d) => d.id === record.id);
|
|
1290
|
+
if (idx >= 0) {
|
|
1291
|
+
this.dispatches[idx] = record;
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
this.dispatches.unshift(record);
|
|
1295
|
+
if (this.dispatches.length > MAX_DISPATCHES) this.dispatches.length = MAX_DISPATCHES;
|
|
1296
|
+
}
|
|
1297
|
+
// ---- snapshots for the HTTP routes ----
|
|
1298
|
+
setDashboardUrl(url) {
|
|
1299
|
+
this.dashboardUrl = url;
|
|
1300
|
+
}
|
|
1301
|
+
recentDispatches(limit = 20) {
|
|
1302
|
+
return this.dispatches.slice(0, Math.max(0, limit));
|
|
1303
|
+
}
|
|
1304
|
+
statusJson() {
|
|
1305
|
+
const list = [...this.workspaces.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
1306
|
+
const lastOverall = list.reduce((acc, ws) => {
|
|
1307
|
+
if (!ws.lastEventAt) return acc;
|
|
1308
|
+
if (!acc || ws.lastEventAt > acc) return ws.lastEventAt;
|
|
1309
|
+
return acc;
|
|
1310
|
+
}, null);
|
|
1311
|
+
return {
|
|
1312
|
+
cabane_url: this.opts.baseUrl,
|
|
1313
|
+
device_id: this.deviceId,
|
|
1314
|
+
device_label: this.deviceLabel,
|
|
1315
|
+
device_error: this.deviceError,
|
|
1316
|
+
connected: list.some((ws) => ws.connected),
|
|
1317
|
+
workspaces: list.map((ws) => ({
|
|
1318
|
+
slug: ws.slug,
|
|
1319
|
+
name: ws.name,
|
|
1320
|
+
last_event_at: ws.lastEventAt,
|
|
1321
|
+
dispatch_count_today: ws.dispatchCountToday,
|
|
1322
|
+
connected: ws.connected,
|
|
1323
|
+
auth_failed: ws.authFailed,
|
|
1324
|
+
agents: [...ws.agents.values()].sort((a, b) => a.username.localeCompare(b.username)).map((a) => ({
|
|
1325
|
+
agent_id: a.agentId,
|
|
1326
|
+
username: a.username,
|
|
1327
|
+
display_name: a.displayName,
|
|
1328
|
+
has_credential: a.hasCredential,
|
|
1329
|
+
missing_secrets: a.missingSecrets
|
|
1330
|
+
}))
|
|
1331
|
+
})),
|
|
1332
|
+
last_event_at_overall: lastOverall,
|
|
1333
|
+
dashboard_url: this.dashboardUrl,
|
|
1334
|
+
companion_version: this.opts.companionVersion,
|
|
1335
|
+
instance_id: this.opts.instanceId ?? null,
|
|
1336
|
+
harnesses: this.harnesses
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
emitStatus() {
|
|
1340
|
+
this.emit("status:changed", this.statusJson());
|
|
1559
1341
|
}
|
|
1560
|
-
|
|
1561
|
-
return body.instance_id === state.instanceId ? "ours" : "stale";
|
|
1562
|
-
}
|
|
1342
|
+
};
|
|
1563
1343
|
|
|
1564
1344
|
// src/heartbeat-status.ts
|
|
1565
1345
|
import { z as z3 } from "zod";
|
|
@@ -2516,27 +2296,27 @@ function requestSignal(signal, timeoutMs = 5e3) {
|
|
|
2516
2296
|
// src/credentials.ts
|
|
2517
2297
|
import {
|
|
2518
2298
|
chmodSync as chmodSync3,
|
|
2519
|
-
existsSync as
|
|
2299
|
+
existsSync as existsSync6,
|
|
2520
2300
|
mkdirSync as mkdirSync5,
|
|
2521
2301
|
readFileSync as readFileSync4,
|
|
2522
2302
|
renameSync as renameSync3,
|
|
2523
2303
|
rmSync as rmSync4,
|
|
2524
2304
|
writeFileSync as writeFileSync3
|
|
2525
2305
|
} from "fs";
|
|
2526
|
-
import { join as
|
|
2306
|
+
import { join as join9 } from "path";
|
|
2527
2307
|
import { z as z5 } from "zod";
|
|
2528
2308
|
var credentialMapSchema = z5.record(z5.string(), z5.string());
|
|
2529
2309
|
function credentialsDir() {
|
|
2530
|
-
return
|
|
2310
|
+
return join9(cabaneDir(), "credentials");
|
|
2531
2311
|
}
|
|
2532
2312
|
function legacyPath() {
|
|
2533
|
-
return
|
|
2313
|
+
return join9(cabaneDir(), "credentials.json");
|
|
2534
2314
|
}
|
|
2535
2315
|
function devicePath(deviceId) {
|
|
2536
|
-
return
|
|
2316
|
+
return join9(credentialsDir(), `${deviceId.replace(/[^A-Za-z0-9_-]/g, "_")}.json`);
|
|
2537
2317
|
}
|
|
2538
2318
|
function read(path) {
|
|
2539
|
-
if (!
|
|
2319
|
+
if (!existsSync6(path)) return null;
|
|
2540
2320
|
let raw;
|
|
2541
2321
|
try {
|
|
2542
2322
|
raw = readFileSync4(path, "utf8");
|
|
@@ -2601,18 +2381,18 @@ function pruneCredentials(deviceId, keepAgentIds) {
|
|
|
2601
2381
|
}
|
|
2602
2382
|
|
|
2603
2383
|
// src/dispatch-dedupe.ts
|
|
2604
|
-
import { mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync4, renameSync as renameSync4, existsSync as
|
|
2605
|
-
import { join as
|
|
2384
|
+
import { mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync4, renameSync as renameSync4, existsSync as existsSync7 } from "fs";
|
|
2385
|
+
import { join as join10 } from "path";
|
|
2606
2386
|
var MAX_IDS = 256;
|
|
2607
2387
|
function dir(log) {
|
|
2608
|
-
return
|
|
2388
|
+
return join10(cabaneDir(), log);
|
|
2609
2389
|
}
|
|
2610
2390
|
function pathFor(log, workspaceId) {
|
|
2611
|
-
return
|
|
2391
|
+
return join10(dir(log), encodeURIComponent(workspaceId));
|
|
2612
2392
|
}
|
|
2613
2393
|
function readIds(log, workspaceId) {
|
|
2614
2394
|
const path = pathFor(log, workspaceId);
|
|
2615
|
-
if (!
|
|
2395
|
+
if (!existsSync7(path)) return [];
|
|
2616
2396
|
try {
|
|
2617
2397
|
return readFileSync5(path, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
2618
2398
|
} catch {
|
|
@@ -2645,15 +2425,15 @@ function markCompleted(workspaceId, eventId) {
|
|
|
2645
2425
|
}
|
|
2646
2426
|
var MAX_RESUME_ATTEMPTS = 3;
|
|
2647
2427
|
function resumeDir() {
|
|
2648
|
-
return
|
|
2428
|
+
return join10(cabaneDir(), "resume-attempts");
|
|
2649
2429
|
}
|
|
2650
2430
|
function resumePathFor(workspaceId) {
|
|
2651
|
-
return
|
|
2431
|
+
return join10(resumeDir(), encodeURIComponent(workspaceId));
|
|
2652
2432
|
}
|
|
2653
2433
|
function readResumeCounts(workspaceId) {
|
|
2654
2434
|
const out = /* @__PURE__ */ new Map();
|
|
2655
2435
|
const path = resumePathFor(workspaceId);
|
|
2656
|
-
if (!
|
|
2436
|
+
if (!existsSync7(path)) return out;
|
|
2657
2437
|
try {
|
|
2658
2438
|
for (const line of readFileSync5(path, "utf8").split("\n")) {
|
|
2659
2439
|
const trimmed = line.trim();
|
|
@@ -2684,16 +2464,16 @@ function bumpResumeAttempt(workspaceId, eventId) {
|
|
|
2684
2464
|
return next;
|
|
2685
2465
|
}
|
|
2686
2466
|
function turnDir() {
|
|
2687
|
-
return
|
|
2467
|
+
return join10(cabaneDir(), "turns");
|
|
2688
2468
|
}
|
|
2689
2469
|
function turnPathFor(workspaceId) {
|
|
2690
|
-
return
|
|
2470
|
+
return join10(turnDir(), encodeURIComponent(workspaceId));
|
|
2691
2471
|
}
|
|
2692
2472
|
var TURN_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2693
2473
|
function readTurnIds(workspaceId) {
|
|
2694
2474
|
const out = /* @__PURE__ */ new Map();
|
|
2695
2475
|
const path = turnPathFor(workspaceId);
|
|
2696
|
-
if (!
|
|
2476
|
+
if (!existsSync7(path)) return out;
|
|
2697
2477
|
try {
|
|
2698
2478
|
for (const line of readFileSync5(path, "utf8").split("\n")) {
|
|
2699
2479
|
const trimmed = line.trim();
|
|
@@ -2875,7 +2655,7 @@ var turnOutcomes = ["success", "failure", "cancelled", "skipped"];
|
|
|
2875
2655
|
var turnSessionModes = ["fresh", "resumed", "degraded"];
|
|
2876
2656
|
var turnRuntimeResultKinds = ["success", "error", "no_terminal"];
|
|
2877
2657
|
var turnFinalSources = [
|
|
2878
|
-
// The agent declared its answer with the `answer` turn act
|
|
2658
|
+
// The agent declared its answer with the `answer` turn act.
|
|
2879
2659
|
"declared",
|
|
2880
2660
|
"runtime_text",
|
|
2881
2661
|
"progress_promotion",
|
|
@@ -3016,15 +2796,6 @@ function standingSegments(req) {
|
|
|
3016
2796
|
return req.standing ?? { core: req.systemPrompt, conversation: "" };
|
|
3017
2797
|
}
|
|
3018
2798
|
|
|
3019
|
-
// packages/agent-runtime/src/conformance.ts
|
|
3020
|
-
import { z as z9 } from "zod";
|
|
3021
|
-
var conformanceFixtureSchema = z9.object({
|
|
3022
|
-
name: z9.string(),
|
|
3023
|
-
request: turnRequestSchema,
|
|
3024
|
-
nativeStream: z9.array(z9.unknown()),
|
|
3025
|
-
expected: z9.array(turnEventSchema)
|
|
3026
|
-
});
|
|
3027
|
-
|
|
3028
2799
|
// packages/agent-runtime/src/transcript.ts
|
|
3029
2800
|
function createTerminalTextBuffer() {
|
|
3030
2801
|
return { held: null };
|
|
@@ -3292,7 +3063,7 @@ function envResolvedFrom(server, secretEnv) {
|
|
|
3292
3063
|
}
|
|
3293
3064
|
|
|
3294
3065
|
// packages/agent-runtime/src/claude-code/policy.ts
|
|
3295
|
-
import { z as
|
|
3066
|
+
import { z as z9 } from "zod";
|
|
3296
3067
|
var SUBAGENT_TOOLS = ["Task", "Agent"];
|
|
3297
3068
|
var UNSUPPORTED_TOOLS = ["AskUserQuestion"];
|
|
3298
3069
|
var STRANDING_TOOLS = [
|
|
@@ -3319,18 +3090,18 @@ function withThinkingSummaries(thinking) {
|
|
|
3319
3090
|
if (thinking.type === "disabled") return thinking;
|
|
3320
3091
|
return { display: "summarized", ...thinking };
|
|
3321
3092
|
}
|
|
3322
|
-
var claudeCodeDialectSchema =
|
|
3323
|
-
thinking:
|
|
3324
|
-
|
|
3325
|
-
type:
|
|
3326
|
-
display:
|
|
3093
|
+
var claudeCodeDialectSchema = z9.object({
|
|
3094
|
+
thinking: z9.discriminatedUnion("type", [
|
|
3095
|
+
z9.object({
|
|
3096
|
+
type: z9.literal("adaptive"),
|
|
3097
|
+
display: z9.enum(["summarized", "omitted"]).optional()
|
|
3327
3098
|
}),
|
|
3328
|
-
|
|
3329
|
-
type:
|
|
3330
|
-
budgetTokens:
|
|
3331
|
-
display:
|
|
3099
|
+
z9.object({
|
|
3100
|
+
type: z9.literal("enabled"),
|
|
3101
|
+
budgetTokens: z9.number().int().positive().optional(),
|
|
3102
|
+
display: z9.enum(["summarized", "omitted"]).optional()
|
|
3332
3103
|
}),
|
|
3333
|
-
|
|
3104
|
+
z9.object({ type: z9.literal("disabled") })
|
|
3334
3105
|
]).optional()
|
|
3335
3106
|
}).loose();
|
|
3336
3107
|
function readThinking(runtimeOptions) {
|
|
@@ -3393,7 +3164,7 @@ async function preToolUseHook(input) {
|
|
|
3393
3164
|
hookSpecificOutput: {
|
|
3394
3165
|
hookEventName: "PreToolUse",
|
|
3395
3166
|
permissionDecision: "allow",
|
|
3396
|
-
permissionDecisionReason: "headless never-prompt
|
|
3167
|
+
permissionDecisionReason: "headless never-prompt",
|
|
3397
3168
|
updatedInput: toolInput
|
|
3398
3169
|
}
|
|
3399
3170
|
};
|
|
@@ -3918,7 +3689,7 @@ async function* runWithResumeRecovery(queryFn, req, signal, initial, augmentOpti
|
|
|
3918
3689
|
if (recoverResume) {
|
|
3919
3690
|
if (signal.aborted) return;
|
|
3920
3691
|
onWarn?.(
|
|
3921
|
-
`claude-code adapter: stored session ${initial.resume} ended before system/init \u2014 starting one fresh session
|
|
3692
|
+
`claude-code adapter: stored session ${initial.resume} ended before system/init \u2014 starting one fresh session`,
|
|
3922
3693
|
{
|
|
3923
3694
|
cwd: req.local.cwd,
|
|
3924
3695
|
reason: "session_start_failed",
|
|
@@ -3947,7 +3718,7 @@ function createClaudeCodeAdapter(deps = {}) {
|
|
|
3947
3718
|
let built = buildClaudeCodeOptions(req, deps.augmentOptions);
|
|
3948
3719
|
if (built.freshReason === "cwd_mismatch") {
|
|
3949
3720
|
deps.onWarn?.(
|
|
3950
|
-
"claude-code adapter: stored session cwd no longer matches the current environment \u2014 starting a fresh session
|
|
3721
|
+
"claude-code adapter: stored session cwd no longer matches the current environment \u2014 starting a fresh session",
|
|
3951
3722
|
{ cwd: req.local.cwd }
|
|
3952
3723
|
);
|
|
3953
3724
|
}
|
|
@@ -3979,538 +3750,6 @@ function createClaudeCodeAdapter(deps = {}) {
|
|
|
3979
3750
|
}
|
|
3980
3751
|
var claudeCodeAdapter = createClaudeCodeAdapter();
|
|
3981
3752
|
|
|
3982
|
-
// packages/agent-runtime/src/claude-code/conformance.ts
|
|
3983
|
-
var ABORT_SENTINEL = { __abortHere: true };
|
|
3984
|
-
var CWD = "/env/here";
|
|
3985
|
-
var HEALTHY_MCP_INVENTORY = {
|
|
3986
|
-
initReceived: true,
|
|
3987
|
-
servers: [{ name: "cabane", status: "connected" }],
|
|
3988
|
-
toolCount: 1
|
|
3989
|
-
};
|
|
3990
|
-
function makeRequest(overrides2 = {}) {
|
|
3991
|
-
return {
|
|
3992
|
-
systemPrompt: "system",
|
|
3993
|
-
prompt: "hi there",
|
|
3994
|
-
content: [{ type: "text", text: "hi there" }],
|
|
3995
|
-
// The adapter rejects a null model, so the fixture carries a real bare SDK id.
|
|
3996
|
-
config: { model: "claude-opus-4-8" },
|
|
3997
|
-
session: null,
|
|
3998
|
-
cabane: { mcpUrl: "https://cabane.test/api/mcp", bearer: "tok", activeConversationId: "conv" },
|
|
3999
|
-
local: { cwd: CWD },
|
|
4000
|
-
extra: { mcpServers: {} },
|
|
4001
|
-
...overrides2
|
|
4002
|
-
};
|
|
4003
|
-
}
|
|
4004
|
-
var init = (sessionId, model) => ({
|
|
4005
|
-
type: "system",
|
|
4006
|
-
subtype: "init",
|
|
4007
|
-
session_id: sessionId,
|
|
4008
|
-
mcp_servers: HEALTHY_MCP_INVENTORY.servers,
|
|
4009
|
-
tools: ["mcp__cabane__query"],
|
|
4010
|
-
...model ? { model } : {}
|
|
4011
|
-
});
|
|
4012
|
-
var assistantText = (text) => ({
|
|
4013
|
-
type: "assistant",
|
|
4014
|
-
message: { content: [{ type: "text", text }] }
|
|
4015
|
-
});
|
|
4016
|
-
var assistantTextTool = (text, tool2) => ({
|
|
4017
|
-
type: "assistant",
|
|
4018
|
-
message: {
|
|
4019
|
-
content: [
|
|
4020
|
-
{ type: "text", text },
|
|
4021
|
-
{ type: "tool_use", id: tool2.id, name: tool2.name, input: tool2.input }
|
|
4022
|
-
]
|
|
4023
|
-
}
|
|
4024
|
-
});
|
|
4025
|
-
var toolResult = (id, content, isError = false) => ({
|
|
4026
|
-
type: "user",
|
|
4027
|
-
message: { content: [{ type: "tool_result", tool_use_id: id, is_error: isError, content }] }
|
|
4028
|
-
});
|
|
4029
|
-
var resultSuccess = (sessionId) => ({
|
|
4030
|
-
type: "result",
|
|
4031
|
-
subtype: "success",
|
|
4032
|
-
session_id: sessionId
|
|
4033
|
-
});
|
|
4034
|
-
var resultSuccessFull = (sessionId, extra) => ({
|
|
4035
|
-
...resultSuccess(sessionId),
|
|
4036
|
-
...extra
|
|
4037
|
-
});
|
|
4038
|
-
var resultError = (subtype) => ({
|
|
4039
|
-
type: "result",
|
|
4040
|
-
subtype,
|
|
4041
|
-
session_id: "s"
|
|
4042
|
-
});
|
|
4043
|
-
var resultErrorFull = (subtype, extra = {}) => ({
|
|
4044
|
-
type: "result",
|
|
4045
|
-
subtype,
|
|
4046
|
-
session_id: "s",
|
|
4047
|
-
...extra
|
|
4048
|
-
});
|
|
4049
|
-
var rateLimitEvent = (status, resetsAt, rateLimitType) => ({
|
|
4050
|
-
type: "rate_limit_event",
|
|
4051
|
-
rate_limit_info: {
|
|
4052
|
-
status,
|
|
4053
|
-
...resetsAt !== void 0 ? { resetsAt } : {},
|
|
4054
|
-
...rateLimitType !== void 0 ? { rateLimitType } : {}
|
|
4055
|
-
},
|
|
4056
|
-
session_id: "s"
|
|
4057
|
-
});
|
|
4058
|
-
var authStatus = (error) => ({
|
|
4059
|
-
type: "auth_status",
|
|
4060
|
-
isAuthenticating: false,
|
|
4061
|
-
output: [],
|
|
4062
|
-
error,
|
|
4063
|
-
session_id: "s"
|
|
4064
|
-
});
|
|
4065
|
-
var sessionEvent = (sdkSessionId, cwd = CWD, degraded = false) => ({
|
|
4066
|
-
type: "session",
|
|
4067
|
-
state: encodeSession({ sdkSessionId, cwd }),
|
|
4068
|
-
...degraded ? { degraded: true } : {}
|
|
4069
|
-
});
|
|
4070
|
-
var BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES = [
|
|
4071
|
-
{
|
|
4072
|
-
// A plain text reply: the held text-only block flushes as the terminal final.
|
|
4073
|
-
name: "clean turn",
|
|
4074
|
-
request: makeRequest(),
|
|
4075
|
-
nativeStream: [init("s1"), assistantText("Hello there."), resultSuccess("s1")],
|
|
4076
|
-
expected: [
|
|
4077
|
-
sessionEvent("s1"),
|
|
4078
|
-
{ type: "text", body: "Hello there.", terminal: true },
|
|
4079
|
-
{ type: "result", ok: true }
|
|
4080
|
-
]
|
|
4081
|
-
},
|
|
4082
|
-
{
|
|
4083
|
-
// Narration + a tool call + result + closing reply.
|
|
4084
|
-
name: "tool turn",
|
|
4085
|
-
request: makeRequest(),
|
|
4086
|
-
nativeStream: [
|
|
4087
|
-
init("s1"),
|
|
4088
|
-
assistantTextTool("Reading your notes.", {
|
|
4089
|
-
id: "tu1",
|
|
4090
|
-
name: "mcp__cabane__cabane_read",
|
|
4091
|
-
input: { path: "sleep.md" }
|
|
4092
|
-
}),
|
|
4093
|
-
toolResult("tu1", [{ type: "text", text: "sleep early" }]),
|
|
4094
|
-
assistantText("Here's the summary."),
|
|
4095
|
-
resultSuccess("s1")
|
|
4096
|
-
],
|
|
4097
|
-
expected: [
|
|
4098
|
-
sessionEvent("s1"),
|
|
4099
|
-
{ type: "text", body: "Reading your notes.", terminal: false },
|
|
4100
|
-
{
|
|
4101
|
-
type: "tool",
|
|
4102
|
-
id: "tu1",
|
|
4103
|
-
name: "cabane_read",
|
|
4104
|
-
phase: "start",
|
|
4105
|
-
summary: "sleep.md",
|
|
4106
|
-
input: { path: "sleep.md" },
|
|
4107
|
-
// Derived from the raw `mcp__cabane__` name.
|
|
4108
|
-
mcpServer: "cabane"
|
|
4109
|
-
},
|
|
4110
|
-
{
|
|
4111
|
-
type: "tool",
|
|
4112
|
-
id: "tu1",
|
|
4113
|
-
name: "cabane_read",
|
|
4114
|
-
phase: "done",
|
|
4115
|
-
summary: "sleep.md",
|
|
4116
|
-
input: { path: "sleep.md" },
|
|
4117
|
-
result: [{ type: "text", text: "sleep early" }],
|
|
4118
|
-
mcpServer: "cabane"
|
|
4119
|
-
},
|
|
4120
|
-
{ type: "text", body: "Here's the summary.", terminal: true },
|
|
4121
|
-
{ type: "result", ok: true }
|
|
4122
|
-
]
|
|
4123
|
-
},
|
|
4124
|
-
{
|
|
4125
|
-
// Highest risk: cancel mid-stream. The abort lands after the tool result; the adapter stops
|
|
4126
|
-
// before the closing reply — no final text, no `result` event (the server writes the stopped
|
|
4127
|
-
// marker).
|
|
4128
|
-
name: "cancel mid-stream",
|
|
4129
|
-
request: makeRequest(),
|
|
4130
|
-
nativeStream: [
|
|
4131
|
-
init("s1"),
|
|
4132
|
-
assistantTextTool("Working on it.", {
|
|
4133
|
-
id: "tu1",
|
|
4134
|
-
name: "mcp__cabane__cabane_list",
|
|
4135
|
-
input: {}
|
|
4136
|
-
}),
|
|
4137
|
-
toolResult("tu1"),
|
|
4138
|
-
ABORT_SENTINEL,
|
|
4139
|
-
assistantText("This reply must never be emitted."),
|
|
4140
|
-
resultSuccess("s1")
|
|
4141
|
-
],
|
|
4142
|
-
expected: [
|
|
4143
|
-
sessionEvent("s1"),
|
|
4144
|
-
{ type: "text", body: "Working on it.", terminal: false },
|
|
4145
|
-
{
|
|
4146
|
-
type: "tool",
|
|
4147
|
-
id: "tu1",
|
|
4148
|
-
name: "cabane_list",
|
|
4149
|
-
phase: "start",
|
|
4150
|
-
summary: "",
|
|
4151
|
-
input: {},
|
|
4152
|
-
mcpServer: "cabane"
|
|
4153
|
-
},
|
|
4154
|
-
{
|
|
4155
|
-
type: "tool",
|
|
4156
|
-
id: "tu1",
|
|
4157
|
-
name: "cabane_list",
|
|
4158
|
-
phase: "done",
|
|
4159
|
-
summary: "",
|
|
4160
|
-
input: {},
|
|
4161
|
-
result: void 0,
|
|
4162
|
-
mcpServer: "cabane"
|
|
4163
|
-
}
|
|
4164
|
-
]
|
|
4165
|
-
},
|
|
4166
|
-
{
|
|
4167
|
-
// An error result: partial narration lands as `progress` (terminal:false),
|
|
4168
|
-
// and the `result` event carries the machine reason.
|
|
4169
|
-
name: "error result",
|
|
4170
|
-
request: makeRequest(),
|
|
4171
|
-
nativeStream: [init("s1"), assistantText("Partial work."), resultError("error_max_turns")],
|
|
4172
|
-
expected: [
|
|
4173
|
-
sessionEvent("s1"),
|
|
4174
|
-
{ type: "text", body: "Partial work.", terminal: false },
|
|
4175
|
-
{ type: "result", ok: false, reason: "result_error:error_max_turns" }
|
|
4176
|
-
]
|
|
4177
|
-
},
|
|
4178
|
-
{
|
|
4179
|
-
// The empty-runtime anomaly's captured trace: `system/init` then a successful `result`, nothing
|
|
4180
|
-
// in between. Distinct from `empty-final` (a tool ran, no speech); this produced nothing. The
|
|
4181
|
-
// adapter reports faithfully — `result{ok:true}` — and the host decides that a success with no
|
|
4182
|
-
// content-bearing event is a failed turn.
|
|
4183
|
-
name: "zero-content success (empty runtime result)",
|
|
4184
|
-
request: makeRequest(),
|
|
4185
|
-
nativeStream: [init("s1"), resultSuccess("s1")],
|
|
4186
|
-
expected: [sessionEvent("s1"), { type: "result", ok: true }]
|
|
4187
|
-
},
|
|
4188
|
-
{
|
|
4189
|
-
// On resume, a completed background task can emit a notification and its own zero-token result
|
|
4190
|
-
// before the prompt's frames. That result is not the turn boundary; decoding continues through
|
|
4191
|
-
// the reply and real usage.
|
|
4192
|
-
name: "task-notification result does not end a resumed turn",
|
|
4193
|
-
request: makeRequest({ session: encodeSession({ sdkSessionId: "s1", cwd: CWD }) }),
|
|
4194
|
-
nativeStream: [
|
|
4195
|
-
init("s1"),
|
|
4196
|
-
{
|
|
4197
|
-
type: "system",
|
|
4198
|
-
subtype: "task_notification",
|
|
4199
|
-
task_id: "task-1",
|
|
4200
|
-
status: "completed",
|
|
4201
|
-
output_file: "/tmp/task-1.output",
|
|
4202
|
-
summary: "Background work completed.",
|
|
4203
|
-
session_id: "s1"
|
|
4204
|
-
},
|
|
4205
|
-
resultSuccessFull("s1", {
|
|
4206
|
-
is_error: false,
|
|
4207
|
-
origin: { kind: "task-notification" },
|
|
4208
|
-
usage: {
|
|
4209
|
-
input_tokens: 0,
|
|
4210
|
-
output_tokens: 0,
|
|
4211
|
-
cache_read_input_tokens: 0,
|
|
4212
|
-
cache_creation_input_tokens: 0
|
|
4213
|
-
}
|
|
4214
|
-
}),
|
|
4215
|
-
assistantTextTool("Checking the returned work.", {
|
|
4216
|
-
id: "tu1",
|
|
4217
|
-
name: "mcp__cabane__cabane_read",
|
|
4218
|
-
input: { path: "result.md" }
|
|
4219
|
-
}),
|
|
4220
|
-
toolResult("tu1", [{ type: "text", text: "ready" }]),
|
|
4221
|
-
assistantText("The prompt completed normally."),
|
|
4222
|
-
resultSuccessFull("s1", {
|
|
4223
|
-
is_error: false,
|
|
4224
|
-
usage: {
|
|
4225
|
-
input_tokens: 120,
|
|
4226
|
-
output_tokens: 30,
|
|
4227
|
-
cache_read_input_tokens: 80,
|
|
4228
|
-
cache_creation_input_tokens: 10
|
|
4229
|
-
}
|
|
4230
|
-
})
|
|
4231
|
-
],
|
|
4232
|
-
expected: [
|
|
4233
|
-
{ type: "text", body: "Checking the returned work.", terminal: false },
|
|
4234
|
-
{
|
|
4235
|
-
type: "tool",
|
|
4236
|
-
id: "tu1",
|
|
4237
|
-
name: "cabane_read",
|
|
4238
|
-
phase: "start",
|
|
4239
|
-
summary: "result.md",
|
|
4240
|
-
input: { path: "result.md" },
|
|
4241
|
-
mcpServer: "cabane"
|
|
4242
|
-
},
|
|
4243
|
-
{
|
|
4244
|
-
type: "tool",
|
|
4245
|
-
id: "tu1",
|
|
4246
|
-
name: "cabane_read",
|
|
4247
|
-
phase: "done",
|
|
4248
|
-
summary: "result.md",
|
|
4249
|
-
input: { path: "result.md" },
|
|
4250
|
-
result: [{ type: "text", text: "ready" }],
|
|
4251
|
-
mcpServer: "cabane"
|
|
4252
|
-
},
|
|
4253
|
-
{ type: "text", body: "The prompt completed normally.", terminal: true },
|
|
4254
|
-
{
|
|
4255
|
-
type: "result",
|
|
4256
|
-
ok: true,
|
|
4257
|
-
usage: {
|
|
4258
|
-
inputTokens: 210,
|
|
4259
|
-
outputTokens: 30,
|
|
4260
|
-
cacheReadTokens: 80,
|
|
4261
|
-
cacheCreationTokens: 10
|
|
4262
|
-
}
|
|
4263
|
-
}
|
|
4264
|
-
]
|
|
4265
|
-
},
|
|
4266
|
-
{
|
|
4267
|
-
// The filter is deliberately narrow: an origin-less zero-token result still ends the turn, even
|
|
4268
|
-
// if more frames follow.
|
|
4269
|
-
name: "origin-less result still ends a resumed turn",
|
|
4270
|
-
request: makeRequest({ session: encodeSession({ sdkSessionId: "s1", cwd: CWD }) }),
|
|
4271
|
-
nativeStream: [
|
|
4272
|
-
init("s1"),
|
|
4273
|
-
resultSuccessFull("s1", {
|
|
4274
|
-
is_error: false,
|
|
4275
|
-
usage: {
|
|
4276
|
-
input_tokens: 0,
|
|
4277
|
-
output_tokens: 0,
|
|
4278
|
-
cache_read_input_tokens: 0,
|
|
4279
|
-
cache_creation_input_tokens: 0
|
|
4280
|
-
}
|
|
4281
|
-
}),
|
|
4282
|
-
assistantText("This must not be emitted."),
|
|
4283
|
-
resultSuccess("s1")
|
|
4284
|
-
],
|
|
4285
|
-
expected: [
|
|
4286
|
-
{
|
|
4287
|
-
type: "result",
|
|
4288
|
-
ok: true,
|
|
4289
|
-
usage: {
|
|
4290
|
-
inputTokens: 0,
|
|
4291
|
-
outputTokens: 0,
|
|
4292
|
-
cacheReadTokens: 0,
|
|
4293
|
-
cacheCreationTokens: 0
|
|
4294
|
-
}
|
|
4295
|
-
}
|
|
4296
|
-
]
|
|
4297
|
-
},
|
|
4298
|
-
{
|
|
4299
|
-
// Empty-final: a clean turn that ended on a tool call with no closing text.
|
|
4300
|
-
// The adapter emits NO final text — empty-final promotion is host/pump
|
|
4301
|
-
// territory (this fixture pins that boundary).
|
|
4302
|
-
name: "empty-final (no closing text)",
|
|
4303
|
-
request: makeRequest(),
|
|
4304
|
-
nativeStream: [
|
|
4305
|
-
init("s1"),
|
|
4306
|
-
assistantTextTool("", {
|
|
4307
|
-
id: "tu1",
|
|
4308
|
-
name: "mcp__cabane__cabane_write",
|
|
4309
|
-
input: { path: "a.md" }
|
|
4310
|
-
}),
|
|
4311
|
-
toolResult("tu1"),
|
|
4312
|
-
resultSuccess("s1")
|
|
4313
|
-
],
|
|
4314
|
-
expected: [
|
|
4315
|
-
sessionEvent("s1"),
|
|
4316
|
-
{
|
|
4317
|
-
type: "tool",
|
|
4318
|
-
id: "tu1",
|
|
4319
|
-
name: "cabane_write",
|
|
4320
|
-
phase: "start",
|
|
4321
|
-
summary: "a.md",
|
|
4322
|
-
input: { path: "a.md" },
|
|
4323
|
-
mcpServer: "cabane"
|
|
4324
|
-
},
|
|
4325
|
-
{
|
|
4326
|
-
type: "tool",
|
|
4327
|
-
id: "tu1",
|
|
4328
|
-
name: "cabane_write",
|
|
4329
|
-
phase: "done",
|
|
4330
|
-
summary: "a.md",
|
|
4331
|
-
input: { path: "a.md" },
|
|
4332
|
-
result: void 0,
|
|
4333
|
-
mcpServer: "cabane"
|
|
4334
|
-
},
|
|
4335
|
-
{ type: "result", ok: true }
|
|
4336
|
-
]
|
|
4337
|
-
},
|
|
4338
|
-
{
|
|
4339
|
-
// A subscription cap. The SDK emits a rejected `rate_limit_event` with a named subscription
|
|
4340
|
-
// window; the terminal error result classifies as `usage_capped`. The held provider line
|
|
4341
|
-
// independently classifies to the same kind, so it lands as a runtime notice, not agent
|
|
4342
|
-
// progress.
|
|
4343
|
-
name: "subscription cap \u2192 usage_capped",
|
|
4344
|
-
request: makeRequest(),
|
|
4345
|
-
nativeStream: [
|
|
4346
|
-
init("s1"),
|
|
4347
|
-
assistantText("You've hit your session limit \xB7 resets 8:20pm (UTC)"),
|
|
4348
|
-
rateLimitEvent("rejected", void 0, "five_hour"),
|
|
4349
|
-
resultError("error_during_execution")
|
|
4350
|
-
],
|
|
4351
|
-
expected: [
|
|
4352
|
-
sessionEvent("s1"),
|
|
4353
|
-
{
|
|
4354
|
-
type: "runtime_notice",
|
|
4355
|
-
body: "You've hit your session limit \xB7 resets 8:20pm (UTC)"
|
|
4356
|
-
},
|
|
4357
|
-
{ type: "result", ok: false, reason: "usage_capped" }
|
|
4358
|
-
]
|
|
4359
|
-
},
|
|
4360
|
-
{
|
|
4361
|
-
// A failed turn can still contain the agent's own partial narration. No text classification
|
|
4362
|
-
// means no authorship rewrite, even though the structured result is the same usage cap.
|
|
4363
|
-
name: "partial narration before subscription cap stays progress",
|
|
4364
|
-
request: makeRequest(),
|
|
4365
|
-
nativeStream: [
|
|
4366
|
-
init("s1"),
|
|
4367
|
-
assistantText("Partial work."),
|
|
4368
|
-
rateLimitEvent("rejected", void 0, "five_hour"),
|
|
4369
|
-
resultError("error_during_execution")
|
|
4370
|
-
],
|
|
4371
|
-
expected: [
|
|
4372
|
-
sessionEvent("s1"),
|
|
4373
|
-
{ type: "text", body: "Partial work.", terminal: false },
|
|
4374
|
-
{ type: "result", ok: false, reason: "usage_capped" }
|
|
4375
|
-
]
|
|
4376
|
-
},
|
|
4377
|
-
{
|
|
4378
|
-
// Recognized runtime prose stays authored when its kind disagrees with the structured terminal
|
|
4379
|
-
// failure.
|
|
4380
|
-
name: "cap notice before server failure stays progress",
|
|
4381
|
-
request: makeRequest(),
|
|
4382
|
-
nativeStream: [
|
|
4383
|
-
init("s1"),
|
|
4384
|
-
assistantText("You've hit your session limit \xB7 resets 8:20pm (UTC)"),
|
|
4385
|
-
resultErrorFull("error_during_execution", {
|
|
4386
|
-
is_error: true,
|
|
4387
|
-
result: '529 {"type":"error","error":{"type":"overloaded_error"}}'
|
|
4388
|
-
})
|
|
4389
|
-
],
|
|
4390
|
-
expected: [
|
|
4391
|
-
sessionEvent("s1"),
|
|
4392
|
-
{
|
|
4393
|
-
type: "text",
|
|
4394
|
-
body: "You've hit your session limit \xB7 resets 8:20pm (UTC)",
|
|
4395
|
-
terminal: false
|
|
4396
|
-
},
|
|
4397
|
-
{ type: "result", ok: false, reason: "server_error" }
|
|
4398
|
-
]
|
|
4399
|
-
},
|
|
4400
|
-
{
|
|
4401
|
-
// `classifyErrorText` deliberately searches arbitrary error blobs for status tokens. Assistant
|
|
4402
|
-
// narration needs the stricter whole-notice classifier, or an incidental count would be
|
|
4403
|
-
// de-authored when the turn later fails with the matching provider kind.
|
|
4404
|
-
name: "incidental 5xx-shaped narration before server failure stays progress",
|
|
4405
|
-
request: makeRequest(),
|
|
4406
|
-
nativeStream: [
|
|
4407
|
-
init("s1"),
|
|
4408
|
-
assistantText("Indexed 500 files. Writing the summary next."),
|
|
4409
|
-
resultErrorFull("error_during_execution", {
|
|
4410
|
-
is_error: true,
|
|
4411
|
-
result: '529 {"type":"error","error":{"type":"overloaded_error"}}'
|
|
4412
|
-
})
|
|
4413
|
-
],
|
|
4414
|
-
expected: [
|
|
4415
|
-
sessionEvent("s1"),
|
|
4416
|
-
{
|
|
4417
|
-
type: "text",
|
|
4418
|
-
body: "Indexed 500 files. Writing the summary next.",
|
|
4419
|
-
terminal: false
|
|
4420
|
-
},
|
|
4421
|
-
{ type: "result", ok: false, reason: "server_error" }
|
|
4422
|
-
]
|
|
4423
|
-
},
|
|
4424
|
-
{
|
|
4425
|
-
// A cap signalled only by the result's `terminal_reason:'blocking_limit'` (no rate-limit event)
|
|
4426
|
-
// still classifies as `usage_capped`.
|
|
4427
|
-
name: "blocking_limit terminal reason \u2192 usage_capped",
|
|
4428
|
-
request: makeRequest(),
|
|
4429
|
-
nativeStream: [
|
|
4430
|
-
init("s1"),
|
|
4431
|
-
resultErrorFull("error_during_execution", {
|
|
4432
|
-
is_error: true,
|
|
4433
|
-
terminal_reason: "blocking_limit",
|
|
4434
|
-
result: "stopped"
|
|
4435
|
-
})
|
|
4436
|
-
],
|
|
4437
|
-
expected: [sessionEvent("s1"), { type: "result", ok: false, reason: "usage_capped" }]
|
|
4438
|
-
},
|
|
4439
|
-
{
|
|
4440
|
-
// An expired login surfaces as an `auth_status` error frame; the failing result classifies as
|
|
4441
|
-
// `auth_expired`.
|
|
4442
|
-
name: "expired login \u2192 auth_expired",
|
|
4443
|
-
request: makeRequest(),
|
|
4444
|
-
nativeStream: [
|
|
4445
|
-
init("s1"),
|
|
4446
|
-
authStatus("OAuth token has expired, please run /login"),
|
|
4447
|
-
resultErrorFull("error_during_execution", {
|
|
4448
|
-
is_error: true,
|
|
4449
|
-
result: "authentication failed"
|
|
4450
|
-
})
|
|
4451
|
-
],
|
|
4452
|
-
expected: [sessionEvent("s1"), { type: "result", ok: false, reason: "auth_expired" }]
|
|
4453
|
-
},
|
|
4454
|
-
{
|
|
4455
|
-
// A transient provider outage in the terminal result text (no structured event or enum) — a 529
|
|
4456
|
-
// overload — classifies as `server_error` via the text fallback. It must not read as the cap or
|
|
4457
|
-
// stamp the connector `limited`.
|
|
4458
|
-
name: "provider overload text \u2192 server_error",
|
|
4459
|
-
request: makeRequest(),
|
|
4460
|
-
nativeStream: [
|
|
4461
|
-
init("s1"),
|
|
4462
|
-
resultErrorFull("error_during_execution", {
|
|
4463
|
-
is_error: true,
|
|
4464
|
-
result: '529 {"type":"error","error":{"type":"overloaded_error"}}'
|
|
4465
|
-
})
|
|
4466
|
-
],
|
|
4467
|
-
expected: [sessionEvent("s1"), { type: "result", ok: false, reason: "server_error" }]
|
|
4468
|
-
},
|
|
4469
|
-
{
|
|
4470
|
-
// The resolved model is captured off `system/init.model` and attached to the terminal `result`
|
|
4471
|
-
// even on a plain resume (this fixture resumes, so no `session` event fires). What's asserted is
|
|
4472
|
-
// that the concrete id the harness reported comes home.
|
|
4473
|
-
name: "resolved model captured from system/init",
|
|
4474
|
-
request: makeRequest({ session: encodeSession({ sdkSessionId: "s1", cwd: CWD }) }),
|
|
4475
|
-
nativeStream: [init("s1", "claude-opus-4-8"), assistantText("Done."), resultSuccess("s1")],
|
|
4476
|
-
expected: [
|
|
4477
|
-
{ type: "text", body: "Done.", terminal: true },
|
|
4478
|
-
{ type: "result", ok: true, resolvedModel: "claude-opus-4-8" }
|
|
4479
|
-
]
|
|
4480
|
-
},
|
|
4481
|
-
{
|
|
4482
|
-
// Session resume echo: the stored state matches the cwd, so its SDK id is resumed and
|
|
4483
|
-
// `system/init` echoes the same id — no session event fires.
|
|
4484
|
-
name: "session resume (no re-emit on echo)",
|
|
4485
|
-
request: makeRequest({ session: encodeSession({ sdkSessionId: "s1", cwd: CWD }) }),
|
|
4486
|
-
nativeStream: [init("s1"), assistantText("Back again."), resultSuccess("s1")],
|
|
4487
|
-
expected: [
|
|
4488
|
-
{ type: "text", body: "Back again.", terminal: true },
|
|
4489
|
-
{ type: "result", ok: true }
|
|
4490
|
-
]
|
|
4491
|
-
},
|
|
4492
|
-
{
|
|
4493
|
-
// Cwd-mismatch degrade: the stored state was created under another cwd, so the adapter starts
|
|
4494
|
-
// fresh — `system/init` reports a new id, and the session event records the current cwd.
|
|
4495
|
-
name: "session cwd-mismatch degrades to fresh",
|
|
4496
|
-
request: makeRequest({ session: encodeSession({ sdkSessionId: "old", cwd: "/env/gone" }) }),
|
|
4497
|
-
nativeStream: [init("fresh1"), assistantText("Fresh start."), resultSuccess("fresh1")],
|
|
4498
|
-
expected: [
|
|
4499
|
-
// The degrade is flagged on the session event so the host relays it on settle and the server
|
|
4500
|
-
// rewinds the catch-up mark (recovering context the discarded session no longer carries).
|
|
4501
|
-
sessionEvent("fresh1", CWD, true),
|
|
4502
|
-
{ type: "text", body: "Fresh start.", terminal: true },
|
|
4503
|
-
{ type: "result", ok: true }
|
|
4504
|
-
]
|
|
4505
|
-
}
|
|
4506
|
-
];
|
|
4507
|
-
var CLAUDE_CODE_CONFORMANCE_FIXTURES = BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES.map((fixture) => ({
|
|
4508
|
-
...fixture,
|
|
4509
|
-
expected: fixture.expected.map(
|
|
4510
|
-
(event) => event.type === "result" ? { ...event, mcpInventory: HEALTHY_MCP_INVENTORY } : event
|
|
4511
|
-
)
|
|
4512
|
-
}));
|
|
4513
|
-
|
|
4514
3753
|
// packages/agent-runtime/src/opencode/addendum.ts
|
|
4515
3754
|
var OPENCODE_ADDENDUM = [
|
|
4516
3755
|
"Every tool here goes by its plain name: the workspace tools are `query`, `mutate` and",
|
|
@@ -4752,7 +3991,7 @@ function sealHeld(held, terminal) {
|
|
|
4752
3991
|
}
|
|
4753
3992
|
|
|
4754
3993
|
// packages/agent-runtime/src/opencode/policy.ts
|
|
4755
|
-
import { z as
|
|
3994
|
+
import { z as z10 } from "zod";
|
|
4756
3995
|
var OPENCODE_SCHEDULING_TOOLS = [];
|
|
4757
3996
|
var OPENCODE_UI_PROMPT_TOOLS = ["question"];
|
|
4758
3997
|
function opencodeTools() {
|
|
@@ -4761,8 +4000,8 @@ function opencodeTools() {
|
|
|
4761
4000
|
tools[name] = false;
|
|
4762
4001
|
return tools;
|
|
4763
4002
|
}
|
|
4764
|
-
var opencodeDialectSchema =
|
|
4765
|
-
agent:
|
|
4003
|
+
var opencodeDialectSchema = z10.object({
|
|
4004
|
+
agent: z10.string().min(1).optional()
|
|
4766
4005
|
}).loose();
|
|
4767
4006
|
function readOpencodeDialect(runtimeOptions) {
|
|
4768
4007
|
const parsed = opencodeDialectSchema.safeParse(runtimeOptions?.["opencode"] ?? {});
|
|
@@ -5146,516 +4385,40 @@ function createOpencodeAdapter(deps = {}) {
|
|
|
5146
4385
|
const resume = "resume" in decision ? decision.resume : null;
|
|
5147
4386
|
if ("fresh" in decision && decision.reason === "directory_mismatch") {
|
|
5148
4387
|
deps.onWarn?.(
|
|
5149
|
-
"opencode adapter: stored session directory no longer matches the current environment \u2014 starting a fresh session
|
|
4388
|
+
"opencode adapter: stored session directory no longer matches the current environment \u2014 starting a fresh session",
|
|
5150
4389
|
{ directory }
|
|
5151
4390
|
);
|
|
5152
|
-
}
|
|
5153
|
-
let degraded = "fresh" in decision && decision.reason !== void 0 && decision.reason !== "no_session";
|
|
5154
|
-
if ("fresh" in decision && decision.reason !== void 0 && decision.reason !== "no_session" && hooks?.fresh) {
|
|
5155
|
-
const recomposed = await hooks.fresh(decision.reason);
|
|
5156
|
-
if (signal.aborted) return;
|
|
5157
|
-
if (recomposed) {
|
|
5158
|
-
req = { ...req, ...recomposed, session: null };
|
|
5159
|
-
degraded = false;
|
|
5160
|
-
}
|
|
5161
|
-
}
|
|
5162
|
-
const spec = buildRunSpec(req, resume);
|
|
5163
|
-
const result = await transport.run(spec, signal);
|
|
5164
|
-
if (signal.aborted) return;
|
|
5165
|
-
if (result.created || result.sessionId !== resume) {
|
|
5166
|
-
yield {
|
|
5167
|
-
type: "session",
|
|
5168
|
-
state: encodeSession2({ sessionId: result.sessionId, directory: result.directory }),
|
|
5169
|
-
...degraded ? { degraded: true } : {}
|
|
5170
|
-
};
|
|
5171
|
-
}
|
|
5172
|
-
yield* decodeOpencodeStream(result.events, {
|
|
5173
|
-
signal,
|
|
5174
|
-
sessionId: result.sessionId,
|
|
5175
|
-
// The keys the turn's servers were installed under: what the decoder
|
|
5176
|
-
// splits a raw `<server>_<tool>` id on.
|
|
5177
|
-
mcpServers: Object.keys(spec.mcp)
|
|
5178
|
-
});
|
|
5179
|
-
}
|
|
5180
|
-
};
|
|
5181
|
-
}
|
|
5182
|
-
var opencodeAdapter = createOpencodeAdapter();
|
|
5183
|
-
|
|
5184
|
-
// packages/agent-runtime/src/opencode/conformance.ts
|
|
5185
|
-
var ABORT_SENTINEL2 = { __abortHere: true };
|
|
5186
|
-
var NEW_SESSION_ID = "sess_new";
|
|
5187
|
-
var BOUNDED_NOTICE = "You've hit your usage limit \xB7 ".padEnd(200, "x");
|
|
5188
|
-
var DIR = "/env/here";
|
|
5189
|
-
function makeRequest2(overrides2 = {}) {
|
|
5190
|
-
return {
|
|
5191
|
-
systemPrompt: "system",
|
|
5192
|
-
prompt: "hi there",
|
|
5193
|
-
content: [{ type: "text", text: "hi there" }],
|
|
5194
|
-
config: { model: "deepseek/deepseek-chat" },
|
|
5195
|
-
session: null,
|
|
5196
|
-
cabane: { mcpUrl: "https://cabane.test/api/mcp", bearer: "tok", activeConversationId: "conv" },
|
|
5197
|
-
local: { cwd: DIR },
|
|
5198
|
-
extra: { mcpServers: {} },
|
|
5199
|
-
...overrides2
|
|
5200
|
-
};
|
|
5201
|
-
}
|
|
5202
|
-
var textPart = (id, text) => ({
|
|
5203
|
-
type: "message.part.updated",
|
|
5204
|
-
properties: { part: { type: "text", id, text } }
|
|
5205
|
-
});
|
|
5206
|
-
var reasoningPart = (id, text) => ({
|
|
5207
|
-
type: "message.part.updated",
|
|
5208
|
-
properties: { part: { type: "reasoning", id, text } }
|
|
5209
|
-
});
|
|
5210
|
-
var toolPart = (id, tool2, state, callID) => ({
|
|
5211
|
-
type: "message.part.updated",
|
|
5212
|
-
properties: { part: { type: "tool", id, tool: tool2, state, ...callID ? { callID } : {} } }
|
|
5213
|
-
});
|
|
5214
|
-
var messageUpdated = (role, id) => ({
|
|
5215
|
-
type: "message.updated",
|
|
5216
|
-
properties: { info: { role, id } }
|
|
5217
|
-
});
|
|
5218
|
-
var echoedUserPart = (partId, messageID, text) => ({
|
|
5219
|
-
type: "message.part.updated",
|
|
5220
|
-
properties: { part: { type: "text", id: partId, messageID, text } }
|
|
5221
|
-
});
|
|
5222
|
-
var idle = () => ({ type: "session.idle", properties: {} });
|
|
5223
|
-
var errored = (message) => ({
|
|
5224
|
-
type: "session.error",
|
|
5225
|
-
properties: { error: { message } }
|
|
5226
|
-
});
|
|
5227
|
-
var sessionEvent2 = (sessionId, directory = DIR, degraded = false) => ({
|
|
5228
|
-
type: "session",
|
|
5229
|
-
state: encodeSession2({ sessionId, directory }),
|
|
5230
|
-
...degraded ? { degraded: true } : {}
|
|
5231
|
-
});
|
|
5232
|
-
var OPENCODE_CONFORMANCE_FIXTURES = [
|
|
5233
|
-
{
|
|
5234
|
-
// A plain text reply: the held text part seals as the terminal final at idle.
|
|
5235
|
-
name: "clean turn",
|
|
5236
|
-
request: makeRequest2(),
|
|
5237
|
-
nativeStream: [textPart("p1", "Hello there."), idle()],
|
|
5238
|
-
expected: [
|
|
5239
|
-
sessionEvent2(NEW_SESSION_ID),
|
|
5240
|
-
{ type: "text", body: "Hello there.", terminal: true },
|
|
5241
|
-
{ type: "result", ok: true }
|
|
5242
|
-
]
|
|
5243
|
-
},
|
|
5244
|
-
{
|
|
5245
|
-
// Narration + a tool call + result + closing reply. The narration part is
|
|
5246
|
-
// sealed as interim (a tool follows it); the trailing text is the final.
|
|
5247
|
-
name: "tool turn",
|
|
5248
|
-
request: makeRequest2(),
|
|
5249
|
-
nativeStream: [
|
|
5250
|
-
textPart("p1", "Reading your notes."),
|
|
5251
|
-
toolPart("t1", "read", { status: "running", input: { path: "sleep.md" } }, "call_1"),
|
|
5252
|
-
toolPart(
|
|
5253
|
-
"t1",
|
|
5254
|
-
"read",
|
|
5255
|
-
{ status: "completed", input: { path: "sleep.md" }, output: "sleep early" },
|
|
5256
|
-
"call_1"
|
|
5257
|
-
),
|
|
5258
|
-
textPart("p2", "Here's the summary."),
|
|
5259
|
-
idle()
|
|
5260
|
-
],
|
|
5261
|
-
expected: [
|
|
5262
|
-
sessionEvent2(NEW_SESSION_ID),
|
|
5263
|
-
{ type: "text", body: "Reading your notes.", terminal: false },
|
|
5264
|
-
{
|
|
5265
|
-
type: "tool",
|
|
5266
|
-
id: "call_1",
|
|
5267
|
-
name: "read",
|
|
5268
|
-
phase: "start",
|
|
5269
|
-
summary: "sleep.md",
|
|
5270
|
-
input: { path: "sleep.md" }
|
|
5271
|
-
},
|
|
5272
|
-
{
|
|
5273
|
-
type: "tool",
|
|
5274
|
-
id: "call_1",
|
|
5275
|
-
name: "read",
|
|
5276
|
-
phase: "done",
|
|
5277
|
-
summary: "sleep.md",
|
|
5278
|
-
input: { path: "sleep.md" },
|
|
5279
|
-
result: "sleep early"
|
|
5280
|
-
},
|
|
5281
|
-
{ type: "text", body: "Here's the summary.", terminal: true },
|
|
5282
|
-
{ type: "result", ok: true }
|
|
5283
|
-
]
|
|
5284
|
-
},
|
|
5285
|
-
{
|
|
5286
|
-
// A streaming provider's tool input arrives late. opencode re-emits the tool part as a full
|
|
5287
|
-
// snapshot as the args parse: DeepSeek sends `input: {}` on the early `pending`/`running` frame
|
|
5288
|
-
// and the full args on a later one (captured on opencode 1.17: `pending {}` → `running {full}` →
|
|
5289
|
-
// `completed {full}`). `start` fires off the first (empty) frame — the live card — but `done`
|
|
5290
|
-
// (and the row it upserts) must carry the latest non-empty input and its summary. Every other
|
|
5291
|
-
// tool fixture bakes the full input into the first snapshot, so only this one catches it.
|
|
5292
|
-
name: "streaming tool input \u2014 late args refresh the done event (CT301)",
|
|
5293
|
-
request: makeRequest2(),
|
|
5294
|
-
nativeStream: [
|
|
5295
|
-
toolPart("t1", "cabane_list", { status: "pending", input: {} }, "call_1"),
|
|
5296
|
-
toolPart(
|
|
5297
|
-
"t1",
|
|
5298
|
-
"cabane_list",
|
|
5299
|
-
{ status: "running", input: { path: "/product/tasks" } },
|
|
5300
|
-
"call_1"
|
|
5301
|
-
),
|
|
5302
|
-
toolPart(
|
|
5303
|
-
"t1",
|
|
5304
|
-
"cabane_list",
|
|
5305
|
-
{ status: "completed", input: { path: "/product/tasks" }, output: "CT301.md" },
|
|
5306
|
-
"call_1"
|
|
5307
|
-
),
|
|
5308
|
-
idle()
|
|
5309
|
-
],
|
|
5310
|
-
expected: [
|
|
5311
|
-
sessionEvent2(NEW_SESSION_ID),
|
|
5312
|
-
// start: fires off the first (empty) snapshot — empty input + summary, by design.
|
|
5313
|
-
{
|
|
5314
|
-
type: "tool",
|
|
5315
|
-
id: "call_1",
|
|
5316
|
-
name: "cabane_list",
|
|
5317
|
-
phase: "start",
|
|
5318
|
-
summary: "",
|
|
5319
|
-
input: {},
|
|
5320
|
-
mcpServer: "cabane"
|
|
5321
|
-
},
|
|
5322
|
-
// done: carries the full args captured from the later snapshot, and the
|
|
5323
|
-
// summary recomputed from them — NOT the first-seen `{}`.
|
|
5324
|
-
{
|
|
5325
|
-
type: "tool",
|
|
5326
|
-
id: "call_1",
|
|
5327
|
-
name: "cabane_list",
|
|
5328
|
-
phase: "done",
|
|
5329
|
-
summary: "/product/tasks",
|
|
5330
|
-
input: { path: "/product/tasks" },
|
|
5331
|
-
result: "CT301.md",
|
|
5332
|
-
mcpServer: "cabane"
|
|
5333
|
-
},
|
|
5334
|
-
{ type: "result", ok: true }
|
|
5335
|
-
]
|
|
5336
|
-
},
|
|
5337
|
-
{
|
|
5338
|
-
// The turn-control server's `answer` (CT1973). opencode names an MCP tool `<server>_<tool>`,
|
|
5339
|
-
// so the installed keys split the raw id: `cabane_companion_answer` is the turn-control
|
|
5340
|
-
// server's, never the workspace server's `companion_answer`. The pump binds on that.
|
|
5341
|
-
name: "turn-control answer is read as the turn-control server\u2019s, not cabane\u2019s",
|
|
5342
|
-
request: makeRequest2({
|
|
5343
|
-
cabane: {
|
|
5344
|
-
mcpUrl: "https://cabane.test/api/mcp",
|
|
5345
|
-
turnControlUrl: "https://cabane.test/api/turn-control",
|
|
5346
|
-
bearer: "tok",
|
|
5347
|
-
activeConversationId: "conv"
|
|
5348
|
-
}
|
|
5349
|
-
}),
|
|
5350
|
-
nativeStream: [
|
|
5351
|
-
textPart("p1", "On it."),
|
|
5352
|
-
toolPart("t1", "cabane_companion_answer", { status: "running", input: {} }, "call_1"),
|
|
5353
|
-
toolPart(
|
|
5354
|
-
"t1",
|
|
5355
|
-
"cabane_companion_answer",
|
|
5356
|
-
{ status: "completed", input: {}, output: '{"ok":true,"kind":"answer"}' },
|
|
5357
|
-
"call_1"
|
|
5358
|
-
),
|
|
5359
|
-
textPart("p2", "The answer."),
|
|
5360
|
-
idle()
|
|
5361
|
-
],
|
|
5362
|
-
expected: [
|
|
5363
|
-
sessionEvent2(NEW_SESSION_ID),
|
|
5364
|
-
{ type: "text", body: "On it.", terminal: false },
|
|
5365
|
-
{
|
|
5366
|
-
type: "tool",
|
|
5367
|
-
id: "call_1",
|
|
5368
|
-
name: "cabane_companion_answer",
|
|
5369
|
-
phase: "start",
|
|
5370
|
-
summary: "",
|
|
5371
|
-
input: {},
|
|
5372
|
-
mcpServer: "cabane_companion"
|
|
5373
|
-
},
|
|
5374
|
-
{
|
|
5375
|
-
type: "tool",
|
|
5376
|
-
id: "call_1",
|
|
5377
|
-
name: "cabane_companion_answer",
|
|
5378
|
-
phase: "done",
|
|
5379
|
-
summary: "",
|
|
5380
|
-
input: {},
|
|
5381
|
-
result: '{"ok":true,"kind":"answer"}',
|
|
5382
|
-
mcpServer: "cabane_companion"
|
|
5383
|
-
},
|
|
5384
|
-
{ type: "text", body: "The answer.", terminal: true },
|
|
5385
|
-
{ type: "result", ok: true }
|
|
5386
|
-
]
|
|
5387
|
-
},
|
|
5388
|
-
{
|
|
5389
|
-
// Highest risk: cancel mid-stream. The abort lands after the tool result; the adapter stops before
|
|
5390
|
-
// the closing reply — no final text, no `result` event (the server writes the stopped marker).
|
|
5391
|
-
name: "cancel mid-stream",
|
|
5392
|
-
request: makeRequest2(),
|
|
5393
|
-
nativeStream: [
|
|
5394
|
-
textPart("p1", "Working on it."),
|
|
5395
|
-
toolPart("t1", "list", { status: "running", input: {} }, "call_1"),
|
|
5396
|
-
toolPart("t1", "list", { status: "completed", input: {}, output: "ok" }, "call_1"),
|
|
5397
|
-
ABORT_SENTINEL2,
|
|
5398
|
-
textPart("p2", "This reply must never be emitted."),
|
|
5399
|
-
idle()
|
|
5400
|
-
],
|
|
5401
|
-
expected: [
|
|
5402
|
-
sessionEvent2(NEW_SESSION_ID),
|
|
5403
|
-
{ type: "text", body: "Working on it.", terminal: false },
|
|
5404
|
-
{ type: "tool", id: "call_1", name: "list", phase: "start", summary: "", input: {} },
|
|
5405
|
-
{
|
|
5406
|
-
type: "tool",
|
|
5407
|
-
id: "call_1",
|
|
5408
|
-
name: "list",
|
|
5409
|
-
phase: "done",
|
|
5410
|
-
summary: "",
|
|
5411
|
-
input: {},
|
|
5412
|
-
result: "ok"
|
|
5413
|
-
}
|
|
5414
|
-
]
|
|
5415
|
-
},
|
|
5416
|
-
{
|
|
5417
|
-
// An error result: partial narration lands as `progress` (terminal:false),
|
|
5418
|
-
// and the `result` event carries the machine reason.
|
|
5419
|
-
name: "error result",
|
|
5420
|
-
request: makeRequest2(),
|
|
5421
|
-
nativeStream: [textPart("p1", "Partial work."), errored("provider exploded")],
|
|
5422
|
-
expected: [
|
|
5423
|
-
sessionEvent2(NEW_SESSION_ID),
|
|
5424
|
-
{ type: "text", body: "Partial work.", terminal: false },
|
|
5425
|
-
{ type: "runtime_notice", body: "provider exploded" },
|
|
5426
|
-
{ type: "result", ok: false, reason: "error:provider exploded" }
|
|
5427
|
-
]
|
|
5428
|
-
},
|
|
5429
|
-
{
|
|
5430
|
-
// A provider 429 throttle. The wrapped `RateLimitError` is unwrapped by `readSessionError` and
|
|
5431
|
-
// text-classified: a bare "rate limit (429)" with no cap noun is the throttle (`rate_limited`),
|
|
5432
|
-
// not the account cap.
|
|
5433
|
-
name: "provider 429 \u2192 rate_limited (throttle, no reset)",
|
|
5434
|
-
request: makeRequest2(),
|
|
5435
|
-
nativeStream: [
|
|
5436
|
-
{
|
|
5437
|
-
type: "session.error",
|
|
5438
|
-
properties: { error: { name: "RateLimitError", message: "rate limit exceeded (429)" } }
|
|
5439
|
-
}
|
|
5440
|
-
],
|
|
5441
|
-
expected: [
|
|
5442
|
-
sessionEvent2(NEW_SESSION_ID),
|
|
5443
|
-
{ type: "runtime_notice", body: "RateLimitError: rate limit exceeded (429)" },
|
|
5444
|
-
{ type: "result", ok: false, reason: "rate_limited" }
|
|
5445
|
-
]
|
|
5446
|
-
},
|
|
5447
|
-
{
|
|
5448
|
-
// A subscription cap surfaced as text ("usage limit reached") → `usage_capped`. opencode carries
|
|
5449
|
-
// no reset hint, so it's reset-less (the server says "try again later").
|
|
5450
|
-
name: "usage cap text \u2192 usage_capped (no reset)",
|
|
5451
|
-
request: makeRequest2(),
|
|
5452
|
-
nativeStream: [
|
|
5453
|
-
{
|
|
5454
|
-
type: "session.error",
|
|
5455
|
-
properties: { error: { name: "Error", message: "Claude usage limit reached" } }
|
|
5456
|
-
}
|
|
5457
|
-
],
|
|
5458
|
-
expected: [
|
|
5459
|
-
sessionEvent2(NEW_SESSION_ID),
|
|
5460
|
-
{ type: "runtime_notice", body: "Claude usage limit reached" },
|
|
5461
|
-
{ type: "result", ok: false, reason: "usage_capped" }
|
|
5462
|
-
]
|
|
5463
|
-
},
|
|
5464
|
-
{
|
|
5465
|
-
// A transient provider outage (5xx / overload) surfaced as text → `server_error`, never the cap.
|
|
5466
|
-
name: "provider overload text \u2192 server_error",
|
|
5467
|
-
request: makeRequest2(),
|
|
5468
|
-
nativeStream: [
|
|
5469
|
-
{
|
|
5470
|
-
type: "session.error",
|
|
5471
|
-
properties: {
|
|
5472
|
-
error: { name: "Error", message: "AI_APICallError: overloaded_error (529)" }
|
|
5473
|
-
}
|
|
5474
|
-
}
|
|
5475
|
-
],
|
|
5476
|
-
expected: [
|
|
5477
|
-
sessionEvent2(NEW_SESSION_ID),
|
|
5478
|
-
{ type: "runtime_notice", body: "AI_APICallError: overloaded_error (529)" },
|
|
5479
|
-
{ type: "result", ok: false, reason: "server_error" }
|
|
5480
|
-
]
|
|
5481
|
-
},
|
|
5482
|
-
{
|
|
5483
|
-
// An expired or invalid credential classifies as `auth_expired`.
|
|
5484
|
-
name: "auth failure \u2192 auth_expired",
|
|
5485
|
-
request: makeRequest2(),
|
|
5486
|
-
nativeStream: [
|
|
5487
|
-
{
|
|
5488
|
-
type: "session.error",
|
|
5489
|
-
properties: { error: { name: "AuthenticationError", message: "HTTP 401 Unauthorized" } }
|
|
5490
|
-
}
|
|
5491
|
-
],
|
|
5492
|
-
expected: [
|
|
5493
|
-
sessionEvent2(NEW_SESSION_ID),
|
|
5494
|
-
{ type: "runtime_notice", body: "AuthenticationError: HTTP 401 Unauthorized" },
|
|
5495
|
-
{ type: "result", ok: false, reason: "auth_expired" }
|
|
5496
|
-
]
|
|
5497
|
-
},
|
|
5498
|
-
{
|
|
5499
|
-
// An unclassified failure stays informative: its provider text is visible, and the raw machine
|
|
5500
|
-
// reason is unchanged.
|
|
5501
|
-
name: "unclassified failure keeps a runtime notice and raw reason",
|
|
5502
|
-
request: makeRequest2(),
|
|
5503
|
-
nativeStream: [errored("something local broke")],
|
|
5504
|
-
expected: [
|
|
5505
|
-
sessionEvent2(NEW_SESSION_ID),
|
|
5506
|
-
{ type: "runtime_notice", body: "something local broke" },
|
|
5507
|
-
{ type: "result", ok: false, reason: "error:something local broke" }
|
|
5508
|
-
]
|
|
5509
|
-
},
|
|
5510
|
-
{
|
|
5511
|
-
name: "runtime notice is trimmed and bounded to 200 characters",
|
|
5512
|
-
request: makeRequest2(),
|
|
5513
|
-
nativeStream: [errored(` ${BOUNDED_NOTICE}${"y".repeat(20)} `)],
|
|
5514
|
-
expected: [
|
|
5515
|
-
sessionEvent2(NEW_SESSION_ID),
|
|
5516
|
-
{ type: "runtime_notice", body: BOUNDED_NOTICE },
|
|
5517
|
-
{ type: "result", ok: false, reason: "usage_capped" }
|
|
5518
|
-
]
|
|
5519
|
-
},
|
|
5520
|
-
{
|
|
5521
|
-
// Empty, whitespace-only and bare status-token failures still set the machine reason but add no
|
|
5522
|
-
// empty or unhelpful transcript notice.
|
|
5523
|
-
name: "empty failure text emits no runtime notice",
|
|
5524
|
-
request: makeRequest2(),
|
|
5525
|
-
nativeStream: [errored("")],
|
|
5526
|
-
expected: [
|
|
5527
|
-
sessionEvent2(NEW_SESSION_ID),
|
|
5528
|
-
{ type: "result", ok: false, reason: "error:unknown" }
|
|
5529
|
-
]
|
|
5530
|
-
},
|
|
5531
|
-
{
|
|
5532
|
-
name: "whitespace-only failure text emits no runtime notice",
|
|
5533
|
-
request: makeRequest2(),
|
|
5534
|
-
nativeStream: [errored(" ")],
|
|
5535
|
-
expected: [sessionEvent2(NEW_SESSION_ID), { type: "result", ok: false, reason: "error: " }]
|
|
5536
|
-
},
|
|
5537
|
-
{
|
|
5538
|
-
name: "bare status token emits no runtime notice",
|
|
5539
|
-
request: makeRequest2(),
|
|
5540
|
-
nativeStream: [errored("429")],
|
|
5541
|
-
expected: [sessionEvent2(NEW_SESSION_ID), { type: "result", ok: false, reason: "rate_limited" }]
|
|
5542
|
-
},
|
|
5543
|
-
{
|
|
5544
|
-
name: "single-word provider sentence emits a runtime notice",
|
|
5545
|
-
request: makeRequest2(),
|
|
5546
|
-
nativeStream: [errored("Unavailable.")],
|
|
5547
|
-
expected: [
|
|
5548
|
-
sessionEvent2(NEW_SESSION_ID),
|
|
5549
|
-
{ type: "runtime_notice", body: "Unavailable." },
|
|
5550
|
-
{ type: "result", ok: false, reason: "error:Unavailable." }
|
|
5551
|
-
]
|
|
5552
|
-
},
|
|
5553
|
-
{
|
|
5554
|
-
// The zero-content success the claude-code suite pins, in opencode's vocabulary — the session
|
|
5555
|
-
// goes idle having emitted no part. No opencode sample has been seen; the fixture exists because
|
|
5556
|
-
// the host's classification is runtime-neutral, so a runtime that can produce this envelope must
|
|
5557
|
-
// be shown producing it in the shape the classifier reads.
|
|
5558
|
-
name: "zero-content success (empty runtime result)",
|
|
5559
|
-
request: makeRequest2(),
|
|
5560
|
-
nativeStream: [idle()],
|
|
5561
|
-
expected: [sessionEvent2(NEW_SESSION_ID), { type: "result", ok: true }]
|
|
5562
|
-
},
|
|
5563
|
-
{
|
|
5564
|
-
// Empty-final: a clean turn that ended on a tool call with no closing text.
|
|
5565
|
-
// The adapter emits NO final text — empty-final promotion is host/pump
|
|
5566
|
-
// territory (this fixture pins that boundary).
|
|
5567
|
-
name: "empty-final (no closing text)",
|
|
5568
|
-
request: makeRequest2(),
|
|
5569
|
-
nativeStream: [
|
|
5570
|
-
toolPart("t1", "write", { status: "running", input: { path: "a.md" } }, "call_1"),
|
|
5571
|
-
toolPart("t1", "write", { status: "completed", input: { path: "a.md" } }, "call_1"),
|
|
5572
|
-
idle()
|
|
5573
|
-
],
|
|
5574
|
-
expected: [
|
|
5575
|
-
sessionEvent2(NEW_SESSION_ID),
|
|
5576
|
-
{
|
|
5577
|
-
type: "tool",
|
|
5578
|
-
id: "call_1",
|
|
5579
|
-
name: "write",
|
|
5580
|
-
phase: "start",
|
|
5581
|
-
summary: "a.md",
|
|
5582
|
-
input: { path: "a.md" }
|
|
5583
|
-
},
|
|
5584
|
-
{
|
|
5585
|
-
type: "tool",
|
|
5586
|
-
id: "call_1",
|
|
5587
|
-
name: "write",
|
|
5588
|
-
phase: "done",
|
|
5589
|
-
summary: "a.md",
|
|
5590
|
-
input: { path: "a.md" },
|
|
5591
|
-
result: void 0
|
|
5592
|
-
},
|
|
5593
|
-
{ type: "result", ok: true }
|
|
5594
|
-
]
|
|
5595
|
-
},
|
|
5596
|
-
{
|
|
5597
|
-
// Thinking: a reasoning part seals as a transient `thinking` when the reply
|
|
5598
|
-
// text supersedes it; the text is the final.
|
|
5599
|
-
name: "thinking then reply",
|
|
5600
|
-
request: makeRequest2(),
|
|
5601
|
-
nativeStream: [
|
|
5602
|
-
reasoningPart("r1", "Let me check the notes."),
|
|
5603
|
-
textPart("p1", "All set."),
|
|
5604
|
-
idle()
|
|
5605
|
-
],
|
|
5606
|
-
expected: [
|
|
5607
|
-
sessionEvent2(NEW_SESSION_ID),
|
|
5608
|
-
{ type: "thinking", text: "Let me check the notes." },
|
|
5609
|
-
{ type: "text", body: "All set.", terminal: true },
|
|
5610
|
-
{ type: "result", ok: true }
|
|
5611
|
-
]
|
|
5612
|
-
},
|
|
5613
|
-
{
|
|
5614
|
-
// The stored state matches the directory, so the adapter resumes its id and the transport reports
|
|
5615
|
-
// `created:false` on the same id — no session event fires. Resume is what carries cross-turn
|
|
5616
|
-
// memory.
|
|
5617
|
-
name: "session resume (no re-emit on echo)",
|
|
5618
|
-
request: makeRequest2({ session: encodeSession2({ sessionId: "s1", directory: DIR }) }),
|
|
5619
|
-
nativeStream: [textPart("p1", "Back again."), idle()],
|
|
5620
|
-
expected: [
|
|
5621
|
-
{ type: "text", body: "Back again.", terminal: true },
|
|
5622
|
-
{ type: "result", ok: true }
|
|
5623
|
-
]
|
|
5624
|
-
},
|
|
5625
|
-
{
|
|
5626
|
-
// On resume, as on a fresh turn, the decoder learns the user-message id from the stream:
|
|
5627
|
-
// `message.updated` user frame precedes the echoed part — and drops the echo, so the injected
|
|
5628
|
-
// per-turn context can't leak into Steps. Only the reply surfaces; no session event.
|
|
5629
|
-
name: "session resume \u2014 prompt echo filtered via stream-derived user id",
|
|
5630
|
-
request: makeRequest2({ session: encodeSession2({ sessionId: "s1", directory: DIR }) }),
|
|
5631
|
-
nativeStream: [
|
|
5632
|
-
messageUpdated("user", "msg_u1"),
|
|
5633
|
-
echoedUserPart("up1", "msg_u1", "INJECTED CONTEXT \u2014 must not leak"),
|
|
5634
|
-
messageUpdated("assistant", "msg_a1"),
|
|
5635
|
-
echoedUserPart("ap1", "msg_a1", "The real reply."),
|
|
5636
|
-
idle()
|
|
5637
|
-
],
|
|
5638
|
-
expected: [
|
|
5639
|
-
{ type: "text", body: "The real reply.", terminal: true },
|
|
5640
|
-
{ type: "result", ok: true }
|
|
5641
|
-
]
|
|
5642
|
-
},
|
|
5643
|
-
{
|
|
5644
|
-
// Directory-mismatch degrade: the stored state was created under another directory, so the
|
|
5645
|
-
// adapter starts fresh — the transport creates a new session, and the session event records the
|
|
5646
|
-
// current directory.
|
|
5647
|
-
name: "session directory-mismatch degrades to fresh",
|
|
5648
|
-
request: makeRequest2({ session: encodeSession2({ sessionId: "old", directory: "/env/gone" }) }),
|
|
5649
|
-
nativeStream: [textPart("p1", "Fresh start."), idle()],
|
|
5650
|
-
expected: [
|
|
5651
|
-
// The degrade is flagged on the session event so the host relays it on settle and the server
|
|
5652
|
-
// rewinds the catch-up mark, recovering context the discarded session dropped.
|
|
5653
|
-
sessionEvent2(NEW_SESSION_ID, DIR, true),
|
|
5654
|
-
{ type: "text", body: "Fresh start.", terminal: true },
|
|
5655
|
-
{ type: "result", ok: true }
|
|
5656
|
-
]
|
|
5657
|
-
}
|
|
5658
|
-
];
|
|
4391
|
+
}
|
|
4392
|
+
let degraded = "fresh" in decision && decision.reason !== void 0 && decision.reason !== "no_session";
|
|
4393
|
+
if ("fresh" in decision && decision.reason !== void 0 && decision.reason !== "no_session" && hooks?.fresh) {
|
|
4394
|
+
const recomposed = await hooks.fresh(decision.reason);
|
|
4395
|
+
if (signal.aborted) return;
|
|
4396
|
+
if (recomposed) {
|
|
4397
|
+
req = { ...req, ...recomposed, session: null };
|
|
4398
|
+
degraded = false;
|
|
4399
|
+
}
|
|
4400
|
+
}
|
|
4401
|
+
const spec = buildRunSpec(req, resume);
|
|
4402
|
+
const result = await transport.run(spec, signal);
|
|
4403
|
+
if (signal.aborted) return;
|
|
4404
|
+
if (result.created || result.sessionId !== resume) {
|
|
4405
|
+
yield {
|
|
4406
|
+
type: "session",
|
|
4407
|
+
state: encodeSession2({ sessionId: result.sessionId, directory: result.directory }),
|
|
4408
|
+
...degraded ? { degraded: true } : {}
|
|
4409
|
+
};
|
|
4410
|
+
}
|
|
4411
|
+
yield* decodeOpencodeStream(result.events, {
|
|
4412
|
+
signal,
|
|
4413
|
+
sessionId: result.sessionId,
|
|
4414
|
+
// The keys the turn's servers were installed under: what the decoder
|
|
4415
|
+
// splits a raw `<server>_<tool>` id on.
|
|
4416
|
+
mcpServers: Object.keys(spec.mcp)
|
|
4417
|
+
});
|
|
4418
|
+
}
|
|
4419
|
+
};
|
|
4420
|
+
}
|
|
4421
|
+
var opencodeAdapter = createOpencodeAdapter();
|
|
5659
4422
|
|
|
5660
4423
|
// packages/agent-runtime/src/codex/addendum.ts
|
|
5661
4424
|
var CODEX_ADDENDUM = [
|
|
@@ -6068,13 +4831,13 @@ function sealHeld2(held, terminal) {
|
|
|
6068
4831
|
}
|
|
6069
4832
|
|
|
6070
4833
|
// packages/agent-runtime/src/codex/policy.ts
|
|
6071
|
-
import { z as
|
|
4834
|
+
import { z as z11 } from "zod";
|
|
6072
4835
|
function codexSandboxMode(local) {
|
|
6073
4836
|
return local.codex?.configSetsSandboxMode ? null : "danger-full-access";
|
|
6074
4837
|
}
|
|
6075
4838
|
var CODEX_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max", "ultra"];
|
|
6076
|
-
var codexDialectSchema =
|
|
6077
|
-
modelReasoningEffort:
|
|
4839
|
+
var codexDialectSchema = z11.object({
|
|
4840
|
+
modelReasoningEffort: z11.enum(CODEX_REASONING_EFFORTS).optional()
|
|
6078
4841
|
}).loose();
|
|
6079
4842
|
function readCodexDialect(runtimeOptions) {
|
|
6080
4843
|
const parsed = codexDialectSchema.safeParse(runtimeOptions?.["codex"] ?? {});
|
|
@@ -6377,7 +5140,7 @@ function createCodexAdapter(deps = {}) {
|
|
|
6377
5140
|
let threadUsageBaseline = "resume" in decision ? decision.usageBaseline : null;
|
|
6378
5141
|
if ("fresh" in decision && decision.reason === "cwd_mismatch") {
|
|
6379
5142
|
deps.onWarn?.(
|
|
6380
|
-
"codex adapter: stored session directory no longer matches the current environment \u2014 starting a fresh thread
|
|
5143
|
+
"codex adapter: stored session directory no longer matches the current environment \u2014 starting a fresh thread",
|
|
6381
5144
|
{ directory }
|
|
6382
5145
|
);
|
|
6383
5146
|
}
|
|
@@ -6422,7 +5185,7 @@ function createCodexAdapter(deps = {}) {
|
|
|
6422
5185
|
if (imageUrls.length > 0) {
|
|
6423
5186
|
if (!deps.prepareImages) {
|
|
6424
5187
|
deps.onWarn?.(
|
|
6425
|
-
"codex adapter: no image preparer wired on this host \u2014 the turn runs without its images
|
|
5188
|
+
"codex adapter: no image preparer wired on this host \u2014 the turn runs without its images",
|
|
6426
5189
|
{ images: imageUrls.length }
|
|
6427
5190
|
);
|
|
6428
5191
|
} else {
|
|
@@ -6431,7 +5194,7 @@ function createCodexAdapter(deps = {}) {
|
|
|
6431
5194
|
} catch (err) {
|
|
6432
5195
|
if (!signal.aborted) {
|
|
6433
5196
|
deps.onWarn?.(
|
|
6434
|
-
"codex adapter: preparing the images failed \u2014 the turn runs without them
|
|
5197
|
+
"codex adapter: preparing the images failed \u2014 the turn runs without them",
|
|
6435
5198
|
{ err: err instanceof Error ? err.message : String(err) }
|
|
6436
5199
|
);
|
|
6437
5200
|
}
|
|
@@ -6507,7 +5270,7 @@ function createCodexAdapter(deps = {}) {
|
|
|
6507
5270
|
try {
|
|
6508
5271
|
await images.cleanup();
|
|
6509
5272
|
} catch (err) {
|
|
6510
|
-
deps.onWarn?.("codex adapter: image cleanup failed
|
|
5273
|
+
deps.onWarn?.("codex adapter: image cleanup failed", {
|
|
6511
5274
|
err: err instanceof Error ? err.message : String(err)
|
|
6512
5275
|
});
|
|
6513
5276
|
}
|
|
@@ -6518,993 +5281,6 @@ function createCodexAdapter(deps = {}) {
|
|
|
6518
5281
|
}
|
|
6519
5282
|
var codexAdapter = createCodexAdapter();
|
|
6520
5283
|
|
|
6521
|
-
// packages/agent-runtime/src/codex/conformance.ts
|
|
6522
|
-
var ABORT_SENTINEL3 = { __abortHere: true };
|
|
6523
|
-
var NEW_THREAD_ID = "th_new";
|
|
6524
|
-
var BOUNDED_NOTICE2 = "You've hit your usage limit \xB7 ".padEnd(200, "x");
|
|
6525
|
-
var DIR2 = "/env/here";
|
|
6526
|
-
var PROMPT = "system";
|
|
6527
|
-
function makeRequest3(overrides2 = {}) {
|
|
6528
|
-
return {
|
|
6529
|
-
systemPrompt: PROMPT,
|
|
6530
|
-
prompt: "hi there",
|
|
6531
|
-
content: [{ type: "text", text: "hi there" }],
|
|
6532
|
-
// The resolved model and reasoning effort ride the terminal `result` off the run spec's model.
|
|
6533
|
-
// Behavioural fixtures use the model-less token (no `resolvedModel` emitted); a dedicated capture
|
|
6534
|
-
// fixture sets a real model and effort.
|
|
6535
|
-
config: { model: null },
|
|
6536
|
-
session: null,
|
|
6537
|
-
cabane: { mcpUrl: "https://cabane.test/api/mcp", bearer: "tok", activeConversationId: "conv" },
|
|
6538
|
-
local: { cwd: DIR2 },
|
|
6539
|
-
extra: { mcpServers: {} },
|
|
6540
|
-
...overrides2
|
|
6541
|
-
};
|
|
6542
|
-
}
|
|
6543
|
-
var threadStarted = (threadId) => ({
|
|
6544
|
-
type: "thread.started",
|
|
6545
|
-
thread_id: threadId
|
|
6546
|
-
});
|
|
6547
|
-
var agentMessage = (id, text) => ({
|
|
6548
|
-
type: "item.completed",
|
|
6549
|
-
item: { id, type: "agent_message", text }
|
|
6550
|
-
});
|
|
6551
|
-
var reasoning = (id, text) => ({
|
|
6552
|
-
type: "item.completed",
|
|
6553
|
-
item: { id, type: "reasoning", text }
|
|
6554
|
-
});
|
|
6555
|
-
var toolFrame = (evType, item) => ({ type: evType, item });
|
|
6556
|
-
var turnCompleted = (usage) => ({
|
|
6557
|
-
type: "turn.completed",
|
|
6558
|
-
...usage ? { usage } : {}
|
|
6559
|
-
});
|
|
6560
|
-
var turnFailed = (message) => ({
|
|
6561
|
-
type: "turn.failed",
|
|
6562
|
-
error: { message }
|
|
6563
|
-
});
|
|
6564
|
-
var fatalError = (message) => ({ type: "error", message });
|
|
6565
|
-
var errorItem = (id, message) => ({
|
|
6566
|
-
type: "item.completed",
|
|
6567
|
-
item: { id, type: "error", message }
|
|
6568
|
-
});
|
|
6569
|
-
var OP_SUCCESS = { ok: true, result: { source: "native" } };
|
|
6570
|
-
var OP_FAILURE = {
|
|
6571
|
-
ok: false,
|
|
6572
|
-
error: {
|
|
6573
|
-
kind: "output_too_large",
|
|
6574
|
-
code: "output_too_large",
|
|
6575
|
-
message: "files.read returned more than the result ceiling"
|
|
6576
|
-
}
|
|
6577
|
-
};
|
|
6578
|
-
var startedSession = (threadId, cwd = DIR2, degraded = false) => ({
|
|
6579
|
-
type: "session",
|
|
6580
|
-
state: encodeSession3({ threadId, cwd, promptFingerprint: null, usageBaseline: null }),
|
|
6581
|
-
...degraded ? { degraded: true } : {}
|
|
6582
|
-
});
|
|
6583
|
-
var closingSession = (threadId, usageBaseline = null) => ({
|
|
6584
|
-
type: "session",
|
|
6585
|
-
state: encodeSession3({
|
|
6586
|
-
threadId,
|
|
6587
|
-
cwd: DIR2,
|
|
6588
|
-
promptFingerprint: fingerprintPrompt(PROMPT),
|
|
6589
|
-
usageBaseline,
|
|
6590
|
-
compactions: 0
|
|
6591
|
-
})
|
|
6592
|
-
});
|
|
6593
|
-
var CODEX_CONFORMANCE_FIXTURES = [
|
|
6594
|
-
{
|
|
6595
|
-
// A plain text reply: the held agent message seals as the terminal final at `turn.completed`;
|
|
6596
|
-
// usage rides the `result`.
|
|
6597
|
-
//
|
|
6598
|
-
// The counters nest — `cached_input_tokens` is a slice of `input_tokens`,
|
|
6599
|
-
// `reasoning_output_tokens` a slice of `output_tokens` — so input is 100 (not 120) and output 50
|
|
6600
|
-
// (not 60), with the cache read broken out beside them.
|
|
6601
|
-
name: "clean turn",
|
|
6602
|
-
request: makeRequest3(),
|
|
6603
|
-
nativeStream: [
|
|
6604
|
-
threadStarted(NEW_THREAD_ID),
|
|
6605
|
-
agentMessage("m1", "Hello there."),
|
|
6606
|
-
turnCompleted({
|
|
6607
|
-
input_tokens: 100,
|
|
6608
|
-
cached_input_tokens: 20,
|
|
6609
|
-
cache_write_input_tokens: 5,
|
|
6610
|
-
output_tokens: 50,
|
|
6611
|
-
reasoning_output_tokens: 10
|
|
6612
|
-
})
|
|
6613
|
-
],
|
|
6614
|
-
expected: [
|
|
6615
|
-
startedSession(NEW_THREAD_ID),
|
|
6616
|
-
{ type: "text", body: "Hello there.", terminal: true },
|
|
6617
|
-
// The closing session write — the thread's ending totals, recorded as the baseline the next turn
|
|
6618
|
-
// subtracts. A fresh thread, so the `result` usage below is the frame verbatim.
|
|
6619
|
-
closingSession(NEW_THREAD_ID, {
|
|
6620
|
-
inputTokens: 100,
|
|
6621
|
-
outputTokens: 50,
|
|
6622
|
-
cacheReadTokens: 20,
|
|
6623
|
-
cacheCreationTokens: 5
|
|
6624
|
-
}),
|
|
6625
|
-
{
|
|
6626
|
-
type: "result",
|
|
6627
|
-
ok: true,
|
|
6628
|
-
usage: {
|
|
6629
|
-
inputTokens: 100,
|
|
6630
|
-
outputTokens: 50,
|
|
6631
|
-
cacheReadTokens: 20,
|
|
6632
|
-
cacheCreationTokens: 5
|
|
6633
|
-
}
|
|
6634
|
-
}
|
|
6635
|
-
]
|
|
6636
|
-
},
|
|
6637
|
-
{
|
|
6638
|
-
// Narration + an MCP tool call + result + closing reply. The narration message
|
|
6639
|
-
// is sealed as interim (a tool follows it); the trailing message is the final.
|
|
6640
|
-
name: "tool turn",
|
|
6641
|
-
request: makeRequest3(),
|
|
6642
|
-
nativeStream: [
|
|
6643
|
-
threadStarted(NEW_THREAD_ID),
|
|
6644
|
-
agentMessage("m1", "Reading your notes."),
|
|
6645
|
-
toolFrame("item.started", {
|
|
6646
|
-
id: "t1",
|
|
6647
|
-
type: "mcp_tool_call",
|
|
6648
|
-
server: "cabane",
|
|
6649
|
-
tool: "read",
|
|
6650
|
-
arguments: { path: "sleep.md" },
|
|
6651
|
-
status: "in_progress"
|
|
6652
|
-
}),
|
|
6653
|
-
toolFrame("item.completed", {
|
|
6654
|
-
id: "t1",
|
|
6655
|
-
type: "mcp_tool_call",
|
|
6656
|
-
server: "cabane",
|
|
6657
|
-
tool: "read",
|
|
6658
|
-
arguments: { path: "sleep.md" },
|
|
6659
|
-
status: "completed",
|
|
6660
|
-
result: { content: [{ type: "text", text: "sleep early" }] }
|
|
6661
|
-
}),
|
|
6662
|
-
agentMessage("m2", "Here's the summary."),
|
|
6663
|
-
turnCompleted()
|
|
6664
|
-
],
|
|
6665
|
-
expected: [
|
|
6666
|
-
startedSession(NEW_THREAD_ID),
|
|
6667
|
-
{ type: "text", body: "Reading your notes.", terminal: false },
|
|
6668
|
-
{
|
|
6669
|
-
type: "tool",
|
|
6670
|
-
id: "t1",
|
|
6671
|
-
name: "read",
|
|
6672
|
-
mcpServer: "cabane",
|
|
6673
|
-
phase: "start",
|
|
6674
|
-
summary: "sleep.md",
|
|
6675
|
-
input: { path: "sleep.md" }
|
|
6676
|
-
},
|
|
6677
|
-
{
|
|
6678
|
-
type: "tool",
|
|
6679
|
-
id: "t1",
|
|
6680
|
-
name: "read",
|
|
6681
|
-
mcpServer: "cabane",
|
|
6682
|
-
phase: "done",
|
|
6683
|
-
summary: "sleep.md",
|
|
6684
|
-
input: { path: "sleep.md" },
|
|
6685
|
-
result: [{ type: "text", text: "sleep early" }]
|
|
6686
|
-
},
|
|
6687
|
-
{ type: "text", body: "Here's the summary.", terminal: true },
|
|
6688
|
-
closingSession(NEW_THREAD_ID),
|
|
6689
|
-
{ type: "result", ok: true }
|
|
6690
|
-
]
|
|
6691
|
-
},
|
|
6692
|
-
{
|
|
6693
|
-
// The recorded MCP success shape: Codex exposes the text copy and the native object together;
|
|
6694
|
-
// normalization must keep the object.
|
|
6695
|
-
name: "MCP success prefers native structured content",
|
|
6696
|
-
request: makeRequest3(),
|
|
6697
|
-
nativeStream: [
|
|
6698
|
-
threadStarted(NEW_THREAD_ID),
|
|
6699
|
-
toolFrame("item.started", {
|
|
6700
|
-
id: "query-success",
|
|
6701
|
-
type: "mcp_tool_call",
|
|
6702
|
-
server: "cabane",
|
|
6703
|
-
tool: "query",
|
|
6704
|
-
arguments: { op: "files.read", args: { path: "/ABOUT.md" } },
|
|
6705
|
-
status: "in_progress"
|
|
6706
|
-
}),
|
|
6707
|
-
toolFrame("item.completed", {
|
|
6708
|
-
id: "query-success",
|
|
6709
|
-
type: "mcp_tool_call",
|
|
6710
|
-
server: "cabane",
|
|
6711
|
-
tool: "query",
|
|
6712
|
-
arguments: { op: "files.read", args: { path: "/ABOUT.md" } },
|
|
6713
|
-
status: "completed",
|
|
6714
|
-
result: {
|
|
6715
|
-
content: [{ type: "text", text: JSON.stringify(OP_SUCCESS) }],
|
|
6716
|
-
structured_content: OP_SUCCESS
|
|
6717
|
-
}
|
|
6718
|
-
}),
|
|
6719
|
-
turnCompleted()
|
|
6720
|
-
],
|
|
6721
|
-
expected: [
|
|
6722
|
-
startedSession(NEW_THREAD_ID),
|
|
6723
|
-
{
|
|
6724
|
-
type: "tool",
|
|
6725
|
-
id: "query-success",
|
|
6726
|
-
name: "query",
|
|
6727
|
-
mcpServer: "cabane",
|
|
6728
|
-
phase: "start",
|
|
6729
|
-
summary: "",
|
|
6730
|
-
input: { op: "files.read", args: { path: "/ABOUT.md" } }
|
|
6731
|
-
},
|
|
6732
|
-
{
|
|
6733
|
-
type: "tool",
|
|
6734
|
-
id: "query-success",
|
|
6735
|
-
name: "query",
|
|
6736
|
-
mcpServer: "cabane",
|
|
6737
|
-
phase: "done",
|
|
6738
|
-
summary: "",
|
|
6739
|
-
input: { op: "files.read", args: { path: "/ABOUT.md" } },
|
|
6740
|
-
result: OP_SUCCESS
|
|
6741
|
-
},
|
|
6742
|
-
closingSession(NEW_THREAD_ID),
|
|
6743
|
-
{ type: "result", ok: true }
|
|
6744
|
-
]
|
|
6745
|
-
},
|
|
6746
|
-
{
|
|
6747
|
-
// MCP `isError` keeps the failed phase, while the raw camel-case structured payload survives
|
|
6748
|
-
// instead of the generic `error` token.
|
|
6749
|
-
name: "failed MCP call preserves structured receipt while remaining error phase",
|
|
6750
|
-
request: makeRequest3(),
|
|
6751
|
-
nativeStream: [
|
|
6752
|
-
threadStarted(NEW_THREAD_ID),
|
|
6753
|
-
toolFrame("item.started", {
|
|
6754
|
-
id: "query-failed",
|
|
6755
|
-
type: "mcp_tool_call",
|
|
6756
|
-
server: "cabane",
|
|
6757
|
-
tool: "query",
|
|
6758
|
-
arguments: { op: "files.read", args: { path: "/ABOUT.md" } },
|
|
6759
|
-
status: "in_progress"
|
|
6760
|
-
}),
|
|
6761
|
-
toolFrame("item.completed", {
|
|
6762
|
-
id: "query-failed",
|
|
6763
|
-
type: "mcp_tool_call",
|
|
6764
|
-
server: "cabane",
|
|
6765
|
-
tool: "query",
|
|
6766
|
-
arguments: { op: "files.read", args: { path: "/ABOUT.md" } },
|
|
6767
|
-
status: "failed",
|
|
6768
|
-
result: {
|
|
6769
|
-
content: [{ type: "text", text: JSON.stringify(OP_FAILURE) }],
|
|
6770
|
-
structuredContent: OP_FAILURE,
|
|
6771
|
-
isError: true
|
|
6772
|
-
},
|
|
6773
|
-
error: { message: "error" }
|
|
6774
|
-
}),
|
|
6775
|
-
turnCompleted()
|
|
6776
|
-
],
|
|
6777
|
-
expected: [
|
|
6778
|
-
startedSession(NEW_THREAD_ID),
|
|
6779
|
-
{
|
|
6780
|
-
type: "tool",
|
|
6781
|
-
id: "query-failed",
|
|
6782
|
-
name: "query",
|
|
6783
|
-
mcpServer: "cabane",
|
|
6784
|
-
phase: "start",
|
|
6785
|
-
summary: "",
|
|
6786
|
-
input: { op: "files.read", args: { path: "/ABOUT.md" } }
|
|
6787
|
-
},
|
|
6788
|
-
{
|
|
6789
|
-
type: "tool",
|
|
6790
|
-
id: "query-failed",
|
|
6791
|
-
name: "query",
|
|
6792
|
-
mcpServer: "cabane",
|
|
6793
|
-
phase: "error",
|
|
6794
|
-
summary: "",
|
|
6795
|
-
input: { op: "files.read", args: { path: "/ABOUT.md" } },
|
|
6796
|
-
result: OP_FAILURE
|
|
6797
|
-
},
|
|
6798
|
-
closingSession(NEW_THREAD_ID),
|
|
6799
|
-
{ type: "result", ok: true }
|
|
6800
|
-
]
|
|
6801
|
-
},
|
|
6802
|
-
{
|
|
6803
|
-
// The turn-control server's `answer` (CT1973): the tool event carries the server Codex names
|
|
6804
|
-
// beside the tool, which is what the pump binds the declaration to. A non-cabane server keeps
|
|
6805
|
-
// its prefix on the card as before.
|
|
6806
|
-
name: "turn-control answer carries its server",
|
|
6807
|
-
request: makeRequest3(),
|
|
6808
|
-
nativeStream: [
|
|
6809
|
-
threadStarted(NEW_THREAD_ID),
|
|
6810
|
-
agentMessage("m1", "On it."),
|
|
6811
|
-
toolFrame("item.completed", {
|
|
6812
|
-
id: "answer-1",
|
|
6813
|
-
type: "mcp_tool_call",
|
|
6814
|
-
server: "cabane_companion",
|
|
6815
|
-
tool: "answer",
|
|
6816
|
-
arguments: {},
|
|
6817
|
-
status: "completed",
|
|
6818
|
-
result: { content: [{ type: "text", text: '{"ok":true,"kind":"answer"}' }] }
|
|
6819
|
-
}),
|
|
6820
|
-
agentMessage("m2", "The answer."),
|
|
6821
|
-
turnCompleted()
|
|
6822
|
-
],
|
|
6823
|
-
expected: [
|
|
6824
|
-
startedSession(NEW_THREAD_ID),
|
|
6825
|
-
{ type: "text", body: "On it.", terminal: false },
|
|
6826
|
-
{
|
|
6827
|
-
type: "tool",
|
|
6828
|
-
id: "answer-1",
|
|
6829
|
-
name: "cabane_companion.answer",
|
|
6830
|
-
mcpServer: "cabane_companion",
|
|
6831
|
-
phase: "start",
|
|
6832
|
-
summary: "",
|
|
6833
|
-
input: {}
|
|
6834
|
-
},
|
|
6835
|
-
{
|
|
6836
|
-
type: "tool",
|
|
6837
|
-
id: "answer-1",
|
|
6838
|
-
name: "cabane_companion.answer",
|
|
6839
|
-
mcpServer: "cabane_companion",
|
|
6840
|
-
phase: "done",
|
|
6841
|
-
summary: "",
|
|
6842
|
-
input: {},
|
|
6843
|
-
result: [{ type: "text", text: '{"ok":true,"kind":"answer"}' }]
|
|
6844
|
-
},
|
|
6845
|
-
{ type: "text", body: "The answer.", terminal: true },
|
|
6846
|
-
closingSession(NEW_THREAD_ID),
|
|
6847
|
-
{ type: "result", ok: true }
|
|
6848
|
-
]
|
|
6849
|
-
},
|
|
6850
|
-
{
|
|
6851
|
-
// A transport/tool failure with no MCP result payload still uses the real adapter error message,
|
|
6852
|
-
// never `undefined`.
|
|
6853
|
-
name: "failed MCP call without result keeps error message fallback",
|
|
6854
|
-
request: makeRequest3(),
|
|
6855
|
-
nativeStream: [
|
|
6856
|
-
threadStarted(NEW_THREAD_ID),
|
|
6857
|
-
toolFrame("item.completed", {
|
|
6858
|
-
id: "mcp-failed",
|
|
6859
|
-
type: "mcp_tool_call",
|
|
6860
|
-
server: "other",
|
|
6861
|
-
tool: "lookup",
|
|
6862
|
-
arguments: { q: "receipt" },
|
|
6863
|
-
status: "failed",
|
|
6864
|
-
result: { content: [], isError: true },
|
|
6865
|
-
error: { message: "connection closed" }
|
|
6866
|
-
}),
|
|
6867
|
-
turnCompleted()
|
|
6868
|
-
],
|
|
6869
|
-
expected: [
|
|
6870
|
-
startedSession(NEW_THREAD_ID),
|
|
6871
|
-
{
|
|
6872
|
-
type: "tool",
|
|
6873
|
-
id: "mcp-failed",
|
|
6874
|
-
name: "other.lookup",
|
|
6875
|
-
mcpServer: "other",
|
|
6876
|
-
phase: "start",
|
|
6877
|
-
summary: "receipt",
|
|
6878
|
-
input: { q: "receipt" }
|
|
6879
|
-
},
|
|
6880
|
-
{
|
|
6881
|
-
type: "tool",
|
|
6882
|
-
id: "mcp-failed",
|
|
6883
|
-
name: "other.lookup",
|
|
6884
|
-
mcpServer: "other",
|
|
6885
|
-
phase: "error",
|
|
6886
|
-
summary: "receipt",
|
|
6887
|
-
input: { q: "receipt" },
|
|
6888
|
-
result: "connection closed"
|
|
6889
|
-
},
|
|
6890
|
-
closingSession(NEW_THREAD_ID),
|
|
6891
|
-
{ type: "result", ok: true }
|
|
6892
|
-
]
|
|
6893
|
-
},
|
|
6894
|
-
{
|
|
6895
|
-
// A connector's structured metadata beside an image-only MCP response: no image/base64 payload
|
|
6896
|
-
// enters activity receipts.
|
|
6897
|
-
name: "image-only MCP result remains empty despite structured metadata",
|
|
6898
|
-
request: makeRequest3(),
|
|
6899
|
-
nativeStream: [
|
|
6900
|
-
threadStarted(NEW_THREAD_ID),
|
|
6901
|
-
toolFrame("item.completed", {
|
|
6902
|
-
id: "mcp-image",
|
|
6903
|
-
type: "mcp_tool_call",
|
|
6904
|
-
server: "other",
|
|
6905
|
-
tool: "render",
|
|
6906
|
-
arguments: {},
|
|
6907
|
-
status: "completed",
|
|
6908
|
-
result: {
|
|
6909
|
-
content: [{ type: "image", data: "base64-bytes", mimeType: "image/png" }],
|
|
6910
|
-
structured_content: { image: "base64-bytes" }
|
|
6911
|
-
}
|
|
6912
|
-
}),
|
|
6913
|
-
turnCompleted()
|
|
6914
|
-
],
|
|
6915
|
-
expected: [
|
|
6916
|
-
startedSession(NEW_THREAD_ID),
|
|
6917
|
-
{
|
|
6918
|
-
type: "tool",
|
|
6919
|
-
id: "mcp-image",
|
|
6920
|
-
name: "other.render",
|
|
6921
|
-
mcpServer: "other",
|
|
6922
|
-
phase: "start",
|
|
6923
|
-
summary: "",
|
|
6924
|
-
input: {}
|
|
6925
|
-
},
|
|
6926
|
-
{
|
|
6927
|
-
type: "tool",
|
|
6928
|
-
id: "mcp-image",
|
|
6929
|
-
name: "other.render",
|
|
6930
|
-
mcpServer: "other",
|
|
6931
|
-
phase: "done",
|
|
6932
|
-
summary: "",
|
|
6933
|
-
input: {},
|
|
6934
|
-
result: [{ type: "image", data: "base64-bytes", mimeType: "image/png" }]
|
|
6935
|
-
},
|
|
6936
|
-
closingSession(NEW_THREAD_ID),
|
|
6937
|
-
{ type: "result", ok: true }
|
|
6938
|
-
]
|
|
6939
|
-
},
|
|
6940
|
-
{
|
|
6941
|
-
// A streaming tool whose args arrive on a later snapshot: `item.started` carries
|
|
6942
|
-
// empty args, `item.updated` fills them, `item.completed` finishes. The `start`
|
|
6943
|
-
// fires off the first (empty) frame — that's the live card — but the `done` must
|
|
6944
|
-
// carry the LATEST non-empty input + its recomputed summary, not the first `{}`.
|
|
6945
|
-
name: "streaming tool input \u2014 late args refresh the done event",
|
|
6946
|
-
request: makeRequest3(),
|
|
6947
|
-
nativeStream: [
|
|
6948
|
-
threadStarted(NEW_THREAD_ID),
|
|
6949
|
-
toolFrame("item.started", {
|
|
6950
|
-
id: "t1",
|
|
6951
|
-
type: "mcp_tool_call",
|
|
6952
|
-
server: "cabane",
|
|
6953
|
-
tool: "list",
|
|
6954
|
-
arguments: {},
|
|
6955
|
-
status: "in_progress"
|
|
6956
|
-
}),
|
|
6957
|
-
toolFrame("item.updated", {
|
|
6958
|
-
id: "t1",
|
|
6959
|
-
type: "mcp_tool_call",
|
|
6960
|
-
server: "cabane",
|
|
6961
|
-
tool: "list",
|
|
6962
|
-
arguments: { path: "/product/tasks" },
|
|
6963
|
-
status: "in_progress"
|
|
6964
|
-
}),
|
|
6965
|
-
toolFrame("item.completed", {
|
|
6966
|
-
id: "t1",
|
|
6967
|
-
type: "mcp_tool_call",
|
|
6968
|
-
server: "cabane",
|
|
6969
|
-
tool: "list",
|
|
6970
|
-
arguments: { path: "/product/tasks" },
|
|
6971
|
-
status: "completed",
|
|
6972
|
-
result: { content: [{ type: "text", text: "CT481.md" }] }
|
|
6973
|
-
}),
|
|
6974
|
-
turnCompleted()
|
|
6975
|
-
],
|
|
6976
|
-
expected: [
|
|
6977
|
-
startedSession(NEW_THREAD_ID),
|
|
6978
|
-
{
|
|
6979
|
-
type: "tool",
|
|
6980
|
-
id: "t1",
|
|
6981
|
-
name: "list",
|
|
6982
|
-
mcpServer: "cabane",
|
|
6983
|
-
phase: "start",
|
|
6984
|
-
summary: "",
|
|
6985
|
-
input: {}
|
|
6986
|
-
},
|
|
6987
|
-
{
|
|
6988
|
-
type: "tool",
|
|
6989
|
-
id: "t1",
|
|
6990
|
-
name: "list",
|
|
6991
|
-
mcpServer: "cabane",
|
|
6992
|
-
phase: "done",
|
|
6993
|
-
summary: "/product/tasks",
|
|
6994
|
-
input: { path: "/product/tasks" },
|
|
6995
|
-
result: [{ type: "text", text: "CT481.md" }]
|
|
6996
|
-
},
|
|
6997
|
-
closingSession(NEW_THREAD_ID),
|
|
6998
|
-
{ type: "result", ok: true }
|
|
6999
|
-
]
|
|
7000
|
-
},
|
|
7001
|
-
{
|
|
7002
|
-
// web_search lifecycle. A `web_search` item carries no `status` — completion is signalled by the
|
|
7003
|
-
// frame type (`item.started` → `item.completed`). The `start` fires off the first frame (empty
|
|
7004
|
-
// query); the `done` must resolve off `item.completed` with the populated query, not stay
|
|
7005
|
-
// "running".
|
|
7006
|
-
name: "web_search lifecycle \u2014 completes off frame type, populated query on done",
|
|
7007
|
-
request: makeRequest3(),
|
|
7008
|
-
nativeStream: [
|
|
7009
|
-
threadStarted(NEW_THREAD_ID),
|
|
7010
|
-
toolFrame("item.started", { id: "ws1", type: "web_search", query: "" }),
|
|
7011
|
-
toolFrame("item.completed", { id: "ws1", type: "web_search", query: "best pizza in nyc" }),
|
|
7012
|
-
turnCompleted()
|
|
7013
|
-
],
|
|
7014
|
-
expected: [
|
|
7015
|
-
startedSession(NEW_THREAD_ID),
|
|
7016
|
-
{
|
|
7017
|
-
type: "tool",
|
|
7018
|
-
id: "ws1",
|
|
7019
|
-
name: "web_search",
|
|
7020
|
-
phase: "start",
|
|
7021
|
-
summary: "",
|
|
7022
|
-
input: { query: "" }
|
|
7023
|
-
},
|
|
7024
|
-
{
|
|
7025
|
-
type: "tool",
|
|
7026
|
-
id: "ws1",
|
|
7027
|
-
name: "web_search",
|
|
7028
|
-
phase: "done",
|
|
7029
|
-
summary: "best pizza in nyc",
|
|
7030
|
-
input: { query: "best pizza in nyc" }
|
|
7031
|
-
},
|
|
7032
|
-
closingSession(NEW_THREAD_ID),
|
|
7033
|
-
{ type: "result", ok: true }
|
|
7034
|
-
]
|
|
7035
|
-
},
|
|
7036
|
-
{
|
|
7037
|
-
// A non-text web action (`action.type: "other"`) legitimately completes with an empty query —
|
|
7038
|
-
// Codex's own data. It must still resolve to `done`.
|
|
7039
|
-
name: "web_search lifecycle \u2014 empty-query completion still resolves to done",
|
|
7040
|
-
request: makeRequest3(),
|
|
7041
|
-
nativeStream: [
|
|
7042
|
-
threadStarted(NEW_THREAD_ID),
|
|
7043
|
-
toolFrame("item.started", { id: "ws2", type: "web_search", query: "" }),
|
|
7044
|
-
toolFrame("item.completed", { id: "ws2", type: "web_search", query: "" }),
|
|
7045
|
-
turnCompleted()
|
|
7046
|
-
],
|
|
7047
|
-
expected: [
|
|
7048
|
-
startedSession(NEW_THREAD_ID),
|
|
7049
|
-
{
|
|
7050
|
-
type: "tool",
|
|
7051
|
-
id: "ws2",
|
|
7052
|
-
name: "web_search",
|
|
7053
|
-
phase: "start",
|
|
7054
|
-
summary: "",
|
|
7055
|
-
input: { query: "" }
|
|
7056
|
-
},
|
|
7057
|
-
{
|
|
7058
|
-
type: "tool",
|
|
7059
|
-
id: "ws2",
|
|
7060
|
-
name: "web_search",
|
|
7061
|
-
phase: "done",
|
|
7062
|
-
summary: "",
|
|
7063
|
-
input: { query: "" }
|
|
7064
|
-
},
|
|
7065
|
-
closingSession(NEW_THREAD_ID),
|
|
7066
|
-
{ type: "result", ok: true }
|
|
7067
|
-
]
|
|
7068
|
-
},
|
|
7069
|
-
{
|
|
7070
|
-
// Highest risk: cancel mid-stream. The abort lands after the tool result; the adapter stops before
|
|
7071
|
-
// the closing reply — no final text, no `result` event (the server writes the stopped marker).
|
|
7072
|
-
name: "cancel mid-stream",
|
|
7073
|
-
request: makeRequest3(),
|
|
7074
|
-
nativeStream: [
|
|
7075
|
-
threadStarted(NEW_THREAD_ID),
|
|
7076
|
-
agentMessage("m1", "Working on it."),
|
|
7077
|
-
toolFrame("item.started", {
|
|
7078
|
-
id: "t1",
|
|
7079
|
-
type: "command_execution",
|
|
7080
|
-
command: "ls",
|
|
7081
|
-
status: "in_progress"
|
|
7082
|
-
}),
|
|
7083
|
-
toolFrame("item.completed", {
|
|
7084
|
-
id: "t1",
|
|
7085
|
-
type: "command_execution",
|
|
7086
|
-
command: "ls",
|
|
7087
|
-
aggregated_output: "ok",
|
|
7088
|
-
exit_code: 0,
|
|
7089
|
-
status: "completed"
|
|
7090
|
-
}),
|
|
7091
|
-
ABORT_SENTINEL3,
|
|
7092
|
-
agentMessage("m2", "This reply must never be emitted."),
|
|
7093
|
-
turnCompleted()
|
|
7094
|
-
],
|
|
7095
|
-
expected: [
|
|
7096
|
-
startedSession(NEW_THREAD_ID),
|
|
7097
|
-
{ type: "text", body: "Working on it.", terminal: false },
|
|
7098
|
-
{
|
|
7099
|
-
type: "tool",
|
|
7100
|
-
id: "t1",
|
|
7101
|
-
name: "shell",
|
|
7102
|
-
phase: "start",
|
|
7103
|
-
summary: "ls",
|
|
7104
|
-
input: { command: "ls" }
|
|
7105
|
-
},
|
|
7106
|
-
{
|
|
7107
|
-
type: "tool",
|
|
7108
|
-
id: "t1",
|
|
7109
|
-
name: "shell",
|
|
7110
|
-
phase: "done",
|
|
7111
|
-
summary: "ls",
|
|
7112
|
-
input: { command: "ls" },
|
|
7113
|
-
result: { output: "ok", exit_code: 0 }
|
|
7114
|
-
}
|
|
7115
|
-
]
|
|
7116
|
-
},
|
|
7117
|
-
{
|
|
7118
|
-
// A failed turn: partial narration lands as `progress` (terminal:false), and the
|
|
7119
|
-
// `result` event carries the machine reason off `turn.failed`.
|
|
7120
|
-
name: "turn.failed result",
|
|
7121
|
-
request: makeRequest3(),
|
|
7122
|
-
nativeStream: [
|
|
7123
|
-
threadStarted(NEW_THREAD_ID),
|
|
7124
|
-
agentMessage("m1", "Partial work."),
|
|
7125
|
-
turnFailed("provider exploded")
|
|
7126
|
-
],
|
|
7127
|
-
expected: [
|
|
7128
|
-
startedSession(NEW_THREAD_ID),
|
|
7129
|
-
{ type: "text", body: "Partial work.", terminal: false },
|
|
7130
|
-
{ type: "runtime_notice", body: "provider exploded" },
|
|
7131
|
-
{ type: "result", ok: false, reason: "error:provider exploded" }
|
|
7132
|
-
]
|
|
7133
|
-
},
|
|
7134
|
-
{
|
|
7135
|
-
// A fatal top-level `error` frame (distinct from `turn.failed`): held narration
|
|
7136
|
-
// seals as interim, then a failed `result`.
|
|
7137
|
-
name: "fatal stream error",
|
|
7138
|
-
request: makeRequest3(),
|
|
7139
|
-
nativeStream: [
|
|
7140
|
-
threadStarted(NEW_THREAD_ID),
|
|
7141
|
-
agentMessage("m1", "Starting."),
|
|
7142
|
-
fatalError("stream died")
|
|
7143
|
-
],
|
|
7144
|
-
expected: [
|
|
7145
|
-
startedSession(NEW_THREAD_ID),
|
|
7146
|
-
{ type: "text", body: "Starting.", terminal: false },
|
|
7147
|
-
{ type: "runtime_notice", body: "stream died" },
|
|
7148
|
-
{ type: "result", ok: false, reason: "error:stream died" }
|
|
7149
|
-
]
|
|
7150
|
-
},
|
|
7151
|
-
{
|
|
7152
|
-
// The failure text goes through the shared classifier (Codex is text-only), so a provider 429 /
|
|
7153
|
-
// outage / auth / cap failure splits into the honest taxonomy. One fixture per kind:
|
|
7154
|
-
name: "turn.failed 429 \u2192 rate_limited (throttle)",
|
|
7155
|
-
request: makeRequest3(),
|
|
7156
|
-
nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("429 Too Many Requests")],
|
|
7157
|
-
expected: [
|
|
7158
|
-
startedSession(NEW_THREAD_ID),
|
|
7159
|
-
{ type: "runtime_notice", body: "429 Too Many Requests" },
|
|
7160
|
-
{ type: "result", ok: false, reason: "rate_limited" }
|
|
7161
|
-
]
|
|
7162
|
-
},
|
|
7163
|
-
{
|
|
7164
|
-
name: "turn.failed overload \u2192 server_error",
|
|
7165
|
-
request: makeRequest3(),
|
|
7166
|
-
nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("503 Service Unavailable")],
|
|
7167
|
-
expected: [
|
|
7168
|
-
startedSession(NEW_THREAD_ID),
|
|
7169
|
-
{ type: "runtime_notice", body: "503 Service Unavailable" },
|
|
7170
|
-
{ type: "result", ok: false, reason: "server_error" }
|
|
7171
|
-
]
|
|
7172
|
-
},
|
|
7173
|
-
{
|
|
7174
|
-
name: "turn.failed usage cap \u2192 usage_capped",
|
|
7175
|
-
request: makeRequest3(),
|
|
7176
|
-
nativeStream: [
|
|
7177
|
-
threadStarted(NEW_THREAD_ID),
|
|
7178
|
-
turnFailed("You've hit your session limit \xB7 resets 8:20pm (UTC)")
|
|
7179
|
-
],
|
|
7180
|
-
expected: [
|
|
7181
|
-
startedSession(NEW_THREAD_ID),
|
|
7182
|
-
{
|
|
7183
|
-
type: "runtime_notice",
|
|
7184
|
-
body: "You've hit your session limit \xB7 resets 8:20pm (UTC)"
|
|
7185
|
-
},
|
|
7186
|
-
{ type: "result", ok: false, reason: "usage_capped" }
|
|
7187
|
-
]
|
|
7188
|
-
},
|
|
7189
|
-
{
|
|
7190
|
-
name: "turn.failed auth \u2192 auth_expired",
|
|
7191
|
-
request: makeRequest3(),
|
|
7192
|
-
nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("401 Unauthorized: invalid api key")],
|
|
7193
|
-
expected: [
|
|
7194
|
-
startedSession(NEW_THREAD_ID),
|
|
7195
|
-
{ type: "runtime_notice", body: "401 Unauthorized: invalid api key" },
|
|
7196
|
-
{ type: "result", ok: false, reason: "auth_expired" }
|
|
7197
|
-
]
|
|
7198
|
-
},
|
|
7199
|
-
{
|
|
7200
|
-
// A failure text the classifier can't place keeps the raw `error:<text>` reason (the `unknown`
|
|
7201
|
-
// fallback).
|
|
7202
|
-
name: "turn.failed unclassified \u2192 raw error reason (unchanged)",
|
|
7203
|
-
request: makeRequest3(),
|
|
7204
|
-
nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("something local broke")],
|
|
7205
|
-
expected: [
|
|
7206
|
-
startedSession(NEW_THREAD_ID),
|
|
7207
|
-
{ type: "runtime_notice", body: "something local broke" },
|
|
7208
|
-
{ type: "result", ok: false, reason: "error:something local broke" }
|
|
7209
|
-
]
|
|
7210
|
-
},
|
|
7211
|
-
{
|
|
7212
|
-
name: "runtime notice is trimmed and bounded to 200 characters",
|
|
7213
|
-
request: makeRequest3(),
|
|
7214
|
-
nativeStream: [
|
|
7215
|
-
threadStarted(NEW_THREAD_ID),
|
|
7216
|
-
turnFailed(` ${BOUNDED_NOTICE2}${"y".repeat(20)} `)
|
|
7217
|
-
],
|
|
7218
|
-
expected: [
|
|
7219
|
-
startedSession(NEW_THREAD_ID),
|
|
7220
|
-
{ type: "runtime_notice", body: BOUNDED_NOTICE2 },
|
|
7221
|
-
{ type: "result", ok: false, reason: "usage_capped" }
|
|
7222
|
-
]
|
|
7223
|
-
},
|
|
7224
|
-
{
|
|
7225
|
-
// Empty, whitespace-only and bare status-token failures still set the machine reason but add no
|
|
7226
|
-
// empty or unhelpful transcript notice.
|
|
7227
|
-
name: "empty failure text emits no runtime notice",
|
|
7228
|
-
request: makeRequest3(),
|
|
7229
|
-
nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("")],
|
|
7230
|
-
expected: [
|
|
7231
|
-
startedSession(NEW_THREAD_ID),
|
|
7232
|
-
{ type: "result", ok: false, reason: "error:unknown" }
|
|
7233
|
-
]
|
|
7234
|
-
},
|
|
7235
|
-
{
|
|
7236
|
-
name: "whitespace-only failure text emits no runtime notice",
|
|
7237
|
-
request: makeRequest3(),
|
|
7238
|
-
nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed(" ")],
|
|
7239
|
-
expected: [startedSession(NEW_THREAD_ID), { type: "result", ok: false, reason: "error: " }]
|
|
7240
|
-
},
|
|
7241
|
-
{
|
|
7242
|
-
name: "bare status token emits no runtime notice",
|
|
7243
|
-
request: makeRequest3(),
|
|
7244
|
-
nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("429")],
|
|
7245
|
-
expected: [
|
|
7246
|
-
startedSession(NEW_THREAD_ID),
|
|
7247
|
-
{ type: "result", ok: false, reason: "rate_limited" }
|
|
7248
|
-
]
|
|
7249
|
-
},
|
|
7250
|
-
{
|
|
7251
|
-
name: "single-word provider sentence emits a runtime notice",
|
|
7252
|
-
request: makeRequest3(),
|
|
7253
|
-
nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("Unavailable.")],
|
|
7254
|
-
expected: [
|
|
7255
|
-
startedSession(NEW_THREAD_ID),
|
|
7256
|
-
{ type: "runtime_notice", body: "Unavailable." },
|
|
7257
|
-
{ type: "result", ok: false, reason: "error:Unavailable." }
|
|
7258
|
-
]
|
|
7259
|
-
},
|
|
7260
|
-
{
|
|
7261
|
-
// Fail loud on unknown model metadata. An unknown `--model` makes Codex emit an `error` item
|
|
7262
|
-
// ("… Defaulting to fallback metadata …") and misbehave (the spurious tool call scripted after it
|
|
7263
|
-
// must never be emitted). The adapter ends the turn when the signal lands, with a machine reason.
|
|
7264
|
-
name: "unknown model metadata \u2192 fail loud (guard)",
|
|
7265
|
-
request: makeRequest3(),
|
|
7266
|
-
nativeStream: [
|
|
7267
|
-
threadStarted(NEW_THREAD_ID),
|
|
7268
|
-
errorItem(
|
|
7269
|
-
"e0",
|
|
7270
|
-
"Model metadata for `gpt-5-codex` not found. Defaulting to fallback metadata; this can degrade performance and cause issues."
|
|
7271
|
-
),
|
|
7272
|
-
// Everything after the guard fires must be dropped (the haywire the guard prevents).
|
|
7273
|
-
toolFrame("item.started", {
|
|
7274
|
-
id: "t1",
|
|
7275
|
-
type: "web_search",
|
|
7276
|
-
query: "anything",
|
|
7277
|
-
status: "in_progress"
|
|
7278
|
-
}),
|
|
7279
|
-
agentMessage("m1", "This degraded reply must never be emitted."),
|
|
7280
|
-
turnCompleted()
|
|
7281
|
-
],
|
|
7282
|
-
expected: [
|
|
7283
|
-
startedSession(NEW_THREAD_ID),
|
|
7284
|
-
{
|
|
7285
|
-
type: "result",
|
|
7286
|
-
ok: false,
|
|
7287
|
-
reason: "model_unavailable:Model metadata for `gpt-5-codex` not found. Defaulting to fallback metadata; this can degrade performance and cause issues."
|
|
7288
|
-
}
|
|
7289
|
-
]
|
|
7290
|
-
},
|
|
7291
|
-
{
|
|
7292
|
-
// A NON-metadata error item is non-fatal — it's skipped, and the turn proceeds
|
|
7293
|
-
// to its normal reply (guard specificity: only the fallback-metadata phrase trips).
|
|
7294
|
-
name: "non-metadata error item is skipped (non-fatal)",
|
|
7295
|
-
request: makeRequest3(),
|
|
7296
|
-
nativeStream: [
|
|
7297
|
-
threadStarted(NEW_THREAD_ID),
|
|
7298
|
-
errorItem("e0", "a transient hiccup"),
|
|
7299
|
-
agentMessage("m1", "All good now."),
|
|
7300
|
-
turnCompleted()
|
|
7301
|
-
],
|
|
7302
|
-
expected: [
|
|
7303
|
-
startedSession(NEW_THREAD_ID),
|
|
7304
|
-
{ type: "text", body: "All good now.", terminal: true },
|
|
7305
|
-
closingSession(NEW_THREAD_ID),
|
|
7306
|
-
{ type: "result", ok: true }
|
|
7307
|
-
]
|
|
7308
|
-
},
|
|
7309
|
-
{
|
|
7310
|
-
// The zero-content success the claude-code suite pins, in Codex's vocabulary — the thread
|
|
7311
|
-
// starts, the turn completes, nothing in between. No Codex sample has been seen; the fixture
|
|
7312
|
-
// exists because the host's classification is runtime-neutral, so a runtime that can produce
|
|
7313
|
-
// this envelope must be shown producing it in the shape the classifier reads.
|
|
7314
|
-
name: "zero-content success (empty runtime result)",
|
|
7315
|
-
request: makeRequest3(),
|
|
7316
|
-
nativeStream: [threadStarted(NEW_THREAD_ID), turnCompleted()],
|
|
7317
|
-
expected: [
|
|
7318
|
-
startedSession(NEW_THREAD_ID),
|
|
7319
|
-
closingSession(NEW_THREAD_ID),
|
|
7320
|
-
{ type: "result", ok: true }
|
|
7321
|
-
]
|
|
7322
|
-
},
|
|
7323
|
-
{
|
|
7324
|
-
// Empty-final: a clean turn that ended on a tool call with no closing message.
|
|
7325
|
-
// The adapter emits NO final text — empty-final promotion is host/pump territory
|
|
7326
|
-
// (this fixture pins that boundary).
|
|
7327
|
-
name: "empty-final (no closing message)",
|
|
7328
|
-
request: makeRequest3(),
|
|
7329
|
-
nativeStream: [
|
|
7330
|
-
threadStarted(NEW_THREAD_ID),
|
|
7331
|
-
toolFrame("item.started", {
|
|
7332
|
-
id: "t1",
|
|
7333
|
-
type: "file_change",
|
|
7334
|
-
changes: [{ path: "a.md", kind: "add" }],
|
|
7335
|
-
status: "in_progress"
|
|
7336
|
-
}),
|
|
7337
|
-
toolFrame("item.completed", {
|
|
7338
|
-
id: "t1",
|
|
7339
|
-
type: "file_change",
|
|
7340
|
-
changes: [{ path: "a.md", kind: "add" }],
|
|
7341
|
-
status: "completed"
|
|
7342
|
-
}),
|
|
7343
|
-
turnCompleted()
|
|
7344
|
-
],
|
|
7345
|
-
expected: [
|
|
7346
|
-
startedSession(NEW_THREAD_ID),
|
|
7347
|
-
{
|
|
7348
|
-
type: "tool",
|
|
7349
|
-
id: "t1",
|
|
7350
|
-
name: "apply_patch",
|
|
7351
|
-
phase: "start",
|
|
7352
|
-
summary: "",
|
|
7353
|
-
input: { changes: [{ path: "a.md", kind: "add" }] }
|
|
7354
|
-
},
|
|
7355
|
-
{
|
|
7356
|
-
type: "tool",
|
|
7357
|
-
id: "t1",
|
|
7358
|
-
name: "apply_patch",
|
|
7359
|
-
phase: "done",
|
|
7360
|
-
summary: "",
|
|
7361
|
-
input: { changes: [{ path: "a.md", kind: "add" }] },
|
|
7362
|
-
result: [{ path: "a.md", kind: "add" }]
|
|
7363
|
-
},
|
|
7364
|
-
closingSession(NEW_THREAD_ID),
|
|
7365
|
-
{ type: "result", ok: true }
|
|
7366
|
-
]
|
|
7367
|
-
},
|
|
7368
|
-
{
|
|
7369
|
-
// Thinking: a reasoning item seals as a transient `thinking`, then the reply
|
|
7370
|
-
// message is the final.
|
|
7371
|
-
name: "thinking then reply",
|
|
7372
|
-
request: makeRequest3(),
|
|
7373
|
-
nativeStream: [
|
|
7374
|
-
threadStarted(NEW_THREAD_ID),
|
|
7375
|
-
reasoning("r1", "Let me check the notes."),
|
|
7376
|
-
agentMessage("m1", "All set."),
|
|
7377
|
-
turnCompleted()
|
|
7378
|
-
],
|
|
7379
|
-
expected: [
|
|
7380
|
-
startedSession(NEW_THREAD_ID),
|
|
7381
|
-
{ type: "thinking", text: "Let me check the notes." },
|
|
7382
|
-
{ type: "text", body: "All set.", terminal: true },
|
|
7383
|
-
closingSession(NEW_THREAD_ID),
|
|
7384
|
-
{ type: "result", ok: true }
|
|
7385
|
-
]
|
|
7386
|
-
},
|
|
7387
|
-
{
|
|
7388
|
-
// Codex reports no model in-stream, so the resolved model and reasoning effort come from what the
|
|
7389
|
-
// thread was started with (the run spec) and ride the terminal `result`: a real model and a
|
|
7390
|
-
// `runtimeOptions.codex.modelReasoningEffort` are asserted through to `resolvedModel` (bare,
|
|
7391
|
-
// `openai/`-stripped) and `resolvedConfig.reasoningEffort`.
|
|
7392
|
-
name: "resolved model + reasoning effort captured from run spec",
|
|
7393
|
-
request: makeRequest3({
|
|
7394
|
-
config: {
|
|
7395
|
-
model: "openai/gpt-5-codex",
|
|
7396
|
-
runtimeOptions: { codex: { modelReasoningEffort: "high" } }
|
|
7397
|
-
}
|
|
7398
|
-
}),
|
|
7399
|
-
nativeStream: [threadStarted(NEW_THREAD_ID), agentMessage("m1", "Done."), turnCompleted()],
|
|
7400
|
-
expected: [
|
|
7401
|
-
startedSession(NEW_THREAD_ID),
|
|
7402
|
-
{ type: "text", body: "Done.", terminal: true },
|
|
7403
|
-
closingSession(NEW_THREAD_ID),
|
|
7404
|
-
{
|
|
7405
|
-
type: "result",
|
|
7406
|
-
ok: true,
|
|
7407
|
-
resolvedModel: "gpt-5-codex",
|
|
7408
|
-
resolvedConfig: { reasoningEffort: "high" }
|
|
7409
|
-
}
|
|
7410
|
-
]
|
|
7411
|
-
},
|
|
7412
|
-
{
|
|
7413
|
-
// Session resume: the stored state matches the directory, so the thread id is resumed and
|
|
7414
|
-
// `thread.started` echoes it — no session event fires. This is what carries cross-turn memory.
|
|
7415
|
-
//
|
|
7416
|
-
// The stored state also says the thread already holds the composed prompt, so this turn
|
|
7417
|
-
// withholds the preamble and the state it would write is byte-identical: the silence proves both
|
|
7418
|
-
// an unchanged id and a prompt that needed no re-send.
|
|
7419
|
-
name: "session resume (no re-emit on echo)",
|
|
7420
|
-
request: makeRequest3({
|
|
7421
|
-
session: encodeSession3({
|
|
7422
|
-
threadId: "th1",
|
|
7423
|
-
cwd: DIR2,
|
|
7424
|
-
promptFingerprint: fingerprintPrompt(PROMPT),
|
|
7425
|
-
usageBaseline: null,
|
|
7426
|
-
compactions: 0
|
|
7427
|
-
})
|
|
7428
|
-
}),
|
|
7429
|
-
nativeStream: [threadStarted("th1"), agentMessage("m1", "Back again."), turnCompleted()],
|
|
7430
|
-
expected: [
|
|
7431
|
-
{ type: "text", body: "Back again.", terminal: true },
|
|
7432
|
-
{ type: "result", ok: true }
|
|
7433
|
-
]
|
|
7434
|
-
},
|
|
7435
|
-
{
|
|
7436
|
-
// A resumed thread whose stored state carries no `promptFingerprint` field. The adapter reads the
|
|
7437
|
-
// absence as "can't say which prompt this thread holds", sends the full prompt, and re-emits the
|
|
7438
|
-
// state with the fingerprint — on the same thread id. That re-emit is the upgrade path: without
|
|
7439
|
-
// it a long-lived thread would re-send the whole prompt forever.
|
|
7440
|
-
//
|
|
7441
|
-
// This fixture fails if the standing half is withheld on a thread we have no evidence about.
|
|
7442
|
-
name: "session resume with a pre-CT1075 state re-emits with the prompt fingerprint",
|
|
7443
|
-
request: makeRequest3({
|
|
7444
|
-
session: JSON.stringify({ threadId: "th1", cwd: DIR2 })
|
|
7445
|
-
}),
|
|
7446
|
-
nativeStream: [threadStarted("th1"), agentMessage("m1", "Back again."), turnCompleted()],
|
|
7447
|
-
expected: [
|
|
7448
|
-
startedSession("th1"),
|
|
7449
|
-
{ type: "text", body: "Back again.", terminal: true },
|
|
7450
|
-
closingSession("th1"),
|
|
7451
|
-
{ type: "result", ok: true }
|
|
7452
|
-
]
|
|
7453
|
-
},
|
|
7454
|
-
{
|
|
7455
|
-
// The thread carries a different prompt than this turn composed — a charter edit, a renamed
|
|
7456
|
-
// agent, a redeployed prompt body. The fingerprint doesn't match, so the new prompt is sent as on
|
|
7457
|
-
// a fresh thread and the state re-recorded.
|
|
7458
|
-
//
|
|
7459
|
-
// This fixture fails if a stale prompt is treated as good enough; a boolean flag would pass the
|
|
7460
|
-
// resume fixture above and withhold every future charter edit.
|
|
7461
|
-
name: "session resume with a stale prompt fingerprint re-sends the prompt",
|
|
7462
|
-
request: makeRequest3({
|
|
7463
|
-
session: encodeSession3({
|
|
7464
|
-
threadId: "th1",
|
|
7465
|
-
cwd: DIR2,
|
|
7466
|
-
promptFingerprint: fingerprintPrompt("an older composed prompt"),
|
|
7467
|
-
usageBaseline: null
|
|
7468
|
-
})
|
|
7469
|
-
}),
|
|
7470
|
-
nativeStream: [threadStarted("th1"), agentMessage("m1", "Back again."), turnCompleted()],
|
|
7471
|
-
expected: [
|
|
7472
|
-
startedSession("th1"),
|
|
7473
|
-
{ type: "text", body: "Back again.", terminal: true },
|
|
7474
|
-
closingSession("th1"),
|
|
7475
|
-
{ type: "result", ok: true }
|
|
7476
|
-
]
|
|
7477
|
-
},
|
|
7478
|
-
{
|
|
7479
|
-
// Directory-mismatch degrade: the stored state was created under another directory, so the
|
|
7480
|
-
// adapter starts fresh — `thread.started` reports a new id, and the session event records the
|
|
7481
|
-
// current directory with `degraded: true` so the host rewinds the catch-up mark.
|
|
7482
|
-
//
|
|
7483
|
-
// The refused resume means the new thread is empty, so the full prompt goes in even though the
|
|
7484
|
-
// stored state vouched for a copy in a thread we're no longer in.
|
|
7485
|
-
name: "session directory-mismatch degrades to fresh",
|
|
7486
|
-
request: makeRequest3({
|
|
7487
|
-
session: encodeSession3({
|
|
7488
|
-
threadId: "old",
|
|
7489
|
-
cwd: "/env/gone",
|
|
7490
|
-
promptFingerprint: fingerprintPrompt(PROMPT),
|
|
7491
|
-
usageBaseline: null
|
|
7492
|
-
})
|
|
7493
|
-
}),
|
|
7494
|
-
nativeStream: [
|
|
7495
|
-
threadStarted(NEW_THREAD_ID),
|
|
7496
|
-
agentMessage("m1", "Fresh start."),
|
|
7497
|
-
turnCompleted()
|
|
7498
|
-
],
|
|
7499
|
-
expected: [
|
|
7500
|
-
startedSession(NEW_THREAD_ID, DIR2, true),
|
|
7501
|
-
{ type: "text", body: "Fresh start.", terminal: true },
|
|
7502
|
-
closingSession(NEW_THREAD_ID),
|
|
7503
|
-
{ type: "result", ok: true }
|
|
7504
|
-
]
|
|
7505
|
-
}
|
|
7506
|
-
];
|
|
7507
|
-
|
|
7508
5284
|
// packages/agent-runtime/src/claude-code/sdk.ts
|
|
7509
5285
|
import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
|
|
7510
5286
|
|
|
@@ -7644,47 +5420,47 @@ function classifyStopError(message) {
|
|
|
7644
5420
|
}
|
|
7645
5421
|
|
|
7646
5422
|
// src/runner/protocol.ts
|
|
7647
|
-
import { z as
|
|
5423
|
+
import { z as z13 } from "zod";
|
|
7648
5424
|
|
|
7649
5425
|
// src/run-config.ts
|
|
7650
|
-
import { z as
|
|
7651
|
-
var mcpStdioServerSchema =
|
|
7652
|
-
type:
|
|
7653
|
-
command:
|
|
7654
|
-
args:
|
|
7655
|
-
env:
|
|
5426
|
+
import { z as z12 } from "zod";
|
|
5427
|
+
var mcpStdioServerSchema = z12.object({
|
|
5428
|
+
type: z12.literal("stdio").optional(),
|
|
5429
|
+
command: z12.string().min(1),
|
|
5430
|
+
args: z12.array(z12.string()).optional(),
|
|
5431
|
+
env: z12.record(z12.string(), z12.string()).optional(),
|
|
7656
5432
|
// Variable → the name of a `secretEnv` entry holding its value. Set by the server on a
|
|
7657
5433
|
// room's shared browser, whose connect URL is a credential.
|
|
7658
|
-
envFromEnv:
|
|
5434
|
+
envFromEnv: z12.record(z12.string(), z12.string()).optional()
|
|
7659
5435
|
});
|
|
7660
|
-
var mcpHttpServerSchema =
|
|
7661
|
-
type:
|
|
7662
|
-
url:
|
|
7663
|
-
headers:
|
|
5436
|
+
var mcpHttpServerSchema = z12.object({
|
|
5437
|
+
type: z12.literal("http"),
|
|
5438
|
+
url: z12.string().url(),
|
|
5439
|
+
headers: z12.record(z12.string(), z12.string()).optional(),
|
|
7664
5440
|
// Header → the name of a `secretEnv` entry holding its value. Set by
|
|
7665
5441
|
// the server on a room connection's entry, and by `resolveMcpSecrets` on a
|
|
7666
5442
|
// header whose `${VAR}` it resolved.
|
|
7667
|
-
headersFromEnv:
|
|
5443
|
+
headersFromEnv: z12.record(z12.string(), z12.string()).optional()
|
|
7668
5444
|
});
|
|
7669
|
-
var mcpSseServerSchema =
|
|
7670
|
-
type:
|
|
7671
|
-
url:
|
|
7672
|
-
headers:
|
|
5445
|
+
var mcpSseServerSchema = z12.object({
|
|
5446
|
+
type: z12.literal("sse"),
|
|
5447
|
+
url: z12.string().url(),
|
|
5448
|
+
headers: z12.record(z12.string(), z12.string()).optional(),
|
|
7673
5449
|
// Header → the name of a `secretEnv` entry holding its value. Set by
|
|
7674
5450
|
// the server on a room connection's entry, and by `resolveMcpSecrets` on a
|
|
7675
5451
|
// header whose `${VAR}` it resolved.
|
|
7676
|
-
headersFromEnv:
|
|
5452
|
+
headersFromEnv: z12.record(z12.string(), z12.string()).optional()
|
|
7677
5453
|
});
|
|
7678
|
-
var mcpServerDefSchema =
|
|
7679
|
-
var thinkingConfigSchema =
|
|
7680
|
-
|
|
7681
|
-
|
|
7682
|
-
|
|
5454
|
+
var mcpServerDefSchema = z12.union([mcpHttpServerSchema, mcpSseServerSchema, mcpStdioServerSchema]);
|
|
5455
|
+
var thinkingConfigSchema = z12.discriminatedUnion("type", [
|
|
5456
|
+
z12.object({ type: z12.literal("adaptive") }),
|
|
5457
|
+
z12.object({ type: z12.literal("enabled"), budgetTokens: z12.number().int().positive().optional() }),
|
|
5458
|
+
z12.object({ type: z12.literal("disabled") })
|
|
7683
5459
|
]);
|
|
7684
|
-
var effortSchema =
|
|
7685
|
-
var runConfigSchema =
|
|
7686
|
-
mcpServers:
|
|
7687
|
-
model:
|
|
5460
|
+
var effortSchema = z12.enum(["low", "medium", "high", "xhigh", "max"]);
|
|
5461
|
+
var runConfigSchema = z12.object({
|
|
5462
|
+
mcpServers: z12.record(z12.string(), mcpServerDefSchema).optional(),
|
|
5463
|
+
model: z12.string().min(1).optional(),
|
|
7688
5464
|
thinking: thinkingConfigSchema.optional(),
|
|
7689
5465
|
effort: effortSchema.optional()
|
|
7690
5466
|
});
|
|
@@ -7712,61 +5488,61 @@ var RUNNER_ENV = {
|
|
|
7712
5488
|
// Everything else the turn is constructed from, as JSON: see `RunnerSpec`.
|
|
7713
5489
|
spec: "CABANE_RUNNER_SPEC"
|
|
7714
5490
|
};
|
|
7715
|
-
var runtimeHealthSchema =
|
|
7716
|
-
quotaState:
|
|
7717
|
-
limitedUntil:
|
|
7718
|
-
lastError:
|
|
5491
|
+
var runtimeHealthSchema = z13.object({
|
|
5492
|
+
quotaState: z13.enum(["ok", "limited", "auth_expired", "unknown"]),
|
|
5493
|
+
limitedUntil: z13.string().nullable(),
|
|
5494
|
+
lastError: z13.string().nullable()
|
|
7719
5495
|
});
|
|
7720
|
-
var runnerSpecSchema =
|
|
7721
|
-
deviceId:
|
|
5496
|
+
var runnerSpecSchema = z13.object({
|
|
5497
|
+
deviceId: z13.string().optional(),
|
|
7722
5498
|
// The launcher's boot id: the one its heartbeat carries, so a runner's lease
|
|
7723
5499
|
// matches the device that holds it.
|
|
7724
|
-
bootId:
|
|
7725
|
-
workspaceId:
|
|
7726
|
-
workspaceSlug:
|
|
7727
|
-
agentId:
|
|
7728
|
-
agentUsername:
|
|
7729
|
-
agentName:
|
|
5500
|
+
bootId: z13.string(),
|
|
5501
|
+
workspaceId: z13.string(),
|
|
5502
|
+
workspaceSlug: z13.string(),
|
|
5503
|
+
agentId: z13.string(),
|
|
5504
|
+
agentUsername: z13.string(),
|
|
5505
|
+
agentName: z13.string(),
|
|
7730
5506
|
local: localAgentConfigSchema,
|
|
7731
5507
|
runConfig: runConfigSchema,
|
|
7732
5508
|
// The device's `root`, resolved by the launcher: the pair directory the turn
|
|
7733
5509
|
// stands in when nothing else names one lives under it (`work-dir.ts`).
|
|
7734
|
-
workRoot:
|
|
7735
|
-
opencodeServerUrl:
|
|
7736
|
-
claudeCodeAvailable:
|
|
7737
|
-
codexAvailable:
|
|
7738
|
-
idleTimeoutMs:
|
|
7739
|
-
totalTimeoutMs:
|
|
7740
|
-
leaseRenewalMs:
|
|
7741
|
-
preparingRowDelayMs:
|
|
5510
|
+
workRoot: z13.string(),
|
|
5511
|
+
opencodeServerUrl: z13.string().optional(),
|
|
5512
|
+
claudeCodeAvailable: z13.boolean(),
|
|
5513
|
+
codexAvailable: z13.boolean(),
|
|
5514
|
+
idleTimeoutMs: z13.number().int().positive().optional(),
|
|
5515
|
+
totalTimeoutMs: z13.number().int().positive().optional(),
|
|
5516
|
+
leaseRenewalMs: z13.number().int().positive().optional(),
|
|
5517
|
+
preparingRowDelayMs: z13.number().int().nonnegative().optional(),
|
|
7742
5518
|
// Whether the turn writes a transcript, into the device's own directory
|
|
7743
5519
|
// (`transcriptsDir()`).
|
|
7744
|
-
transcripts:
|
|
7745
|
-
logLevel:
|
|
5520
|
+
transcripts: z13.boolean(),
|
|
5521
|
+
logLevel: z13.string(),
|
|
7746
5522
|
// The launcher's connector-health observations, by runtime, as of the start.
|
|
7747
|
-
connectorHealth:
|
|
5523
|
+
connectorHealth: z13.record(z13.string(), runtimeHealthSchema),
|
|
7748
5524
|
// True when this (conversation, agent) pair was already told its session
|
|
7749
5525
|
// could not be saved: the notice is once per pair, not once per process.
|
|
7750
|
-
sessionWriteNoticeClaimed:
|
|
5526
|
+
sessionWriteNoticeClaimed: z13.boolean(),
|
|
7751
5527
|
// The runner's identity, minted with its credential. The start
|
|
7752
5528
|
// PATCH carries it so the lease knows which runner holds the span.
|
|
7753
|
-
runnerId:
|
|
5529
|
+
runnerId: z13.string(),
|
|
7754
5530
|
// Whether the server took gzip request bodies at the latest heartbeat, as of
|
|
7755
5531
|
// the start. The launcher writes `gzip_bodies` when that changes mid-turn.
|
|
7756
|
-
gzipBodies:
|
|
5532
|
+
gzipBodies: z13.boolean().optional()
|
|
7757
5533
|
});
|
|
7758
|
-
var dispatchSchema =
|
|
7759
|
-
type:
|
|
7760
|
-
agentId:
|
|
7761
|
-
deviceId:
|
|
7762
|
-
conversationId:
|
|
7763
|
-
messageId:
|
|
7764
|
-
episode:
|
|
5534
|
+
var dispatchSchema = z13.object({
|
|
5535
|
+
type: z13.literal("device:dispatch_requested"),
|
|
5536
|
+
agentId: z13.string(),
|
|
5537
|
+
deviceId: z13.string(),
|
|
5538
|
+
conversationId: z13.string(),
|
|
5539
|
+
messageId: z13.string(),
|
|
5540
|
+
episode: z13.number().int().optional()
|
|
7765
5541
|
});
|
|
7766
|
-
var credentialsSchema =
|
|
7767
|
-
credential:
|
|
7768
|
-
secrets:
|
|
7769
|
-
grants:
|
|
5542
|
+
var credentialsSchema = z13.object({
|
|
5543
|
+
credential: z13.string().startsWith("cabrn_"),
|
|
5544
|
+
secrets: z13.record(z13.string(), z13.string()).optional(),
|
|
5545
|
+
grants: z13.record(z13.string(), z13.record(z13.string(), z13.string())).optional()
|
|
7770
5546
|
});
|
|
7771
5547
|
function runnerEnv(input) {
|
|
7772
5548
|
return {
|
|
@@ -7801,6 +5577,30 @@ function decodeRunnerEvent(line) {
|
|
|
7801
5577
|
return value;
|
|
7802
5578
|
}
|
|
7803
5579
|
|
|
5580
|
+
// src/credential-broker/client-config.ts
|
|
5581
|
+
import { parse, stringify } from "smol-toml";
|
|
5582
|
+
var BROKERED_CREDENTIAL_NAMES = /* @__PURE__ */ new Set([
|
|
5583
|
+
"ANTHROPIC_API_KEY",
|
|
5584
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
5585
|
+
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
5586
|
+
"CLAUDE_CODE_OAUTH_REFRESH_TOKEN",
|
|
5587
|
+
"CLAUDE_CODE_SESSION_ACCESS_TOKEN",
|
|
5588
|
+
"CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR",
|
|
5589
|
+
"CLAUDE_CODE_HOST_CREDS_FILE",
|
|
5590
|
+
"CODEX_API_KEY",
|
|
5591
|
+
"OPENAI_API_KEY",
|
|
5592
|
+
"OPENAI_ACCESS_TOKEN",
|
|
5593
|
+
"GH_TOKEN",
|
|
5594
|
+
"GITHUB_TOKEN",
|
|
5595
|
+
"GH_ENTERPRISE_TOKEN",
|
|
5596
|
+
"GITHUB_ENTERPRISE_TOKEN"
|
|
5597
|
+
]);
|
|
5598
|
+
function withoutBrokeredCredentials(values) {
|
|
5599
|
+
return Object.fromEntries(
|
|
5600
|
+
Object.entries(values).filter(([name]) => !BROKERED_CREDENTIAL_NAMES.has(name))
|
|
5601
|
+
);
|
|
5602
|
+
}
|
|
5603
|
+
|
|
7804
5604
|
// src/runner/launch.ts
|
|
7805
5605
|
function gzipBodiesSignal() {
|
|
7806
5606
|
let on = false;
|
|
@@ -8064,12 +5864,22 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
|
8064
5864
|
}
|
|
8065
5865
|
try {
|
|
8066
5866
|
const wrapped = launch.wrapped === true;
|
|
8067
|
-
|
|
8068
|
-
|
|
5867
|
+
if (launch.brokeredCredentials && !wrapped)
|
|
5868
|
+
throw new Error("credential broker requires a wrapped runner");
|
|
5869
|
+
const inherited = runnerExecEnv(launch.env ?? process.env, wrapped);
|
|
5870
|
+
const env = launch.brokeredCredentials ? withoutBrokeredCredentials(inherited) : inherited;
|
|
5871
|
+
const directGrants = minted.deviceGrants.length > 0 ? launch.grants?.(minted.deviceGrants) ?? {} : {};
|
|
5872
|
+
const grants = launch.brokeredCredentials ? Object.fromEntries(
|
|
5873
|
+
Object.entries(directGrants).map(([key, values]) => [
|
|
5874
|
+
key,
|
|
5875
|
+
withoutBrokeredCredentials(values)
|
|
5876
|
+
])
|
|
5877
|
+
) : directGrants;
|
|
5878
|
+
const secrets = wrapped ? launch.secrets?.() ?? {} : void 0;
|
|
8069
5879
|
const input = {
|
|
8070
5880
|
baseUrl: opts.baseUrl,
|
|
8071
5881
|
credential: minted.credential,
|
|
8072
|
-
...
|
|
5882
|
+
...secrets ? { secrets: launch.brokeredCredentials ? withoutBrokeredCredentials(secrets) : secrets } : {},
|
|
8073
5883
|
...Object.keys(grants).length > 0 ? { grants } : {},
|
|
8074
5884
|
dispatch: payload,
|
|
8075
5885
|
turnId,
|
|
@@ -8236,12 +6046,12 @@ import { randomUUID as randomUUID2 } from "crypto";
|
|
|
8236
6046
|
|
|
8237
6047
|
// src/turn-execution.ts
|
|
8238
6048
|
import { createHash as createHash2, randomUUID as randomUUID4 } from "crypto";
|
|
8239
|
-
import { existsSync as
|
|
6049
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync9 } from "fs";
|
|
8240
6050
|
|
|
8241
6051
|
// src/grants.ts
|
|
8242
|
-
import { existsSync as
|
|
8243
|
-
import { join as
|
|
8244
|
-
import { z as
|
|
6052
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
|
|
6053
|
+
import { join as join11 } from "path";
|
|
6054
|
+
import { z as z14 } from "zod";
|
|
8245
6055
|
var VARIABLE_NAME_RE = /^[A-Z_][A-Z0-9_]*$/;
|
|
8246
6056
|
function allowedGrantVariable(name) {
|
|
8247
6057
|
return name.length <= 100 && VARIABLE_NAME_RE.test(name) && !name.startsWith("CABANE_");
|
|
@@ -8250,9 +6060,9 @@ function environmentSafe(value) {
|
|
|
8250
6060
|
return !value.includes("\0");
|
|
8251
6061
|
}
|
|
8252
6062
|
function grantsPath() {
|
|
8253
|
-
return
|
|
6063
|
+
return join11(cabaneDir(), "grants.json");
|
|
8254
6064
|
}
|
|
8255
|
-
var grantsFileSchema =
|
|
6065
|
+
var grantsFileSchema = z14.record(z14.string(), z14.record(z14.string(), z14.string()));
|
|
8256
6066
|
function readGrants(keys, report) {
|
|
8257
6067
|
if (keys.length === 0) return {};
|
|
8258
6068
|
const file = loadGrantsFile(report);
|
|
@@ -8270,7 +6080,7 @@ function readGrants(keys, report) {
|
|
|
8270
6080
|
}
|
|
8271
6081
|
function loadGrantsFile(report) {
|
|
8272
6082
|
const path = grantsPath();
|
|
8273
|
-
if (!
|
|
6083
|
+
if (!existsSync8(path)) return {};
|
|
8274
6084
|
let raw;
|
|
8275
6085
|
try {
|
|
8276
6086
|
raw = readFileSync7(path, "utf8");
|
|
@@ -8299,11 +6109,11 @@ function loadGrantsFile(report) {
|
|
|
8299
6109
|
|
|
8300
6110
|
// src/work-dir.ts
|
|
8301
6111
|
import { statSync as statSync3 } from "fs";
|
|
8302
|
-
import { join as
|
|
6112
|
+
import { join as join12 } from "path";
|
|
8303
6113
|
|
|
8304
6114
|
// src/turn-control-tools.ts
|
|
8305
6115
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
8306
|
-
import { z as
|
|
6116
|
+
import { z as z15 } from "zod";
|
|
8307
6117
|
var COMPANION_LOCAL_MCP_SERVER = TURN_CONTROL_MCP_SERVER;
|
|
8308
6118
|
var COMPANION_LOCAL_TOOL_GLOB = `mcp__${COMPANION_LOCAL_MCP_SERVER}__*`;
|
|
8309
6119
|
var ASK_TOOL = "ask";
|
|
@@ -8316,39 +6126,39 @@ var CANCEL_WAKE_TOOL = "cancel_wake";
|
|
|
8316
6126
|
var CANCEL_WAKE_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${CANCEL_WAKE_TOOL}`;
|
|
8317
6127
|
|
|
8318
6128
|
// src/codex-config.ts
|
|
8319
|
-
import { readFile
|
|
6129
|
+
import { readFile } from "fs/promises";
|
|
8320
6130
|
import { homedir as homedir6 } from "os";
|
|
8321
|
-
import { join as
|
|
8322
|
-
import { parse } from "smol-toml";
|
|
6131
|
+
import { join as join13 } from "path";
|
|
6132
|
+
import { parse as parse2 } from "smol-toml";
|
|
8323
6133
|
|
|
8324
6134
|
// src/codex-images.ts
|
|
8325
6135
|
import { mkdtemp, rm, writeFile } from "fs/promises";
|
|
8326
6136
|
import { tmpdir } from "os";
|
|
8327
|
-
import { join as
|
|
6137
|
+
import { join as join14 } from "path";
|
|
8328
6138
|
|
|
8329
6139
|
// src/codex-compactions.ts
|
|
8330
6140
|
import { createReadStream } from "fs";
|
|
8331
6141
|
import { readdir } from "fs/promises";
|
|
8332
6142
|
import { homedir as homedir7 } from "os";
|
|
8333
|
-
import { join as
|
|
6143
|
+
import { join as join15 } from "path";
|
|
8334
6144
|
import { createInterface as createInterface2 } from "readline";
|
|
8335
6145
|
|
|
8336
6146
|
// src/prepared.ts
|
|
8337
|
-
import { mkdirSync as mkdirSync7, readFileSync as readFileSync8, rmSync as rmSync5, writeFileSync as writeFileSync5, existsSync as
|
|
8338
|
-
import { join as
|
|
6147
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync8, rmSync as rmSync5, writeFileSync as writeFileSync5, existsSync as existsSync9 } from "fs";
|
|
6148
|
+
import { join as join16 } from "path";
|
|
8339
6149
|
|
|
8340
6150
|
// src/secrets.ts
|
|
8341
|
-
import { existsSync as
|
|
8342
|
-
import { join as
|
|
8343
|
-
import { z as
|
|
6151
|
+
import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
|
|
6152
|
+
import { join as join17 } from "path";
|
|
6153
|
+
import { z as z16 } from "zod";
|
|
8344
6154
|
var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
8345
6155
|
function secretsPath() {
|
|
8346
|
-
return
|
|
6156
|
+
return join17(cabaneDir(), "secrets.json");
|
|
8347
6157
|
}
|
|
8348
|
-
var secretStoreSchema =
|
|
6158
|
+
var secretStoreSchema = z16.record(z16.string(), z16.string());
|
|
8349
6159
|
function loadSecretValues() {
|
|
8350
6160
|
const path = secretsPath();
|
|
8351
|
-
if (!
|
|
6161
|
+
if (!existsSync10(path)) return {};
|
|
8352
6162
|
let raw;
|
|
8353
6163
|
try {
|
|
8354
6164
|
raw = readFileSync9(path, "utf8");
|
|
@@ -8533,9 +6343,9 @@ function runBounded(path, args, env) {
|
|
|
8533
6343
|
|
|
8534
6344
|
// src/transcript-writer.ts
|
|
8535
6345
|
import { appendFileSync, chmodSync as chmodSync4, copyFileSync, mkdirSync as mkdirSync8, readdirSync, rmSync as rmSync6 } from "fs";
|
|
8536
|
-
import { basename as basename3, dirname as
|
|
6346
|
+
import { basename as basename3, dirname as dirname6, join as join18 } from "path";
|
|
8537
6347
|
function transcriptsDir() {
|
|
8538
|
-
return
|
|
6348
|
+
return join18(cabaneDir(), "transcripts");
|
|
8539
6349
|
}
|
|
8540
6350
|
|
|
8541
6351
|
// src/turn-execution.ts
|
|
@@ -8645,7 +6455,7 @@ var Dispatcher = class {
|
|
|
8645
6455
|
};
|
|
8646
6456
|
|
|
8647
6457
|
// src/host-report.ts
|
|
8648
|
-
import { existsSync as
|
|
6458
|
+
import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
|
|
8649
6459
|
import { userInfo as userInfo2 } from "os";
|
|
8650
6460
|
function inContainer(signals) {
|
|
8651
6461
|
if (signals.dockerenv) return true;
|
|
@@ -8660,7 +6470,7 @@ function readContainerSignals() {
|
|
|
8660
6470
|
} catch {
|
|
8661
6471
|
cgroup = null;
|
|
8662
6472
|
}
|
|
8663
|
-
return { dockerenv:
|
|
6473
|
+
return { dockerenv: existsSync12("/.dockerenv"), cgroup, env: process.env };
|
|
8664
6474
|
}
|
|
8665
6475
|
function osUser() {
|
|
8666
6476
|
try {
|
|
@@ -8897,7 +6707,7 @@ function checkReason(check) {
|
|
|
8897
6707
|
|
|
8898
6708
|
// src/harness-copy.ts
|
|
8899
6709
|
import { homedir as homedir8 } from "os";
|
|
8900
|
-
import { join as
|
|
6710
|
+
import { join as join19, win32 } from "path";
|
|
8901
6711
|
var HARNESS_LABELS = {
|
|
8902
6712
|
"claude-code": "Claude Code",
|
|
8903
6713
|
codex: "Codex",
|
|
@@ -8911,7 +6721,7 @@ function failureText(runtime, reason) {
|
|
|
8911
6721
|
case "spawn":
|
|
8912
6722
|
return `Couldn't check whether ${label} is signed in: Cabane's bundled ${label} wouldn't start. Repair it with ${repairSteps()} and try again.${spaced(repairCost())}`;
|
|
8913
6723
|
case "config":
|
|
8914
|
-
return `Couldn't check whether ${label} is signed in: its configuration file didn't load${runtime === "codex" ? ` (${tildePath(
|
|
6724
|
+
return `Couldn't check whether ${label} is signed in: its configuration file didn't load${runtime === "codex" ? ` (${tildePath(join19(homedir8(), ".codex", "config.toml"))})` : ""}. Fix it, then try again.`;
|
|
8915
6725
|
case "unreadable":
|
|
8916
6726
|
return `Couldn't check whether ${label} is signed in: it answered in a way Cabane couldn't read. Try again, and if it keeps happening, update Cabane.`;
|
|
8917
6727
|
}
|
|
@@ -8962,12 +6772,12 @@ async function detectHarnesses(cfg, deps = {}) {
|
|
|
8962
6772
|
);
|
|
8963
6773
|
return version !== null ? { state: "usable", via: "serve", version, url } : { state: "not_running", url };
|
|
8964
6774
|
};
|
|
8965
|
-
const [claude, codex,
|
|
6775
|
+
const [claude, codex, serve] = await Promise.all([
|
|
8966
6776
|
bundled("claude-code"),
|
|
8967
6777
|
bundled("codex"),
|
|
8968
6778
|
opencode()
|
|
8969
6779
|
]);
|
|
8970
|
-
return { "claude-code": claude, codex, opencode:
|
|
6780
|
+
return { "claude-code": claude, codex, opencode: serve };
|
|
8971
6781
|
}
|
|
8972
6782
|
function usableRuntimes(detection) {
|
|
8973
6783
|
return HARNESS_RUNTIMES.filter((runtime) => detection[runtime].state === "usable");
|
|
@@ -9168,7 +6978,7 @@ var DispatchClaims = class {
|
|
|
9168
6978
|
|
|
9169
6979
|
// src/outbox.ts
|
|
9170
6980
|
import {
|
|
9171
|
-
existsSync as
|
|
6981
|
+
existsSync as existsSync13,
|
|
9172
6982
|
mkdirSync as mkdirSync10,
|
|
9173
6983
|
readdirSync as readdirSync2,
|
|
9174
6984
|
readFileSync as readFileSync11,
|
|
@@ -9176,7 +6986,7 @@ import {
|
|
|
9176
6986
|
rmSync as rmSync7,
|
|
9177
6987
|
writeFileSync as writeFileSync6
|
|
9178
6988
|
} from "fs";
|
|
9179
|
-
import { join as
|
|
6989
|
+
import { join as join20 } from "path";
|
|
9180
6990
|
var MAX_ENTRIES = 2e3;
|
|
9181
6991
|
var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
9182
6992
|
var Outbox = class {
|
|
@@ -9189,10 +6999,10 @@ var Outbox = class {
|
|
|
9189
6999
|
// Resolved lazily (per call) so tests that swap HOME between
|
|
9190
7000
|
// cases route writes at the right tmpdir.
|
|
9191
7001
|
dir() {
|
|
9192
|
-
return
|
|
7002
|
+
return join20(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
|
|
9193
7003
|
}
|
|
9194
7004
|
fileFor(turnId, seq) {
|
|
9195
|
-
return
|
|
7005
|
+
return join20(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
|
|
9196
7006
|
}
|
|
9197
7007
|
// Persist a commit for later draining. Atomic (temp file + rename) so a
|
|
9198
7008
|
// concurrent `list()` never reads a half-written entry, then enforces the
|
|
@@ -9224,7 +7034,7 @@ var Outbox = class {
|
|
|
9224
7034
|
// wedging the drain.
|
|
9225
7035
|
list() {
|
|
9226
7036
|
const dir2 = this.dir();
|
|
9227
|
-
if (!
|
|
7037
|
+
if (!existsSync13(dir2)) return [];
|
|
9228
7038
|
let names;
|
|
9229
7039
|
try {
|
|
9230
7040
|
names = readdirSync2(dir2);
|
|
@@ -9234,7 +7044,7 @@ var Outbox = class {
|
|
|
9234
7044
|
const entries = [];
|
|
9235
7045
|
for (const name of names) {
|
|
9236
7046
|
if (!name.endsWith(".json")) continue;
|
|
9237
|
-
const full =
|
|
7047
|
+
const full = join20(dir2, name);
|
|
9238
7048
|
try {
|
|
9239
7049
|
const parsed = JSON.parse(readFileSync11(full, "utf8"));
|
|
9240
7050
|
if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
|
|
@@ -9270,7 +7080,7 @@ var Outbox = class {
|
|
|
9270
7080
|
// different: it is positive evidence that nothing was ever queued here.
|
|
9271
7081
|
maxSeqForTurn(turnId) {
|
|
9272
7082
|
const dir2 = this.dir();
|
|
9273
|
-
if (!
|
|
7083
|
+
if (!existsSync13(dir2)) return 0;
|
|
9274
7084
|
const names = readdirSync2(dir2);
|
|
9275
7085
|
const prefix = `${encodeURIComponent(turnId)}__`;
|
|
9276
7086
|
let max = 0;
|
|
@@ -9290,7 +7100,7 @@ var Outbox = class {
|
|
|
9290
7100
|
}
|
|
9291
7101
|
size() {
|
|
9292
7102
|
const dir2 = this.dir();
|
|
9293
|
-
if (!
|
|
7103
|
+
if (!existsSync13(dir2)) return 0;
|
|
9294
7104
|
try {
|
|
9295
7105
|
return readdirSync2(dir2).filter((n) => n.endsWith(".json")).length;
|
|
9296
7106
|
} catch {
|
|
@@ -9457,8 +7267,8 @@ var SseSubscriber = class {
|
|
|
9457
7267
|
|
|
9458
7268
|
// src/updater.ts
|
|
9459
7269
|
import { spawn as spawn5 } from "child_process";
|
|
9460
|
-
import { copyFileSync as copyFileSync2, existsSync as
|
|
9461
|
-
import { dirname as
|
|
7270
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync15, mkdirSync as mkdirSync14, renameSync as renameSync9, rmSync as rmSync10 } from "fs";
|
|
7271
|
+
import { dirname as dirname10, join as join24 } from "path";
|
|
9462
7272
|
|
|
9463
7273
|
// src/boot-rule.ts
|
|
9464
7274
|
import {
|
|
@@ -9470,7 +7280,7 @@ import {
|
|
|
9470
7280
|
symlinkSync,
|
|
9471
7281
|
writeFileSync as writeFileSync7
|
|
9472
7282
|
} from "fs";
|
|
9473
|
-
import { dirname as
|
|
7283
|
+
import { dirname as dirname7, join as join21 } from "path";
|
|
9474
7284
|
var UNCONFIRMED_WINDOW_MS = 10 * 6e4;
|
|
9475
7285
|
var MAX_STAGE_ATTEMPTS = 3;
|
|
9476
7286
|
function superseded(pending, ...running) {
|
|
@@ -9493,7 +7303,7 @@ function rolledBackState(state, pending, reason, now) {
|
|
|
9493
7303
|
|
|
9494
7304
|
// src/install-layout.ts
|
|
9495
7305
|
import {
|
|
9496
|
-
existsSync as
|
|
7306
|
+
existsSync as existsSync14,
|
|
9497
7307
|
lstatSync,
|
|
9498
7308
|
mkdirSync as mkdirSync12,
|
|
9499
7309
|
readFileSync as readFileSync13,
|
|
@@ -9503,26 +7313,26 @@ import {
|
|
|
9503
7313
|
rmSync as rmSync9,
|
|
9504
7314
|
symlinkSync as symlinkSync2
|
|
9505
7315
|
} from "fs";
|
|
9506
|
-
import { dirname as
|
|
9507
|
-
import { fileURLToPath as
|
|
7316
|
+
import { dirname as dirname8, join as join22 } from "path";
|
|
7317
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
9508
7318
|
function versionsDir() {
|
|
9509
|
-
return
|
|
7319
|
+
return join22(cabaneDir(), "versions");
|
|
9510
7320
|
}
|
|
9511
7321
|
function currentLink() {
|
|
9512
|
-
return
|
|
7322
|
+
return join22(cabaneDir(), "current");
|
|
9513
7323
|
}
|
|
9514
7324
|
function bootGuardPath() {
|
|
9515
|
-
return
|
|
7325
|
+
return join22(cabaneDir(), "boot-guard.mjs");
|
|
9516
7326
|
}
|
|
9517
7327
|
function versionPackageRoot(version) {
|
|
9518
|
-
return
|
|
7328
|
+
return join22(versionsDir(), version, "node_modules", "@cabane", "companion");
|
|
9519
7329
|
}
|
|
9520
7330
|
function ownPackageRoot(moduleUrl = import.meta.url) {
|
|
9521
|
-
return
|
|
7331
|
+
return dirname8(dirname8(fileURLToPath4(moduleUrl)));
|
|
9522
7332
|
}
|
|
9523
7333
|
function packageVersion(root) {
|
|
9524
7334
|
try {
|
|
9525
|
-
const pkg2 = JSON.parse(readFileSync13(
|
|
7335
|
+
const pkg2 = JSON.parse(readFileSync13(join22(root, "package.json"), "utf8"));
|
|
9526
7336
|
return typeof pkg2.version === "string" ? pkg2.version : null;
|
|
9527
7337
|
} catch {
|
|
9528
7338
|
return null;
|
|
@@ -9531,14 +7341,14 @@ function packageVersion(root) {
|
|
|
9531
7341
|
function currentRoot() {
|
|
9532
7342
|
try {
|
|
9533
7343
|
const root = realpathSync5(currentLink());
|
|
9534
|
-
return
|
|
7344
|
+
return existsSync14(join22(root, "dist", "cli.js")) ? root : null;
|
|
9535
7345
|
} catch {
|
|
9536
7346
|
return null;
|
|
9537
7347
|
}
|
|
9538
7348
|
}
|
|
9539
7349
|
function pointCurrentAt(root) {
|
|
9540
7350
|
const link = currentLink();
|
|
9541
|
-
mkdirSync12(
|
|
7351
|
+
mkdirSync12(dirname8(link), { recursive: true });
|
|
9542
7352
|
const fresh = `${link}.${process.pid}.${Date.now()}`;
|
|
9543
7353
|
symlinkSync2(root, fresh);
|
|
9544
7354
|
try {
|
|
@@ -9550,11 +7360,11 @@ function pointCurrentAt(root) {
|
|
|
9550
7360
|
}
|
|
9551
7361
|
function pruneVersions(keep) {
|
|
9552
7362
|
const dir2 = versionsDir();
|
|
9553
|
-
if (!
|
|
7363
|
+
if (!existsSync14(dir2)) return [];
|
|
9554
7364
|
const removed = [];
|
|
9555
7365
|
for (const name of readdirSync3(dir2)) {
|
|
9556
7366
|
if (keep.includes(name)) continue;
|
|
9557
|
-
rmSync9(
|
|
7367
|
+
rmSync9(join22(dir2, name), { recursive: true, force: true });
|
|
9558
7368
|
removed.push(name);
|
|
9559
7369
|
}
|
|
9560
7370
|
return removed;
|
|
@@ -9573,31 +7383,31 @@ function compareVersions(a, b) {
|
|
|
9573
7383
|
|
|
9574
7384
|
// src/update-state.ts
|
|
9575
7385
|
import { mkdirSync as mkdirSync13, readFileSync as readFileSync14, renameSync as renameSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
9576
|
-
import { dirname as
|
|
9577
|
-
import { z as
|
|
9578
|
-
var resultSchema =
|
|
9579
|
-
kind:
|
|
9580
|
-
from:
|
|
9581
|
-
to:
|
|
9582
|
-
at:
|
|
9583
|
-
reason:
|
|
7386
|
+
import { dirname as dirname9, join as join23 } from "path";
|
|
7387
|
+
import { z as z17 } from "zod";
|
|
7388
|
+
var resultSchema = z17.object({
|
|
7389
|
+
kind: z17.enum(["updated", "rolled_back", "failed"]),
|
|
7390
|
+
from: z17.string(),
|
|
7391
|
+
to: z17.string(),
|
|
7392
|
+
at: z17.string(),
|
|
7393
|
+
reason: z17.string().optional()
|
|
9584
7394
|
});
|
|
9585
|
-
var pendingSchema =
|
|
9586
|
-
from:
|
|
7395
|
+
var pendingSchema = z17.object({
|
|
7396
|
+
from: z17.string(),
|
|
9587
7397
|
// The package root `current` pointed at before the swap, so a rollback can point it back.
|
|
9588
|
-
fromRoot:
|
|
9589
|
-
to:
|
|
9590
|
-
at:
|
|
9591
|
-
boots:
|
|
7398
|
+
fromRoot: z17.string(),
|
|
7399
|
+
to: z17.string(),
|
|
7400
|
+
at: z17.string(),
|
|
7401
|
+
boots: z17.number().int().nonnegative()
|
|
9592
7402
|
});
|
|
9593
|
-
var stateSchema =
|
|
9594
|
-
server:
|
|
7403
|
+
var stateSchema = z17.object({
|
|
7404
|
+
server: z17.object({ latest: z17.string(), minimum: z17.string(), at: z17.string() }).optional(),
|
|
9595
7405
|
pending: pendingSchema.optional(),
|
|
9596
7406
|
lastResult: resultSchema.optional(),
|
|
9597
|
-
failed:
|
|
7407
|
+
failed: z17.object({ version: z17.string(), attempts: z17.number().int(), at: z17.string() }).optional()
|
|
9598
7408
|
});
|
|
9599
7409
|
function updateStatePath(dir2 = cabaneDir()) {
|
|
9600
|
-
return
|
|
7410
|
+
return join23(dir2, "update.json");
|
|
9601
7411
|
}
|
|
9602
7412
|
function readUpdateState(path = updateStatePath()) {
|
|
9603
7413
|
try {
|
|
@@ -9608,7 +7418,7 @@ function readUpdateState(path = updateStatePath()) {
|
|
|
9608
7418
|
}
|
|
9609
7419
|
}
|
|
9610
7420
|
function writeUpdateState(state, path = updateStatePath()) {
|
|
9611
|
-
mkdirSync13(
|
|
7421
|
+
mkdirSync13(dirname9(path), { recursive: true });
|
|
9612
7422
|
const tmp = `${path}.${process.pid}.tmp`;
|
|
9613
7423
|
writeFileSync8(tmp, `${JSON.stringify(state, null, 2)}
|
|
9614
7424
|
`, { mode: 384 });
|
|
@@ -9642,14 +7452,14 @@ function versionToStage(input) {
|
|
|
9642
7452
|
return latest;
|
|
9643
7453
|
}
|
|
9644
7454
|
function rollBack(pending, reason, now = /* @__PURE__ */ new Date()) {
|
|
9645
|
-
if (
|
|
7455
|
+
if (existsSync15(pending.fromRoot)) pointCurrentAt(pending.fromRoot);
|
|
9646
7456
|
return updateUpdateState((state) => rolledBackState(state, pending, reason, now));
|
|
9647
7457
|
}
|
|
9648
7458
|
function writeBootGuard(root = ownPackageRoot()) {
|
|
9649
|
-
const built =
|
|
9650
|
-
if (!
|
|
7459
|
+
const built = join24(root, "dist", "boot-guard.js");
|
|
7460
|
+
if (!existsSync15(built)) return false;
|
|
9651
7461
|
const target = bootGuardPath();
|
|
9652
|
-
mkdirSync14(
|
|
7462
|
+
mkdirSync14(dirname10(target), { recursive: true });
|
|
9653
7463
|
const tmp = `${target}.${process.pid}.tmp`;
|
|
9654
7464
|
copyFileSync2(built, tmp);
|
|
9655
7465
|
renameSync9(tmp, target);
|
|
@@ -9686,17 +7496,17 @@ var runProcess = (command, args, opts = {}) => new Promise((resolve) => {
|
|
|
9686
7496
|
async function stageVersion(version, harnesses, deps = {}) {
|
|
9687
7497
|
const run = deps.run ?? runProcess;
|
|
9688
7498
|
const node = deps.execPath ?? process.execPath;
|
|
9689
|
-
const target =
|
|
7499
|
+
const target = join24(versionsDir(), version);
|
|
9690
7500
|
const placed = versionPackageRoot(version);
|
|
9691
|
-
if (
|
|
7501
|
+
if (existsSync15(placed)) {
|
|
9692
7502
|
if (!await verifyInstall(placed, version, harnesses, { run, execPath: node }))
|
|
9693
7503
|
return { ok: true, root: placed };
|
|
9694
7504
|
}
|
|
9695
|
-
const npm =
|
|
9696
|
-
if (!
|
|
9697
|
-
return { ok: false, reason: `there is no npm beside Node at ${
|
|
7505
|
+
const npm = join24(dirname10(node), "npm");
|
|
7506
|
+
if (!existsSync15(npm))
|
|
7507
|
+
return { ok: false, reason: `there is no npm beside Node at ${dirname10(node)}` };
|
|
9698
7508
|
mkdirSync14(versionsDir(), { recursive: true });
|
|
9699
|
-
const staging =
|
|
7509
|
+
const staging = join24(versionsDir(), `.${version}.staging-${process.pid}`);
|
|
9700
7510
|
rmSync10(staging, { recursive: true, force: true });
|
|
9701
7511
|
const installed = await run(
|
|
9702
7512
|
npm,
|
|
@@ -9713,7 +7523,7 @@ async function stageVersion(version, harnesses, deps = {}) {
|
|
|
9713
7523
|
],
|
|
9714
7524
|
{
|
|
9715
7525
|
// npm's install scripts run on this Node, not whichever `node` is first on PATH.
|
|
9716
|
-
env: { ...process.env, PATH: `${
|
|
7526
|
+
env: { ...process.env, PATH: `${dirname10(node)}:${process.env.PATH ?? ""}` },
|
|
9717
7527
|
timeoutMs: 15 * 6e4
|
|
9718
7528
|
}
|
|
9719
7529
|
);
|
|
@@ -9725,7 +7535,7 @@ async function stageVersion(version, harnesses, deps = {}) {
|
|
|
9725
7535
|
return { ok: false, reason: `version ${version} isn't published yet`, unpublished: true };
|
|
9726
7536
|
return { ok: false, reason: `npm couldn't install it: ${lastLine(installed.stderr)}` };
|
|
9727
7537
|
}
|
|
9728
|
-
const root =
|
|
7538
|
+
const root = join24(staging, "node_modules", "@cabane", "companion");
|
|
9729
7539
|
const verdict = await verifyInstall(root, version, harnesses, { run, execPath: node });
|
|
9730
7540
|
if (verdict) {
|
|
9731
7541
|
rmSync10(staging, { recursive: true, force: true });
|
|
@@ -9737,7 +7547,7 @@ async function stageVersion(version, harnesses, deps = {}) {
|
|
|
9737
7547
|
}
|
|
9738
7548
|
async function verifyInstall(root, version, harnesses, deps) {
|
|
9739
7549
|
if (packageVersion(root) !== version) return `the installed package isn't version ${version}`;
|
|
9740
|
-
const answered = await deps.run(deps.execPath, [
|
|
7550
|
+
const answered = await deps.run(deps.execPath, [join24(root, "dist", "bin.js"), "--version"], {
|
|
9741
7551
|
timeoutMs: 6e4,
|
|
9742
7552
|
env: { ...process.env, CABANE_LAUNCH_SELF: "1" }
|
|
9743
7553
|
});
|
|
@@ -9745,7 +7555,7 @@ async function verifyInstall(root, version, harnesses, deps) {
|
|
|
9745
7555
|
return `it doesn't start: ${lastLine(answered.stderr) || "no version printed"}`;
|
|
9746
7556
|
}
|
|
9747
7557
|
if (harnesses.length === 0) return null;
|
|
9748
|
-
const probed = await deps.run(deps.execPath, [
|
|
7558
|
+
const probed = await deps.run(deps.execPath, [join24(root, "dist", "postinstall.js"), "--json"], {
|
|
9749
7559
|
timeoutMs: 6e4
|
|
9750
7560
|
});
|
|
9751
7561
|
let resolutions = {};
|
|
@@ -10761,6 +8571,7 @@ var CompanionSupervisor = class {
|
|
|
10761
8571
|
),
|
|
10762
8572
|
...this.config.runner?.command ? {
|
|
10763
8573
|
wrapped: true,
|
|
8574
|
+
brokeredCredentials: this.config.runner.brokeredCredentials,
|
|
10764
8575
|
secrets: () => loadSecretValuesTolerant(
|
|
10765
8576
|
(m) => this.log.warn({ err: m }, "Could not read the secrets file for a wrapped turn.")
|
|
10766
8577
|
)
|
|
@@ -11517,10 +9328,10 @@ function handleUncaught(log, err, origin) {
|
|
|
11517
9328
|
}
|
|
11518
9329
|
|
|
11519
9330
|
// src/crash-marker.ts
|
|
11520
|
-
import { existsSync as
|
|
11521
|
-
import { join as
|
|
9331
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync15, readFileSync as readFileSync15, rmSync as rmSync11, writeFileSync as writeFileSync9 } from "fs";
|
|
9332
|
+
import { join as join25 } from "path";
|
|
11522
9333
|
function crashMarkerPath() {
|
|
11523
|
-
return
|
|
9334
|
+
return join25(cabaneDir(), "last-error.json");
|
|
11524
9335
|
}
|
|
11525
9336
|
function recordCrash(rec) {
|
|
11526
9337
|
try {
|
|
@@ -11532,7 +9343,7 @@ function recordCrash(rec) {
|
|
|
11532
9343
|
function clearCrash() {
|
|
11533
9344
|
try {
|
|
11534
9345
|
const path = crashMarkerPath();
|
|
11535
|
-
if (
|
|
9346
|
+
if (existsSync16(path)) rmSync11(path, { force: true });
|
|
11536
9347
|
} catch {
|
|
11537
9348
|
}
|
|
11538
9349
|
}
|
|
@@ -11540,7 +9351,7 @@ function clearCrash() {
|
|
|
11540
9351
|
// src/stray-companion.ts
|
|
11541
9352
|
import { spawnSync } from "child_process";
|
|
11542
9353
|
import { readFileSync as readFileSync16 } from "fs";
|
|
11543
|
-
import { join as
|
|
9354
|
+
import { join as join26 } from "path";
|
|
11544
9355
|
var COMPANION_ENTRY = /@cabane\/companion\/|cabane-companion|(^|[\s/])cabane\s|\.cabane\/(versions|current|boot-guard)/;
|
|
11545
9356
|
var START = /\sstart(\s|$)/;
|
|
11546
9357
|
function companionStarts(psOutput) {
|
|
@@ -11557,7 +9368,7 @@ function companionStarts(psOutput) {
|
|
|
11557
9368
|
}
|
|
11558
9369
|
function stateDirOf(env) {
|
|
11559
9370
|
const home = env.CABANE_COMPANION_HOME || env.HOME;
|
|
11560
|
-
return home ?
|
|
9371
|
+
return home ? join26(home, ".cabane") : null;
|
|
11561
9372
|
}
|
|
11562
9373
|
function envFromPsE(output, command) {
|
|
11563
9374
|
const line = output.replace(/\n$/, "");
|
|
@@ -11629,7 +9440,7 @@ function strayCompanionLines(strays, retry) {
|
|
|
11629
9440
|
// src/runtime.ts
|
|
11630
9441
|
function embeddedRunner(host) {
|
|
11631
9442
|
if (!host.electron) return null;
|
|
11632
|
-
const cli =
|
|
9443
|
+
const cli = fileURLToPath5(new URL("./cli.js", host.moduleUrl));
|
|
11633
9444
|
return {
|
|
11634
9445
|
runnerCommand: [host.execPath, cli, "run-turn"],
|
|
11635
9446
|
runnerEnv: { ...host.env, ELECTRON_RUN_AS_NODE: "1" }
|
|
@@ -11724,11 +9535,12 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
11724
9535
|
update: (auto) => active2.update(auto),
|
|
11725
9536
|
stop: () => void stop().then(() => active2.requestStop())
|
|
11726
9537
|
});
|
|
11727
|
-
if (opts.
|
|
11728
|
-
const preferredPort =
|
|
11729
|
-
dashboard = await startDashboard({
|
|
9538
|
+
if (opts.startDashboard) {
|
|
9539
|
+
const preferredPort = cfg.dashboardPort;
|
|
9540
|
+
dashboard = await opts.startDashboard({
|
|
11730
9541
|
supervisor,
|
|
11731
9542
|
hub,
|
|
9543
|
+
logPath: companionLogPath(),
|
|
11732
9544
|
...preferredPort !== void 0 ? { port: preferredPort } : {}
|
|
11733
9545
|
});
|
|
11734
9546
|
hub.setDashboardUrl(dashboard.url);
|
|
@@ -11788,7 +9600,10 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
11788
9600
|
};
|
|
11789
9601
|
}
|
|
11790
9602
|
export {
|
|
9603
|
+
ApiError,
|
|
9604
|
+
CompanionError,
|
|
11791
9605
|
createCompanionRuntime,
|
|
9606
|
+
dispatchToWire,
|
|
11792
9607
|
embeddedRunner,
|
|
11793
9608
|
strayCompanionLines
|
|
11794
9609
|
};
|