doer-agent 0.7.5 → 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.
@@ -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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doer-agent",
3
- "version": "0.7.5",
3
+ "version": "0.7.6",
4
4
  "description": "Reverse-polling agent runtime for doer",
5
5
  "type": "module",
6
6
  "main": "dist/agent.js",