doer-agent 0.4.5 → 0.4.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/agent-fs-rpc.js +74 -67
- package/dist/agent-runtime-utils.js +88 -14
- package/dist/agent-runtime-utils.test.js +38 -0
- package/dist/agent-task-execution.js +1 -215
- package/dist/agent.js +9 -26
- package/package.json +3 -2
package/dist/agent-fs-rpc.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
2
|
+
import { mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
4
|
import { StringCodec } from "nats";
|
|
5
|
+
import { create as createTar, extract as extractTar } from "tar";
|
|
6
|
+
import { validateImageBytes } from "./agent-runtime-utils.js";
|
|
5
7
|
const fsRpcCodec = StringCodec();
|
|
6
8
|
function normalizeFsRpcPath(workspaceRoot, rawPath) {
|
|
7
9
|
const raw = typeof rawPath === "string" && rawPath.trim() ? rawPath.trim() : ".";
|
|
@@ -25,8 +27,7 @@ function parseFsRpcAction(value) {
|
|
|
25
27
|
value === "stat" ||
|
|
26
28
|
value === "upload_file" ||
|
|
27
29
|
value === "read_text" ||
|
|
28
|
-
value === "
|
|
29
|
-
value === "write_file" ||
|
|
30
|
+
value === "write_text" ||
|
|
30
31
|
value === "download_file" ||
|
|
31
32
|
value === "delete_path" ||
|
|
32
33
|
value === "archive_dir" ||
|
|
@@ -88,33 +89,20 @@ function inferMimeType(filePath) {
|
|
|
88
89
|
}
|
|
89
90
|
return "application/octet-stream";
|
|
90
91
|
}
|
|
91
|
-
function
|
|
92
|
-
|
|
93
|
-
if (!normalized || normalized.includes("..")) {
|
|
94
|
-
throw new Error("invalid archive entry path");
|
|
95
|
-
}
|
|
96
|
-
return normalized;
|
|
92
|
+
function sha256Hex(bytes) {
|
|
93
|
+
return crypto.createHash("sha256").update(bytes).digest("hex");
|
|
97
94
|
}
|
|
98
|
-
async function
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
const bytes = await readFile(child);
|
|
111
|
-
files.push({
|
|
112
|
-
relPath: normalizeArchiveRelativePath(path.relative(rootDir, child)),
|
|
113
|
-
contentBase64: Buffer.from(bytes).toString("base64"),
|
|
114
|
-
sizeBytes: bytes.byteLength,
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
return files;
|
|
95
|
+
async function createTarGzipBuffer(cwd, entries) {
|
|
96
|
+
const stream = createTar({
|
|
97
|
+
cwd,
|
|
98
|
+
gzip: true,
|
|
99
|
+
portable: true,
|
|
100
|
+
}, entries);
|
|
101
|
+
const chunks = [];
|
|
102
|
+
for await (const chunk of stream) {
|
|
103
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
104
|
+
}
|
|
105
|
+
return Buffer.concat(chunks);
|
|
118
106
|
}
|
|
119
107
|
async function executeFsRpc(args) {
|
|
120
108
|
const action = parseFsRpcAction(args.request.action);
|
|
@@ -168,13 +156,18 @@ async function executeFsRpc(args) {
|
|
|
168
156
|
throw new Error("archivePath is required");
|
|
169
157
|
}
|
|
170
158
|
const archiveTarget = normalizeFsRpcPath(args.workspaceRoot, rawArchivePath);
|
|
171
|
-
|
|
172
|
-
|
|
159
|
+
try {
|
|
160
|
+
const manifestEntry = await stat(path.join(abs, "SKILL.md"));
|
|
161
|
+
if (!manifestEntry.isFile()) {
|
|
162
|
+
throw new Error("Selected skill directory must contain SKILL.md");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
173
166
|
throw new Error("Selected skill directory must contain SKILL.md");
|
|
174
167
|
}
|
|
175
|
-
const payload = gzipSync(Buffer.from(JSON.stringify({ files }), "utf8"));
|
|
176
168
|
await mkdir(path.dirname(archiveTarget.abs), { recursive: true });
|
|
177
|
-
await
|
|
169
|
+
const archiveBytes = await createTarGzipBuffer(abs, ["."]);
|
|
170
|
+
await writeFile(archiveTarget.abs, archiveBytes);
|
|
178
171
|
const archiveStat = await stat(archiveTarget.abs);
|
|
179
172
|
return {
|
|
180
173
|
ok: true,
|
|
@@ -256,15 +249,12 @@ async function executeFsRpc(args) {
|
|
|
256
249
|
upload,
|
|
257
250
|
};
|
|
258
251
|
}
|
|
259
|
-
if (action === "
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
throw new Error("contentBase64 is required");
|
|
263
|
-
}
|
|
252
|
+
if (action === "write_text") {
|
|
253
|
+
const text = typeof args.request.text === "string" ? args.request.text : "";
|
|
254
|
+
const encoding = typeof args.request.encoding === "string" && args.request.encoding ? args.request.encoding : "utf8";
|
|
264
255
|
const parentDir = path.dirname(abs);
|
|
265
256
|
await mkdir(parentDir, { recursive: true });
|
|
266
|
-
|
|
267
|
-
await writeFile(abs, bytes);
|
|
257
|
+
await writeFile(abs, text, { encoding: encoding });
|
|
268
258
|
const entry = await stat(abs);
|
|
269
259
|
return {
|
|
270
260
|
ok: true,
|
|
@@ -274,6 +264,7 @@ async function executeFsRpc(args) {
|
|
|
274
264
|
size: entry.size,
|
|
275
265
|
mimeType: inferMimeType(abs),
|
|
276
266
|
mtimeMs: entry.mtimeMs,
|
|
267
|
+
encoding,
|
|
277
268
|
};
|
|
278
269
|
}
|
|
279
270
|
if (action === "delete_path") {
|
|
@@ -305,9 +296,16 @@ async function executeFsRpc(args) {
|
|
|
305
296
|
throw new Error(text || `download failed: ${response.status}`);
|
|
306
297
|
}
|
|
307
298
|
const bytes = Buffer.from(await response.arrayBuffer());
|
|
299
|
+
const validationError = validateImageBytes(abs, bytes);
|
|
300
|
+
if (validationError) {
|
|
301
|
+
throw new Error(validationError);
|
|
302
|
+
}
|
|
308
303
|
const parentDir = path.dirname(abs);
|
|
309
304
|
await mkdir(parentDir, { recursive: true });
|
|
310
305
|
await writeFile(abs, bytes);
|
|
306
|
+
if (abs.endsWith(".skillpkg")) {
|
|
307
|
+
console.log(`[doer-agent] skillpkg downloaded path=${formatPath(abs)} size=${bytes.byteLength} sha256=${sha256Hex(bytes)}`);
|
|
308
|
+
}
|
|
311
309
|
const entry = await stat(abs);
|
|
312
310
|
return {
|
|
313
311
|
ok: true,
|
|
@@ -330,18 +328,42 @@ async function executeFsRpc(args) {
|
|
|
330
328
|
}
|
|
331
329
|
const destinationTarget = normalizeFsRpcPath(args.workspaceRoot, rawDestinationPath);
|
|
332
330
|
const archiveBytes = await readFile(abs);
|
|
333
|
-
const
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
331
|
+
const magic = archiveBytes.subarray(0, 8).toString("hex");
|
|
332
|
+
const digest = sha256Hex(archiveBytes);
|
|
333
|
+
const destinationParent = path.dirname(destinationTarget.abs);
|
|
334
|
+
const tempDestinationAbs = path.join(destinationParent, `.tmp-extract-${path.basename(destinationTarget.abs)}-${crypto.randomBytes(6).toString("hex")}`);
|
|
335
|
+
await mkdir(destinationParent, { recursive: true });
|
|
336
|
+
try {
|
|
337
|
+
const existing = await stat(destinationTarget.abs);
|
|
338
|
+
if (existing.isDirectory()) {
|
|
339
|
+
const entries = await readdir(destinationTarget.abs);
|
|
340
|
+
if (entries.length > 0) {
|
|
341
|
+
throw new Error("destinationPath already exists");
|
|
342
|
+
}
|
|
341
343
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
344
|
+
else {
|
|
345
|
+
throw new Error("destinationPath already exists");
|
|
346
|
+
}
|
|
347
|
+
await rm(destinationTarget.abs, { recursive: true, force: true });
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
if (!(error instanceof Error) || !error.message.includes("ENOENT")) {
|
|
351
|
+
throw error;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
await mkdir(tempDestinationAbs, { recursive: true });
|
|
355
|
+
try {
|
|
356
|
+
await extractTar({
|
|
357
|
+
cwd: tempDestinationAbs,
|
|
358
|
+
file: abs,
|
|
359
|
+
gzip: true,
|
|
360
|
+
});
|
|
361
|
+
await rename(tempDestinationAbs, destinationTarget.abs);
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
await rm(tempDestinationAbs, { recursive: true, force: true }).catch(() => undefined);
|
|
365
|
+
const message = error instanceof Error ? error.message : "extract failed";
|
|
366
|
+
throw new Error(`${message} (magic=${magic} size=${archiveBytes.byteLength} sha256=${digest})`);
|
|
345
367
|
}
|
|
346
368
|
return {
|
|
347
369
|
ok: true,
|
|
@@ -354,21 +376,6 @@ async function executeFsRpc(args) {
|
|
|
354
376
|
if (!entry.isFile()) {
|
|
355
377
|
throw new Error("path is not a file");
|
|
356
378
|
}
|
|
357
|
-
if (action === "read_file") {
|
|
358
|
-
const maxBytes = Math.max(1, Math.min(normalizeFsRpcNumber(args.request.maxBytes, 2_000_000), 5_000_000));
|
|
359
|
-
const data = await readFile(abs);
|
|
360
|
-
const truncated = data.byteLength > maxBytes;
|
|
361
|
-
const bytes = truncated ? data.subarray(0, maxBytes) : data;
|
|
362
|
-
return {
|
|
363
|
-
ok: true,
|
|
364
|
-
action,
|
|
365
|
-
path: formatPath(abs),
|
|
366
|
-
mimeType: inferMimeType(abs),
|
|
367
|
-
size: entry.size,
|
|
368
|
-
truncated,
|
|
369
|
-
contentBase64: bytes.toString("base64"),
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
379
|
const offset = Math.max(0, normalizeFsRpcNumber(args.request.offset, 0));
|
|
373
380
|
const length = Math.max(1, Math.min(normalizeFsRpcNumber(args.request.length, 65536), 262144));
|
|
374
381
|
const encoding = typeof args.request.encoding === "string" && args.request.encoding ? args.request.encoding : "utf8";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
2
3
|
export function sanitizeUserId(userId) {
|
|
3
4
|
const normalized = userId.trim().replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
4
5
|
return normalized.length > 0 ? normalized : "anonymous";
|
|
@@ -93,6 +94,93 @@ export function normalizeRunImagePaths(value) {
|
|
|
93
94
|
}
|
|
94
95
|
return out;
|
|
95
96
|
}
|
|
97
|
+
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
98
|
+
const crc32Table = (() => {
|
|
99
|
+
const table = new Uint32Array(256);
|
|
100
|
+
for (let index = 0; index < 256; index += 1) {
|
|
101
|
+
let crc = index;
|
|
102
|
+
for (let bit = 0; bit < 8; bit += 1) {
|
|
103
|
+
crc = (crc & 1) !== 0 ? 0xedb88320 ^ (crc >>> 1) : crc >>> 1;
|
|
104
|
+
}
|
|
105
|
+
table[index] = crc >>> 0;
|
|
106
|
+
}
|
|
107
|
+
return table;
|
|
108
|
+
})();
|
|
109
|
+
function crc32(parts) {
|
|
110
|
+
let crc = 0xffffffff;
|
|
111
|
+
for (const part of parts) {
|
|
112
|
+
for (let index = 0; index < part.length; index += 1) {
|
|
113
|
+
crc = crc32Table[(crc ^ part[index]) & 0xff] ^ (crc >>> 8);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
117
|
+
}
|
|
118
|
+
function validatePngBytes(bytes) {
|
|
119
|
+
if (bytes.length < PNG_SIGNATURE.length || !bytes.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)) {
|
|
120
|
+
return "missing PNG signature";
|
|
121
|
+
}
|
|
122
|
+
let offset = PNG_SIGNATURE.length;
|
|
123
|
+
let sawIend = false;
|
|
124
|
+
while (offset < bytes.length) {
|
|
125
|
+
if (offset + 12 > bytes.length) {
|
|
126
|
+
return "truncated PNG chunk";
|
|
127
|
+
}
|
|
128
|
+
const chunkLength = bytes.readUInt32BE(offset);
|
|
129
|
+
const typeStart = offset + 4;
|
|
130
|
+
const dataStart = offset + 8;
|
|
131
|
+
const dataEnd = dataStart + chunkLength;
|
|
132
|
+
const crcOffset = dataEnd;
|
|
133
|
+
const nextOffset = crcOffset + 4;
|
|
134
|
+
if (dataEnd > bytes.length || nextOffset > bytes.length) {
|
|
135
|
+
return "truncated PNG chunk payload";
|
|
136
|
+
}
|
|
137
|
+
const type = bytes.subarray(typeStart, dataStart);
|
|
138
|
+
const expectedCrc = bytes.readUInt32BE(crcOffset);
|
|
139
|
+
const actualCrc = crc32([type, bytes.subarray(dataStart, dataEnd)]);
|
|
140
|
+
if (expectedCrc !== actualCrc) {
|
|
141
|
+
const chunkName = type.toString("ascii");
|
|
142
|
+
return `PNG CRC mismatch in ${chunkName} chunk`;
|
|
143
|
+
}
|
|
144
|
+
if (type.equals(Buffer.from("IEND"))) {
|
|
145
|
+
sawIend = true;
|
|
146
|
+
if (nextOffset !== bytes.length) {
|
|
147
|
+
return "unexpected trailing bytes after PNG IEND";
|
|
148
|
+
}
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
offset = nextOffset;
|
|
152
|
+
}
|
|
153
|
+
return sawIend ? null : "missing PNG IEND chunk";
|
|
154
|
+
}
|
|
155
|
+
export function validateImageBytes(filePath, bytes) {
|
|
156
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
157
|
+
if (ext === ".png") {
|
|
158
|
+
return validatePngBytes(bytes);
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
export async function filterValidRunImagePaths(args) {
|
|
163
|
+
const valid = [];
|
|
164
|
+
for (const imagePath of args.imagePaths) {
|
|
165
|
+
const absPath = path.isAbsolute(imagePath) ? imagePath : path.resolve(args.workspaceRoot, imagePath);
|
|
166
|
+
let bytes;
|
|
167
|
+
try {
|
|
168
|
+
bytes = await readFile(absPath);
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
const reason = error instanceof Error ? error.message : "failed to read image";
|
|
172
|
+
args.onInvalidImage?.(imagePath, reason);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const validationError = validateImageBytes(absPath, bytes);
|
|
176
|
+
if (validationError) {
|
|
177
|
+
args.onInvalidImage?.(imagePath, validationError);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
valid.push(imagePath);
|
|
181
|
+
}
|
|
182
|
+
return valid;
|
|
183
|
+
}
|
|
96
184
|
export function fatalExit(message, error, writeAgentError) {
|
|
97
185
|
const detail = error instanceof Error ? error.message : typeof error === "string" ? error : error ? String(error) : "";
|
|
98
186
|
const full = detail ? `${message}: ${detail}` : message;
|
|
@@ -102,17 +190,6 @@ export function fatalExit(message, error, writeAgentError) {
|
|
|
102
190
|
export function sleep(ms) {
|
|
103
191
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
104
192
|
}
|
|
105
|
-
export function writeTaskStream(taskId, stream, chunk) {
|
|
106
|
-
const target = stream === "stdout" ? process.stdout : process.stderr;
|
|
107
|
-
const lines = chunk.replace(/\r/g, "\n").split("\n");
|
|
108
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
109
|
-
const line = lines[i];
|
|
110
|
-
if (line.length === 0 && i === lines.length - 1) {
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
target.write(`[doer-agent][task=${taskId}][${stream}] ${line}\n`);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
193
|
export function writeTaskUpload(taskId, message) {
|
|
117
194
|
process.stdout.write(`[doer-agent][task=${taskId}][upload] ${message}\n`);
|
|
118
195
|
}
|
|
@@ -127,9 +204,6 @@ export function writeRpcStream(requestId, stream, chunk) {
|
|
|
127
204
|
target.write(`[doer-agent][rpc=${requestId}][${stream}] ${line}\n`);
|
|
128
205
|
}
|
|
129
206
|
}
|
|
130
|
-
export function writeRpcStatus(requestId, message) {
|
|
131
|
-
process.stdout.write(`[doer-agent][rpc=${requestId}][status] ${message}\n`);
|
|
132
|
-
}
|
|
133
207
|
export function writeRunStatus(runId, message) {
|
|
134
208
|
process.stdout.write(`[doer-agent][run=${runId}][status] ${message}\n`);
|
|
135
209
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
6
|
+
import { filterValidRunImagePaths } from "./agent-runtime-utils.js";
|
|
7
|
+
const invalidTinyPngBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aK1cAAAAASUVORK5CYII=";
|
|
8
|
+
function buildValidTinyPng() {
|
|
9
|
+
const bytes = Buffer.from(invalidTinyPngBase64, "base64");
|
|
10
|
+
bytes.writeUInt32BE(0xefa2a75b, 0x34);
|
|
11
|
+
return bytes;
|
|
12
|
+
}
|
|
13
|
+
test("filterValidRunImagePaths drops PNGs with CRC mismatches", async () => {
|
|
14
|
+
const workspaceRoot = await mkdtemp(path.join(os.tmpdir(), "doer-agent-image-filter-"));
|
|
15
|
+
try {
|
|
16
|
+
const invalidPngPath = path.join(workspaceRoot, "bad.png");
|
|
17
|
+
const validPngPath = path.join(workspaceRoot, "good.png");
|
|
18
|
+
const validJpgPath = path.join(workspaceRoot, "photo.jpg");
|
|
19
|
+
await writeFile(invalidPngPath, Buffer.from(invalidTinyPngBase64, "base64"));
|
|
20
|
+
await writeFile(validPngPath, buildValidTinyPng());
|
|
21
|
+
await writeFile(validJpgPath, Buffer.from([0xff, 0xd8, 0xff, 0xd9]));
|
|
22
|
+
const invalid = [];
|
|
23
|
+
const result = await filterValidRunImagePaths({
|
|
24
|
+
workspaceRoot,
|
|
25
|
+
imagePaths: ["bad.png", "good.png", "photo.jpg"],
|
|
26
|
+
onInvalidImage: (imagePath, reason) => {
|
|
27
|
+
invalid.push({ imagePath, reason });
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
assert.deepEqual(result, ["good.png", "photo.jpg"]);
|
|
31
|
+
assert.equal(invalid.length, 1);
|
|
32
|
+
assert.equal(invalid[0]?.imagePath, "bad.png");
|
|
33
|
+
assert.match(invalid[0]?.reason ?? "", /CRC mismatch/i);
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
await rm(workspaceRoot, { recursive: true, force: true });
|
|
37
|
+
}
|
|
38
|
+
});
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { mkdir } from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
1
|
export function sendSignalToTaskProcess(child, signal) {
|
|
5
2
|
if (process.platform !== "win32" && typeof child.pid === "number") {
|
|
6
3
|
try {
|
|
@@ -30,17 +27,8 @@ export function sendSignalToPid(pid, signal) {
|
|
|
30
27
|
}
|
|
31
28
|
process.kill(pid, signal);
|
|
32
29
|
}
|
|
33
|
-
async function checkCancelRequested(args) {
|
|
34
|
-
const query = new URLSearchParams({
|
|
35
|
-
userId: args.userId,
|
|
36
|
-
agentToken: args.agentToken,
|
|
37
|
-
});
|
|
38
|
-
const response = await args.getJson(`${args.serverBaseUrl}/api/agent/tasks/${encodeURIComponent(args.taskId)}/events?${query.toString()}`);
|
|
39
|
-
return Boolean(response.task?.cancelRequested);
|
|
40
|
-
}
|
|
41
30
|
async function syncCodexAuthState(args) {
|
|
42
31
|
const envPatch = {};
|
|
43
|
-
const synced = false;
|
|
44
32
|
if (args.authMode === "api_key" && args.apiKey) {
|
|
45
33
|
envPatch.OPENAI_API_KEY = args.apiKey;
|
|
46
34
|
}
|
|
@@ -54,19 +42,10 @@ async function syncCodexAuthState(args) {
|
|
|
54
42
|
codexAuthHasAuthJson: Boolean(args.authJson),
|
|
55
43
|
codexAuthIssuedAt: args.issuedAt ?? null,
|
|
56
44
|
codexAuthExpiresAt: args.expiresAt ?? null,
|
|
57
|
-
codexAuthSynced:
|
|
45
|
+
codexAuthSynced: false,
|
|
58
46
|
},
|
|
59
47
|
};
|
|
60
48
|
}
|
|
61
|
-
async function prepareTaskCodexAuth(args) {
|
|
62
|
-
void args;
|
|
63
|
-
return await syncCodexAuthState({
|
|
64
|
-
source: "agent_local",
|
|
65
|
-
authJson: null,
|
|
66
|
-
issuedAt: null,
|
|
67
|
-
expiresAt: null,
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
49
|
export async function prepareCodexAuthBundle(bundle) {
|
|
71
50
|
if (!bundle) {
|
|
72
51
|
return null;
|
|
@@ -80,196 +59,3 @@ export async function prepareCodexAuthBundle(bundle) {
|
|
|
80
59
|
expiresAt: bundle.expiresAt ?? null,
|
|
81
60
|
});
|
|
82
61
|
}
|
|
83
|
-
export async function runTask(args) {
|
|
84
|
-
args.setActiveTaskLogContext({
|
|
85
|
-
jetstream: args.jetstream,
|
|
86
|
-
serverBaseUrl: args.serverBaseUrl,
|
|
87
|
-
taskId: args.taskId,
|
|
88
|
-
userId: args.userId,
|
|
89
|
-
});
|
|
90
|
-
const shellPath = args.resolveShellPath();
|
|
91
|
-
const taskWorkspace = args.resolveTaskWorkspace(args.cwd);
|
|
92
|
-
const codexHome = args.resolveCodexHomePath();
|
|
93
|
-
await mkdir(codexHome, { recursive: true });
|
|
94
|
-
const runtimeConfig = await args.prepareTaskRuntimeConfig({
|
|
95
|
-
serverBaseUrl: args.serverBaseUrl,
|
|
96
|
-
taskId: args.taskId,
|
|
97
|
-
userId: args.userId,
|
|
98
|
-
agentToken: args.agentToken,
|
|
99
|
-
});
|
|
100
|
-
const codexAuth = await prepareTaskCodexAuth({
|
|
101
|
-
serverBaseUrl: args.serverBaseUrl,
|
|
102
|
-
taskId: args.taskId,
|
|
103
|
-
userId: args.userId,
|
|
104
|
-
agentToken: args.agentToken,
|
|
105
|
-
});
|
|
106
|
-
const localAgentSettings = await args.readAgentSettingsConfig({ workspaceRoot: args.resolveWorkspaceRoot() });
|
|
107
|
-
const baseTaskEnvPatch = {
|
|
108
|
-
CODEX_HOME: codexHome,
|
|
109
|
-
...args.buildAgentSettingsEnvPatch(localAgentSettings),
|
|
110
|
-
...(runtimeConfig?.envPatch ?? {}),
|
|
111
|
-
...(codexAuth?.envPatch ?? {}),
|
|
112
|
-
WORKSPACE: taskWorkspace,
|
|
113
|
-
};
|
|
114
|
-
const taskGitEnv = await args.prepareTaskGitEnv({
|
|
115
|
-
cwd: taskWorkspace,
|
|
116
|
-
baseEnvPatch: baseTaskEnvPatch,
|
|
117
|
-
});
|
|
118
|
-
await args.recordAgentEvent({
|
|
119
|
-
jetstream: args.jetstream,
|
|
120
|
-
serverBaseUrl: args.serverBaseUrl,
|
|
121
|
-
taskId: args.taskId,
|
|
122
|
-
userId: args.userId,
|
|
123
|
-
type: "meta",
|
|
124
|
-
seq: args.reserveNextEventSeq(args.taskId),
|
|
125
|
-
payload: {
|
|
126
|
-
host: process.platform,
|
|
127
|
-
pid: process.pid,
|
|
128
|
-
startedAt: args.formatLocalTimestamp(),
|
|
129
|
-
command: args.command,
|
|
130
|
-
cwd: taskWorkspace,
|
|
131
|
-
requestedCwd: args.cwd,
|
|
132
|
-
shell: shellPath,
|
|
133
|
-
...(runtimeConfig?.meta ?? { runtimeConfigSynced: false }),
|
|
134
|
-
...(codexAuth?.meta ?? { codexAuthSynced: false }),
|
|
135
|
-
...(taskGitEnv.meta ?? {}),
|
|
136
|
-
},
|
|
137
|
-
});
|
|
138
|
-
try {
|
|
139
|
-
let terminationReason = null;
|
|
140
|
-
let cancelStage1Timer = null;
|
|
141
|
-
let cancelStage2Timer = null;
|
|
142
|
-
let stopCancelPolling = false;
|
|
143
|
-
let cancelSignalSent = false;
|
|
144
|
-
const runtimeBinPath = path.join(args.agentProjectDir, "runtime/bin");
|
|
145
|
-
const taskPath = [runtimeBinPath, process.env.PATH || ""].filter(Boolean).join(path.delimiter);
|
|
146
|
-
const child = spawn(args.command, {
|
|
147
|
-
cwd: taskWorkspace,
|
|
148
|
-
shell: shellPath,
|
|
149
|
-
detached: process.platform !== "win32",
|
|
150
|
-
env: {
|
|
151
|
-
...process.env,
|
|
152
|
-
...baseTaskEnvPatch,
|
|
153
|
-
...taskGitEnv.envPatch,
|
|
154
|
-
PATH: taskPath,
|
|
155
|
-
DOER_AGENT_TOKEN: args.agentToken,
|
|
156
|
-
},
|
|
157
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
158
|
-
});
|
|
159
|
-
child.stdout?.setEncoding("utf8");
|
|
160
|
-
child.stderr?.setEncoding("utf8");
|
|
161
|
-
const requestCancel = () => {
|
|
162
|
-
if (cancelSignalSent || terminationReason === "cancel") {
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
cancelSignalSent = true;
|
|
166
|
-
terminationReason = "cancel";
|
|
167
|
-
sendSignalToTaskProcess(child, "SIGINT");
|
|
168
|
-
cancelStage1Timer = setTimeout(() => {
|
|
169
|
-
sendSignalToTaskProcess(child, "SIGTERM");
|
|
170
|
-
}, 1200);
|
|
171
|
-
cancelStage1Timer.unref?.();
|
|
172
|
-
cancelStage2Timer = setTimeout(() => {
|
|
173
|
-
sendSignalToTaskProcess(child, "SIGKILL");
|
|
174
|
-
}, 3500);
|
|
175
|
-
cancelStage2Timer.unref?.();
|
|
176
|
-
};
|
|
177
|
-
child.stdout?.on("data", (chunk) => {
|
|
178
|
-
args.writeTaskStream(args.taskId, "stdout", chunk);
|
|
179
|
-
const seq = args.reserveNextEventSeq(args.taskId);
|
|
180
|
-
args.persistEventOrFatal({
|
|
181
|
-
jetstream: args.jetstream,
|
|
182
|
-
serverBaseUrl: args.serverBaseUrl,
|
|
183
|
-
taskId: args.taskId,
|
|
184
|
-
userId: args.userId,
|
|
185
|
-
type: "stdout",
|
|
186
|
-
seq,
|
|
187
|
-
payload: { chunk, at: args.formatLocalTimestamp() },
|
|
188
|
-
context: "stdout persist failed",
|
|
189
|
-
});
|
|
190
|
-
});
|
|
191
|
-
child.stderr?.on("data", (chunk) => {
|
|
192
|
-
args.writeTaskStream(args.taskId, "stderr", chunk);
|
|
193
|
-
const seq = args.reserveNextEventSeq(args.taskId);
|
|
194
|
-
args.persistEventOrFatal({
|
|
195
|
-
jetstream: args.jetstream,
|
|
196
|
-
serverBaseUrl: args.serverBaseUrl,
|
|
197
|
-
taskId: args.taskId,
|
|
198
|
-
userId: args.userId,
|
|
199
|
-
type: "stderr",
|
|
200
|
-
seq,
|
|
201
|
-
payload: { chunk, at: args.formatLocalTimestamp() },
|
|
202
|
-
context: "stderr persist failed",
|
|
203
|
-
});
|
|
204
|
-
});
|
|
205
|
-
const cancelPoller = (async () => {
|
|
206
|
-
while (!stopCancelPolling) {
|
|
207
|
-
await args.sleep(5000);
|
|
208
|
-
if (stopCancelPolling || terminationReason === "cancel") {
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
const cancelRequested = await checkCancelRequested({
|
|
212
|
-
serverBaseUrl: args.serverBaseUrl,
|
|
213
|
-
taskId: args.taskId,
|
|
214
|
-
userId: args.userId,
|
|
215
|
-
agentToken: args.agentToken,
|
|
216
|
-
getJson: args.getJson,
|
|
217
|
-
}).catch(() => false);
|
|
218
|
-
if (!cancelRequested) {
|
|
219
|
-
continue;
|
|
220
|
-
}
|
|
221
|
-
requestCancel();
|
|
222
|
-
}
|
|
223
|
-
})();
|
|
224
|
-
const result = await new Promise((resolve, reject) => {
|
|
225
|
-
child.once("error", reject);
|
|
226
|
-
child.once("close", (code, signal) => {
|
|
227
|
-
resolve({ code, signal });
|
|
228
|
-
});
|
|
229
|
-
}).finally(() => {
|
|
230
|
-
stopCancelPolling = true;
|
|
231
|
-
if (cancelStage1Timer) {
|
|
232
|
-
clearTimeout(cancelStage1Timer);
|
|
233
|
-
}
|
|
234
|
-
if (cancelStage2Timer) {
|
|
235
|
-
clearTimeout(cancelStage2Timer);
|
|
236
|
-
}
|
|
237
|
-
});
|
|
238
|
-
await cancelPoller.catch(() => undefined);
|
|
239
|
-
const canceled = await checkCancelRequested({
|
|
240
|
-
serverBaseUrl: args.serverBaseUrl,
|
|
241
|
-
taskId: args.taskId,
|
|
242
|
-
userId: args.userId,
|
|
243
|
-
agentToken: args.agentToken,
|
|
244
|
-
getJson: args.getJson,
|
|
245
|
-
}).catch(() => false);
|
|
246
|
-
const status = canceled || terminationReason === "cancel"
|
|
247
|
-
? "canceled"
|
|
248
|
-
: (result.code ?? 1) === 0
|
|
249
|
-
? "completed"
|
|
250
|
-
: "failed";
|
|
251
|
-
const statusPayload = {
|
|
252
|
-
status,
|
|
253
|
-
exitCode: typeof result.code === "number" ? result.code : null,
|
|
254
|
-
signal: result.signal,
|
|
255
|
-
finishedAt: args.formatLocalTimestamp(),
|
|
256
|
-
error: status === "failed"
|
|
257
|
-
? `Command exited with code ${result.code ?? "null"}`
|
|
258
|
-
: null,
|
|
259
|
-
};
|
|
260
|
-
await args.recordAgentEvent({
|
|
261
|
-
jetstream: args.jetstream,
|
|
262
|
-
serverBaseUrl: args.serverBaseUrl,
|
|
263
|
-
taskId: args.taskId,
|
|
264
|
-
userId: args.userId,
|
|
265
|
-
type: "status",
|
|
266
|
-
seq: args.reserveNextEventSeq(args.taskId),
|
|
267
|
-
payload: statusPayload,
|
|
268
|
-
});
|
|
269
|
-
args.writeAgentInfo(`task=${args.taskId} status=${status} exitCode=${typeof result.code === "number" ? result.code : "null"} signal=${result.signal ?? "null"}`);
|
|
270
|
-
}
|
|
271
|
-
finally {
|
|
272
|
-
args.clearActiveTaskLogContext(args.taskId);
|
|
273
|
-
await codexAuth?.cleanup().catch(() => undefined);
|
|
274
|
-
}
|
|
275
|
-
}
|
package/dist/agent.js
CHANGED
|
@@ -16,7 +16,7 @@ import { subscribeToSkillRpc } from "./agent-skill-rpc.js";
|
|
|
16
16
|
import { prepareCodexAuthBundle, sendSignalToPid, sendSignalToTaskProcess, } from "./agent-task-execution.js";
|
|
17
17
|
import { collectSessionJsonlFiles, detectPendingRunSession, findSessionFilePathBySessionId, stopAllSessionWatchers, subscribeToSessionRpc, } from "./agent-session-rpc.js";
|
|
18
18
|
import { handleNonStartRunRpc, normalizeRunRpcRequest, publishRunRpcResponse, } from "./agent-run-rpc.js";
|
|
19
|
-
import { buildAgentCodexAuthRpcSubject, buildAgentFsRpcSubject, buildAgentGitRpcSubject, buildAgentRunEventsSubject, buildAgentRunRpcSubject, buildAgentSessionRpcSubject, buildAgentSettingsRpcSubject, buildAgentSkillRpcSubject, formatLocalTimestamp, normalizeEnvPatch, normalizeRunImagePaths, parseArgs, resolveAgentVersion, resolveArgOrEnv, resolveContainerReachableServerBaseUrl, sanitizeUserId, sleep, writeRunStatus, writeRunStream, } from "./agent-runtime-utils.js";
|
|
19
|
+
import { buildAgentCodexAuthRpcSubject, buildAgentFsRpcSubject, buildAgentGitRpcSubject, buildAgentRunEventsSubject, buildAgentRunRpcSubject, buildAgentSessionRpcSubject, buildAgentSettingsRpcSubject, buildAgentSkillRpcSubject, formatLocalTimestamp, normalizeEnvPatch, filterValidRunImagePaths, normalizeRunImagePaths, parseArgs, resolveAgentVersion, resolveArgOrEnv, resolveContainerReachableServerBaseUrl, sanitizeUserId, sleep, writeRunStatus, writeRunStream, } from "./agent-runtime-utils.js";
|
|
20
20
|
import { createRuntimeEnvHelpers } from "./agent-runtime-env.js";
|
|
21
21
|
import { createEventPersistenceHelpers, heartbeatAgentSession, postJson, } from "./agent-runtime-io.js";
|
|
22
22
|
import { handleSettingsRpcMessage } from "./agent-settings-rpc.js";
|
|
@@ -32,30 +32,6 @@ function resolveWorkspaceRoot() {
|
|
|
32
32
|
return workspaceRootOverride ?? (process.env.WORKSPACE?.trim() || process.cwd());
|
|
33
33
|
}
|
|
34
34
|
const runRpcCodec = StringCodec();
|
|
35
|
-
async function prepareTaskRuntimeConfig(args) {
|
|
36
|
-
const bundle = await postJson(`${args.serverBaseUrl}/api/agent/tasks/${encodeURIComponent(args.taskId)}/runtime-config`, {
|
|
37
|
-
userId: args.userId,
|
|
38
|
-
agentToken: args.agentToken,
|
|
39
|
-
}).catch((error) => {
|
|
40
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
41
|
-
writeAgentError(`task=${args.taskId} runtime config sync skipped: ${message}`);
|
|
42
|
-
return null;
|
|
43
|
-
});
|
|
44
|
-
if (!bundle) {
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
const envPatch = normalizeEnvPatch(bundle.envPatch);
|
|
48
|
-
return {
|
|
49
|
-
envPatch,
|
|
50
|
-
meta: {
|
|
51
|
-
runtimeConfigIssuedAt: bundle.issuedAt ?? null,
|
|
52
|
-
runtimeConfigExpiresAt: bundle.expiresAt ?? null,
|
|
53
|
-
runtimeConfigVarCount: Object.keys(envPatch).length,
|
|
54
|
-
runtimeConfigSynced: true,
|
|
55
|
-
...(bundle.meta && typeof bundle.meta === "object" && !Array.isArray(bundle.meta) ? bundle.meta : {}),
|
|
56
|
-
},
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
35
|
function writeAgentInfo(message) {
|
|
60
36
|
process.stdout.write(`[doer-agent] ${message}\n`);
|
|
61
37
|
eventPersistenceHelpers.emitAgentMetaLog("info", message);
|
|
@@ -275,6 +251,13 @@ async function handleRunRpcMessage(args) {
|
|
|
275
251
|
const workspaceRoot = resolveWorkspaceRoot();
|
|
276
252
|
const localAgentSettings = await readAgentSettingsConfig({ workspaceRoot });
|
|
277
253
|
const customInstructions = await readAgentModelInstructions(workspaceRoot);
|
|
254
|
+
const validImagePaths = await filterValidRunImagePaths({
|
|
255
|
+
workspaceRoot,
|
|
256
|
+
imagePaths: request.imagePaths,
|
|
257
|
+
onInvalidImage: (imagePath, reason) => {
|
|
258
|
+
writeRunStatus(runId, `skipping invalid image path=${imagePath} reason=${reason}`);
|
|
259
|
+
},
|
|
260
|
+
});
|
|
278
261
|
const task = await startManagedRun({
|
|
279
262
|
requestId,
|
|
280
263
|
runId,
|
|
@@ -285,7 +268,7 @@ async function handleRunRpcMessage(args) {
|
|
|
285
268
|
sessionId: request.sessionId,
|
|
286
269
|
codexArgs: buildManagedCodexArgs({
|
|
287
270
|
prompt: request.prompt ?? "",
|
|
288
|
-
imagePaths:
|
|
271
|
+
imagePaths: validImagePaths,
|
|
289
272
|
sessionId: request.sessionId,
|
|
290
273
|
model: request.model,
|
|
291
274
|
personality: localAgentSettings.general.personality,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "doer-agent",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.7",
|
|
4
4
|
"description": "Reverse-polling agent runtime for doer",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/agent.js",
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
28
28
|
"@openai/codex-sdk": "^0.115.0",
|
|
29
|
-
"nats": "^2.29.3"
|
|
29
|
+
"nats": "^2.29.3",
|
|
30
|
+
"tar": "^7.5.13"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
32
33
|
"@types/node": "^20",
|