doer-agent 0.7.4 → 0.7.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.
@@ -62,6 +62,9 @@ function normalizeCodexAppRpcRequest(args) {
62
62
  const requestAgentId = typeof args.request.agentId === "string" ? args.request.agentId.trim() : "";
63
63
  const actionRaw = typeof args.request.action === "string" ? args.request.action.trim() : "";
64
64
  const method = typeof args.request.method === "string" ? args.request.method.trim() : "";
65
+ const timeoutMs = typeof args.request.timeoutMs === "number" && Number.isFinite(args.request.timeoutMs)
66
+ ? Math.min(180_000, Math.max(1_000, Math.trunc(args.request.timeoutMs)))
67
+ : undefined;
65
68
  if (!requestId || !requestAgentId || requestAgentId !== args.agentId || actionRaw !== "request" || !method) {
66
69
  throw new Error("invalid codex app rpc request");
67
70
  }
@@ -70,6 +73,7 @@ function normalizeCodexAppRpcRequest(args) {
70
73
  action: "request",
71
74
  method,
72
75
  params: args.request.params,
76
+ timeoutMs,
73
77
  };
74
78
  }
75
79
  async function handleCodexAppRpcMessage(args) {
@@ -78,7 +82,7 @@ async function handleCodexAppRpcMessage(args) {
78
82
  const payload = JSON.parse(codexAppRpcCodec.decode(args.msg.data));
79
83
  const request = normalizeCodexAppRpcRequest({ request: payload, agentId: args.agentId });
80
84
  requestId = request.requestId;
81
- const result = applyCodexAppRpcOmitRules(request.method, await args.manager.request(request.method, request.params));
85
+ const result = applyCodexAppRpcOmitRules(request.method, await args.manager.request(request.method, request.params, request.timeoutMs));
82
86
  args.msg.respond(codexAppRpcCodec.encode(JSON.stringify({
83
87
  requestId,
84
88
  ok: true,
@@ -2,7 +2,7 @@ import path from "node:path";
2
2
  import { mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
3
3
  import crypto from "node:crypto";
4
4
  import { StringCodec } from "nats";
5
- import { create as createTar, extract as extractTar } from "tar";
5
+ import { extract as extractTar } from "tar";
6
6
  import { validateImageBytes } from "./agent-runtime-utils.js";
7
7
  const fsRpcCodec = StringCodec();
8
8
  function normalizeFsRpcPath(workspaceRoot, rawPath) {
@@ -11,13 +11,10 @@ function normalizeFsRpcPath(workspaceRoot, rawPath) {
11
11
  const useAbsolute = path.isAbsolute(normalizedRaw);
12
12
  const rel = normalizedRaw.replace(/^\/+/, "") || ".";
13
13
  const abs = useAbsolute ? path.resolve(normalizedRaw) : path.resolve(workspaceRoot, rel);
14
- if (!useAbsolute && abs !== workspaceRoot && !abs.startsWith(workspaceRoot + path.sep)) {
14
+ if (abs !== workspaceRoot && !abs.startsWith(workspaceRoot + path.sep)) {
15
15
  throw new Error("path escapes workspace root");
16
16
  }
17
17
  const formatPath = (target) => {
18
- if (useAbsolute) {
19
- return target.split(path.sep).join("/") || "/";
20
- }
21
18
  return path.relative(workspaceRoot, target).split(path.sep).join("/") || ".";
22
19
  };
23
20
  return { abs, formatPath };
@@ -30,7 +27,6 @@ function parseFsRpcAction(value) {
30
27
  value === "write_text" ||
31
28
  value === "download_file" ||
32
29
  value === "delete_path" ||
33
- value === "archive_dir" ||
34
30
  value === "extract_archive") {
35
31
  return value;
36
32
  }
@@ -92,18 +88,6 @@ function inferMimeType(filePath) {
92
88
  function sha256Hex(bytes) {
93
89
  return crypto.createHash("sha256").update(bytes).digest("hex");
94
90
  }
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);
106
- }
107
91
  async function executeFsRpc(args) {
108
92
  const action = parseFsRpcAction(args.request.action);
109
93
  const { abs, formatPath } = normalizeFsRpcPath(args.workspaceRoot, args.request.path);
@@ -146,37 +130,6 @@ async function executeFsRpc(args) {
146
130
  total: items.length,
147
131
  };
148
132
  }
149
- if (action === "archive_dir") {
150
- const entry = await stat(abs);
151
- if (!entry.isDirectory()) {
152
- throw new Error("path is not a directory");
153
- }
154
- const rawArchivePath = typeof args.request.archivePath === "string" ? args.request.archivePath : "";
155
- if (!rawArchivePath) {
156
- throw new Error("archivePath is required");
157
- }
158
- const archiveTarget = normalizeFsRpcPath(args.workspaceRoot, rawArchivePath);
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 {
166
- throw new Error("Selected skill directory must contain SKILL.md");
167
- }
168
- await mkdir(path.dirname(archiveTarget.abs), { recursive: true });
169
- const archiveBytes = await createTarGzipBuffer(abs, ["."]);
170
- await writeFile(archiveTarget.abs, archiveBytes);
171
- const archiveStat = await stat(archiveTarget.abs);
172
- return {
173
- ok: true,
174
- action,
175
- path: formatPath(abs),
176
- archivePath: archiveTarget.formatPath(archiveTarget.abs),
177
- size: archiveStat.size,
178
- };
179
- }
180
133
  if (action === "upload_file") {
181
134
  const entry = await stat(abs);
182
135
  if (!entry.isFile()) {
@@ -0,0 +1,231 @@
1
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { StringCodec } from "nats";
4
+ const proxyRpcCodec = StringCodec();
5
+ const PROXY_ID_PATTERN = /^[a-z0-9][a-z0-9-]{1,30}[a-z0-9]$/;
6
+ const MAX_PROXY_BODY_BYTES = 5 * 1024 * 1024;
7
+ function getProxyRegistryPath(workspaceRoot) {
8
+ return path.join(workspaceRoot, ".doer-agent", "http-proxies.json");
9
+ }
10
+ function slugify(value) {
11
+ const slug = value
12
+ .trim()
13
+ .toLowerCase()
14
+ .replace(/[^a-z0-9-]+/g, "-")
15
+ .replace(/^-+|-+$/g, "")
16
+ .replace(/-{2,}/g, "-")
17
+ .slice(0, 32);
18
+ return PROXY_ID_PATTERN.test(slug) ? slug : `p${Date.now().toString(36)}`;
19
+ }
20
+ function normalizeProxyId(value) {
21
+ const id = typeof value === "string" ? value.trim().toLowerCase() : "";
22
+ if (!PROXY_ID_PATTERN.test(id)) {
23
+ throw new Error("invalid proxyId");
24
+ }
25
+ return id;
26
+ }
27
+ function normalizeName(value) {
28
+ if (typeof value !== "string") {
29
+ return null;
30
+ }
31
+ const name = value.trim();
32
+ return name ? name.slice(0, 120) : null;
33
+ }
34
+ function normalizeHost(value) {
35
+ const host = typeof value === "string" && value.trim() ? value.trim() : "127.0.0.1";
36
+ if (host !== "127.0.0.1" && host !== "localhost") {
37
+ throw new Error("proxy host must be localhost or 127.0.0.1");
38
+ }
39
+ return host;
40
+ }
41
+ function normalizePort(value) {
42
+ const port = Number(value);
43
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
44
+ throw new Error("port must be between 1 and 65535");
45
+ }
46
+ return port;
47
+ }
48
+ function normalizeMethod(value) {
49
+ const method = typeof value === "string" ? value.trim().toUpperCase() : "GET";
50
+ if (!/^[A-Z]+$/.test(method)) {
51
+ throw new Error("invalid method");
52
+ }
53
+ return method;
54
+ }
55
+ function normalizePath(value) {
56
+ const raw = typeof value === "string" && value ? value : "/";
57
+ return raw.startsWith("/") ? raw : `/${raw}`;
58
+ }
59
+ function normalizeHeaders(value) {
60
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
61
+ return {};
62
+ }
63
+ const out = {};
64
+ for (const [key, raw] of Object.entries(value)) {
65
+ if (typeof raw !== "string") {
66
+ continue;
67
+ }
68
+ const normalizedKey = key.trim().toLowerCase();
69
+ if (!normalizedKey || normalizedKey === "host" || normalizedKey === "connection") {
70
+ continue;
71
+ }
72
+ out[normalizedKey] = raw;
73
+ }
74
+ return out;
75
+ }
76
+ function normalizeProxyRecord(value) {
77
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
78
+ return null;
79
+ }
80
+ const row = value;
81
+ const id = typeof row.id === "string" ? row.id.trim().toLowerCase() : "";
82
+ const host = normalizeHost(row.host);
83
+ const port = Number(row.port);
84
+ const createdAt = typeof row.createdAt === "string" ? row.createdAt : new Date().toISOString();
85
+ const updatedAt = typeof row.updatedAt === "string" ? row.updatedAt : createdAt;
86
+ if (!PROXY_ID_PATTERN.test(id) || !Number.isInteger(port) || port < 1 || port > 65535) {
87
+ return null;
88
+ }
89
+ return {
90
+ id,
91
+ name: normalizeName(row.name),
92
+ host,
93
+ port,
94
+ createdAt,
95
+ updatedAt,
96
+ };
97
+ }
98
+ async function readProxyRegistry(workspaceRoot) {
99
+ const raw = await readFile(getProxyRegistryPath(workspaceRoot), "utf8").catch(() => "");
100
+ if (!raw) {
101
+ return [];
102
+ }
103
+ const parsed = JSON.parse(raw);
104
+ const rows = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.proxies) ? parsed.proxies : [];
105
+ return rows
106
+ .map((row) => {
107
+ try {
108
+ return normalizeProxyRecord(row);
109
+ }
110
+ catch {
111
+ return null;
112
+ }
113
+ })
114
+ .filter((row) => Boolean(row));
115
+ }
116
+ async function writeProxyRegistry(workspaceRoot, proxies) {
117
+ const registryPath = getProxyRegistryPath(workspaceRoot);
118
+ await mkdir(path.dirname(registryPath), { recursive: true });
119
+ await writeFile(registryPath, `${JSON.stringify({ proxies }, null, 2)}\n`, "utf8");
120
+ }
121
+ async function createProxy(workspaceRoot, request) {
122
+ const proxies = await readProxyRegistry(workspaceRoot);
123
+ const name = normalizeName(request.name);
124
+ const port = normalizePort(request.port);
125
+ const host = normalizeHost(request.host);
126
+ const requestedId = typeof request.proxyId === "string" && request.proxyId.trim()
127
+ ? normalizeProxyId(request.proxyId)
128
+ : slugify(name || `p-${port}`);
129
+ let id = requestedId;
130
+ for (let index = 2; proxies.some((proxy) => proxy.id === id); index += 1) {
131
+ id = `${requestedId.slice(0, 26)}-${index}`;
132
+ }
133
+ const now = new Date().toISOString();
134
+ const proxy = {
135
+ id,
136
+ name,
137
+ host,
138
+ port,
139
+ createdAt: now,
140
+ updatedAt: now,
141
+ };
142
+ await writeProxyRegistry(workspaceRoot, [...proxies, proxy].sort((a, b) => b.createdAt.localeCompare(a.createdAt)));
143
+ return proxy;
144
+ }
145
+ async function deleteProxy(workspaceRoot, proxyId) {
146
+ const proxies = await readProxyRegistry(workspaceRoot);
147
+ await writeProxyRegistry(workspaceRoot, proxies.filter((proxy) => proxy.id !== proxyId));
148
+ }
149
+ async function handleProxyFetch(workspaceRoot, request) {
150
+ const proxyId = normalizeProxyId(request.proxyId);
151
+ const proxy = (await readProxyRegistry(workspaceRoot)).find((item) => item.id === proxyId);
152
+ if (!proxy) {
153
+ throw new Error("proxy not found");
154
+ }
155
+ const method = normalizeMethod(request.method);
156
+ const requestPath = normalizePath(request.path);
157
+ const headers = normalizeHeaders(request.headers);
158
+ const bodyBase64 = typeof request.bodyBase64 === "string" ? request.bodyBase64 : "";
159
+ const body = bodyBase64 ? Buffer.from(bodyBase64, "base64") : undefined;
160
+ if ((body?.byteLength ?? 0) > MAX_PROXY_BODY_BYTES) {
161
+ throw new Error("proxy request body too large");
162
+ }
163
+ const url = new URL(requestPath, `http://${proxy.host}:${proxy.port}`);
164
+ const response = await fetch(url, {
165
+ method,
166
+ headers,
167
+ body: method === "GET" || method === "HEAD" ? undefined : body,
168
+ redirect: "manual",
169
+ });
170
+ const responseBuffer = Buffer.from(await response.arrayBuffer());
171
+ if (responseBuffer.byteLength > MAX_PROXY_BODY_BYTES) {
172
+ throw new Error("proxy response body too large");
173
+ }
174
+ const responseHeaders = {};
175
+ response.headers.forEach((value, key) => {
176
+ responseHeaders[key] = value;
177
+ });
178
+ return {
179
+ status: response.status,
180
+ statusText: response.statusText,
181
+ headers: responseHeaders,
182
+ bodyBase64: responseBuffer.toString("base64"),
183
+ };
184
+ }
185
+ async function executeProxyRpc(args) {
186
+ const action = args.request.action === "create" || args.request.action === "delete" || args.request.action === "handle"
187
+ ? args.request.action
188
+ : "list";
189
+ if (action === "list") {
190
+ return { ok: true, action, proxies: await readProxyRegistry(args.workspaceRoot) };
191
+ }
192
+ if (action === "create") {
193
+ return { ok: true, action, proxy: await createProxy(args.workspaceRoot, args.request) };
194
+ }
195
+ if (action === "delete") {
196
+ await deleteProxy(args.workspaceRoot, normalizeProxyId(args.request.proxyId));
197
+ return { ok: true, action };
198
+ }
199
+ return { ok: true, action, response: await handleProxyFetch(args.workspaceRoot, args.request) };
200
+ }
201
+ export async function handleHttpProxyRpcMessage(args) {
202
+ let requestId = "unknown";
203
+ try {
204
+ const request = JSON.parse(proxyRpcCodec.decode(args.msg.data));
205
+ requestId = typeof request.requestId === "string" ? request.requestId : "unknown";
206
+ const payload = await executeProxyRpc({ workspaceRoot: args.workspaceRoot, request });
207
+ args.msg.respond(proxyRpcCodec.encode(JSON.stringify({ requestId, ...payload })));
208
+ }
209
+ catch (error) {
210
+ const message = error instanceof Error ? error.message : String(error);
211
+ args.onError?.(`http proxy rpc failed requestId=${requestId} error=${message}`);
212
+ args.msg.respond(proxyRpcCodec.encode(JSON.stringify({ requestId, ok: false, error: message })));
213
+ }
214
+ }
215
+ export function subscribeToHttpProxyRpc(args) {
216
+ args.nc.subscribe(args.subject, {
217
+ callback: (error, msg) => {
218
+ if (error) {
219
+ const message = error instanceof Error ? error.message : String(error);
220
+ args.onError(`http proxy rpc subscription error: ${message}`);
221
+ return;
222
+ }
223
+ void handleHttpProxyRpcMessage({
224
+ msg,
225
+ workspaceRoot: args.workspaceRoot,
226
+ onError: args.onError,
227
+ });
228
+ },
229
+ });
230
+ args.onInfo(`http proxy rpc subscribed subject=${args.subject}`);
231
+ }
@@ -25,6 +25,9 @@ export function buildAgentFsRpcSubject(userId, agentId) {
25
25
  export function buildAgentDaemonRpcSubject(userId, agentId) {
26
26
  return `doer.agent.daemon.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
27
27
  }
28
+ export function buildAgentHttpProxyRpcSubject(userId, agentId) {
29
+ return `doer.agent.http.proxy.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
30
+ }
28
31
  export function buildAgentMaintenanceRpcSubject(userId, agentId) {
29
32
  return `doer.agent.maintenance.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
30
33
  }
package/dist/agent.js CHANGED
@@ -13,8 +13,9 @@ import { connectBootstrapWithRetry } from "./agent-jetstream.js";
13
13
  import { runConnectedAgentSession } from "./agent-session-loop.js";
14
14
  import { subscribeToSkillRpc } from "./agent-skill-rpc.js";
15
15
  import { subscribeToMaintenanceRpc } from "./agent-maintenance-rpc.js";
16
+ import { subscribeToHttpProxyRpc } from "./agent-http-proxy-rpc.js";
16
17
  import { sendSignalToTaskProcess } from "./agent-task-execution.js";
17
- import { buildAgentCodexAppEventsSubject, buildAgentCodexAppRpcSubject, buildAgentDaemonRpcSubject, buildAgentFsRpcSubject, buildAgentGitRpcSubject, buildAgentMaintenanceRpcSubject, buildAgentSettingsRpcSubject, buildAgentSkillRpcSubject, formatLocalTimestamp, parseArgs, resolveAgentVersion, resolveArgOrEnv, resolveContainerReachableServerBaseUrl, sanitizeUserId, sleep, } from "./agent-runtime-utils.js";
18
+ import { buildAgentCodexAppEventsSubject, buildAgentCodexAppRpcSubject, buildAgentDaemonRpcSubject, buildAgentFsRpcSubject, buildAgentGitRpcSubject, buildAgentHttpProxyRpcSubject, buildAgentMaintenanceRpcSubject, buildAgentSettingsRpcSubject, buildAgentSkillRpcSubject, formatLocalTimestamp, parseArgs, resolveAgentVersion, resolveArgOrEnv, resolveContainerReachableServerBaseUrl, sanitizeUserId, sleep, } from "./agent-runtime-utils.js";
18
19
  import { createRuntimeEnvHelpers } from "./agent-runtime-env.js";
19
20
  import { createEventPersistenceHelpers, heartbeatAgentSession, postJson, } from "./agent-runtime-io.js";
20
21
  import { handleSettingsRpcMessage } from "./agent-settings-rpc.js";
@@ -306,6 +307,13 @@ async function main() {
306
307
  onInfo: writeAgentInfo,
307
308
  onError: writeAgentError,
308
309
  });
310
+ subscribeToHttpProxyRpc({
311
+ nc: jetstream.nc,
312
+ subject: buildAgentHttpProxyRpcSubject(userId, initialAgentId),
313
+ workspaceRoot: resolveWorkspaceRoot(),
314
+ onInfo: writeAgentInfo,
315
+ onError: writeAgentError,
316
+ });
309
317
  },
310
318
  onInfraError: writeAgentInfraError,
311
319
  sleep,
@@ -22,9 +22,9 @@ export class CodexAppServerClient {
22
22
  constructor(options) {
23
23
  this.options = options;
24
24
  }
25
- async request(method, params) {
25
+ async request(method, params, timeoutMs) {
26
26
  await this.start();
27
- return await this.requestStarted(method, params);
27
+ return await this.requestStarted(method, params, timeoutMs);
28
28
  }
29
29
  async notify(method, params) {
30
30
  await this.start();
@@ -92,14 +92,14 @@ export class CodexAppServerClient {
92
92
  });
93
93
  await this.notify("initialized");
94
94
  }
95
- async requestStarted(method, params) {
95
+ async requestStarted(method, params, timeoutMsOverride) {
96
96
  const child = this.child;
97
97
  if (!child || child.killed) {
98
98
  throw new Error("Codex app-server is not running");
99
99
  }
100
100
  const id = this.nextRequestId++;
101
101
  const payload = params === undefined ? { id, method } : { id, method, params };
102
- const timeoutMs = this.options.requestTimeoutMs ?? 30_000;
102
+ const timeoutMs = timeoutMsOverride ?? this.options.requestTimeoutMs ?? 30_000;
103
103
  return await new Promise((resolve, reject) => {
104
104
  const timer = setTimeout(() => {
105
105
  this.pending.delete(id);
@@ -117,9 +117,9 @@ export function createCodexAppServerManager(args) {
117
117
  }
118
118
  };
119
119
  return {
120
- async request(method, params) {
120
+ async request(method, params, timeoutMs) {
121
121
  const activeClient = await getClient();
122
- return await activeClient.request(method, params);
122
+ return await activeClient.request(method, params, timeoutMs);
123
123
  },
124
124
  async restart(reason) {
125
125
  generation += 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doer-agent",
3
- "version": "0.7.4",
3
+ "version": "0.7.6",
4
4
  "description": "Reverse-polling agent runtime for doer",
5
5
  "type": "module",
6
6
  "main": "dist/agent.js",