@jpcasabiancai/cayenne 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +319 -48
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -73,6 +73,38 @@ var init_constants = __esm({
|
|
|
73
73
|
}
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
+
// ../../packages/shared/src/protocol/attachments.ts
|
|
77
|
+
function sniffImage(bytes) {
|
|
78
|
+
if (startsWith(bytes, PNG)) return "image/png";
|
|
79
|
+
if (startsWith(bytes, JPEG)) return "image/jpeg";
|
|
80
|
+
if (startsWith(bytes, GIF87A) || startsWith(bytes, GIF89A)) return "image/gif";
|
|
81
|
+
if (startsWith(bytes, RIFF) && startsWith(bytes, WEBP, 8)) return "image/webp";
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
var ATTACHMENT_MAX_BYTES, ATTACHMENT_MAX_COUNT, ATTACHMENT_MEDIA_TYPES, ATTACHMENT_NAME, startsWith, ascii, PNG, JPEG, GIF87A, GIF89A, RIFF, WEBP;
|
|
85
|
+
var init_attachments = __esm({
|
|
86
|
+
"../../packages/shared/src/protocol/attachments.ts"() {
|
|
87
|
+
"use strict";
|
|
88
|
+
ATTACHMENT_MAX_BYTES = 5 * 1024 * 1024;
|
|
89
|
+
ATTACHMENT_MAX_COUNT = 4;
|
|
90
|
+
ATTACHMENT_MEDIA_TYPES = {
|
|
91
|
+
"image/png": "png",
|
|
92
|
+
"image/jpeg": "jpg",
|
|
93
|
+
"image/webp": "webp",
|
|
94
|
+
"image/gif": "gif"
|
|
95
|
+
};
|
|
96
|
+
ATTACHMENT_NAME = /^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.(png|jpg|webp|gif)$/;
|
|
97
|
+
startsWith = (bytes, prefix, offset = 0) => bytes.length >= offset + prefix.length && prefix.every((b, i) => bytes[offset + i] === b);
|
|
98
|
+
ascii = (s) => Array.from(s, (c) => c.charCodeAt(0));
|
|
99
|
+
PNG = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
100
|
+
JPEG = [255, 216, 255];
|
|
101
|
+
GIF87A = ascii("GIF87a");
|
|
102
|
+
GIF89A = ascii("GIF89a");
|
|
103
|
+
RIFF = ascii("RIFF");
|
|
104
|
+
WEBP = ascii("WEBP");
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
76
108
|
// ../../packages/shared/src/protocol/git.ts
|
|
77
109
|
import { z } from "zod";
|
|
78
110
|
var WORKTREE_NAME, GIT_REF, REF_FORBIDDEN, isGitRef, ABSOLUTE, escapes, NUL, AbsoluteDir, RepoRelativePath, GitRef, WorktreeName, GitPayloads, GitFileStatus, GitStatusFile, GitStatus, GitDiffFile, GitDiff, GitBranch, GitBranches, GitWorktreeCreated, GitWorktreeRemoved;
|
|
@@ -284,6 +316,7 @@ var PermissionMode, PermissionDecision, Provider, Effort, CommandPayloads, kinds
|
|
|
284
316
|
var init_command = __esm({
|
|
285
317
|
"../../packages/shared/src/protocol/command.ts"() {
|
|
286
318
|
"use strict";
|
|
319
|
+
init_attachments();
|
|
287
320
|
init_git();
|
|
288
321
|
init_github();
|
|
289
322
|
init_terminal();
|
|
@@ -357,7 +390,9 @@ var init_command = __esm({
|
|
|
357
390
|
"session.prompt": z4.object({
|
|
358
391
|
text: z4.string().min(1).max(2e5),
|
|
359
392
|
clientUuid: z4.uuid(),
|
|
360
|
-
attachments
|
|
393
|
+
// Images uploaded to the private `attachments` bucket (D37). No mediaType: the runner sniffs
|
|
394
|
+
// the bytes. The runner still checks each path against the session's own row (R31).
|
|
395
|
+
attachments: z4.array(z4.object({ kind: z4.literal("image"), storagePath: z4.string().regex(ATTACHMENT_NAME) })).max(ATTACHMENT_MAX_COUNT).default([]),
|
|
361
396
|
memoryContext: z4.string().max(2e4).optional()
|
|
362
397
|
}),
|
|
363
398
|
"session.permission.respond": z4.object({
|
|
@@ -428,6 +463,7 @@ var ContentBlock, MessageRole, ChatMessageRow, PermissionRequestRow, CodexQuesti
|
|
|
428
463
|
var init_content = __esm({
|
|
429
464
|
"../../packages/shared/src/protocol/content.ts"() {
|
|
430
465
|
"use strict";
|
|
466
|
+
init_attachments();
|
|
431
467
|
ContentBlock = z5.discriminatedUnion("type", [
|
|
432
468
|
z5.object({ type: z5.literal("text"), text: z5.string() }),
|
|
433
469
|
z5.object({ type: z5.literal("thinking"), text: z5.string() }),
|
|
@@ -444,7 +480,14 @@ var init_content = __esm({
|
|
|
444
480
|
isError: z5.boolean().default(false),
|
|
445
481
|
structured: z5.unknown().optional()
|
|
446
482
|
}),
|
|
447
|
-
|
|
483
|
+
// The path is checked here, not only where it is used: the session owner may insert any
|
|
484
|
+
// `content` (0003 `messages_insert`), and a reader's browser signs whatever path a block names.
|
|
485
|
+
// A block that is not a canonical attachment name drops the row rather than steering that request.
|
|
486
|
+
z5.object({
|
|
487
|
+
type: z5.literal("image"),
|
|
488
|
+
storagePath: z5.string().regex(ATTACHMENT_NAME),
|
|
489
|
+
mediaType: z5.enum(Object.keys(ATTACHMENT_MEDIA_TYPES))
|
|
490
|
+
}),
|
|
448
491
|
z5.object({
|
|
449
492
|
type: z5.literal("result"),
|
|
450
493
|
isError: z5.boolean(),
|
|
@@ -667,6 +710,12 @@ var init_runner = __esm({
|
|
|
667
710
|
* `forkSession` only when this is true. Absent reads as false; optional so an older row parses.
|
|
668
711
|
*/
|
|
669
712
|
sessionFork: z8.boolean().optional(),
|
|
713
|
+
/**
|
|
714
|
+
* Whether this runner sends `session.prompt` attachments to the provider (D37, R31, protocol §9).
|
|
715
|
+
* An older runner parses `attachments` and ignores them, so a client offers images only when
|
|
716
|
+
* this is true. Absent reads as false; optional so an older row parses.
|
|
717
|
+
*/
|
|
718
|
+
promptImages: z8.boolean().optional(),
|
|
670
719
|
/**
|
|
671
720
|
* The raw Ed25519 public key of the running process (32 bytes, unpadded base64url), which signs
|
|
672
721
|
* the local server's `hello` (protocol §6.2). New on every start, republished by every capabilities refresh that changes the row;
|
|
@@ -709,6 +758,7 @@ var init_transport = __esm({
|
|
|
709
758
|
var init_protocol = __esm({
|
|
710
759
|
"../../packages/shared/src/protocol/index.ts"() {
|
|
711
760
|
"use strict";
|
|
761
|
+
init_attachments();
|
|
712
762
|
init_command();
|
|
713
763
|
init_content();
|
|
714
764
|
init_events();
|
|
@@ -3111,7 +3161,7 @@ function createTokenVerifier({
|
|
|
3111
3161
|
}
|
|
3112
3162
|
return false;
|
|
3113
3163
|
};
|
|
3114
|
-
const
|
|
3164
|
+
const refuse2 = (k) => {
|
|
3115
3165
|
cache.set(k, { userId: null, exp: 0, until: now() + negativeTtlMs });
|
|
3116
3166
|
return { status: "refused" };
|
|
3117
3167
|
};
|
|
@@ -3122,19 +3172,19 @@ function createTokenVerifier({
|
|
|
3122
3172
|
const hit = cache.get(k);
|
|
3123
3173
|
if (hit && hit.until > now()) {
|
|
3124
3174
|
if (hit.userId === null) return { status: "refused" };
|
|
3125
|
-
if (hit.exp * 1e3 <= now()) return
|
|
3175
|
+
if (hit.exp * 1e3 <= now()) return refuse2(k);
|
|
3126
3176
|
return { status: "valid", userId: hit.userId, exp: hit.exp };
|
|
3127
3177
|
}
|
|
3128
3178
|
cache.delete(k);
|
|
3129
3179
|
const claims = claimsOf(token);
|
|
3130
|
-
if (!claims || claims.exp * 1e3 <= now() || !admit(claims.sub)) return
|
|
3180
|
+
if (!claims || claims.exp * 1e3 <= now() || !admit(claims.sub)) return refuse2(k);
|
|
3131
3181
|
if (!budgeted()) return { status: "unavailable" };
|
|
3132
3182
|
const user = await db.auth.getUser(token).then(
|
|
3133
3183
|
(r) => r.error ? retryable(r.error) ? void 0 : null : r.data.user,
|
|
3134
3184
|
() => void 0
|
|
3135
3185
|
);
|
|
3136
3186
|
if (user === void 0) return { status: "unavailable" };
|
|
3137
|
-
if (!user || user.id !== claims.sub) return
|
|
3187
|
+
if (!user || user.id !== claims.sub) return refuse2(k);
|
|
3138
3188
|
cache.set(k, { userId: user.id, exp: claims.exp, until: now() + ttlMs });
|
|
3139
3189
|
return { status: "valid", userId: user.id, exp: claims.exp };
|
|
3140
3190
|
}
|
|
@@ -3500,7 +3550,7 @@ function createLocalServer({
|
|
|
3500
3550
|
server.headersTimeout = LOCAL_HANDSHAKE_TIMEOUT_MS;
|
|
3501
3551
|
server.requestTimeout = LOCAL_HANDSHAKE_TIMEOUT_MS;
|
|
3502
3552
|
server.keepAliveTimeout = 1e3;
|
|
3503
|
-
const
|
|
3553
|
+
const refuse2 = (socket, status) => {
|
|
3504
3554
|
const text = status === 403 ? "Forbidden" : "Service Unavailable";
|
|
3505
3555
|
socket.write(`HTTP/1.1 ${status} ${text}\r
|
|
3506
3556
|
Connection: close\r
|
|
@@ -3512,11 +3562,11 @@ Content-Length: 0\r
|
|
|
3512
3562
|
server.on("upgrade", (req, socket, head) => {
|
|
3513
3563
|
const origin = req.headers.origin ?? "";
|
|
3514
3564
|
if (req.url !== LOCAL_WS_PATH || !hostAllowed(req.headers.host) || !originAllowed(origin, origins)) {
|
|
3515
|
-
|
|
3565
|
+
refuse2(socket, 403);
|
|
3516
3566
|
return;
|
|
3517
3567
|
}
|
|
3518
3568
|
if (pendingUpgrades >= LOCAL_MAX_PENDING_UPGRADES || conns.size >= maxSockets) {
|
|
3519
|
-
|
|
3569
|
+
refuse2(socket, 503);
|
|
3520
3570
|
return;
|
|
3521
3571
|
}
|
|
3522
3572
|
pendingUpgrades += 1;
|
|
@@ -3860,6 +3910,69 @@ var init_roster = __esm({
|
|
|
3860
3910
|
}
|
|
3861
3911
|
});
|
|
3862
3912
|
|
|
3913
|
+
// src/sessions/attachments.ts
|
|
3914
|
+
import { rmSync as rmSync5 } from "fs";
|
|
3915
|
+
import { join as join15 } from "path";
|
|
3916
|
+
async function loadAttachments(db, scope, attachments) {
|
|
3917
|
+
const prefix = `${scope.workspaceId.toLowerCase()}/${scope.sessionId.toLowerCase()}/`;
|
|
3918
|
+
const named = attachments.map(({ storagePath }, i) => {
|
|
3919
|
+
const match = ATTACHMENT_NAME.exec(storagePath);
|
|
3920
|
+
const declared = match?.[3] === void 0 ? void 0 : TYPE_OF_EXT.get(match[3]);
|
|
3921
|
+
if (!match || !storagePath.startsWith(prefix) || !declared)
|
|
3922
|
+
throw refuse(i + 1, "not in this chat");
|
|
3923
|
+
return { storagePath, declared };
|
|
3924
|
+
});
|
|
3925
|
+
const loaded = [];
|
|
3926
|
+
for (const [i, { storagePath, declared }] of named.entries()) {
|
|
3927
|
+
const n = i + 1;
|
|
3928
|
+
let blob;
|
|
3929
|
+
try {
|
|
3930
|
+
const { data: data2, error } = await db.storage.from(ATTACHMENTS_BUCKET).download(storagePath);
|
|
3931
|
+
if (error || !data2) throw refuse(n, "could not be read");
|
|
3932
|
+
blob = data2;
|
|
3933
|
+
} catch {
|
|
3934
|
+
throw refuse(n, "could not be read");
|
|
3935
|
+
}
|
|
3936
|
+
if (blob.size > ATTACHMENT_MAX_BYTES) throw refuse(n, "too large");
|
|
3937
|
+
let data;
|
|
3938
|
+
try {
|
|
3939
|
+
data = Buffer.from(await blob.arrayBuffer());
|
|
3940
|
+
} catch {
|
|
3941
|
+
throw refuse(n, "could not be read");
|
|
3942
|
+
}
|
|
3943
|
+
if (data.byteLength > ATTACHMENT_MAX_BYTES) throw refuse(n, "too large");
|
|
3944
|
+
const sniffed = sniffImage(data);
|
|
3945
|
+
if (!sniffed) throw refuse(n, "not a supported image");
|
|
3946
|
+
if (sniffed !== declared) throw refuse(n, "not the image type its name says");
|
|
3947
|
+
loaded.push({ storagePath, mediaType: sniffed, data });
|
|
3948
|
+
}
|
|
3949
|
+
return loaded;
|
|
3950
|
+
}
|
|
3951
|
+
function attachmentsTempDir(home, runnerId, sessionId) {
|
|
3952
|
+
return join15(home, "tmp", "attachments", runnerId, sessionId);
|
|
3953
|
+
}
|
|
3954
|
+
function assertStagingInsideHome(home, dir) {
|
|
3955
|
+
return assertInsideRootsOrAncestor(dir, [home]);
|
|
3956
|
+
}
|
|
3957
|
+
function clearAttachmentsTemp(home, runnerId) {
|
|
3958
|
+
const dir = join15(home, "tmp", "attachments", runnerId);
|
|
3959
|
+
assertStagingInsideHome(home, dir);
|
|
3960
|
+
rmSync5(dir, { recursive: true, force: true });
|
|
3961
|
+
}
|
|
3962
|
+
var ATTACHMENTS_BUCKET, TYPE_OF_EXT, refuse;
|
|
3963
|
+
var init_attachments2 = __esm({
|
|
3964
|
+
"src/sessions/attachments.ts"() {
|
|
3965
|
+
"use strict";
|
|
3966
|
+
init_src();
|
|
3967
|
+
init_paths();
|
|
3968
|
+
ATTACHMENTS_BUCKET = "attachments";
|
|
3969
|
+
TYPE_OF_EXT = new Map(
|
|
3970
|
+
Object.entries(ATTACHMENT_MEDIA_TYPES).map(([type, ext]) => [ext, type])
|
|
3971
|
+
);
|
|
3972
|
+
refuse = (n, reason) => new Error(`VALIDATION: attachment ${n}: ${reason}`);
|
|
3973
|
+
}
|
|
3974
|
+
});
|
|
3975
|
+
|
|
3863
3976
|
// src/github/gh.ts
|
|
3864
3977
|
import { execFile as execFile4 } from "child_process";
|
|
3865
3978
|
import { promisify as promisify3 } from "util";
|
|
@@ -4459,10 +4572,21 @@ var init_session2 = __esm({
|
|
|
4459
4572
|
});
|
|
4460
4573
|
this.pumping = this.pump();
|
|
4461
4574
|
}
|
|
4462
|
-
prompt(text, uuid) {
|
|
4575
|
+
prompt(text, uuid, { images }) {
|
|
4576
|
+
const content = images.length ? [
|
|
4577
|
+
...images.map((image) => ({
|
|
4578
|
+
type: "image",
|
|
4579
|
+
source: {
|
|
4580
|
+
type: "base64",
|
|
4581
|
+
media_type: image.mediaType,
|
|
4582
|
+
data: image.data.toString("base64")
|
|
4583
|
+
}
|
|
4584
|
+
})),
|
|
4585
|
+
{ type: "text", text }
|
|
4586
|
+
] : text;
|
|
4463
4587
|
this.inbox.push({
|
|
4464
4588
|
type: "user",
|
|
4465
|
-
message: { role: "user", content
|
|
4589
|
+
message: { role: "user", content },
|
|
4466
4590
|
parent_tool_use_id: null,
|
|
4467
4591
|
uuid,
|
|
4468
4592
|
session_id: this.providerSessionId ?? void 0
|
|
@@ -4691,7 +4815,13 @@ var init_policy = __esm({
|
|
|
4691
4815
|
|
|
4692
4816
|
// src/codex/session.ts
|
|
4693
4817
|
import { spawn as spawn2 } from "child_process";
|
|
4818
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
4694
4819
|
import { EventEmitter as EventEmitter2 } from "events";
|
|
4820
|
+
import { chmodSync as chmodSync4, lstatSync as lstatSync4, mkdirSync as mkdirSync6, rmdirSync, rmSync as rmSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
4821
|
+
import { dirname as dirname7, join as join16 } from "path";
|
|
4822
|
+
function removeFiles(files) {
|
|
4823
|
+
for (const file of files) rmSync6(file, { force: true });
|
|
4824
|
+
}
|
|
4695
4825
|
function scrubSecrets(text) {
|
|
4696
4826
|
return SECRET_PATTERNS.reduce((acc, [pattern, to]) => acc.replace(pattern, to), text);
|
|
4697
4827
|
}
|
|
@@ -4700,6 +4830,7 @@ var init_session3 = __esm({
|
|
|
4700
4830
|
"src/codex/session.ts"() {
|
|
4701
4831
|
"use strict";
|
|
4702
4832
|
init_src();
|
|
4833
|
+
init_attachments2();
|
|
4703
4834
|
init_normalize2();
|
|
4704
4835
|
init_policy();
|
|
4705
4836
|
init_rpc();
|
|
@@ -4743,6 +4874,8 @@ var init_session3 = __esm({
|
|
|
4743
4874
|
turnId = null;
|
|
4744
4875
|
running = false;
|
|
4745
4876
|
queue = [];
|
|
4877
|
+
/** The image files of the turn in flight, removed when it ends. */
|
|
4878
|
+
turnFiles = [];
|
|
4746
4879
|
approvals = /* @__PURE__ */ new Map();
|
|
4747
4880
|
streaming = /* @__PURE__ */ new Set();
|
|
4748
4881
|
stderrTail = [];
|
|
@@ -4766,6 +4899,7 @@ var init_session3 = __esm({
|
|
|
4766
4899
|
const tail = this.stderrTail.length ? `
|
|
4767
4900
|
${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
|
|
4768
4901
|
const error = this.stopping || code === 0 ? null : `PROVIDER_ERROR: codex exited with ${code}${tail}`;
|
|
4902
|
+
this.removeAllFiles();
|
|
4769
4903
|
this.emit("exit", { error });
|
|
4770
4904
|
});
|
|
4771
4905
|
const rpc = new JsonRpc(child.stdin, child.stdout);
|
|
@@ -4805,19 +4939,88 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
|
|
|
4805
4939
|
});
|
|
4806
4940
|
this.flush();
|
|
4807
4941
|
}
|
|
4808
|
-
|
|
4809
|
-
|
|
4942
|
+
/**
|
|
4943
|
+
* Queue one turn with images `stageImages` already wrote. From here the session owns the files
|
|
4944
|
+
* and removes them when the turn ends, fails, or the session stops.
|
|
4945
|
+
*/
|
|
4946
|
+
prompt(text, _uuid, staged) {
|
|
4947
|
+
if (staged.files.length !== staged.images.length) {
|
|
4948
|
+
throw new Error("PROVIDER_ERROR: codex images must be staged before the prompt");
|
|
4949
|
+
}
|
|
4950
|
+
this.queue.push({ text, files: [...staged.files] });
|
|
4810
4951
|
this.flush();
|
|
4811
4952
|
}
|
|
4953
|
+
/**
|
|
4954
|
+
* Write the images Codex will read, synchronously, before the manager persists anything. A
|
|
4955
|
+
* failure removes whatever was written and throws, so a refused prompt leaves no file behind.
|
|
4956
|
+
*/
|
|
4957
|
+
stageImages(images) {
|
|
4958
|
+
const files = this.writeImages(images);
|
|
4959
|
+
return { images, files, discard: () => removeFiles(files) };
|
|
4960
|
+
}
|
|
4961
|
+
writeImages(images) {
|
|
4962
|
+
if (images.length === 0) return [];
|
|
4963
|
+
const dir = this.opts.attachmentsDir;
|
|
4964
|
+
const home = this.opts.attachmentsHome;
|
|
4965
|
+
if (!dir || !home) {
|
|
4966
|
+
throw new Error("PROVIDER_ERROR: this codex session has no attachments directory");
|
|
4967
|
+
}
|
|
4968
|
+
const written = [];
|
|
4969
|
+
try {
|
|
4970
|
+
assertStagingInsideHome(home, dir);
|
|
4971
|
+
mkdirSync6(dir, { recursive: true, mode: 448 });
|
|
4972
|
+
assertStagingInsideHome(home, dir);
|
|
4973
|
+
for (const path of [dirname7(dir), dir]) {
|
|
4974
|
+
const st = lstatSync4(path);
|
|
4975
|
+
if (st.isSymbolicLink() || !st.isDirectory()) {
|
|
4976
|
+
throw new Error(`${path} is not a plain directory`);
|
|
4977
|
+
}
|
|
4978
|
+
}
|
|
4979
|
+
chmodSync4(dir, 448);
|
|
4980
|
+
for (const image of images) {
|
|
4981
|
+
const path = join16(dir, `${randomUUID4()}.${ATTACHMENT_MEDIA_TYPES[image.mediaType]}`);
|
|
4982
|
+
writeFileSync5(path, image.data, { mode: 384, flag: "wx" });
|
|
4983
|
+
written.push(path);
|
|
4984
|
+
}
|
|
4985
|
+
} catch (err) {
|
|
4986
|
+
removeFiles(written);
|
|
4987
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
4988
|
+
throw new Error(`PROVIDER_ERROR: could not write an image for codex: ${detail}`);
|
|
4989
|
+
}
|
|
4990
|
+
return written;
|
|
4991
|
+
}
|
|
4992
|
+
/** The turn in flight is over: its images have been read, or never will be. */
|
|
4993
|
+
endTurnFiles() {
|
|
4994
|
+
removeFiles(this.turnFiles);
|
|
4995
|
+
this.turnFiles = [];
|
|
4996
|
+
}
|
|
4997
|
+
removeAllFiles() {
|
|
4998
|
+
this.endTurnFiles();
|
|
4999
|
+
for (const turn of this.queue) {
|
|
5000
|
+
removeFiles(turn.files);
|
|
5001
|
+
turn.files = [];
|
|
5002
|
+
}
|
|
5003
|
+
if (this.opts.attachmentsDir) {
|
|
5004
|
+
try {
|
|
5005
|
+
rmdirSync(this.opts.attachmentsDir);
|
|
5006
|
+
} catch {
|
|
5007
|
+
}
|
|
5008
|
+
}
|
|
5009
|
+
}
|
|
4812
5010
|
/** One turn at a time: Codex has no notion of a second concurrent turn on a thread. */
|
|
4813
5011
|
flush() {
|
|
4814
5012
|
if (this.running || !this.rpc || !this.providerSessionId) return;
|
|
4815
5013
|
const next = this.queue.shift();
|
|
4816
5014
|
if (next === void 0) return;
|
|
4817
5015
|
this.running = true;
|
|
5016
|
+
this.turnFiles = next.files;
|
|
4818
5017
|
void this.rpc.request("turn/start", {
|
|
4819
5018
|
threadId: this.providerSessionId,
|
|
4820
|
-
|
|
5019
|
+
// app-server v2 `UserInput`: images as `localImage` paths, then the text.
|
|
5020
|
+
input: [
|
|
5021
|
+
...next.files.map((path) => ({ type: "localImage", path })),
|
|
5022
|
+
{ type: "text", text: next.text }
|
|
5023
|
+
],
|
|
4821
5024
|
sandboxPolicy: this.sandbox,
|
|
4822
5025
|
...this.pendingModel ? { model: this.pendingModel } : {},
|
|
4823
5026
|
...this.pendingEffort ? { effort: this.pendingEffort } : {}
|
|
@@ -4825,6 +5028,7 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
|
|
|
4825
5028
|
this.turnId = res.turnId ?? null;
|
|
4826
5029
|
}).catch((err) => {
|
|
4827
5030
|
this.running = false;
|
|
5031
|
+
this.endTurnFiles();
|
|
4828
5032
|
this.emit("result", {
|
|
4829
5033
|
isError: true,
|
|
4830
5034
|
durationMs: 0,
|
|
@@ -4861,6 +5065,7 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
|
|
|
4861
5065
|
case "turn/failed": {
|
|
4862
5066
|
this.running = false;
|
|
4863
5067
|
this.turnId = null;
|
|
5068
|
+
this.endTurnFiles();
|
|
4864
5069
|
const status = params.status ?? (method === "turn/failed" ? "failed" : "completed");
|
|
4865
5070
|
this.emit("result", {
|
|
4866
5071
|
isError: status === "failed",
|
|
@@ -5015,6 +5220,7 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
|
|
|
5015
5220
|
this.emit("permissionTimeout", { id });
|
|
5016
5221
|
}
|
|
5017
5222
|
await this.interrupt();
|
|
5223
|
+
this.removeAllFiles();
|
|
5018
5224
|
this.rpc?.close();
|
|
5019
5225
|
this.child?.stdin?.end();
|
|
5020
5226
|
const child = this.child;
|
|
@@ -5047,8 +5253,22 @@ var init_create = __esm({
|
|
|
5047
5253
|
}
|
|
5048
5254
|
});
|
|
5049
5255
|
|
|
5256
|
+
// src/providers/types.ts
|
|
5257
|
+
var inMemoryImages;
|
|
5258
|
+
var init_types = __esm({
|
|
5259
|
+
"src/providers/types.ts"() {
|
|
5260
|
+
"use strict";
|
|
5261
|
+
inMemoryImages = (images) => ({
|
|
5262
|
+
images,
|
|
5263
|
+
files: [],
|
|
5264
|
+
discard: () => {
|
|
5265
|
+
}
|
|
5266
|
+
});
|
|
5267
|
+
}
|
|
5268
|
+
});
|
|
5269
|
+
|
|
5050
5270
|
// src/sessions/manager.ts
|
|
5051
|
-
import { randomUUID as
|
|
5271
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
5052
5272
|
import { realpathSync as realpathSync7 } from "fs";
|
|
5053
5273
|
import { isAbsolute as isAbsolute10, sep as sep5 } from "path";
|
|
5054
5274
|
import { isDeepStrictEqual } from "util";
|
|
@@ -5077,12 +5297,15 @@ var init_manager = __esm({
|
|
|
5077
5297
|
"src/sessions/manager.ts"() {
|
|
5078
5298
|
"use strict";
|
|
5079
5299
|
init_src();
|
|
5300
|
+
init_config();
|
|
5080
5301
|
init_introspect();
|
|
5081
5302
|
init_watch();
|
|
5082
5303
|
init_worktree();
|
|
5083
5304
|
init_worktree_owners();
|
|
5084
5305
|
init_paths();
|
|
5085
5306
|
init_create();
|
|
5307
|
+
init_types();
|
|
5308
|
+
init_attachments2();
|
|
5086
5309
|
isAtOrBelow = (p, dir) => p === dir || p.startsWith(dir.endsWith(sep5) ? dir : `${dir}${sep5}`);
|
|
5087
5310
|
DENY_MESSAGE_MAX = 2e3;
|
|
5088
5311
|
EXIT_ERROR_MAX = 8 * 1024;
|
|
@@ -5188,6 +5411,12 @@ var init_manager = __esm({
|
|
|
5188
5411
|
...provider === "codex" ? {
|
|
5189
5412
|
codexPath: this.deps.codexPath,
|
|
5190
5413
|
checkInput: this.codexGate(sessionId, cwd),
|
|
5414
|
+
attachmentsHome: this.deps.cayenneHome ?? cayenneHome(),
|
|
5415
|
+
attachmentsDir: attachmentsTempDir(
|
|
5416
|
+
this.deps.cayenneHome ?? cayenneHome(),
|
|
5417
|
+
this.deps.runnerId,
|
|
5418
|
+
sessionId
|
|
5419
|
+
),
|
|
5191
5420
|
...p.effort ? { effort: p.effort } : {}
|
|
5192
5421
|
} : this.isolation(createdBy)
|
|
5193
5422
|
});
|
|
@@ -5217,30 +5446,61 @@ var init_manager = __esm({
|
|
|
5217
5446
|
if (p.initialPrompt) {
|
|
5218
5447
|
await this.prompt(
|
|
5219
5448
|
sessionId,
|
|
5220
|
-
{ text: p.initialPrompt, clientUuid:
|
|
5449
|
+
{ text: p.initialPrompt, clientUuid: randomUUID5(), attachments: [] },
|
|
5221
5450
|
createdBy
|
|
5222
5451
|
);
|
|
5223
5452
|
}
|
|
5224
5453
|
return { providerSessionId: live.session.providerSessionId };
|
|
5225
5454
|
}
|
|
5226
5455
|
async prompt(sessionId, p, createdBy) {
|
|
5456
|
+
this.assertCodexOwner(sessionId, createdBy);
|
|
5457
|
+
const images = p.attachments.length ? await loadAttachments(
|
|
5458
|
+
this.deps.db,
|
|
5459
|
+
{ workspaceId: await this.sessionWorkspace(sessionId), sessionId },
|
|
5460
|
+
p.attachments
|
|
5461
|
+
) : [];
|
|
5227
5462
|
const live = await this.ensureLive(sessionId, createdBy);
|
|
5228
5463
|
this.assertCodexOwner(sessionId, createdBy);
|
|
5229
5464
|
const text = p.memoryContext ? wrapMemory(p.memoryContext, p.text) : p.text;
|
|
5230
|
-
live.
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5465
|
+
const staged = images.length ? live.session.stageImages?.(images) ?? inMemoryImages(images) : inMemoryImages([]);
|
|
5466
|
+
try {
|
|
5467
|
+
live.echoed.add(p.clientUuid);
|
|
5468
|
+
await this.persist(
|
|
5469
|
+
sessionId,
|
|
5470
|
+
live,
|
|
5471
|
+
"user",
|
|
5472
|
+
[
|
|
5473
|
+
{ type: "text", text: p.text },
|
|
5474
|
+
...images.map(
|
|
5475
|
+
(image) => ({
|
|
5476
|
+
type: "image",
|
|
5477
|
+
storagePath: image.storagePath,
|
|
5478
|
+
mediaType: image.mediaType
|
|
5479
|
+
})
|
|
5480
|
+
)
|
|
5481
|
+
],
|
|
5482
|
+
p.clientUuid,
|
|
5483
|
+
null,
|
|
5484
|
+
null
|
|
5485
|
+
);
|
|
5486
|
+
await this.setStatus(sessionId, "running");
|
|
5487
|
+
live.session.prompt(text, p.clientUuid, staged);
|
|
5488
|
+
} catch (err) {
|
|
5489
|
+
staged.discard();
|
|
5490
|
+
throw err;
|
|
5491
|
+
}
|
|
5242
5492
|
return { accepted: true };
|
|
5243
5493
|
}
|
|
5494
|
+
/**
|
|
5495
|
+
* The session row's own workspace, which R31 checks attachment paths against. Read, never taken
|
|
5496
|
+
* from the payload or assumed to be the runner's: the path prefix is the whole of the check.
|
|
5497
|
+
*/
|
|
5498
|
+
async sessionWorkspace(sessionId) {
|
|
5499
|
+
const { data, error } = await this.deps.db.from("chat_sessions").select("workspace_id").eq("id", sessionId).eq("runner_id", this.deps.runnerId).maybeSingle();
|
|
5500
|
+
if (error) this.deps.log.warn({ err: error.message, sessionId }, "session read failed");
|
|
5501
|
+
if (!data?.workspace_id) throw new Error("SESSION_NOT_FOUND");
|
|
5502
|
+
return data.workspace_id;
|
|
5503
|
+
}
|
|
5244
5504
|
/**
|
|
5245
5505
|
* Refuse unless `requestId` is a request this session is still holding.
|
|
5246
5506
|
*
|
|
@@ -6350,9 +6610,9 @@ function createTerminalChannels({
|
|
|
6350
6610
|
const inflight = joining.get(terminalId);
|
|
6351
6611
|
if (inflight) return inflight;
|
|
6352
6612
|
if (joined.has(terminalId)) return;
|
|
6353
|
-
const
|
|
6354
|
-
joining.set(terminalId,
|
|
6355
|
-
return
|
|
6613
|
+
const join19 = attachOne(terminalId).finally(() => joining.delete(terminalId));
|
|
6614
|
+
joining.set(terminalId, join19);
|
|
6615
|
+
return join19;
|
|
6356
6616
|
},
|
|
6357
6617
|
flush,
|
|
6358
6618
|
detach,
|
|
@@ -6409,7 +6669,7 @@ var init_channels = __esm({
|
|
|
6409
6669
|
// src/terminal/env.ts
|
|
6410
6670
|
import { readFileSync as readFileSync7, realpathSync as realpathSync8 } from "fs";
|
|
6411
6671
|
import { lstat } from "fs/promises";
|
|
6412
|
-
import { dirname as
|
|
6672
|
+
import { dirname as dirname8, isAbsolute as isAbsolute11, relative as relative4 } from "path";
|
|
6413
6673
|
function terminalEnv(base, { runnerOwner, claudeConfigDir }) {
|
|
6414
6674
|
const env = {};
|
|
6415
6675
|
for (const [key, value] of Object.entries(base)) {
|
|
@@ -6465,7 +6725,7 @@ async function rootOwnedPath(fs, real) {
|
|
|
6465
6725
|
return false;
|
|
6466
6726
|
}
|
|
6467
6727
|
if (st.isSymbolicLink() || st.uid !== 0 || (st.mode & 18) !== 0) return false;
|
|
6468
|
-
const parent =
|
|
6728
|
+
const parent = dirname8(path);
|
|
6469
6729
|
if (parent === path) return true;
|
|
6470
6730
|
path = parent;
|
|
6471
6731
|
}
|
|
@@ -7449,7 +7709,7 @@ __export(start_exports, {
|
|
|
7449
7709
|
start: () => start
|
|
7450
7710
|
});
|
|
7451
7711
|
import { homedir as homedir3, hostname } from "os";
|
|
7452
|
-
import { join as
|
|
7712
|
+
import { join as join17 } from "path";
|
|
7453
7713
|
function protectedPaths(claudeConfigDir, home = homedir3()) {
|
|
7454
7714
|
const inHome = [
|
|
7455
7715
|
".claude",
|
|
@@ -7457,12 +7717,12 @@ function protectedPaths(claudeConfigDir, home = homedir3()) {
|
|
|
7457
7717
|
".codex",
|
|
7458
7718
|
".ssh",
|
|
7459
7719
|
".aws",
|
|
7460
|
-
|
|
7720
|
+
join17(".config", "gh"),
|
|
7461
7721
|
".gnupg",
|
|
7462
7722
|
".git-credentials",
|
|
7463
7723
|
".npmrc",
|
|
7464
7724
|
".netrc"
|
|
7465
|
-
].map((p) =>
|
|
7725
|
+
].map((p) => join17(home, p));
|
|
7466
7726
|
return [cayenneHome(), ...claudeConfigDir ? [claudeConfigDir] : [], ...inHome];
|
|
7467
7727
|
}
|
|
7468
7728
|
function logCodex(codex) {
|
|
@@ -7610,7 +7870,9 @@ async function compose(cfg, lock, host, preflight) {
|
|
|
7610
7870
|
// Who the runner's own config names (D34); every heartbeat refresh republishes it.
|
|
7611
7871
|
memberTerminalIds: [...memberTerminalIds],
|
|
7612
7872
|
// D36: this build forks a Claude session on `session.start { forkSession: true }`.
|
|
7613
|
-
sessionFork: true
|
|
7873
|
+
sessionFork: true,
|
|
7874
|
+
// D37: this build loads image attachments on `session.prompt` under R31.
|
|
7875
|
+
promptImages: true
|
|
7614
7876
|
});
|
|
7615
7877
|
const registered = capabilities();
|
|
7616
7878
|
let loggedCodex = null;
|
|
@@ -7644,6 +7906,14 @@ async function compose(cfg, lock, host, preflight) {
|
|
|
7644
7906
|
await local.close();
|
|
7645
7907
|
throw err;
|
|
7646
7908
|
}
|
|
7909
|
+
try {
|
|
7910
|
+
clearAttachmentsTemp(cayenneHome(), identity.runnerId);
|
|
7911
|
+
} catch (err) {
|
|
7912
|
+
logger.warn(
|
|
7913
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
7914
|
+
"could not clear staged attachment images"
|
|
7915
|
+
);
|
|
7916
|
+
}
|
|
7647
7917
|
let stopHeartbeat = null;
|
|
7648
7918
|
let roster = null;
|
|
7649
7919
|
try {
|
|
@@ -7669,9 +7939,9 @@ async function compose(cfg, lock, host, preflight) {
|
|
|
7669
7939
|
// The roster's own array, mutated in place: `serves` reads the live list with no restart.
|
|
7670
7940
|
allowedMemberIds: liveRoster.allowedMemberIds,
|
|
7671
7941
|
// Outside every profile and repository: the creator of a worktree is the machine's record.
|
|
7672
|
-
worktreeOwners: createWorktreeOwners(
|
|
7942
|
+
worktreeOwners: createWorktreeOwners(join17(cayenneHome(), "worktrees")),
|
|
7673
7943
|
// Beside it: where each session was spawned, the only source of a resumed directory.
|
|
7674
|
-
sessionWorktrees: createSessionWorktrees(
|
|
7944
|
+
sessionWorktrees: createSessionWorktrees(join17(cayenneHome(), "session-worktrees")),
|
|
7675
7945
|
roots,
|
|
7676
7946
|
allowBypass: cfg.allowBypassPermissions,
|
|
7677
7947
|
codexPath,
|
|
@@ -7931,6 +8201,7 @@ var init_start = __esm({
|
|
|
7931
8201
|
init_profile();
|
|
7932
8202
|
init_registry();
|
|
7933
8203
|
init_roster();
|
|
8204
|
+
init_attachments2();
|
|
7934
8205
|
init_handlers();
|
|
7935
8206
|
init_manager();
|
|
7936
8207
|
init_supabase();
|
|
@@ -7944,9 +8215,9 @@ var init_start = __esm({
|
|
|
7944
8215
|
});
|
|
7945
8216
|
|
|
7946
8217
|
// src/cli.ts
|
|
7947
|
-
import { existsSync as existsSync5, mkdirSync as
|
|
8218
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync7, readFileSync as readFileSync8, realpathSync as realpathSync9, rmSync as rmSync7, writeFileSync as writeFileSync6 } from "fs";
|
|
7948
8219
|
import { homedir as homedir4, hostname as hostname2, userInfo } from "os";
|
|
7949
|
-
import { dirname as
|
|
8220
|
+
import { dirname as dirname9, join as join18 } from "path";
|
|
7950
8221
|
import { Command } from "commander";
|
|
7951
8222
|
import prompts from "prompts";
|
|
7952
8223
|
|
|
@@ -8401,8 +8672,8 @@ var service = () => {
|
|
|
8401
8672
|
return { exec: exec4, launchd: makeLaunchd(exec4, userInfo().uid), label: plistLabel(currentProfile()) };
|
|
8402
8673
|
};
|
|
8403
8674
|
var writePlistFile = (path, xml2) => {
|
|
8404
|
-
|
|
8405
|
-
|
|
8675
|
+
mkdirSync7(dirname9(path), { recursive: true });
|
|
8676
|
+
writeFileSync6(path, xml2, { mode: 420 });
|
|
8406
8677
|
};
|
|
8407
8678
|
var requireMac = () => {
|
|
8408
8679
|
if (process.platform !== "darwin") {
|
|
@@ -8548,7 +8819,7 @@ program.command("doctor").description("Check the install and say how to fix what
|
|
|
8548
8819
|
if (checks.some((c) => !c.ok)) process.exitCode = 1;
|
|
8549
8820
|
});
|
|
8550
8821
|
program.command("logs").description("Show the service log").option("-f, --follow", "keep streaming").action(async (opts) => {
|
|
8551
|
-
const file =
|
|
8822
|
+
const file = join18(cayenneDir(), "logs", "runner.log");
|
|
8552
8823
|
if (!existsSync5(file)) throw new Error(`no log yet at ${file}. Run \`cayenne up\` first.`);
|
|
8553
8824
|
const print = (chunk) => {
|
|
8554
8825
|
for (const line of chunk.split("\n")) {
|
|
@@ -8594,7 +8865,7 @@ program.command("update").description("Re-pin the installed app and restart the
|
|
|
8594
8865
|
cliPath: record.cliPath,
|
|
8595
8866
|
profile: currentProfile(),
|
|
8596
8867
|
pathEnv: record.pathEnv,
|
|
8597
|
-
logDir:
|
|
8868
|
+
logDir: join18(dir, "logs")
|
|
8598
8869
|
})
|
|
8599
8870
|
);
|
|
8600
8871
|
await launchd.bootout(label);
|
|
@@ -8605,7 +8876,7 @@ program.command("uninstall").description("Stop the service and remove the launch
|
|
|
8605
8876
|
requireMac();
|
|
8606
8877
|
const { launchd, label } = service();
|
|
8607
8878
|
await launchd.bootout(label);
|
|
8608
|
-
|
|
8879
|
+
rmSync7(plistPath(label, homedir4()), { force: true });
|
|
8609
8880
|
if (!opts.purge) {
|
|
8610
8881
|
console.log(`removed ${label}`);
|
|
8611
8882
|
return;
|
|
@@ -8615,7 +8886,7 @@ program.command("uninstall").description("Stop the service and remove the launch
|
|
|
8615
8886
|
if (dir === "/" || dir === home || home.startsWith(`${dir}/`)) {
|
|
8616
8887
|
throw new Error(`refusing to purge ${dir}: it is the home directory or an ancestor of it`);
|
|
8617
8888
|
}
|
|
8618
|
-
if (!existsSync5(
|
|
8889
|
+
if (!existsSync5(join18(dir, "config.json")) && !existsSync5(join18(dir, "app", "install.json"))) {
|
|
8619
8890
|
throw new Error(`refusing to purge ${dir}: it does not look like a Cayenne profile`);
|
|
8620
8891
|
}
|
|
8621
8892
|
if (!opts.yes) {
|
|
@@ -8629,7 +8900,7 @@ program.command("uninstall").description("Stop the service and remove the launch
|
|
|
8629
8900
|
"could not revoke the stored session (offline?) \u2014 sign out from the web app to be sure"
|
|
8630
8901
|
);
|
|
8631
8902
|
}
|
|
8632
|
-
|
|
8903
|
+
rmSync7(dir, { recursive: true, force: true });
|
|
8633
8904
|
console.log(`removed ${label} and ${dir}`);
|
|
8634
8905
|
});
|
|
8635
8906
|
program.parseAsync(process.argv).catch((err) => {
|