doer-agent 0.3.4 → 0.3.6
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/agent.js +317 -80
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -18,6 +18,7 @@ const codexAuthRpcCodec = StringCodec();
|
|
|
18
18
|
const settingsRpcCodec = StringCodec();
|
|
19
19
|
const gitRpcCodec = StringCodec();
|
|
20
20
|
const skillRpcCodec = StringCodec();
|
|
21
|
+
const runEventsCodec = StringCodec();
|
|
21
22
|
const retainedRuns = new Map();
|
|
22
23
|
const activeSessionWatchers = new Map();
|
|
23
24
|
const sessionLineIndexCache = new Map();
|
|
@@ -30,6 +31,9 @@ function sanitizeUserId(userId) {
|
|
|
30
31
|
function buildAgentRunRpcSubject(userId, agentId) {
|
|
31
32
|
return `doer.agent.run.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
32
33
|
}
|
|
34
|
+
function buildAgentRunEventsSubject(userId, agentId) {
|
|
35
|
+
return `doer.agent.run.events.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
36
|
+
}
|
|
33
37
|
function buildAgentSessionRpcSubject(userId, agentId) {
|
|
34
38
|
return `doer.agent.session.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
35
39
|
}
|
|
@@ -514,7 +518,7 @@ async function resolveRunStartLockPath(args) {
|
|
|
514
518
|
if (typeof args.sessionId === "string" && args.sessionId.trim()) {
|
|
515
519
|
return path.join(dir, `session__${sanitizeRunLockSegment(args.sessionId)}.lock`);
|
|
516
520
|
}
|
|
517
|
-
return path.join(dir,
|
|
521
|
+
return path.join(dir, "pending_new_session.lock");
|
|
518
522
|
}
|
|
519
523
|
async function claimRunStartSlot(args) {
|
|
520
524
|
const lockPath = await resolveRunStartLockPath(args);
|
|
@@ -609,7 +613,6 @@ function createDefaultAgentSettingsConfig() {
|
|
|
609
613
|
codex: {
|
|
610
614
|
model: "gpt-5.4",
|
|
611
615
|
authMode: "api_key",
|
|
612
|
-
apiKey: null,
|
|
613
616
|
},
|
|
614
617
|
realtime: {
|
|
615
618
|
model: process.env.OPENAI_REALTIME_MODEL?.trim() || "gpt-realtime",
|
|
@@ -687,7 +690,6 @@ function normalizeAgentSettingsConfig(value, fallback) {
|
|
|
687
690
|
codex: {
|
|
688
691
|
model: typeof codex.model === "string" && codex.model.trim() ? codex.model.trim() : base.codex.model,
|
|
689
692
|
authMode: codex.authMode === "oauth" ? "oauth" : codex.authMode === "api_key" ? "api_key" : base.codex.authMode,
|
|
690
|
-
apiKey: codex.apiKey === null ? null : normalizeNullableString(codex.apiKey) ?? base.codex.apiKey,
|
|
691
693
|
},
|
|
692
694
|
realtime: {
|
|
693
695
|
model: typeof realtime.model === "string" && realtime.model.trim() ? realtime.model.trim() : base.realtime.model,
|
|
@@ -768,7 +770,6 @@ function toMaskedSecret(value) {
|
|
|
768
770
|
return { has: true, masked: maskSecretPreview(value), length: value.length };
|
|
769
771
|
}
|
|
770
772
|
function toAgentSettingsPublic(config) {
|
|
771
|
-
const codexKey = toMaskedSecret(config.codex.apiKey);
|
|
772
773
|
const realtimeKey = toMaskedSecret(config.realtime.apiKey);
|
|
773
774
|
const gitOauth = toMaskedSecret(config.git.oauthToken);
|
|
774
775
|
const awsSecret = toMaskedSecret(config.aws.secretAccessKey);
|
|
@@ -784,9 +785,9 @@ function toAgentSettingsPublic(config) {
|
|
|
784
785
|
codex: {
|
|
785
786
|
model: config.codex.model,
|
|
786
787
|
authMode: config.codex.authMode,
|
|
787
|
-
hasApiKey:
|
|
788
|
-
apiKeyMasked:
|
|
789
|
-
apiKeyLength:
|
|
788
|
+
hasApiKey: false,
|
|
789
|
+
apiKeyMasked: null,
|
|
790
|
+
apiKeyLength: null,
|
|
790
791
|
},
|
|
791
792
|
realtime: {
|
|
792
793
|
model: config.realtime.model,
|
|
@@ -874,7 +875,6 @@ function normalizeAgentSettingsPatch(value) {
|
|
|
874
875
|
move("firstTurnPrompt", "general", "firstTurnPrompt");
|
|
875
876
|
move("codexModel", "codex", "model");
|
|
876
877
|
move("codexAuthMode", "codex", "authMode");
|
|
877
|
-
move("codexApiKey", "codex", "apiKey");
|
|
878
878
|
move("realtimeModel", "realtime", "model");
|
|
879
879
|
move("realtimeVoice", "realtime", "voice");
|
|
880
880
|
move("realtimeWakeName", "realtime", "wakeName");
|
|
@@ -915,9 +915,6 @@ async function resolveAgentSettingsConfig(args) {
|
|
|
915
915
|
}
|
|
916
916
|
function buildAgentSettingsEnvPatch(config) {
|
|
917
917
|
const envPatch = {};
|
|
918
|
-
if (config.codex.authMode === "api_key" && config.codex.apiKey) {
|
|
919
|
-
envPatch.OPENAI_API_KEY = config.codex.apiKey;
|
|
920
|
-
}
|
|
921
918
|
if (config.git.enabled) {
|
|
922
919
|
if (config.git.name)
|
|
923
920
|
envPatch.GIT_AUTHOR_NAME = config.git.name;
|
|
@@ -983,34 +980,136 @@ function cloneRunTask(task, _sinceSeq) {
|
|
|
983
980
|
...task,
|
|
984
981
|
};
|
|
985
982
|
}
|
|
986
|
-
function
|
|
983
|
+
function buildImmediateRunChangedEvent(task) {
|
|
984
|
+
return {
|
|
985
|
+
type: "run.changed",
|
|
986
|
+
agentId: task.agentId,
|
|
987
|
+
sessionId: task.sessionId,
|
|
988
|
+
filePath: task.sessionFilePath,
|
|
989
|
+
runId: task.id,
|
|
990
|
+
updatedAt: task.updatedAt,
|
|
991
|
+
status: task.status,
|
|
992
|
+
cancelRequested: task.cancelRequested,
|
|
993
|
+
resultExitCode: task.resultExitCode,
|
|
994
|
+
resultSignal: task.resultSignal,
|
|
995
|
+
error: task.error,
|
|
996
|
+
finishedAt: task.finishedAt,
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
function publishImmediateRunChangedEvent(args) {
|
|
1000
|
+
args.nc.publish(buildAgentRunEventsSubject(args.userId, args.task.agentId), runEventsCodec.encode(JSON.stringify(buildImmediateRunChangedEvent(args.task))));
|
|
1001
|
+
}
|
|
1002
|
+
async function findSessionFilePathBySessionId(sessionId) {
|
|
1003
|
+
const targetSessionId = sessionId.trim();
|
|
1004
|
+
if (!targetSessionId) {
|
|
1005
|
+
return null;
|
|
1006
|
+
}
|
|
1007
|
+
let sessionsRootStat;
|
|
987
1008
|
try {
|
|
988
|
-
|
|
989
|
-
const lineType = typeof parsed.type === "string" ? parsed.type : "";
|
|
990
|
-
if (!parsed.payload || typeof parsed.payload !== "object" || Array.isArray(parsed.payload)) {
|
|
991
|
-
return { sessionId: null, sessionFilePath: null };
|
|
992
|
-
}
|
|
993
|
-
const payload = parsed.payload;
|
|
994
|
-
const sessionIdCandidate = typeof payload.sessionId === "string" && payload.sessionId.trim()
|
|
995
|
-
? payload.sessionId.trim()
|
|
996
|
-
: typeof payload.session_id === "string" && payload.session_id.trim()
|
|
997
|
-
? payload.session_id.trim()
|
|
998
|
-
: typeof payload.id === "string" && payload.id.trim() && (lineType === "session_meta" || lineType === "session.started")
|
|
999
|
-
? payload.id.trim()
|
|
1000
|
-
: null;
|
|
1001
|
-
const filePathCandidate = typeof payload.rollout_path === "string" && payload.rollout_path.trim()
|
|
1002
|
-
? payload.rollout_path.trim()
|
|
1003
|
-
: typeof payload.filePath === "string" && payload.filePath.trim()
|
|
1004
|
-
? payload.filePath.trim()
|
|
1005
|
-
: null;
|
|
1006
|
-
return {
|
|
1007
|
-
sessionId: sessionIdCandidate,
|
|
1008
|
-
sessionFilePath: filePathCandidate,
|
|
1009
|
-
};
|
|
1009
|
+
sessionsRootStat = await stat(getSessionsRootPath());
|
|
1010
1010
|
}
|
|
1011
1011
|
catch {
|
|
1012
|
-
return
|
|
1012
|
+
return null;
|
|
1013
|
+
}
|
|
1014
|
+
if (!sessionsRootStat.isDirectory()) {
|
|
1015
|
+
return null;
|
|
1016
|
+
}
|
|
1017
|
+
const files = await collectSessionJsonlFiles(getSessionsRootPath());
|
|
1018
|
+
files.sort((a, b) => b.mtimeMs - a.mtimeMs || a.filePath.localeCompare(b.filePath));
|
|
1019
|
+
for (const file of files) {
|
|
1020
|
+
let fileHandle = null;
|
|
1021
|
+
try {
|
|
1022
|
+
fileHandle = await open(file.filePath, "r");
|
|
1023
|
+
const entryStat = await fileHandle.stat();
|
|
1024
|
+
const firstLine = await readFirstLine(fileHandle, entryStat.size);
|
|
1025
|
+
if (!firstLine) {
|
|
1026
|
+
continue;
|
|
1027
|
+
}
|
|
1028
|
+
const parsed = JSON.parse(firstLine);
|
|
1029
|
+
const candidateMeta = parsed && parsed.type === "session_meta" && isObjectRecord(parsed.payload)
|
|
1030
|
+
? parsed.payload
|
|
1031
|
+
: isObjectRecord(parsed.session_meta)
|
|
1032
|
+
? parsed.session_meta
|
|
1033
|
+
: isObjectRecord(parsed.sessionMeta)
|
|
1034
|
+
? parsed.sessionMeta
|
|
1035
|
+
: isObjectRecord(parsed.meta)
|
|
1036
|
+
? parsed.meta
|
|
1037
|
+
: isObjectRecord(parsed.payload)
|
|
1038
|
+
? parsed.payload
|
|
1039
|
+
: parsed;
|
|
1040
|
+
const candidateId = pickSessionString(candidateMeta.sessionId, candidateMeta.session_id, candidateMeta.id);
|
|
1041
|
+
if (candidateId === targetSessionId) {
|
|
1042
|
+
return file.filePath;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
catch {
|
|
1046
|
+
// ignore malformed session files
|
|
1047
|
+
}
|
|
1048
|
+
finally {
|
|
1049
|
+
await fileHandle?.close().catch(() => undefined);
|
|
1050
|
+
}
|
|
1013
1051
|
}
|
|
1052
|
+
return null;
|
|
1053
|
+
}
|
|
1054
|
+
async function readSessionIdFromSessionFile(filePath) {
|
|
1055
|
+
let fileHandle = null;
|
|
1056
|
+
try {
|
|
1057
|
+
fileHandle = await open(filePath, "r");
|
|
1058
|
+
const entryStat = await fileHandle.stat();
|
|
1059
|
+
const firstLine = await readFirstLine(fileHandle, entryStat.size);
|
|
1060
|
+
if (!firstLine) {
|
|
1061
|
+
return null;
|
|
1062
|
+
}
|
|
1063
|
+
const parsed = JSON.parse(firstLine);
|
|
1064
|
+
const candidateMeta = parsed && parsed.type === "session_meta" && isObjectRecord(parsed.payload)
|
|
1065
|
+
? parsed.payload
|
|
1066
|
+
: isObjectRecord(parsed.session_meta)
|
|
1067
|
+
? parsed.session_meta
|
|
1068
|
+
: isObjectRecord(parsed.sessionMeta)
|
|
1069
|
+
? parsed.sessionMeta
|
|
1070
|
+
: isObjectRecord(parsed.meta)
|
|
1071
|
+
? parsed.meta
|
|
1072
|
+
: isObjectRecord(parsed.payload)
|
|
1073
|
+
? parsed.payload
|
|
1074
|
+
: parsed;
|
|
1075
|
+
return pickSessionString(candidateMeta.sessionId, candidateMeta.session_id, candidateMeta.id);
|
|
1076
|
+
}
|
|
1077
|
+
catch {
|
|
1078
|
+
return null;
|
|
1079
|
+
}
|
|
1080
|
+
finally {
|
|
1081
|
+
await fileHandle?.close().catch(() => undefined);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
async function detectPendingRunSession(knownFilePaths) {
|
|
1085
|
+
const sessionsRoot = getSessionsRootPath();
|
|
1086
|
+
let sessionsRootStat;
|
|
1087
|
+
try {
|
|
1088
|
+
sessionsRootStat = await stat(sessionsRoot);
|
|
1089
|
+
}
|
|
1090
|
+
catch {
|
|
1091
|
+
return null;
|
|
1092
|
+
}
|
|
1093
|
+
if (!sessionsRootStat.isDirectory()) {
|
|
1094
|
+
return null;
|
|
1095
|
+
}
|
|
1096
|
+
const files = await collectSessionJsonlFiles(sessionsRoot);
|
|
1097
|
+
files.sort((a, b) => a.mtimeMs - b.mtimeMs || a.filePath.localeCompare(b.filePath));
|
|
1098
|
+
for (const file of files) {
|
|
1099
|
+
if (knownFilePaths.has(file.filePath)) {
|
|
1100
|
+
continue;
|
|
1101
|
+
}
|
|
1102
|
+
const sessionId = await readSessionIdFromSessionFile(file.filePath);
|
|
1103
|
+
if (!sessionId) {
|
|
1104
|
+
continue;
|
|
1105
|
+
}
|
|
1106
|
+
knownFilePaths.add(file.filePath);
|
|
1107
|
+
return {
|
|
1108
|
+
sessionId,
|
|
1109
|
+
sessionFilePath: file.filePath,
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
return null;
|
|
1014
1113
|
}
|
|
1015
1114
|
async function updateRunSessionMetadata(task, metadata) {
|
|
1016
1115
|
let changed = false;
|
|
@@ -1023,11 +1122,25 @@ async function updateRunSessionMetadata(task, metadata) {
|
|
|
1023
1122
|
task.sessionFilePath = metadata.sessionFilePath.trim();
|
|
1024
1123
|
changed = true;
|
|
1025
1124
|
}
|
|
1125
|
+
if (!task.sessionFilePath && task.sessionId) {
|
|
1126
|
+
const resolvedSessionFilePath = await findSessionFilePathBySessionId(task.sessionId).catch(() => null);
|
|
1127
|
+
if (resolvedSessionFilePath) {
|
|
1128
|
+
task.sessionFilePath = resolvedSessionFilePath;
|
|
1129
|
+
changed = true;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1026
1132
|
if (!changed) {
|
|
1027
1133
|
return;
|
|
1028
1134
|
}
|
|
1029
1135
|
task.updatedAt = formatLocalTimestamp();
|
|
1030
1136
|
await persistRunTask(task).catch(() => undefined);
|
|
1137
|
+
if (metadata.nc) {
|
|
1138
|
+
publishImmediateRunChangedEvent({
|
|
1139
|
+
nc: metadata.nc,
|
|
1140
|
+
userId: task.userId,
|
|
1141
|
+
task,
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1031
1144
|
if (!previousSessionId && task.sessionId) {
|
|
1032
1145
|
await updateRunStartSlotSession({
|
|
1033
1146
|
runId: task.id,
|
|
@@ -1113,6 +1226,7 @@ async function startManagedRun(args) {
|
|
|
1113
1226
|
userId: args.userId,
|
|
1114
1227
|
taskId: args.runId,
|
|
1115
1228
|
codexAuthBundle: args.codexAuthBundle,
|
|
1229
|
+
runtimeEnvPatch: args.runtimeEnvPatch,
|
|
1116
1230
|
});
|
|
1117
1231
|
const child = spawnManagedCodexCommand({
|
|
1118
1232
|
codexArgs: args.codexArgs,
|
|
@@ -1138,46 +1252,60 @@ async function startManagedRun(args) {
|
|
|
1138
1252
|
startedAt: now,
|
|
1139
1253
|
finishedAt: null,
|
|
1140
1254
|
};
|
|
1141
|
-
let
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1255
|
+
let pendingSessionPollClosed = false;
|
|
1256
|
+
let pendingSessionPollTimer = null;
|
|
1257
|
+
const knownPendingSessionFiles = new Set();
|
|
1258
|
+
const stopPendingSessionPoll = () => {
|
|
1259
|
+
pendingSessionPollClosed = true;
|
|
1260
|
+
if (pendingSessionPollTimer) {
|
|
1261
|
+
clearInterval(pendingSessionPollTimer);
|
|
1262
|
+
pendingSessionPollTimer = null;
|
|
1263
|
+
}
|
|
1264
|
+
};
|
|
1265
|
+
const pollPendingSession = async () => {
|
|
1266
|
+
if (pendingSessionPollClosed || task.sessionId) {
|
|
1267
|
+
stopPendingSessionPoll();
|
|
1145
1268
|
return;
|
|
1146
1269
|
}
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
}
|
|
1270
|
+
const detected = await detectPendingRunSession(knownPendingSessionFiles).catch(() => null);
|
|
1271
|
+
if (!detected) {
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
await updateRunSessionMetadata(task, {
|
|
1275
|
+
sessionId: detected.sessionId,
|
|
1276
|
+
sessionFilePath: detected.sessionFilePath,
|
|
1277
|
+
nc: args.nc,
|
|
1278
|
+
}).catch(() => undefined);
|
|
1279
|
+
if (task.sessionId) {
|
|
1280
|
+
stopPendingSessionPoll();
|
|
1159
1281
|
}
|
|
1160
1282
|
};
|
|
1161
|
-
|
|
1162
|
-
|
|
1283
|
+
if (!task.sessionId) {
|
|
1284
|
+
const existingFiles = await collectSessionJsonlFiles(getSessionsRootPath()).catch(() => []);
|
|
1285
|
+
for (const file of existingFiles) {
|
|
1286
|
+
knownPendingSessionFiles.add(file.filePath);
|
|
1287
|
+
}
|
|
1288
|
+
pendingSessionPollTimer = setInterval(() => {
|
|
1289
|
+
void pollPendingSession();
|
|
1290
|
+
}, 1000);
|
|
1291
|
+
}
|
|
1292
|
+
child.stdout.on("data", (chunk) => writeRunStream(task.id, "stdout", chunk));
|
|
1293
|
+
child.stderr.on("data", (chunk) => writeRunStream(task.id, "stderr", chunk));
|
|
1163
1294
|
child.once("error", (error) => {
|
|
1295
|
+
stopPendingSessionPoll();
|
|
1164
1296
|
const message = error instanceof Error ? error.message : String(error);
|
|
1165
1297
|
task.status = "failed";
|
|
1166
1298
|
task.error = message;
|
|
1167
1299
|
task.finishedAt = formatLocalTimestamp();
|
|
1168
1300
|
persistRetainedRun(task);
|
|
1301
|
+
publishImmediateRunChangedEvent({ nc: args.nc, userId: task.userId, task });
|
|
1169
1302
|
void removeRunTask(task.id).catch(() => undefined);
|
|
1170
1303
|
void releaseRunStartSlot({ runId: task.id, sessionId: task.sessionId }).catch(() => undefined);
|
|
1171
1304
|
void prepared.codexAuthCleanup().catch(() => undefined);
|
|
1172
1305
|
writeRunStatus(task.id, `failed error=${message}`);
|
|
1173
1306
|
});
|
|
1174
1307
|
child.once("close", async (code, signal) => {
|
|
1175
|
-
|
|
1176
|
-
const metadata = extractCodexSessionMetadata(stdoutBuffer.trim());
|
|
1177
|
-
if (metadata.sessionId || metadata.sessionFilePath) {
|
|
1178
|
-
void updateRunSessionMetadata(task, metadata);
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1308
|
+
stopPendingSessionPoll();
|
|
1181
1309
|
const latest = await getStoredRun(task.id).catch(() => null);
|
|
1182
1310
|
if (latest?.cancelRequested) {
|
|
1183
1311
|
task.cancelRequested = true;
|
|
@@ -1188,6 +1316,7 @@ async function startManagedRun(args) {
|
|
|
1188
1316
|
task.status = task.cancelRequested ? "canceled" : (task.resultExitCode ?? 1) === 0 ? "completed" : "failed";
|
|
1189
1317
|
task.error = task.status === "failed" ? `Command exited with code ${task.resultExitCode ?? "null"}` : null;
|
|
1190
1318
|
persistRetainedRun(task);
|
|
1319
|
+
publishImmediateRunChangedEvent({ nc: args.nc, userId: task.userId, task });
|
|
1191
1320
|
void removeRunTask(task.id).catch(() => undefined);
|
|
1192
1321
|
void releaseRunStartSlot({ runId: task.id, sessionId: task.sessionId }).catch(() => undefined);
|
|
1193
1322
|
void prepared.codexAuthCleanup().catch(() => undefined);
|
|
@@ -1195,7 +1324,11 @@ async function startManagedRun(args) {
|
|
|
1195
1324
|
});
|
|
1196
1325
|
persistRetainedRun(task);
|
|
1197
1326
|
void persistRunTask(task).catch(() => undefined);
|
|
1327
|
+
publishImmediateRunChangedEvent({ nc: args.nc, userId: task.userId, task });
|
|
1198
1328
|
writeRunStatus(task.id, `started requestId=${args.requestId} cwd=${prepared.taskWorkspace}`);
|
|
1329
|
+
if (!task.sessionId) {
|
|
1330
|
+
void pollPendingSession();
|
|
1331
|
+
}
|
|
1199
1332
|
return cloneRunTask(task);
|
|
1200
1333
|
}
|
|
1201
1334
|
function shellSingleQuote(value) {
|
|
@@ -1312,6 +1445,62 @@ async function runLocalCodexCli(args, timeoutMs, envPatch) {
|
|
|
1312
1445
|
});
|
|
1313
1446
|
});
|
|
1314
1447
|
}
|
|
1448
|
+
async function runLocalCodexCliWithInput(args, input, timeoutMs, envPatch) {
|
|
1449
|
+
const command = buildLocalCodexCliCommand(args);
|
|
1450
|
+
const workspaceRoot = workspaceRootOverride ?? (process.env.WORKSPACE?.trim() || process.cwd());
|
|
1451
|
+
const env = {
|
|
1452
|
+
...process.env,
|
|
1453
|
+
...(envPatch ?? {}),
|
|
1454
|
+
WORKSPACE: workspaceRoot,
|
|
1455
|
+
CODEX_HOME: resolveCodexHomePath(),
|
|
1456
|
+
};
|
|
1457
|
+
return await new Promise((resolve, reject) => {
|
|
1458
|
+
const child = spawn(command, {
|
|
1459
|
+
cwd: workspaceRoot,
|
|
1460
|
+
shell: resolveShellPath(),
|
|
1461
|
+
env,
|
|
1462
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1463
|
+
});
|
|
1464
|
+
let stdout = "";
|
|
1465
|
+
let stderr = "";
|
|
1466
|
+
let done = false;
|
|
1467
|
+
let timedOut = false;
|
|
1468
|
+
child.stdout.setEncoding("utf8");
|
|
1469
|
+
child.stderr.setEncoding("utf8");
|
|
1470
|
+
child.stdout.on("data", (chunk) => {
|
|
1471
|
+
stdout += chunk;
|
|
1472
|
+
});
|
|
1473
|
+
child.stderr.on("data", (chunk) => {
|
|
1474
|
+
stderr += chunk;
|
|
1475
|
+
});
|
|
1476
|
+
child.stdin?.write(input);
|
|
1477
|
+
if (!input.endsWith("\n")) {
|
|
1478
|
+
child.stdin?.write("\n");
|
|
1479
|
+
}
|
|
1480
|
+
child.stdin?.end();
|
|
1481
|
+
const timer = setTimeout(() => {
|
|
1482
|
+
timedOut = true;
|
|
1483
|
+
sendSignalToTaskProcess(child, "SIGTERM");
|
|
1484
|
+
setTimeout(() => sendSignalToTaskProcess(child, "SIGKILL"), 1000);
|
|
1485
|
+
}, Math.max(500, timeoutMs));
|
|
1486
|
+
child.once("error", (error) => {
|
|
1487
|
+
if (done) {
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
done = true;
|
|
1491
|
+
clearTimeout(timer);
|
|
1492
|
+
reject(error);
|
|
1493
|
+
});
|
|
1494
|
+
child.once("exit", (code) => {
|
|
1495
|
+
if (done) {
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
done = true;
|
|
1499
|
+
clearTimeout(timer);
|
|
1500
|
+
resolve({ code, stdout, stderr, timedOut });
|
|
1501
|
+
});
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1315
1504
|
function buildSkillGeneratorPrompt(userPrompt) {
|
|
1316
1505
|
return [
|
|
1317
1506
|
"Create a Codex skill from the user's description.",
|
|
@@ -1663,6 +1852,20 @@ async function startLocalCodexLogin() {
|
|
|
1663
1852
|
}
|
|
1664
1853
|
throw new Error(normalized || `Codex login failed with code ${result.code ?? "null"}`);
|
|
1665
1854
|
}
|
|
1855
|
+
async function loginLocalCodexWithApiKey(apiKey) {
|
|
1856
|
+
const result = await runLocalCodexCliWithInput(["login", "--with-api-key"], apiKey, 15000);
|
|
1857
|
+
const normalized = stripAnsi([result.stdout, result.stderr].filter(Boolean).join("\n")).trim();
|
|
1858
|
+
if ((result.code ?? 1) !== 0) {
|
|
1859
|
+
throw new Error(normalized || `Codex API key login failed with code ${result.code ?? "null"}`);
|
|
1860
|
+
}
|
|
1861
|
+
const status = await getLocalCodexLoginStatus().catch(() => null);
|
|
1862
|
+
return {
|
|
1863
|
+
loggedIn: status?.loggedIn === true,
|
|
1864
|
+
output: status?.output || normalized || "Logged in",
|
|
1865
|
+
verificationUri: null,
|
|
1866
|
+
userCode: null,
|
|
1867
|
+
};
|
|
1868
|
+
}
|
|
1666
1869
|
async function logoutLocalCodexAuth() {
|
|
1667
1870
|
if (pendingCodexDeviceAuth && pendingCodexDeviceAuth.child.exitCode === null) {
|
|
1668
1871
|
sendSignalToTaskProcess(pendingCodexDeviceAuth.child, "SIGTERM");
|
|
@@ -1694,11 +1897,15 @@ function normalizeCodexAuthRpcRequest(args) {
|
|
|
1694
1897
|
const responseSubject = typeof args.request.responseSubject === "string" ? args.request.responseSubject.trim() : "";
|
|
1695
1898
|
const requestAgentId = typeof args.request.agentId === "string" ? args.request.agentId.trim() : "";
|
|
1696
1899
|
const actionRaw = typeof args.request.action === "string" ? args.request.action.trim() : "";
|
|
1697
|
-
const action = actionRaw === "start" || actionRaw === "logout" ? actionRaw : "status";
|
|
1900
|
+
const action = actionRaw === "start" || actionRaw === "logout" || actionRaw === "login_api_key" ? actionRaw : "status";
|
|
1901
|
+
const apiKey = typeof args.request.apiKey === "string" && args.request.apiKey.trim() ? args.request.apiKey.trim() : null;
|
|
1698
1902
|
if (!requestId || !responseSubject || !requestAgentId || requestAgentId !== args.agentId) {
|
|
1699
1903
|
throw new Error("invalid codex auth rpc request");
|
|
1700
1904
|
}
|
|
1701
|
-
|
|
1905
|
+
if (action === "login_api_key" && !apiKey) {
|
|
1906
|
+
throw new Error("api key is required");
|
|
1907
|
+
}
|
|
1908
|
+
return { requestId, responseSubject, action, apiKey };
|
|
1702
1909
|
}
|
|
1703
1910
|
function publishCodexAuthRpcResponse(args) {
|
|
1704
1911
|
args.nc.publish(args.responseSubject, codexAuthRpcCodec.encode(JSON.stringify(args.payload)));
|
|
@@ -1712,7 +1919,10 @@ async function handleCodexAuthRpcMessage(args) {
|
|
|
1712
1919
|
requestId = request.requestId;
|
|
1713
1920
|
responseSubject = request.responseSubject;
|
|
1714
1921
|
let result = null;
|
|
1715
|
-
if (request.action === "
|
|
1922
|
+
if (request.action === "login_api_key") {
|
|
1923
|
+
result = await loginLocalCodexWithApiKey(request.apiKey ?? "");
|
|
1924
|
+
}
|
|
1925
|
+
else if (request.action === "start") {
|
|
1716
1926
|
const status = await getLocalCodexLoginStatus();
|
|
1717
1927
|
if (status.loggedIn) {
|
|
1718
1928
|
result = { loggedIn: true, output: status.output };
|
|
@@ -2133,6 +2343,7 @@ async function handleRunRpcMessage(args) {
|
|
|
2133
2343
|
serverBaseUrl: args.serverBaseUrl,
|
|
2134
2344
|
userId: args.userId,
|
|
2135
2345
|
agentId: args.agentId,
|
|
2346
|
+
nc: args.jetstream.nc,
|
|
2136
2347
|
sessionId: request.sessionId,
|
|
2137
2348
|
codexArgs: buildManagedCodexArgs({
|
|
2138
2349
|
prompt: request.prompt ?? "",
|
|
@@ -2182,6 +2393,7 @@ async function handleRunRpcMessage(args) {
|
|
|
2182
2393
|
target.cancelRequested = true;
|
|
2183
2394
|
target.updatedAt = formatLocalTimestamp();
|
|
2184
2395
|
await persistRunTask(target);
|
|
2396
|
+
publishImmediateRunChangedEvent({ nc: args.jetstream.nc, userId: target.userId, task: target });
|
|
2185
2397
|
writeRunStatus(target.id, `cancel requested pid=${target.processPid}`);
|
|
2186
2398
|
sendSignalToPid(target.processPid, "SIGINT");
|
|
2187
2399
|
const task = cloneRunTask(target);
|
|
@@ -3560,16 +3772,18 @@ function normalizeShellRpcCodexAuthBundle(value) {
|
|
|
3560
3772
|
}
|
|
3561
3773
|
const row = value;
|
|
3562
3774
|
const authJson = typeof row.authJson === "string" ? row.authJson : null;
|
|
3563
|
-
|
|
3775
|
+
const authMode = row.authMode === "oauth" ? "oauth" : row.authMode === "api_key" ? "api_key" : undefined;
|
|
3776
|
+
const apiKey = typeof row.apiKey === "string" || row.apiKey === null ? row.apiKey : undefined;
|
|
3777
|
+
if (!authJson && authMode !== "api_key" && apiKey === undefined) {
|
|
3564
3778
|
return null;
|
|
3565
3779
|
}
|
|
3566
3780
|
return {
|
|
3567
3781
|
taskId: typeof row.taskId === "string" ? row.taskId : undefined,
|
|
3568
|
-
authMode
|
|
3782
|
+
authMode,
|
|
3569
3783
|
issuedAt: typeof row.issuedAt === "string" ? row.issuedAt : undefined,
|
|
3570
3784
|
expiresAt: typeof row.expiresAt === "string" ? row.expiresAt : undefined,
|
|
3571
|
-
authJson,
|
|
3572
|
-
apiKey
|
|
3785
|
+
authJson: authJson ?? undefined,
|
|
3786
|
+
apiKey,
|
|
3573
3787
|
};
|
|
3574
3788
|
}
|
|
3575
3789
|
async function postJson(url, body) {
|
|
@@ -3734,27 +3948,49 @@ async function checkCancelRequested(args) {
|
|
|
3734
3948
|
const response = await getJson(`${args.serverBaseUrl}/api/agent/tasks/${encodeURIComponent(args.taskId)}/events?${query.toString()}`);
|
|
3735
3949
|
return Boolean(response.task?.cancelRequested);
|
|
3736
3950
|
}
|
|
3737
|
-
async function
|
|
3738
|
-
|
|
3951
|
+
async function syncCodexAuthState(args) {
|
|
3952
|
+
const envPatch = {};
|
|
3953
|
+
const synced = false;
|
|
3954
|
+
if (args.authMode === "api_key") {
|
|
3955
|
+
if (args.apiKey) {
|
|
3956
|
+
envPatch.OPENAI_API_KEY = args.apiKey;
|
|
3957
|
+
}
|
|
3958
|
+
}
|
|
3739
3959
|
return {
|
|
3740
|
-
envPatch
|
|
3960
|
+
envPatch,
|
|
3741
3961
|
cleanup: async () => { },
|
|
3742
3962
|
meta: {
|
|
3743
|
-
codexAuthSource:
|
|
3744
|
-
|
|
3963
|
+
codexAuthSource: args.source,
|
|
3964
|
+
codexAuthMode: args.authMode ?? null,
|
|
3965
|
+
codexAuthHasApiKey: Boolean(args.apiKey),
|
|
3966
|
+
codexAuthHasAuthJson: Boolean(args.authJson),
|
|
3967
|
+
codexAuthIssuedAt: args.issuedAt ?? null,
|
|
3968
|
+
codexAuthExpiresAt: args.expiresAt ?? null,
|
|
3969
|
+
codexAuthSynced: synced,
|
|
3745
3970
|
},
|
|
3746
3971
|
};
|
|
3747
3972
|
}
|
|
3973
|
+
async function prepareTaskCodexAuth(args) {
|
|
3974
|
+
void args;
|
|
3975
|
+
return await syncCodexAuthState({
|
|
3976
|
+
source: "agent_local",
|
|
3977
|
+
authJson: null,
|
|
3978
|
+
issuedAt: null,
|
|
3979
|
+
expiresAt: null,
|
|
3980
|
+
});
|
|
3981
|
+
}
|
|
3748
3982
|
async function prepareCodexAuthBundle(bundle) {
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3983
|
+
if (!bundle) {
|
|
3984
|
+
return null;
|
|
3985
|
+
}
|
|
3986
|
+
return await syncCodexAuthState({
|
|
3987
|
+
source: "server_bundle",
|
|
3988
|
+
authMode: bundle.authMode,
|
|
3989
|
+
apiKey: bundle.apiKey,
|
|
3990
|
+
authJson: bundle.authJson ?? null,
|
|
3991
|
+
issuedAt: bundle.issuedAt ?? null,
|
|
3992
|
+
expiresAt: bundle.expiresAt ?? null,
|
|
3993
|
+
});
|
|
3758
3994
|
}
|
|
3759
3995
|
async function prepareCommandExecution(args) {
|
|
3760
3996
|
const shellPath = resolveShellPath();
|
|
@@ -3768,6 +4004,7 @@ async function prepareCommandExecution(args) {
|
|
|
3768
4004
|
DOER_USER_ID: args.userId,
|
|
3769
4005
|
DOER_AGENT_TASK_ID: args.taskId,
|
|
3770
4006
|
...buildAgentSettingsEnvPatch(localAgentSettings),
|
|
4007
|
+
...args.runtimeEnvPatch,
|
|
3771
4008
|
...(codexAuth?.envPatch ?? {}),
|
|
3772
4009
|
WORKSPACE: taskWorkspace,
|
|
3773
4010
|
};
|