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.
@@ -1,7 +1,9 @@
1
1
  import path from "node:path";
2
- import { gunzipSync, gzipSync } from "node:zlib";
3
- import { mkdir, open, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
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 === "read_file" ||
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 normalizeArchiveRelativePath(value) {
92
- const normalized = value.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
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 collectDirectoryFiles(absDir, rootDir = absDir) {
99
- const rows = await readdir(absDir, { withFileTypes: true });
100
- const files = [];
101
- for (const row of rows.sort((a, b) => a.name.localeCompare(b.name))) {
102
- const child = path.join(absDir, row.name);
103
- if (row.isDirectory()) {
104
- files.push(...await collectDirectoryFiles(child, rootDir));
105
- continue;
106
- }
107
- if (!row.isFile()) {
108
- continue;
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
- const files = await collectDirectoryFiles(abs);
172
- if (!files.some((file) => file.relPath === "SKILL.md")) {
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 writeFile(archiveTarget.abs, payload);
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 === "write_file") {
260
- const contentBase64 = typeof args.request.contentBase64 === "string" ? args.request.contentBase64 : "";
261
- if (!contentBase64) {
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
- const bytes = Buffer.from(contentBase64, "base64");
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 decoded = JSON.parse(gunzipSync(archiveBytes).toString("utf8"));
334
- const files = Array.isArray(decoded.files) ? decoded.files : [];
335
- await mkdir(destinationTarget.abs, { recursive: true });
336
- for (const file of files) {
337
- const relPath = typeof file.relPath === "string" ? normalizeArchiveRelativePath(file.relPath) : "";
338
- const contentBase64 = typeof file.contentBase64 === "string" ? file.contentBase64 : "";
339
- if (!relPath || !contentBase64) {
340
- throw new Error("archive contains an invalid file entry");
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
- const targetPath = path.join(destinationTarget.abs, relPath);
343
- await mkdir(path.dirname(targetPath), { recursive: true });
344
- await writeFile(targetPath, Buffer.from(contentBase64, "base64"));
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: synced,
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: request.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.5",
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",