@jpcasabiancai/cayenne 0.5.6 → 0.5.7
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 +454 -180
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -31,7 +31,7 @@ var init_cloud = __esm({
|
|
|
31
31
|
CLOUD_SUPABASE_ANON_KEY = assertPublishable(
|
|
32
32
|
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im5kcXJrcXh3a3p4Z25oa3ZjdW5yIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODg3MTMyNzYsImV4cCI6MjEwNDI4OTI3Nn0.Dlsxw91E1QYZ4eoRDjP80Rf4feY77Z-T-fiIwUDY2k0"
|
|
33
33
|
);
|
|
34
|
-
CLOUD_APP_ORIGIN = "https://cayenne
|
|
34
|
+
CLOUD_APP_ORIGIN = "https://app.cayenne.sh";
|
|
35
35
|
}
|
|
36
36
|
});
|
|
37
37
|
|
|
@@ -2729,13 +2729,16 @@ var init_rpc = __esm({
|
|
|
2729
2729
|
}
|
|
2730
2730
|
}
|
|
2731
2731
|
dispatch(line) {
|
|
2732
|
-
let
|
|
2732
|
+
let parsed;
|
|
2733
2733
|
try {
|
|
2734
|
-
|
|
2734
|
+
parsed = JSON.parse(line);
|
|
2735
2735
|
} catch {
|
|
2736
2736
|
return;
|
|
2737
2737
|
}
|
|
2738
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return;
|
|
2739
|
+
const frame = parsed;
|
|
2738
2740
|
if (frame.id !== void 0 && frame.method === void 0) {
|
|
2741
|
+
if (typeof frame.id !== "number") return;
|
|
2739
2742
|
const p = this.pending.get(frame.id);
|
|
2740
2743
|
if (!p) return;
|
|
2741
2744
|
this.pending.delete(frame.id);
|
|
@@ -2748,6 +2751,7 @@ var init_rpc = __esm({
|
|
|
2748
2751
|
void (async () => {
|
|
2749
2752
|
try {
|
|
2750
2753
|
const result = await this.request_?.({
|
|
2754
|
+
id,
|
|
2751
2755
|
method: frame.method,
|
|
2752
2756
|
params: frame.params
|
|
2753
2757
|
});
|
|
@@ -2755,7 +2759,11 @@ var init_rpc = __esm({
|
|
|
2755
2759
|
} catch (err) {
|
|
2756
2760
|
this.write({
|
|
2757
2761
|
id,
|
|
2758
|
-
error: {
|
|
2762
|
+
error: {
|
|
2763
|
+
// A handler that knows the JSON-RPC code for its refusal says so; else "internal".
|
|
2764
|
+
code: err instanceof RpcError ? err.code : -32603,
|
|
2765
|
+
message: err instanceof Error ? err.message : String(err)
|
|
2766
|
+
}
|
|
2759
2767
|
});
|
|
2760
2768
|
}
|
|
2761
2769
|
})();
|
|
@@ -2770,6 +2778,17 @@ var init_rpc = __esm({
|
|
|
2770
2778
|
// src/codex/detect.ts
|
|
2771
2779
|
import { execFile as execFile2, spawn } from "child_process";
|
|
2772
2780
|
import { promisify } from "util";
|
|
2781
|
+
async function allModels(request) {
|
|
2782
|
+
const models = [];
|
|
2783
|
+
let cursor;
|
|
2784
|
+
do {
|
|
2785
|
+
const page = await request(cursor ? { cursor } : {});
|
|
2786
|
+
const data = page.data ?? [];
|
|
2787
|
+
models.push(...data);
|
|
2788
|
+
cursor = data.length > 0 && page.nextCursor ? page.nextCursor : void 0;
|
|
2789
|
+
} while (cursor && models.length < MAX_MODELS);
|
|
2790
|
+
return models;
|
|
2791
|
+
}
|
|
2773
2792
|
function findCodexOnPath(env) {
|
|
2774
2793
|
return findOnPath("codex", env.PATH);
|
|
2775
2794
|
}
|
|
@@ -2798,8 +2817,7 @@ async function listCodexModels(path) {
|
|
|
2798
2817
|
clientInfo: { name: "cayenne-runner", title: "Cayenne", version: "0.0.1" }
|
|
2799
2818
|
});
|
|
2800
2819
|
rpc.notify("initialized", {});
|
|
2801
|
-
|
|
2802
|
-
return res.data ?? [];
|
|
2820
|
+
return await allModels((params) => rpc.request("model/list", params));
|
|
2803
2821
|
} finally {
|
|
2804
2822
|
clearTimeout(timer);
|
|
2805
2823
|
rpc.close();
|
|
@@ -2807,7 +2825,7 @@ async function listCodexModels(path) {
|
|
|
2807
2825
|
}
|
|
2808
2826
|
}
|
|
2809
2827
|
function toProviderModel(m) {
|
|
2810
|
-
const efforts = (m.supportedReasoningEfforts ?? []).map((e) => e.
|
|
2828
|
+
const efforts = (m.supportedReasoningEfforts ?? []).map((e) => e.reasoningEffort).filter((e) => Effort.options.includes(e));
|
|
2811
2829
|
const fallback = Effort.options.includes(m.defaultReasoningEffort) ? m.defaultReasoningEffort : void 0;
|
|
2812
2830
|
return {
|
|
2813
2831
|
id: (m.id ?? "").slice(0, MAX_NAME),
|
|
@@ -7160,10 +7178,10 @@ async function syncDocuments(deps, pass) {
|
|
|
7160
7178
|
if (error2) throw new Error(error2.message);
|
|
7161
7179
|
const row = data;
|
|
7162
7180
|
if (!row?.id) throw new Error("the insert returned no row");
|
|
7163
|
-
const
|
|
7164
|
-
if (
|
|
7181
|
+
const failed5 = await publish2(row, doc);
|
|
7182
|
+
if (failed5 !== null) {
|
|
7165
7183
|
log.warn(
|
|
7166
|
-
{ err:
|
|
7184
|
+
{ err: failed5, relPath: doc.relPath, sessionId: deps.sessionId },
|
|
7167
7185
|
"could not upload a document; removing the row so the next pass retries it"
|
|
7168
7186
|
);
|
|
7169
7187
|
await takeBack(row.id, doc.relPath);
|
|
@@ -7176,10 +7194,10 @@ async function syncDocuments(deps, pass) {
|
|
|
7176
7194
|
const restated = before.status !== "present" || before.title !== doc.title || before.kind !== doc.kind || before.size_bytes !== doc.size;
|
|
7177
7195
|
if (!rewritten && !restated) continue;
|
|
7178
7196
|
if (rewritten) {
|
|
7179
|
-
const
|
|
7180
|
-
if (
|
|
7197
|
+
const failed5 = await upload(before.storage_path, doc);
|
|
7198
|
+
if (failed5 !== null) {
|
|
7181
7199
|
log.warn(
|
|
7182
|
-
{ err:
|
|
7200
|
+
{ err: failed5, relPath: doc.relPath, sessionId: deps.sessionId },
|
|
7183
7201
|
"could not overwrite a document; its row still describes the bytes in the bucket"
|
|
7184
7202
|
);
|
|
7185
7203
|
continue;
|
|
@@ -8047,15 +8065,37 @@ ${this.stderrTail.join("\n")}` : "";
|
|
|
8047
8065
|
});
|
|
8048
8066
|
|
|
8049
8067
|
// src/codex/normalize.ts
|
|
8068
|
+
function moveTarget(kind) {
|
|
8069
|
+
const named = [kind.move_path, kind.movePath].filter((target) => target != null);
|
|
8070
|
+
if (named.length === 0) return void 0;
|
|
8071
|
+
const [first] = named;
|
|
8072
|
+
return typeof first === "string" && named.every((target) => target === first) ? first : "";
|
|
8073
|
+
}
|
|
8074
|
+
function fileChanges(item) {
|
|
8075
|
+
if (!Array.isArray(item.changes)) return [];
|
|
8076
|
+
return item.changes.map((entry) => {
|
|
8077
|
+
const change = isRecord(entry) ? entry : {};
|
|
8078
|
+
const kind = isRecord(change.kind) ? change.kind : {};
|
|
8079
|
+
const movePath = moveTarget(kind);
|
|
8080
|
+
return {
|
|
8081
|
+
path: typeof change.path === "string" ? change.path : "",
|
|
8082
|
+
...typeof kind.type === "string" ? { kind: kind.type } : {},
|
|
8083
|
+
...typeof change.diff === "string" ? { diff: change.diff } : {},
|
|
8084
|
+
...movePath === void 0 ? {} : { movePath }
|
|
8085
|
+
};
|
|
8086
|
+
});
|
|
8087
|
+
}
|
|
8050
8088
|
function normalizeCodexItem(item, phase) {
|
|
8051
8089
|
const base = { providerUuid: item.id, parentToolUseId: null, raw: item };
|
|
8052
|
-
if (item.type === "
|
|
8090
|
+
if (item.type === "agentMessage") {
|
|
8053
8091
|
if (phase !== "completed") return null;
|
|
8054
8092
|
return { role: "assistant", content: [{ type: "text", text: item.text ?? "" }], ...base };
|
|
8055
8093
|
}
|
|
8056
8094
|
if (item.type === "reasoning") {
|
|
8057
8095
|
if (phase !== "completed") return null;
|
|
8058
|
-
|
|
8096
|
+
const text = (Array.isArray(item.summary) ? item.summary : []).join("\n\n");
|
|
8097
|
+
if (!text) return null;
|
|
8098
|
+
return { role: "assistant", content: [{ type: "thinking", text }], ...base };
|
|
8059
8099
|
}
|
|
8060
8100
|
const name = toolName(item);
|
|
8061
8101
|
if (!name) return null;
|
|
@@ -8063,12 +8103,11 @@ function normalizeCodexItem(item, phase) {
|
|
|
8063
8103
|
const block2 = { type: "tool_use", id: item.id, name, input: toolInput(item) };
|
|
8064
8104
|
return { role: "assistant", content: [block2], ...base };
|
|
8065
8105
|
}
|
|
8066
|
-
const isError2 = item.status === "failed" || item.exit_code !== void 0 && item.exit_code !== 0;
|
|
8067
8106
|
const block = {
|
|
8068
8107
|
type: "tool_result",
|
|
8069
8108
|
toolUseId: item.id,
|
|
8070
8109
|
content: resultText(item),
|
|
8071
|
-
isError:
|
|
8110
|
+
isError: failed4(item)
|
|
8072
8111
|
};
|
|
8073
8112
|
return { role: "tool", content: [block], ...base };
|
|
8074
8113
|
}
|
|
@@ -8089,20 +8128,21 @@ function deltaEvents(_itemId, kind, text, first) {
|
|
|
8089
8128
|
function stopEvents() {
|
|
8090
8129
|
return [{ type: "content_block_stop", index: 0 }, { type: "message_stop" }];
|
|
8091
8130
|
}
|
|
8092
|
-
var toolName, toolInput, resultText;
|
|
8131
|
+
var isRecord, toolName, toolInput, resultText, failed4;
|
|
8093
8132
|
var init_normalize2 = __esm({
|
|
8094
8133
|
"src/codex/normalize.ts"() {
|
|
8095
8134
|
"use strict";
|
|
8096
8135
|
init_src();
|
|
8136
|
+
isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
8097
8137
|
toolName = (item) => {
|
|
8098
8138
|
switch (item.type) {
|
|
8099
|
-
case "
|
|
8139
|
+
case "commandExecution":
|
|
8100
8140
|
return CODEX_TOOL_NAMES.command;
|
|
8101
|
-
case "
|
|
8141
|
+
case "fileChange":
|
|
8102
8142
|
return CODEX_TOOL_NAMES.fileChange;
|
|
8103
|
-
case "
|
|
8143
|
+
case "webSearch":
|
|
8104
8144
|
return CODEX_TOOL_NAMES.webSearch;
|
|
8105
|
-
case "
|
|
8145
|
+
case "mcpToolCall":
|
|
8106
8146
|
return `codex.mcp.${(item.server ?? "server").replace(/[^A-Za-z0-9_.-]/g, "")}.${(item.tool ?? "tool").replace(/[^A-Za-z0-9_.-]/g, "")}`.slice(
|
|
8107
8147
|
0,
|
|
8108
8148
|
64
|
|
@@ -8113,21 +8153,25 @@ var init_normalize2 = __esm({
|
|
|
8113
8153
|
};
|
|
8114
8154
|
toolInput = (item) => {
|
|
8115
8155
|
switch (item.type) {
|
|
8116
|
-
case "
|
|
8156
|
+
case "commandExecution":
|
|
8117
8157
|
return { command: item.command ?? "", ...item.cwd ? { cwd: item.cwd } : {} };
|
|
8118
|
-
case "
|
|
8119
|
-
return { changes: item
|
|
8120
|
-
case "
|
|
8158
|
+
case "fileChange":
|
|
8159
|
+
return { changes: fileChanges(item) };
|
|
8160
|
+
case "webSearch":
|
|
8121
8161
|
return { query: item.query ?? "" };
|
|
8122
8162
|
default:
|
|
8123
|
-
|
|
8163
|
+
if (isRecord(item.arguments)) return item.arguments;
|
|
8164
|
+
return item.arguments === void 0 ? {} : { arguments: item.arguments };
|
|
8124
8165
|
}
|
|
8125
8166
|
};
|
|
8126
8167
|
resultText = (item) => {
|
|
8127
|
-
if (item.type === "
|
|
8128
|
-
if (item.type === "
|
|
8168
|
+
if (item.type === "commandExecution") return item.aggregatedOutput ?? "";
|
|
8169
|
+
if (item.type === "fileChange") return JSON.stringify(fileChanges(item));
|
|
8170
|
+
if (item.type === "webSearch") return JSON.stringify(item.action ?? null);
|
|
8171
|
+
if (item.error?.message) return item.error.message;
|
|
8129
8172
|
return typeof item.result === "string" ? item.result : JSON.stringify(item.result ?? null);
|
|
8130
8173
|
};
|
|
8174
|
+
failed4 = (item) => item.status === "failed" || item.status === "declined" || Boolean(item.error) || typeof item.exitCode === "number" && item.exitCode !== 0;
|
|
8131
8175
|
}
|
|
8132
8176
|
});
|
|
8133
8177
|
|
|
@@ -8142,6 +8186,9 @@ function sandboxFor(mode, cwd) {
|
|
|
8142
8186
|
return { type: "workspaceWrite", writableRoots: [cwd], networkAccess: false };
|
|
8143
8187
|
}
|
|
8144
8188
|
}
|
|
8189
|
+
function sandboxMode(policy) {
|
|
8190
|
+
return SANDBOX_MODES[policy.type];
|
|
8191
|
+
}
|
|
8145
8192
|
function autoDecide(mode, kind) {
|
|
8146
8193
|
if (mode === "bypassPermissions") return "accept";
|
|
8147
8194
|
if (mode === "dontAsk") return "decline";
|
|
@@ -8149,9 +8196,15 @@ function autoDecide(mode, kind) {
|
|
|
8149
8196
|
if (mode === "acceptEdits" && kind === "file_change") return "accept";
|
|
8150
8197
|
return "ask";
|
|
8151
8198
|
}
|
|
8199
|
+
var SANDBOX_MODES;
|
|
8152
8200
|
var init_policy = __esm({
|
|
8153
8201
|
"src/codex/policy.ts"() {
|
|
8154
8202
|
"use strict";
|
|
8203
|
+
SANDBOX_MODES = {
|
|
8204
|
+
readOnly: "read-only",
|
|
8205
|
+
workspaceWrite: "workspace-write",
|
|
8206
|
+
dangerFullAccess: "danger-full-access"
|
|
8207
|
+
};
|
|
8155
8208
|
}
|
|
8156
8209
|
});
|
|
8157
8210
|
|
|
@@ -8161,6 +8214,7 @@ import { randomUUID as randomUUID4 } from "crypto";
|
|
|
8161
8214
|
import { EventEmitter as EventEmitter2 } from "events";
|
|
8162
8215
|
import { chmodSync as chmodSync5, lstatSync as lstatSync7, mkdirSync as mkdirSync8, rmdirSync, rmSync as rmSync8, writeFileSync as writeFileSync6 } from "fs";
|
|
8163
8216
|
import { dirname as dirname8, join as join22 } from "path";
|
|
8217
|
+
import { z as z18 } from "zod";
|
|
8164
8218
|
function newStop() {
|
|
8165
8219
|
let resolve6;
|
|
8166
8220
|
let reject;
|
|
@@ -8174,10 +8228,35 @@ function newStop() {
|
|
|
8174
8228
|
function removeFiles(files) {
|
|
8175
8229
|
for (const file of files) rmSync8(file, { force: true });
|
|
8176
8230
|
}
|
|
8231
|
+
function readQuestions(params) {
|
|
8232
|
+
const asked = AskedQuestions.safeParse(params);
|
|
8233
|
+
if (!asked.success) return null;
|
|
8234
|
+
const { questions } = asked.data;
|
|
8235
|
+
const ids = questions.map((q) => q.id);
|
|
8236
|
+
if (new Set(ids).size !== ids.length || questions.some((q) => q.isSecret)) return null;
|
|
8237
|
+
const input = CodexQuestions.safeParse({
|
|
8238
|
+
questions: questions.map((q) => ({
|
|
8239
|
+
text: q.question,
|
|
8240
|
+
...q.options?.length ? { options: q.options.map((o) => o.label) } : {}
|
|
8241
|
+
}))
|
|
8242
|
+
});
|
|
8243
|
+
return input.success ? { ids, input: input.data } : null;
|
|
8244
|
+
}
|
|
8177
8245
|
function scrubSecrets(text) {
|
|
8178
8246
|
return SECRET_PATTERNS.reduce((acc, [pattern, to]) => acc.replace(pattern, to), text);
|
|
8179
8247
|
}
|
|
8180
|
-
|
|
8248
|
+
function failureText(error) {
|
|
8249
|
+
const message = error?.message;
|
|
8250
|
+
if (typeof message !== "string" || !message.trim()) return null;
|
|
8251
|
+
let text = message;
|
|
8252
|
+
try {
|
|
8253
|
+
const inner = JSON.parse(message)?.error?.message;
|
|
8254
|
+
if (typeof inner === "string" && inner.trim()) text = inner;
|
|
8255
|
+
} catch {
|
|
8256
|
+
}
|
|
8257
|
+
return scrubSecrets(text).trim().slice(0, TURN_ERROR_MAX);
|
|
8258
|
+
}
|
|
8259
|
+
var decided, NO_ANSWERS, REFUSALS, METHOD_NOT_FOUND, AskedQuestions, STDERR_TAIL_MAX, STOP_WAIT_MS, TURN_ERROR_MAX, SIGKILL_AFTER_MS, SECRET_PATTERNS, APPROVAL_METHODS, CodexSession;
|
|
8181
8260
|
var init_session3 = __esm({
|
|
8182
8261
|
"src/codex/session.ts"() {
|
|
8183
8262
|
"use strict";
|
|
@@ -8187,8 +8266,27 @@ var init_session3 = __esm({
|
|
|
8187
8266
|
init_normalize2();
|
|
8188
8267
|
init_policy();
|
|
8189
8268
|
init_rpc();
|
|
8269
|
+
decided = (decision) => ({ decision });
|
|
8270
|
+
NO_ANSWERS = { answers: {} };
|
|
8271
|
+
REFUSALS = {
|
|
8272
|
+
"item/permissions/requestApproval": { permissions: {} },
|
|
8273
|
+
"mcpServer/elicitation/request": { action: "decline" },
|
|
8274
|
+
"item/tool/call": { contentItems: [], success: false }
|
|
8275
|
+
};
|
|
8276
|
+
METHOD_NOT_FOUND = -32601;
|
|
8277
|
+
AskedQuestions = z18.object({
|
|
8278
|
+
questions: z18.array(
|
|
8279
|
+
z18.object({
|
|
8280
|
+
id: z18.string().min(1),
|
|
8281
|
+
question: z18.string(),
|
|
8282
|
+
isSecret: z18.boolean().nullish(),
|
|
8283
|
+
options: z18.array(z18.object({ label: z18.string() })).nullish()
|
|
8284
|
+
})
|
|
8285
|
+
)
|
|
8286
|
+
});
|
|
8190
8287
|
STDERR_TAIL_MAX = 2 * 1024;
|
|
8191
8288
|
STOP_WAIT_MS = 2e3;
|
|
8289
|
+
TURN_ERROR_MAX = 500;
|
|
8192
8290
|
SIGKILL_AFTER_MS = 5e3;
|
|
8193
8291
|
SECRET_PATTERNS = [
|
|
8194
8292
|
[
|
|
@@ -8234,8 +8332,17 @@ var init_session3 = __esm({
|
|
|
8234
8332
|
resolvers = /* @__PURE__ */ new Map();
|
|
8235
8333
|
/** Monotonic, so no two held requests can ever share a local id. */
|
|
8236
8334
|
approvalSeq = 0;
|
|
8237
|
-
/**
|
|
8238
|
-
|
|
8335
|
+
/**
|
|
8336
|
+
* The app-server's id of each question a pending `codex.user_input` asked, in the order the
|
|
8337
|
+
* card shows them: an answer goes back under its question's id.
|
|
8338
|
+
*/
|
|
8339
|
+
questionIds = /* @__PURE__ */ new Map();
|
|
8340
|
+
/**
|
|
8341
|
+
* The files of each `fileChange` item in progress, by item id. `item/fileChange/requestApproval`
|
|
8342
|
+
* names the item and not its files: those came with `item/started`, which the app-server sends
|
|
8343
|
+
* first (live 2026-10-13), and with any `item/fileChange/patchUpdated` after.
|
|
8344
|
+
*/
|
|
8345
|
+
fileChanges = /* @__PURE__ */ new Map();
|
|
8239
8346
|
start() {
|
|
8240
8347
|
if (this.child) throw new Error("VALIDATION: session already started");
|
|
8241
8348
|
const spawnFn = this.opts.spawnFn ?? ((p, a, env) => spawn2(p, a, { stdio: "pipe", env }));
|
|
@@ -8255,13 +8362,19 @@ var init_session3 = __esm({
|
|
|
8255
8362
|
${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
|
|
8256
8363
|
const error = this.stopping || code === 0 ? null : `PROVIDER_ERROR: codex exited with ${code}${tail}`;
|
|
8257
8364
|
this.removeAllFiles();
|
|
8365
|
+
this.withdrawAll();
|
|
8258
8366
|
this.emit("exit", { error });
|
|
8259
8367
|
});
|
|
8260
8368
|
const rpc = new JsonRpc(child.stdin, child.stdout);
|
|
8261
8369
|
this.rpc = rpc;
|
|
8262
|
-
rpc.onNotification(
|
|
8263
|
-
|
|
8370
|
+
rpc.onNotification(
|
|
8371
|
+
(n) => this.onNotification(n.method, n.params ?? {})
|
|
8372
|
+
);
|
|
8373
|
+
rpc.onRequest(
|
|
8374
|
+
(r) => this.onRequest(r.id, r.method, r.params ?? {})
|
|
8375
|
+
);
|
|
8264
8376
|
void this.handshake().catch((err) => {
|
|
8377
|
+
this.withdrawAll();
|
|
8265
8378
|
this.emit("exit", {
|
|
8266
8379
|
error: `PROVIDER_ERROR: ${err instanceof Error ? err.message : String(err)}`
|
|
8267
8380
|
});
|
|
@@ -8277,17 +8390,24 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
|
|
|
8277
8390
|
const params = {
|
|
8278
8391
|
cwd: this.opts.cwd,
|
|
8279
8392
|
approvalPolicy: "on-request",
|
|
8280
|
-
|
|
8393
|
+
// The mode word: opening a thread refuses a policy object (`unknown variant `type``, live
|
|
8394
|
+
// 2026-10-13). The policy itself, with its writable root, goes with every `turn/start`.
|
|
8395
|
+
sandbox: sandboxMode(this.sandbox),
|
|
8281
8396
|
...this.pendingModel ? { model: this.pendingModel } : {}
|
|
8282
8397
|
};
|
|
8283
8398
|
const res = this.opts.resume ? await rpc.request("thread/resume", {
|
|
8284
8399
|
threadId: this.opts.resume,
|
|
8285
8400
|
...params
|
|
8286
|
-
}) : await rpc.request("thread/start",
|
|
8287
|
-
|
|
8401
|
+
}) : await rpc.request("thread/start", {
|
|
8402
|
+
...params,
|
|
8403
|
+
...this.opts.ephemeral ? { ephemeral: true } : {}
|
|
8404
|
+
});
|
|
8405
|
+
const threadId = res?.thread?.id;
|
|
8406
|
+
if (!threadId) throw new Error("codex app-server opened no thread");
|
|
8407
|
+
this.providerSessionId = threadId;
|
|
8288
8408
|
this.emit("init", {
|
|
8289
|
-
providerSessionId:
|
|
8290
|
-
model: res
|
|
8409
|
+
providerSessionId: threadId,
|
|
8410
|
+
model: res?.model ?? this.pendingModel ?? "",
|
|
8291
8411
|
tools: [],
|
|
8292
8412
|
slashCommands: [],
|
|
8293
8413
|
permissionMode: this.mode
|
|
@@ -8353,6 +8473,7 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
|
|
|
8353
8473
|
if (!turn) return;
|
|
8354
8474
|
this.turn = null;
|
|
8355
8475
|
removeFiles(turn.files);
|
|
8476
|
+
this.fileChanges.clear();
|
|
8356
8477
|
turn.stop?.resolve();
|
|
8357
8478
|
}
|
|
8358
8479
|
removeAllFiles() {
|
|
@@ -8399,7 +8520,11 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
|
|
|
8399
8520
|
isError: true,
|
|
8400
8521
|
durationMs: 0,
|
|
8401
8522
|
numTurns: 1,
|
|
8402
|
-
|
|
8523
|
+
// The app-server's own words, broadcast like a failed turn's: scrubbed and kept short.
|
|
8524
|
+
stopReason: scrubSecrets(err instanceof Error ? err.message : String(err)).slice(
|
|
8525
|
+
0,
|
|
8526
|
+
TURN_ERROR_MAX
|
|
8527
|
+
)
|
|
8403
8528
|
});
|
|
8404
8529
|
});
|
|
8405
8530
|
}
|
|
@@ -8469,6 +8594,10 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
|
|
|
8469
8594
|
const item = params.item;
|
|
8470
8595
|
if (!item) return;
|
|
8471
8596
|
const phase = method === "item/started" ? "started" : "completed";
|
|
8597
|
+
if (item.type === "fileChange") {
|
|
8598
|
+
if (phase === "started") this.setFiles(item.id, fileChanges(item));
|
|
8599
|
+
else this.fileChanges.delete(item.id);
|
|
8600
|
+
}
|
|
8472
8601
|
if (phase === "completed" && this.streaming.delete(item.id)) {
|
|
8473
8602
|
for (const event of stopEvents())
|
|
8474
8603
|
this.emit("stream", { event, parentToolUseId: null, uuid: item.id });
|
|
@@ -8477,15 +8606,32 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
|
|
|
8477
8606
|
if (row) this.emit("message", row);
|
|
8478
8607
|
return;
|
|
8479
8608
|
}
|
|
8480
|
-
case "
|
|
8481
|
-
|
|
8609
|
+
case "item/fileChange/patchUpdated":
|
|
8610
|
+
if (typeof params.itemId === "string" && this.fileChanges.has(params.itemId)) {
|
|
8611
|
+
this.setFiles(params.itemId, fileChanges(params));
|
|
8612
|
+
}
|
|
8613
|
+
return;
|
|
8614
|
+
case "serverRequest/resolved":
|
|
8615
|
+
for (const [id, entry] of [...this.approvals]) {
|
|
8616
|
+
if (entry.rpcId === params.requestId) this.withdraw(id);
|
|
8617
|
+
}
|
|
8618
|
+
return;
|
|
8619
|
+
case "turn/completed": {
|
|
8482
8620
|
this.endTurn();
|
|
8483
|
-
const
|
|
8621
|
+
const ended = turn?.id ?? params.turnId;
|
|
8622
|
+
for (const [id, entry] of [...this.approvals]) {
|
|
8623
|
+
if (typeof ended !== "string" || entry.turnId === null || entry.turnId === ended) {
|
|
8624
|
+
this.withdraw(id);
|
|
8625
|
+
}
|
|
8626
|
+
}
|
|
8627
|
+
const status = turn?.status ?? params.status ?? "completed";
|
|
8628
|
+
const isError2 = status === "failed";
|
|
8484
8629
|
this.emit("result", {
|
|
8485
|
-
isError:
|
|
8630
|
+
isError: isError2,
|
|
8486
8631
|
durationMs: turn?.durationMs ?? params.durationMs ?? 0,
|
|
8487
8632
|
numTurns: 1,
|
|
8488
|
-
|
|
8633
|
+
// A failed turn says why, when the app-server did.
|
|
8634
|
+
stopReason: isError2 && failureText(turn?.error) || status
|
|
8489
8635
|
});
|
|
8490
8636
|
this.flush();
|
|
8491
8637
|
return;
|
|
@@ -8494,6 +8640,45 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
|
|
|
8494
8640
|
return;
|
|
8495
8641
|
}
|
|
8496
8642
|
}
|
|
8643
|
+
/**
|
|
8644
|
+
* The files of a `fileChange` item, as last told. A card already up for that item shows the
|
|
8645
|
+
* files as they were, and the roots check passed those: it is withdrawn and the approval
|
|
8646
|
+
* declined, since the patch an answer would now allow is not the one the owner is reading.
|
|
8647
|
+
*/
|
|
8648
|
+
setFiles(itemId, changes) {
|
|
8649
|
+
const before = this.fileChanges.get(itemId);
|
|
8650
|
+
this.fileChanges.set(itemId, changes);
|
|
8651
|
+
if (before === void 0 || JSON.stringify(before) === JSON.stringify(changes)) return;
|
|
8652
|
+
for (const [id, entry] of [...this.approvals]) {
|
|
8653
|
+
if (entry.kind === "file_change" && entry.itemId === itemId) this.withdraw(id);
|
|
8654
|
+
}
|
|
8655
|
+
}
|
|
8656
|
+
/**
|
|
8657
|
+
* Take back a held request no answer can reach any more: nobody answered in time, its files
|
|
8658
|
+
* changed under the card, the app-server settled it itself, its turn ended, the session is
|
|
8659
|
+
* stopping, or the app-server is gone. The request is answered with its own refusal (an approval
|
|
8660
|
+
* declines; a question's response is its answers, of which there are none), and
|
|
8661
|
+
* `permissionTimeout` has the manager expire the card's row, after which an answer to it is
|
|
8662
|
+
* refused. One place, so no way of withdrawing can leave behind what another clears.
|
|
8663
|
+
*/
|
|
8664
|
+
withdraw(id) {
|
|
8665
|
+
const entry = this.approvals.get(id);
|
|
8666
|
+
if (!entry) return;
|
|
8667
|
+
clearTimeout(entry.timer);
|
|
8668
|
+
this.approvals.delete(id);
|
|
8669
|
+
this.questionIds.delete(id);
|
|
8670
|
+
this.resolvers.get(id)?.(entry.refusal);
|
|
8671
|
+
this.resolvers.delete(id);
|
|
8672
|
+
this.emit("permissionTimeout", { id });
|
|
8673
|
+
}
|
|
8674
|
+
/**
|
|
8675
|
+
* Every held request, when nothing is left to answer: the session is stopping, the app-server
|
|
8676
|
+
* has exited, or it never opened a thread. The refusals are written if there is still a pipe to
|
|
8677
|
+
* write them to; into one that is gone they are dropped, without a throw (`rpc.test.ts`).
|
|
8678
|
+
*/
|
|
8679
|
+
withdrawAll() {
|
|
8680
|
+
for (const id of [...this.approvals.keys()]) this.withdraw(id);
|
|
8681
|
+
}
|
|
8497
8682
|
delta(params, kind, field) {
|
|
8498
8683
|
const itemId = params.itemId ?? "";
|
|
8499
8684
|
const text = params[field] ?? "";
|
|
@@ -8509,27 +8694,43 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
|
|
|
8509
8694
|
* The reply is held until a human answers or the timer fires, which is what makes a Codex chat
|
|
8510
8695
|
* behave like a Claude one — the SDK's `canUseTool` blocks the same way.
|
|
8511
8696
|
*/
|
|
8512
|
-
onRequest(method, params) {
|
|
8513
|
-
|
|
8514
|
-
|
|
8515
|
-
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
|
|
8519
|
-
|
|
8520
|
-
|
|
8521
|
-
|
|
8697
|
+
onRequest(rpcId, method, params) {
|
|
8698
|
+
const from = { rpcId, turnId: typeof params.turnId === "string" ? params.turnId : null };
|
|
8699
|
+
if (method === "item/tool/requestUserInput") return this.askQuestion(from, params);
|
|
8700
|
+
const kind = Object.hasOwn(APPROVAL_METHODS, method) ? APPROVAL_METHODS[method] : void 0;
|
|
8701
|
+
if (!kind) {
|
|
8702
|
+
if (Object.hasOwn(REFUSALS, method)) return REFUSALS[method];
|
|
8703
|
+
throw new RpcError(METHOD_NOT_FOUND, `cayenne does not handle ${method}`);
|
|
8704
|
+
}
|
|
8705
|
+
const itemId = typeof params.itemId === "string" ? params.itemId : null;
|
|
8706
|
+
const reason = params.reason ? { reason: params.reason } : {};
|
|
8707
|
+
let toolName2;
|
|
8708
|
+
let input;
|
|
8709
|
+
if (kind === "command") {
|
|
8710
|
+
toolName2 = CODEX_TOOL_NAMES.command;
|
|
8711
|
+
const { command, cwd } = params;
|
|
8712
|
+
if ((params.kind ?? "command") !== "command" || params.networkApprovalContext != null || typeof command !== "string" || command.trim() === "" || cwd != null && typeof cwd !== "string") {
|
|
8713
|
+
return decided("decline");
|
|
8714
|
+
}
|
|
8715
|
+
input = { command, cwd: cwd ?? this.opts.cwd, ...reason };
|
|
8716
|
+
} else {
|
|
8717
|
+
toolName2 = CODEX_TOOL_NAMES.fileChange;
|
|
8718
|
+
if (params.grantRoot != null) return decided("decline");
|
|
8719
|
+
const changes = (itemId ? this.fileChanges.get(itemId) : void 0) ?? [];
|
|
8720
|
+
if (changes.length === 0) return decided("decline");
|
|
8721
|
+
input = { changes, ...reason };
|
|
8722
|
+
}
|
|
8522
8723
|
try {
|
|
8523
|
-
this.opts.checkInput
|
|
8724
|
+
this.opts.checkInput(toolName2, input);
|
|
8524
8725
|
} catch {
|
|
8525
|
-
return "decline";
|
|
8726
|
+
return decided("decline");
|
|
8526
8727
|
}
|
|
8527
8728
|
const decision = autoDecide(this.mode, kind);
|
|
8528
|
-
if (decision !== "ask") return decision;
|
|
8529
|
-
return this.hold(this.nextApprovalId(kind), kind, "decline", {
|
|
8729
|
+
if (decision !== "ask") return decided(decision);
|
|
8730
|
+
return this.hold(this.nextApprovalId(kind), kind, decided("decline"), from, {
|
|
8530
8731
|
toolName: toolName2,
|
|
8531
8732
|
input,
|
|
8532
|
-
toolUseId:
|
|
8733
|
+
toolUseId: itemId
|
|
8533
8734
|
});
|
|
8534
8735
|
}
|
|
8535
8736
|
/**
|
|
@@ -8539,15 +8740,20 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
|
|
|
8539
8740
|
* same answer RPC. The shape is validated here, before a row exists — a malformed or oversized
|
|
8540
8741
|
* ask is cancelled rather than rendered, because the questions are text the model produced and
|
|
8541
8742
|
* the card is the one surface where a human is asked to trust what they read.
|
|
8542
|
-
|
|
8543
|
-
|
|
8544
|
-
|
|
8545
|
-
|
|
8743
|
+
*
|
|
8744
|
+
* The app-server's question (`{ id, header, question, options: { label, description }[] }`) is
|
|
8745
|
+
* not the one Cayenne stores (`CodexQuestions`: `{ text, options: string[] }`), and its answer is
|
|
8746
|
+
* keyed by question id where `CodexAnswers` is a list in order. The adapter translates both
|
|
8747
|
+
* ways, so the row, the card and the answer RPC are what they were.
|
|
8748
|
+
*/
|
|
8749
|
+
askQuestion(from, params) {
|
|
8750
|
+
const asked = readQuestions(params);
|
|
8751
|
+
if (!asked) return Promise.resolve(NO_ANSWERS);
|
|
8546
8752
|
const id = this.nextApprovalId("question");
|
|
8547
|
-
this.
|
|
8548
|
-
return this.hold(id, "question",
|
|
8753
|
+
this.questionIds.set(id, asked.ids);
|
|
8754
|
+
return this.hold(id, "question", NO_ANSWERS, from, {
|
|
8549
8755
|
toolName: CODEX_TOOL_NAMES.userInput,
|
|
8550
|
-
input:
|
|
8756
|
+
input: asked.input,
|
|
8551
8757
|
toolUseId: null
|
|
8552
8758
|
});
|
|
8553
8759
|
}
|
|
@@ -8564,20 +8770,21 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
|
|
|
8564
8770
|
this.approvalSeq += 1;
|
|
8565
8771
|
return `${kind}-${this.approvalSeq}`;
|
|
8566
8772
|
}
|
|
8567
|
-
/**
|
|
8568
|
-
|
|
8773
|
+
/**
|
|
8774
|
+
* Park a server request until a human answers it, the timer gives up on them, or it is
|
|
8775
|
+
* withdrawn (`withdraw`).
|
|
8776
|
+
*/
|
|
8777
|
+
hold(id, kind, onTimeout, from, req) {
|
|
8569
8778
|
if (this.approvals.has(id) || this.resolvers.has(id)) {
|
|
8570
8779
|
throw new Error(`VALIDATION: approval id ${id} is already held`);
|
|
8571
8780
|
}
|
|
8572
8781
|
return new Promise((resolve6) => {
|
|
8573
|
-
const timer = setTimeout(
|
|
8574
|
-
this.
|
|
8575
|
-
this.
|
|
8576
|
-
|
|
8577
|
-
|
|
8578
|
-
|
|
8579
|
-
}, this.opts.permissionTimeoutMs ?? PERMISSION_TIMEOUT_MS);
|
|
8580
|
-
this.approvals.set(id, { rpcId: 0, kind, refusal: onTimeout, timer });
|
|
8782
|
+
const timer = setTimeout(
|
|
8783
|
+
() => this.withdraw(id),
|
|
8784
|
+
this.opts.permissionTimeoutMs ?? PERMISSION_TIMEOUT_MS
|
|
8785
|
+
);
|
|
8786
|
+
const itemId = kind === "question" ? null : req.toolUseId;
|
|
8787
|
+
this.approvals.set(id, { ...from, kind, refusal: onTimeout, itemId, timer });
|
|
8581
8788
|
this.resolvers.set(id, resolve6);
|
|
8582
8789
|
this.emit("permission", { id, ...req, suggestions: null });
|
|
8583
8790
|
});
|
|
@@ -8588,20 +8795,24 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
|
|
|
8588
8795
|
if (!entry || !resolve6) return false;
|
|
8589
8796
|
let reply;
|
|
8590
8797
|
if (entry.kind === "question") {
|
|
8591
|
-
if (decision.behavior === "deny") reply =
|
|
8798
|
+
if (decision.behavior === "deny") reply = NO_ANSWERS;
|
|
8592
8799
|
else {
|
|
8800
|
+
const ids = this.questionIds.get(id) ?? [];
|
|
8593
8801
|
const answers = CodexAnswers.safeParse(decision.updatedInput ?? {});
|
|
8594
|
-
if (!answers.success || answers.data.answers.length !==
|
|
8595
|
-
|
|
8596
|
-
|
|
8802
|
+
if (!answers.success || answers.data.answers.length !== ids.length) return false;
|
|
8803
|
+
reply = {
|
|
8804
|
+
answers: Object.fromEntries(
|
|
8805
|
+
ids.map((questionId, n) => [questionId, { answers: [answers.data.answers[n] ?? ""] }])
|
|
8806
|
+
)
|
|
8807
|
+
};
|
|
8597
8808
|
}
|
|
8598
8809
|
} else {
|
|
8599
|
-
reply = decision.behavior === "allow" ? "accept" : "decline";
|
|
8810
|
+
reply = decided(decision.behavior === "allow" ? "accept" : "decline");
|
|
8600
8811
|
}
|
|
8601
8812
|
clearTimeout(entry.timer);
|
|
8602
8813
|
this.approvals.delete(id);
|
|
8603
8814
|
this.resolvers.delete(id);
|
|
8604
|
-
this.
|
|
8815
|
+
this.questionIds.delete(id);
|
|
8605
8816
|
resolve6(reply);
|
|
8606
8817
|
return true;
|
|
8607
8818
|
}
|
|
@@ -8657,13 +8868,7 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
|
|
|
8657
8868
|
}
|
|
8658
8869
|
async stop() {
|
|
8659
8870
|
this.stopping = true;
|
|
8660
|
-
|
|
8661
|
-
clearTimeout(entry.timer);
|
|
8662
|
-
this.approvals.delete(id);
|
|
8663
|
-
this.resolvers.get(id)?.(entry.refusal);
|
|
8664
|
-
this.resolvers.delete(id);
|
|
8665
|
-
this.emit("permissionTimeout", { id });
|
|
8666
|
-
}
|
|
8871
|
+
this.withdrawAll();
|
|
8667
8872
|
const stoppable = this.turn?.started && this.turn.id;
|
|
8668
8873
|
await (stoppable ? this.interrupt().catch(() => void 0) : void 0);
|
|
8669
8874
|
this.removeAllFiles();
|
|
@@ -10071,7 +10276,7 @@ function assertRepoOwner(ownerId, createdBy) {
|
|
|
10071
10276
|
}
|
|
10072
10277
|
function narrowToSession(suggestions) {
|
|
10073
10278
|
if (!suggestions) return suggestions;
|
|
10074
|
-
return suggestions.map((s) =>
|
|
10279
|
+
return suggestions.map((s) => isRecord2(s) ? { ...s, destination: "session" } : s);
|
|
10075
10280
|
}
|
|
10076
10281
|
function shrink(event) {
|
|
10077
10282
|
switch (event.type) {
|
|
@@ -10089,7 +10294,7 @@ function shrink(event) {
|
|
|
10089
10294
|
return null;
|
|
10090
10295
|
}
|
|
10091
10296
|
}
|
|
10092
|
-
var READ_ONLY_TEXT, REFRESH_WITHIN_MS, betweenTurns, UNREADABLE_MARK_CREDS, DRAINED_AFTER_MS, roundUsd, UNIQUE_VIOLATION, ROW_WRITE_TIMEOUT_MS, ORPHANED_STATUSES, RESTARTABLE_STATUSES, isAtOrBelow3, DENY_MESSAGE_MAX, EXIT_ERROR_MAX, INITIAL_PROMPT_MAX, TASK_SKILL_LOOKUP_MS, TASK_REFUSED_MODES, OWNER_DRIVEN_KINDS, REPO_OWNER_ONLY, REPO_NO_DUPLICATE, PROVIDER_SESSION_ID, REPO_WORKTREE_GONE, TASK_ASK_TOOLS, MEMORY_PREAMBLE, MEMORY_SOURCE, wrapMemory, PATH_INPUT_TOOLS, PATH_INPUT_KEYS,
|
|
10297
|
+
var READ_ONLY_TEXT, REFRESH_WITHIN_MS, betweenTurns, UNREADABLE_MARK_CREDS, DRAINED_AFTER_MS, roundUsd, UNIQUE_VIOLATION, ROW_WRITE_TIMEOUT_MS, ORPHANED_STATUSES, RESTARTABLE_STATUSES, isAtOrBelow3, DENY_MESSAGE_MAX, EXIT_ERROR_MAX, INITIAL_PROMPT_MAX, TASK_SKILL_LOOKUP_MS, TASK_REFUSED_MODES, OWNER_DRIVEN_KINDS, REPO_OWNER_ONLY, REPO_NO_DUPLICATE, PROVIDER_SESSION_ID, REPO_WORKTREE_GONE, TASK_ASK_TOOLS, MEMORY_PREAMBLE, MEMORY_SOURCE, wrapMemory, PATH_INPUT_TOOLS, PATH_INPUT_KEYS, isRecord2, errorCode, SessionManager;
|
|
10093
10298
|
var init_manager = __esm({
|
|
10094
10299
|
"src/sessions/manager.ts"() {
|
|
10095
10300
|
"use strict";
|
|
@@ -10176,7 +10381,7 @@ var init_manager = __esm({
|
|
|
10176
10381
|
"LS"
|
|
10177
10382
|
]);
|
|
10178
10383
|
PATH_INPUT_KEYS = ["file_path", "path", "cwd", "notebook_path"];
|
|
10179
|
-
|
|
10384
|
+
isRecord2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
10180
10385
|
errorCode = (message) => message.startsWith("ROOT_NOT_ALLOWED") ? "ROOT_NOT_ALLOWED" : "VALIDATION";
|
|
10181
10386
|
SessionManager = class {
|
|
10182
10387
|
constructor(deps) {
|
|
@@ -11173,6 +11378,7 @@ var init_manager = __esm({
|
|
|
11173
11378
|
turns.drainedAt = Date.now();
|
|
11174
11379
|
turns.early = true;
|
|
11175
11380
|
turns.chased = false;
|
|
11381
|
+
turns.ended += 1;
|
|
11176
11382
|
}
|
|
11177
11383
|
/**
|
|
11178
11384
|
* Put rows on this runner that claim a turn in progress back to `idle`, for the chats that have
|
|
@@ -11690,15 +11896,31 @@ var init_manager = __esm({
|
|
|
11690
11896
|
/**
|
|
11691
11897
|
* A tool's path as the provider opens it: relative means relative to the session's cwd. Every
|
|
11692
11898
|
* judge of a tool's path goes through here (R28's `checkUpdatedInput`, a suggested directory,
|
|
11693
|
-
* Codex's
|
|
11694
|
-
* (surrounding whitespace, a leading `~`, a non-NFC spelling)
|
|
11695
|
-
* written, `~/.ssh/id_rsa` was `<cwd>/~/.ssh/id_rsa`, inside
|
|
11696
|
-
* 2026-09-29).
|
|
11899
|
+
* and Codex's paths by way of `codexPath`, which admits no relative one). A value the Claude CLI
|
|
11900
|
+
* would rewrite before it opens it (surrounding whitespace, a leading `~`, a non-NFC spelling)
|
|
11901
|
+
* is refused, not judged: judged as written, `~/.ssh/id_rsa` was `<cwd>/~/.ssh/id_rsa`, inside
|
|
11902
|
+
* any root (security re-review 2026-09-29).
|
|
11697
11903
|
*/
|
|
11698
11904
|
fromCwd(cwd, value) {
|
|
11699
11905
|
if (!isPlainToolPath(value)) throw new Error(notPlainPathRefusal(value));
|
|
11700
11906
|
return isAbsolute21(value) ? value : `${cwd}${sep17}${value}`;
|
|
11701
11907
|
}
|
|
11908
|
+
/**
|
|
11909
|
+
* A path Codex named, for the roots check: absolute, or refused. The runner cannot prove what
|
|
11910
|
+
* directory the app-server would resolve a relative one against: a turn's cwd is the
|
|
11911
|
+
* app-server's, and a command can move it. Every live run against codex-cli 0.162 named
|
|
11912
|
+
* absolute paths only, so a relative one is an unknown shape, refused exactly as an
|
|
11913
|
+
* out-of-roots path is. An absolute value still passes through `fromCwd`, which refuses one
|
|
11914
|
+
* that is not a plain path.
|
|
11915
|
+
*/
|
|
11916
|
+
codexPath(cwd, value) {
|
|
11917
|
+
if (!isAbsolute21(value)) {
|
|
11918
|
+
throw new Error(
|
|
11919
|
+
`ROOT_NOT_ALLOWED: ${JSON.stringify(value)} is not an absolute path; a Codex change or command names every path by its absolute path`
|
|
11920
|
+
);
|
|
11921
|
+
}
|
|
11922
|
+
return this.fromCwd(cwd, value);
|
|
11923
|
+
}
|
|
11702
11924
|
/**
|
|
11703
11925
|
* §2.9: a path in `updatedInput` passes the same boundary the session's `cwd` did, and the
|
|
11704
11926
|
* SDK receives the canonical path the guard proved rather than the spelling typed (S2). The
|
|
@@ -11721,6 +11943,11 @@ var init_manager = __esm({
|
|
|
11721
11943
|
* not admit is denied before a row exists: no answer the owner could give would make it
|
|
11722
11944
|
* acceptable, exactly as R28 reasons for the Claude path.
|
|
11723
11945
|
*
|
|
11946
|
+
* A change writes its `path` and, when it moves the file, its `movePath`: both are read here,
|
|
11947
|
+
* each as a path of its own, so the card and the auto-accept gate judge the same set. A command
|
|
11948
|
+
* must name its `cwd`: absent, blank or not a string, the command is refused, not passed.
|
|
11949
|
+
* Every path, and that `cwd`, must be absolute (`codexPath`).
|
|
11950
|
+
*
|
|
11724
11951
|
* When the roots reach the managed root (D50: an allowed root such as `~`), a file change is
|
|
11725
11952
|
* also held to the managed `.git` guard Claude sessions get: a change under a managed clone's
|
|
11726
11953
|
* git dir, or to a worktree's `.git` file, is refused with the same text. In `acceptEdits` or
|
|
@@ -11733,19 +11960,30 @@ var init_manager = __esm({
|
|
|
11733
11960
|
throw new Error("VALIDATION: codex file change carries no changes to check");
|
|
11734
11961
|
}
|
|
11735
11962
|
const guarded = this.reachesManagedRoot(roots);
|
|
11963
|
+
const check = (path) => {
|
|
11964
|
+
assertInsideRootsOrAncestor(this.codexPath(cwd, path), roots);
|
|
11965
|
+
if (guarded && writesManagedGit(path, this.deps.managedRoot, cwd)) {
|
|
11966
|
+
throw new Error(MANAGED_GIT_REFUSAL);
|
|
11967
|
+
}
|
|
11968
|
+
};
|
|
11736
11969
|
for (const change of changes) {
|
|
11737
|
-
if (!
|
|
11970
|
+
if (!isRecord2(change) || typeof change.path !== "string" || change.path.trim() === "") {
|
|
11738
11971
|
throw new Error("VALIDATION: codex file change entry names no path to check");
|
|
11739
11972
|
}
|
|
11740
|
-
|
|
11741
|
-
if (
|
|
11742
|
-
|
|
11973
|
+
check(change.path);
|
|
11974
|
+
if (change.movePath === void 0) continue;
|
|
11975
|
+
if (typeof change.movePath !== "string" || change.movePath.trim() === "") {
|
|
11976
|
+
throw new Error("VALIDATION: codex file change entry names no move target to check");
|
|
11743
11977
|
}
|
|
11978
|
+
check(change.movePath);
|
|
11744
11979
|
}
|
|
11745
11980
|
return;
|
|
11746
11981
|
}
|
|
11747
|
-
if (toolName2 === CODEX_TOOL_NAMES.command
|
|
11748
|
-
|
|
11982
|
+
if (toolName2 === CODEX_TOOL_NAMES.command) {
|
|
11983
|
+
if (typeof input.cwd !== "string" || input.cwd.trim() === "") {
|
|
11984
|
+
throw new Error("VALIDATION: codex command names no cwd to check");
|
|
11985
|
+
}
|
|
11986
|
+
assertInsideRoots(this.codexPath(cwd, input.cwd), roots);
|
|
11749
11987
|
}
|
|
11750
11988
|
}
|
|
11751
11989
|
/**
|
|
@@ -11818,7 +12056,7 @@ var init_manager = __esm({
|
|
|
11818
12056
|
checkUpdatedPermissions(live, entry, updates) {
|
|
11819
12057
|
if (!updates) return void 0;
|
|
11820
12058
|
return updates.map((update) => {
|
|
11821
|
-
if (!
|
|
12059
|
+
if (!isRecord2(update))
|
|
11822
12060
|
throw new Error("VALIDATION: updatedPermissions entry is not an object");
|
|
11823
12061
|
if (update.destination !== "session") {
|
|
11824
12062
|
throw new Error(
|
|
@@ -12004,9 +12242,19 @@ var init_manager = __esm({
|
|
|
12004
12242
|
status: "starting",
|
|
12005
12243
|
echoed: /* @__PURE__ */ new Set(),
|
|
12006
12244
|
permissions: /* @__PURE__ */ new Map(),
|
|
12245
|
+
permissionsInFlight: /* @__PURE__ */ new Set(),
|
|
12246
|
+
permissionsWithdrawn: /* @__PURE__ */ new Set(),
|
|
12007
12247
|
cwd: opts.cwd,
|
|
12008
12248
|
stopping: false,
|
|
12009
|
-
turns: {
|
|
12249
|
+
turns: {
|
|
12250
|
+
owed: 0,
|
|
12251
|
+
begun: false,
|
|
12252
|
+
drainedAt: null,
|
|
12253
|
+
stops: 0,
|
|
12254
|
+
early: false,
|
|
12255
|
+
chased: false,
|
|
12256
|
+
ended: 0
|
|
12257
|
+
},
|
|
12010
12258
|
dropping: null,
|
|
12011
12259
|
inflight: /* @__PURE__ */ new Set(),
|
|
12012
12260
|
writes: Promise.resolve(),
|
|
@@ -12089,7 +12337,7 @@ var init_manager = __esm({
|
|
|
12089
12337
|
*/
|
|
12090
12338
|
watchWorkingCopy(sessionId, live) {
|
|
12091
12339
|
const factory = this.deps.watch ?? ((cwd, onChange2, onError2) => watchChanges(cwd, onChange2, CHANGES_DEBOUNCE_MS, onError2));
|
|
12092
|
-
let
|
|
12340
|
+
let failed5 = false;
|
|
12093
12341
|
let built = null;
|
|
12094
12342
|
const closeFailed = (watcher) => {
|
|
12095
12343
|
if (live.watcher === watcher) live.watcher = null;
|
|
@@ -12098,8 +12346,8 @@ var init_manager = __esm({
|
|
|
12098
12346
|
);
|
|
12099
12347
|
};
|
|
12100
12348
|
const onError = (err) => {
|
|
12101
|
-
if (
|
|
12102
|
-
|
|
12349
|
+
if (failed5) return;
|
|
12350
|
+
failed5 = true;
|
|
12103
12351
|
this.deps.log.warn(
|
|
12104
12352
|
{ err, sessionId },
|
|
12105
12353
|
"working-copy watcher error; live change refresh stopped"
|
|
@@ -12107,7 +12355,7 @@ var init_manager = __esm({
|
|
|
12107
12355
|
if (built) closeFailed(built);
|
|
12108
12356
|
};
|
|
12109
12357
|
const onChange = () => {
|
|
12110
|
-
if (
|
|
12358
|
+
if (failed5 || this.live.get(sessionId) !== live) return;
|
|
12111
12359
|
this.bumpChanges(sessionId, live);
|
|
12112
12360
|
};
|
|
12113
12361
|
try {
|
|
@@ -12116,7 +12364,7 @@ var init_manager = __esm({
|
|
|
12116
12364
|
this.deps.log.warn({ err, sessionId }, "working-copy watcher failed to start");
|
|
12117
12365
|
return null;
|
|
12118
12366
|
}
|
|
12119
|
-
if (
|
|
12367
|
+
if (failed5) {
|
|
12120
12368
|
closeFailed(built);
|
|
12121
12369
|
return null;
|
|
12122
12370
|
}
|
|
@@ -12218,89 +12466,95 @@ var init_manager = __esm({
|
|
|
12218
12466
|
);
|
|
12219
12467
|
})();
|
|
12220
12468
|
});
|
|
12469
|
+
const raise = async (rawReq) => {
|
|
12470
|
+
const req = { ...rawReq, suggestions: narrowToSession(rawReq.suggestions) };
|
|
12471
|
+
try {
|
|
12472
|
+
this.checkUpdatedInput(live, req.toolName, req.input);
|
|
12473
|
+
this.checkCodexInput(live.cwd, live.roots, req.toolName, req.input);
|
|
12474
|
+
} catch (err) {
|
|
12475
|
+
const message = this.cap(err instanceof Error ? err.message : String(err));
|
|
12476
|
+
session.respondPermission(req.id, { behavior: "deny", message, interrupt: false });
|
|
12477
|
+
this.deps.log.warn(
|
|
12478
|
+
{ sessionId, tool: req.toolName, err: message },
|
|
12479
|
+
"permission request refused before a row existed"
|
|
12480
|
+
);
|
|
12481
|
+
await this.broadcast(sessionId, { type: "error", code: errorCode(message), message });
|
|
12482
|
+
return;
|
|
12483
|
+
}
|
|
12484
|
+
const input = this.secrets.redact(req.input);
|
|
12485
|
+
const suggestions = this.secrets.redact(req.suggestions);
|
|
12486
|
+
const { data, error } = await this.deps.db.from("permission_requests").insert({
|
|
12487
|
+
session_id: sessionId,
|
|
12488
|
+
workspace_id: this.deps.workspaceId,
|
|
12489
|
+
tool_use_id: req.toolUseId,
|
|
12490
|
+
tool_name: req.toolName,
|
|
12491
|
+
input,
|
|
12492
|
+
suggestions
|
|
12493
|
+
}).select("*").single();
|
|
12494
|
+
if (error || !data) {
|
|
12495
|
+
const message = this.cap(
|
|
12496
|
+
`VALIDATION: could not persist the permission request for ${req.toolName}: ${error?.message ?? "no row"}`
|
|
12497
|
+
);
|
|
12498
|
+
session.respondPermission(req.id, { behavior: "deny", message, interrupt: false });
|
|
12499
|
+
await this.broadcast(sessionId, { type: "error", code: "VALIDATION", message });
|
|
12500
|
+
throw new Error(message);
|
|
12501
|
+
}
|
|
12502
|
+
if (live.permissionsWithdrawn.has(req.id)) {
|
|
12503
|
+
await this.expirePermission(sessionId, data.id);
|
|
12504
|
+
return;
|
|
12505
|
+
}
|
|
12506
|
+
live.permissions.set(data.id, {
|
|
12507
|
+
localId: req.id,
|
|
12508
|
+
toolName: req.toolName,
|
|
12509
|
+
input,
|
|
12510
|
+
suggestions
|
|
12511
|
+
});
|
|
12512
|
+
await this.setStatus(sessionId, "awaiting_permission");
|
|
12513
|
+
if (!live.permissions.has(data.id)) return;
|
|
12514
|
+
try {
|
|
12515
|
+
await this.broadcast(sessionId, {
|
|
12516
|
+
type: "permission.request",
|
|
12517
|
+
request: data
|
|
12518
|
+
});
|
|
12519
|
+
} catch (err) {
|
|
12520
|
+
if (!(err instanceof Error) || !err.message.startsWith("VALIDATION:")) throw err;
|
|
12521
|
+
this.deps.log.warn(
|
|
12522
|
+
{ sessionId, requestId: data.id, err: err.message },
|
|
12523
|
+
"permission request too large to broadcast; clients reconcile from the row"
|
|
12524
|
+
);
|
|
12525
|
+
await this.broadcast(sessionId, {
|
|
12526
|
+
type: "error",
|
|
12527
|
+
code: "VALIDATION",
|
|
12528
|
+
message: err.message
|
|
12529
|
+
});
|
|
12530
|
+
}
|
|
12531
|
+
};
|
|
12221
12532
|
session.on(
|
|
12222
12533
|
"permission",
|
|
12223
12534
|
(rawReq) => guard(async () => {
|
|
12224
|
-
|
|
12535
|
+
live.permissionsInFlight.add(rawReq.id);
|
|
12225
12536
|
try {
|
|
12226
|
-
|
|
12227
|
-
|
|
12228
|
-
|
|
12229
|
-
|
|
12230
|
-
session.respondPermission(req.id, { behavior: "deny", message, interrupt: false });
|
|
12231
|
-
this.deps.log.warn(
|
|
12232
|
-
{ sessionId, tool: req.toolName, err: message },
|
|
12233
|
-
"permission request refused before a row existed"
|
|
12234
|
-
);
|
|
12235
|
-
await this.broadcast(sessionId, { type: "error", code: errorCode(message), message });
|
|
12236
|
-
return;
|
|
12237
|
-
}
|
|
12238
|
-
const input = this.secrets.redact(req.input);
|
|
12239
|
-
const suggestions = this.secrets.redact(req.suggestions);
|
|
12240
|
-
const { data, error } = await this.deps.db.from("permission_requests").insert({
|
|
12241
|
-
session_id: sessionId,
|
|
12242
|
-
workspace_id: this.deps.workspaceId,
|
|
12243
|
-
tool_use_id: req.toolUseId,
|
|
12244
|
-
tool_name: req.toolName,
|
|
12245
|
-
input,
|
|
12246
|
-
suggestions
|
|
12247
|
-
}).select("*").single();
|
|
12248
|
-
if (error || !data) {
|
|
12249
|
-
const message = this.cap(
|
|
12250
|
-
`VALIDATION: could not persist the permission request for ${req.toolName}: ${error?.message ?? "no row"}`
|
|
12251
|
-
);
|
|
12252
|
-
session.respondPermission(req.id, { behavior: "deny", message, interrupt: false });
|
|
12253
|
-
await this.broadcast(sessionId, { type: "error", code: "VALIDATION", message });
|
|
12254
|
-
throw new Error(message);
|
|
12255
|
-
}
|
|
12256
|
-
live.permissions.set(data.id, {
|
|
12257
|
-
localId: req.id,
|
|
12258
|
-
toolName: req.toolName,
|
|
12259
|
-
input,
|
|
12260
|
-
suggestions
|
|
12261
|
-
});
|
|
12262
|
-
await this.setStatus(sessionId, "awaiting_permission");
|
|
12263
|
-
try {
|
|
12264
|
-
await this.broadcast(sessionId, {
|
|
12265
|
-
type: "permission.request",
|
|
12266
|
-
request: data
|
|
12267
|
-
});
|
|
12268
|
-
} catch (err) {
|
|
12269
|
-
if (!(err instanceof Error) || !err.message.startsWith("VALIDATION:")) throw err;
|
|
12270
|
-
this.deps.log.warn(
|
|
12271
|
-
{ sessionId, requestId: data.id, err: err.message },
|
|
12272
|
-
"permission request too large to broadcast; clients reconcile from the row"
|
|
12273
|
-
);
|
|
12274
|
-
await this.broadcast(sessionId, {
|
|
12275
|
-
type: "error",
|
|
12276
|
-
code: "VALIDATION",
|
|
12277
|
-
message: err.message
|
|
12278
|
-
});
|
|
12537
|
+
await raise(rawReq);
|
|
12538
|
+
} finally {
|
|
12539
|
+
live.permissionsInFlight.delete(rawReq.id);
|
|
12540
|
+
live.permissionsWithdrawn.delete(rawReq.id);
|
|
12279
12541
|
}
|
|
12280
12542
|
})()
|
|
12281
12543
|
);
|
|
12282
12544
|
session.on(
|
|
12283
12545
|
"permissionTimeout",
|
|
12284
12546
|
({ id }) => guard(async () => {
|
|
12547
|
+
const ended = live.turns.ended;
|
|
12285
12548
|
const requestId = [...live.permissions.entries()].find(
|
|
12286
12549
|
([, entry]) => entry.localId === id
|
|
12287
12550
|
)?.[0];
|
|
12288
|
-
if (!requestId)
|
|
12289
|
-
|
|
12290
|
-
|
|
12291
|
-
if (error) {
|
|
12292
|
-
this.deps.log.warn(
|
|
12293
|
-
{ requestId, err: error.message },
|
|
12294
|
-
"could not expire permission request"
|
|
12295
|
-
);
|
|
12296
|
-
} else if (data && data.length > 0) {
|
|
12297
|
-
await this.broadcast(sessionId, {
|
|
12298
|
-
type: "permission.resolved",
|
|
12299
|
-
requestId,
|
|
12300
|
-
status: "expired"
|
|
12301
|
-
});
|
|
12551
|
+
if (!requestId) {
|
|
12552
|
+
if (live.permissionsInFlight.has(id)) live.permissionsWithdrawn.add(id);
|
|
12553
|
+
return;
|
|
12302
12554
|
}
|
|
12303
|
-
|
|
12555
|
+
live.permissions.delete(requestId);
|
|
12556
|
+
await this.expirePermission(sessionId, requestId);
|
|
12557
|
+
if (live.permissions.size === 0 && !live.stopping && live.status === "awaiting_permission" && live.turns.ended === ended) {
|
|
12304
12558
|
await this.setStatus(sessionId, "running");
|
|
12305
12559
|
}
|
|
12306
12560
|
})()
|
|
@@ -12343,6 +12597,26 @@ var init_manager = __esm({
|
|
|
12343
12597
|
})();
|
|
12344
12598
|
});
|
|
12345
12599
|
}
|
|
12600
|
+
/**
|
|
12601
|
+
* Expire a request the session settled itself, and say so when this write is what settled the
|
|
12602
|
+
* row. `permissions_expire_by_runner` is the one transition the runner may write. Filtered on
|
|
12603
|
+
* status = 'pending' so an owner answer in the same instant wins: the row is then terminal and
|
|
12604
|
+
* this matches nothing rather than failing. A write that fails is logged, never thrown.
|
|
12605
|
+
*/
|
|
12606
|
+
async expirePermission(sessionId, requestId) {
|
|
12607
|
+
const { data, error } = await this.deps.db.from("permission_requests").update({ status: "expired" }).eq("id", requestId).eq("status", "pending").select("id");
|
|
12608
|
+
if (error) {
|
|
12609
|
+
this.deps.log.warn({ requestId, err: error.message }, "could not expire permission request");
|
|
12610
|
+
return;
|
|
12611
|
+
}
|
|
12612
|
+
if (data && data.length > 0) {
|
|
12613
|
+
await this.broadcast(sessionId, {
|
|
12614
|
+
type: "permission.resolved",
|
|
12615
|
+
requestId,
|
|
12616
|
+
status: "expired"
|
|
12617
|
+
});
|
|
12618
|
+
}
|
|
12619
|
+
}
|
|
12346
12620
|
/**
|
|
12347
12621
|
* Forget `live` and release its channel — once, and only if it is still the current entry.
|
|
12348
12622
|
*
|
|
@@ -15418,7 +15692,7 @@ var configForWrite = () => {
|
|
|
15418
15692
|
});
|
|
15419
15693
|
return cfg;
|
|
15420
15694
|
};
|
|
15421
|
-
var WEB_URL = "https://cayenne
|
|
15695
|
+
var WEB_URL = "https://app.cayenne.sh";
|
|
15422
15696
|
var PKG = "@jpcasabiancai/cayenne";
|
|
15423
15697
|
var service = () => {
|
|
15424
15698
|
const exec4 = nodeExec();
|