doer-agent 0.4.6 → 0.4.8
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/README.md +1 -1
- package/dist/agent-codex-cli.js +26 -0
- package/dist/agent-daemon-rpc.js +475 -0
- package/dist/agent-fs-rpc.js +5 -0
- package/dist/agent-run-state.js +61 -3
- package/dist/agent-runtime-utils.js +91 -14
- package/dist/agent-runtime-utils.test.js +38 -0
- package/dist/agent-task-execution.js +1 -215
- package/dist/agent.js +29 -29
- package/dist/daemon-log-runner.js +176 -0
- package/dist/daemon-mcp-server.js +166 -0
- package/package.json +1 -1
- package/runtime/bin/apply_patch +0 -5
package/README.md
CHANGED
|
@@ -142,6 +142,6 @@ curl -X POST 'http://localhost:2020/api/users/<userId>/agent/secret' \
|
|
|
142
142
|
|
|
143
143
|
## 참고
|
|
144
144
|
|
|
145
|
-
- `runtime/`에는 실행 보조 스크립트가 들어 있습니다.
|
|
145
|
+
- `runtime/`에는 Git 인증 등 실행 보조 스크립트가 들어 있습니다.
|
|
146
146
|
- Playwright MCP 프록시는 에이전트 상태 디렉터리(`~/.doer-agent`) 아래 소켓을 사용합니다.
|
|
147
147
|
- 이 저장소에는 예전 README에 있던 `scripts/build.sh`, `scripts/publish.sh`, `docker-compose.dev.yml`이 없습니다. 현재 문서는 실제 파일 구조 기준으로 정리되어 있습니다.
|
package/dist/agent-codex-cli.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
2
4
|
const ANSI_RE = /\u001b\[[0-9;]*m/g;
|
|
3
5
|
function shellSingleQuote(value) {
|
|
4
6
|
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
@@ -6,6 +8,9 @@ function shellSingleQuote(value) {
|
|
|
6
8
|
function toTomlStringLiteral(value) {
|
|
7
9
|
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
8
10
|
}
|
|
11
|
+
function toTomlStringArray(values) {
|
|
12
|
+
return `[${values.map((value) => toTomlStringLiteral(value)).join(", ")}]`;
|
|
13
|
+
}
|
|
9
14
|
function hasDirectCodexBinary() {
|
|
10
15
|
const result = spawnSync("bash", ["-lc", "command -v codex >/dev/null 2>&1"], {
|
|
11
16
|
stdio: "ignore",
|
|
@@ -27,6 +32,7 @@ export function buildManagedCodexArgs(args) {
|
|
|
27
32
|
...(args.modelInstructionsFile
|
|
28
33
|
? ["--config", `model_instructions_file=${toTomlStringLiteral(args.modelInstructionsFile)}`]
|
|
29
34
|
: []),
|
|
35
|
+
...(args.configOverrides ?? []),
|
|
30
36
|
];
|
|
31
37
|
const imageArgs = args.imagePaths.flatMap((imagePath) => ["--image", imagePath]);
|
|
32
38
|
return [
|
|
@@ -39,6 +45,26 @@ export function buildManagedCodexArgs(args) {
|
|
|
39
45
|
: ["exec", ...imageArgs, ...promptArgs]),
|
|
40
46
|
];
|
|
41
47
|
}
|
|
48
|
+
export function buildDaemonMcpConfigArgs(args) {
|
|
49
|
+
const serverName = args.serverName?.trim() || "doer_daemon";
|
|
50
|
+
const distEntry = path.join(args.agentProjectDir, "dist", "daemon-mcp-server.js");
|
|
51
|
+
const srcEntry = path.join(args.agentProjectDir, "src", "daemon-mcp-server.ts");
|
|
52
|
+
const tsxLoaderPath = path.join(args.agentProjectDir, "node_modules", "tsx", "dist", "loader.mjs");
|
|
53
|
+
const command = process.execPath;
|
|
54
|
+
const commandArgs = existsSync(distEntry)
|
|
55
|
+
? [distEntry, "--workspace-root", args.workspaceRoot]
|
|
56
|
+
: ["--import", tsxLoaderPath, srcEntry, "--workspace-root", args.workspaceRoot];
|
|
57
|
+
return [
|
|
58
|
+
"--config",
|
|
59
|
+
`mcp_servers.${serverName}.command=${toTomlStringLiteral(command)}`,
|
|
60
|
+
"--config",
|
|
61
|
+
`mcp_servers.${serverName}.args=${toTomlStringArray(commandArgs)}`,
|
|
62
|
+
"--config",
|
|
63
|
+
`mcp_servers.${serverName}.env.DOER_DAEMON_WORKSPACE_ROOT=${toTomlStringLiteral(args.workspaceRoot)}`,
|
|
64
|
+
"--config",
|
|
65
|
+
`mcp_servers.${serverName}.enabled=true`,
|
|
66
|
+
];
|
|
67
|
+
}
|
|
42
68
|
export function buildLocalCodexCliCommand(args) {
|
|
43
69
|
const quotedArgs = args.map(shellSingleQuote).join(" ");
|
|
44
70
|
const direct = `exec codex ${quotedArgs}`;
|
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { StringCodec } from "nats";
|
|
7
|
+
import { buildAgentSettingsEnvPatch } from "./agent-settings.js";
|
|
8
|
+
import { normalizeEnvPatch, sleep } from "./agent-runtime-utils.js";
|
|
9
|
+
import { sendSignalToPid } from "./agent-task-execution.js";
|
|
10
|
+
const daemonRpcCodec = StringCodec();
|
|
11
|
+
const DAEMON_ID_PATTERN = /^[A-Za-z0-9_-]{6,32}$/;
|
|
12
|
+
function getDaemonsRoot(workspaceRoot) {
|
|
13
|
+
return path.join(workspaceRoot, ".doer-agent", "daemons");
|
|
14
|
+
}
|
|
15
|
+
function getDaemonDir(workspaceRoot, daemonId) {
|
|
16
|
+
return path.join(getDaemonsRoot(workspaceRoot), daemonId);
|
|
17
|
+
}
|
|
18
|
+
function getDaemonStatePath(workspaceRoot, daemonId) {
|
|
19
|
+
return path.join(getDaemonDir(workspaceRoot, daemonId), "state.json");
|
|
20
|
+
}
|
|
21
|
+
function getDaemonEventsPath(workspaceRoot, daemonId) {
|
|
22
|
+
return path.join(getDaemonDir(workspaceRoot, daemonId), "events.jsonl");
|
|
23
|
+
}
|
|
24
|
+
function isPidAlive(pid) {
|
|
25
|
+
if (!Number.isInteger(pid) || (pid ?? 0) <= 0) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
process.kill(pid, 0);
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function createDaemonId() {
|
|
37
|
+
return crypto.randomBytes(9).toString("base64url").slice(0, 12);
|
|
38
|
+
}
|
|
39
|
+
function normalizeAction(value) {
|
|
40
|
+
if (value === "list" || value === "inspect" || value === "start" || value === "stop" || value === "restart" || value === "delete" || value === "logs") {
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
throw new Error("unsupported action");
|
|
44
|
+
}
|
|
45
|
+
function normalizeDaemonId(value) {
|
|
46
|
+
const daemonId = typeof value === "string" ? value.trim() : "";
|
|
47
|
+
if (!DAEMON_ID_PATTERN.test(daemonId)) {
|
|
48
|
+
throw new Error("invalid daemonId");
|
|
49
|
+
}
|
|
50
|
+
return daemonId;
|
|
51
|
+
}
|
|
52
|
+
function normalizeCommand(value) {
|
|
53
|
+
const command = typeof value === "string" ? value.trim() : "";
|
|
54
|
+
if (!command) {
|
|
55
|
+
throw new Error("command is required");
|
|
56
|
+
}
|
|
57
|
+
return command;
|
|
58
|
+
}
|
|
59
|
+
function normalizeLabel(value) {
|
|
60
|
+
if (typeof value !== "string") {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const label = value.trim();
|
|
64
|
+
return label ? label.slice(0, 120) : null;
|
|
65
|
+
}
|
|
66
|
+
function normalizeLimit(value, fallback) {
|
|
67
|
+
const numeric = Number(value);
|
|
68
|
+
if (!Number.isFinite(numeric)) {
|
|
69
|
+
return fallback;
|
|
70
|
+
}
|
|
71
|
+
return Math.max(1, Math.min(Math.floor(numeric), 1000));
|
|
72
|
+
}
|
|
73
|
+
function normalizeStateRecord(value) {
|
|
74
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const row = value;
|
|
78
|
+
const id = typeof row.id === "string" ? row.id.trim() : "";
|
|
79
|
+
const command = typeof row.command === "string" ? row.command.trim() : "";
|
|
80
|
+
const cwd = typeof row.cwd === "string" ? row.cwd.trim() : "";
|
|
81
|
+
const startedAt = typeof row.startedAt === "string" ? row.startedAt.trim() : "";
|
|
82
|
+
const label = typeof row.label === "string" && row.label.trim() ? row.label.trim() : null;
|
|
83
|
+
const stoppedAt = typeof row.stoppedAt === "string" && row.stoppedAt.trim() ? row.stoppedAt.trim() : null;
|
|
84
|
+
const pid = typeof row.pid === "number" && Number.isInteger(row.pid) && row.pid > 0 ? row.pid : null;
|
|
85
|
+
const runnerPid = typeof row.runnerPid === "number" && Number.isInteger(row.runnerPid) && row.runnerPid > 0 ? row.runnerPid : null;
|
|
86
|
+
const lastExitCode = typeof row.lastExitCode === "number" && Number.isInteger(row.lastExitCode) ? row.lastExitCode : null;
|
|
87
|
+
if (!DAEMON_ID_PATTERN.test(id) || !command || !cwd || !startedAt) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
id,
|
|
92
|
+
label,
|
|
93
|
+
command,
|
|
94
|
+
cwd,
|
|
95
|
+
pid,
|
|
96
|
+
runnerPid,
|
|
97
|
+
startedAt,
|
|
98
|
+
stoppedAt,
|
|
99
|
+
lastExitCode,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function normalizeLogEvent(value) {
|
|
103
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const row = value;
|
|
107
|
+
const ts = typeof row.ts === "string" ? row.ts.trim() : "";
|
|
108
|
+
const type = row.type === "start" || row.type === "stdout" || row.type === "stderr" || row.type === "exit" || row.type === "signal" || row.type === "error"
|
|
109
|
+
? row.type
|
|
110
|
+
: null;
|
|
111
|
+
if (!ts || !type) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
ts,
|
|
116
|
+
type,
|
|
117
|
+
text: typeof row.text === "string" ? row.text : null,
|
|
118
|
+
pid: typeof row.pid === "number" && Number.isInteger(row.pid) && row.pid > 0 ? row.pid : null,
|
|
119
|
+
code: typeof row.code === "number" && Number.isInteger(row.code) ? row.code : null,
|
|
120
|
+
signal: typeof row.signal === "string" && row.signal.trim() ? row.signal.trim() : null,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
async function readDaemonState(workspaceRoot, daemonId) {
|
|
124
|
+
const raw = await readFile(getDaemonStatePath(workspaceRoot, daemonId), "utf8").catch(() => null);
|
|
125
|
+
if (!raw) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
const parsed = JSON.parse(raw);
|
|
129
|
+
return normalizeStateRecord(parsed);
|
|
130
|
+
}
|
|
131
|
+
async function writeDaemonState(workspaceRoot, state) {
|
|
132
|
+
await mkdir(getDaemonDir(workspaceRoot, state.id), { recursive: true });
|
|
133
|
+
await writeFile(getDaemonStatePath(workspaceRoot, state.id), `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
134
|
+
}
|
|
135
|
+
function toDaemonSnapshot(state) {
|
|
136
|
+
const runnerAlive = isPidAlive(state.runnerPid);
|
|
137
|
+
const processAlive = isPidAlive(state.pid);
|
|
138
|
+
const status = runnerAlive || processAlive ? "running" : state.lastExitCode !== null && state.lastExitCode !== 0 ? "failed" : "stopped";
|
|
139
|
+
return {
|
|
140
|
+
...state,
|
|
141
|
+
status,
|
|
142
|
+
displayName: state.label ?? state.command,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
async function readJsonlTail(filePath, limit) {
|
|
146
|
+
const raw = await readFile(filePath, "utf8").catch(() => "");
|
|
147
|
+
if (!raw) {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
return raw
|
|
151
|
+
.split("\n")
|
|
152
|
+
.map((line) => line.trim())
|
|
153
|
+
.filter(Boolean)
|
|
154
|
+
.slice(-limit)
|
|
155
|
+
.map((line) => {
|
|
156
|
+
try {
|
|
157
|
+
return normalizeLogEvent(JSON.parse(line));
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
.filter((event) => Boolean(event));
|
|
164
|
+
}
|
|
165
|
+
function resolveDaemonRunnerEntry(agentProjectDir) {
|
|
166
|
+
const distEntry = path.join(agentProjectDir, "dist", "daemon-log-runner.js");
|
|
167
|
+
const srcEntry = path.join(agentProjectDir, "src", "daemon-log-runner.ts");
|
|
168
|
+
const tsxLoaderPath = path.join(agentProjectDir, "node_modules", "tsx", "dist", "loader.mjs");
|
|
169
|
+
if (existsSync(distEntry)) {
|
|
170
|
+
return {
|
|
171
|
+
command: process.execPath,
|
|
172
|
+
args: [distEntry],
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
command: process.execPath,
|
|
177
|
+
args: ["--import", tsxLoaderPath, srcEntry],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
export async function listAgentDaemonsLocal(workspaceRoot) {
|
|
181
|
+
const root = getDaemonsRoot(workspaceRoot);
|
|
182
|
+
const entries = await readdir(root, { withFileTypes: true }).catch(() => []);
|
|
183
|
+
const states = await Promise.all(entries
|
|
184
|
+
.filter((entry) => entry.isDirectory() && DAEMON_ID_PATTERN.test(entry.name))
|
|
185
|
+
.map(async (entry) => readDaemonState(workspaceRoot, entry.name)));
|
|
186
|
+
return states
|
|
187
|
+
.filter((state) => Boolean(state))
|
|
188
|
+
.map((state) => toDaemonSnapshot(state))
|
|
189
|
+
.sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
190
|
+
}
|
|
191
|
+
export async function getAgentDaemonLocal(workspaceRoot, daemonId) {
|
|
192
|
+
const state = await readDaemonState(workspaceRoot, daemonId);
|
|
193
|
+
if (!state) {
|
|
194
|
+
throw new Error("daemon not found");
|
|
195
|
+
}
|
|
196
|
+
return toDaemonSnapshot(state);
|
|
197
|
+
}
|
|
198
|
+
export async function stopAgentDaemonLocal(workspaceRoot, daemonId) {
|
|
199
|
+
const state = await readDaemonState(workspaceRoot, daemonId);
|
|
200
|
+
if (!state) {
|
|
201
|
+
throw new Error("daemon not found");
|
|
202
|
+
}
|
|
203
|
+
const targetPid = isPidAlive(state.runnerPid) ? state.runnerPid : state.pid;
|
|
204
|
+
if (!targetPid) {
|
|
205
|
+
const stopped = {
|
|
206
|
+
...state,
|
|
207
|
+
pid: null,
|
|
208
|
+
runnerPid: null,
|
|
209
|
+
stoppedAt: state.stoppedAt ?? new Date().toISOString(),
|
|
210
|
+
};
|
|
211
|
+
await writeDaemonState(workspaceRoot, stopped);
|
|
212
|
+
return toDaemonSnapshot(stopped);
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
sendSignalToPid(targetPid, "SIGTERM");
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
// Ignore and fall through to polling.
|
|
219
|
+
}
|
|
220
|
+
for (let index = 0; index < 30; index += 1) {
|
|
221
|
+
await sleep(100);
|
|
222
|
+
const latest = await readDaemonState(workspaceRoot, daemonId);
|
|
223
|
+
if (!latest || (!isPidAlive(latest.runnerPid) && !isPidAlive(latest.pid))) {
|
|
224
|
+
return latest ? toDaemonSnapshot(latest) : toDaemonSnapshot({
|
|
225
|
+
...state,
|
|
226
|
+
pid: null,
|
|
227
|
+
runnerPid: null,
|
|
228
|
+
stoppedAt: new Date().toISOString(),
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
sendSignalToPid(targetPid, "SIGKILL");
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
// noop
|
|
237
|
+
}
|
|
238
|
+
for (let index = 0; index < 15; index += 1) {
|
|
239
|
+
await sleep(100);
|
|
240
|
+
const latest = await readDaemonState(workspaceRoot, daemonId);
|
|
241
|
+
if (!latest || (!isPidAlive(latest.runnerPid) && !isPidAlive(latest.pid))) {
|
|
242
|
+
return latest ? toDaemonSnapshot(latest) : toDaemonSnapshot({
|
|
243
|
+
...state,
|
|
244
|
+
pid: null,
|
|
245
|
+
runnerPid: null,
|
|
246
|
+
stoppedAt: new Date().toISOString(),
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
throw new Error("failed to stop daemon");
|
|
251
|
+
}
|
|
252
|
+
async function spawnDaemonLocal(args) {
|
|
253
|
+
const daemonId = args.daemonId ?? createDaemonId();
|
|
254
|
+
const daemonDir = getDaemonDir(args.workspaceRoot, daemonId);
|
|
255
|
+
const statePath = getDaemonStatePath(args.workspaceRoot, daemonId);
|
|
256
|
+
const eventsPath = getDaemonEventsPath(args.workspaceRoot, daemonId);
|
|
257
|
+
await mkdir(daemonDir, { recursive: true });
|
|
258
|
+
await mkdir(path.join(args.workspaceRoot, ".codex"), { recursive: true });
|
|
259
|
+
const settings = await args.readAgentSettingsConfig({ workspaceRoot: args.workspaceRoot });
|
|
260
|
+
const runtimeBinPath = path.join(args.agentProjectDir, "runtime/bin");
|
|
261
|
+
const baseState = {
|
|
262
|
+
id: daemonId,
|
|
263
|
+
label: args.label,
|
|
264
|
+
command: args.command,
|
|
265
|
+
cwd: args.cwd,
|
|
266
|
+
pid: null,
|
|
267
|
+
runnerPid: null,
|
|
268
|
+
startedAt: new Date().toISOString(),
|
|
269
|
+
stoppedAt: null,
|
|
270
|
+
lastExitCode: null,
|
|
271
|
+
};
|
|
272
|
+
await writeDaemonState(args.workspaceRoot, baseState);
|
|
273
|
+
const env = {
|
|
274
|
+
...process.env,
|
|
275
|
+
...buildAgentSettingsEnvPatch(settings),
|
|
276
|
+
...args.envPatch,
|
|
277
|
+
WORKSPACE: args.cwd,
|
|
278
|
+
CODEX_HOME: path.join(args.workspaceRoot, ".codex"),
|
|
279
|
+
PATH: [runtimeBinPath, process.env.PATH || ""].filter(Boolean).join(path.delimiter),
|
|
280
|
+
DOER_DAEMON_ID: daemonId,
|
|
281
|
+
DOER_DAEMON_COMMAND: args.command,
|
|
282
|
+
DOER_DAEMON_CWD: args.cwd,
|
|
283
|
+
DOER_DAEMON_STATE_PATH: statePath,
|
|
284
|
+
DOER_DAEMON_EVENTS_PATH: eventsPath,
|
|
285
|
+
DOER_DAEMON_SHELL_PATH: args.resolveShellPath(),
|
|
286
|
+
};
|
|
287
|
+
const runner = resolveDaemonRunnerEntry(args.agentProjectDir);
|
|
288
|
+
const child = spawn(runner.command, runner.args, {
|
|
289
|
+
cwd: args.cwd,
|
|
290
|
+
env,
|
|
291
|
+
detached: process.platform !== "win32",
|
|
292
|
+
stdio: "ignore",
|
|
293
|
+
});
|
|
294
|
+
if (typeof child.pid !== "number" || child.pid <= 0) {
|
|
295
|
+
throw new Error("failed to start daemon process");
|
|
296
|
+
}
|
|
297
|
+
child.unref();
|
|
298
|
+
await writeDaemonState(args.workspaceRoot, {
|
|
299
|
+
...baseState,
|
|
300
|
+
runnerPid: child.pid,
|
|
301
|
+
});
|
|
302
|
+
for (let index = 0; index < 20; index += 1) {
|
|
303
|
+
await sleep(50);
|
|
304
|
+
const latest = await readDaemonState(args.workspaceRoot, daemonId);
|
|
305
|
+
if (latest && (latest.pid || latest.lastExitCode !== null)) {
|
|
306
|
+
return toDaemonSnapshot(latest);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const latest = await readDaemonState(args.workspaceRoot, daemonId);
|
|
310
|
+
return toDaemonSnapshot(latest ?? { ...baseState, runnerPid: child.pid });
|
|
311
|
+
}
|
|
312
|
+
export async function startAgentDaemonLocal(args) {
|
|
313
|
+
return await spawnDaemonLocal({
|
|
314
|
+
workspaceRoot: args.workspaceRoot,
|
|
315
|
+
agentProjectDir: args.agentProjectDir,
|
|
316
|
+
command: normalizeCommand(args.request.command),
|
|
317
|
+
cwd: args.resolveTaskWorkspace(typeof args.request.cwd === "string" ? args.request.cwd : null),
|
|
318
|
+
label: normalizeLabel(args.request.label),
|
|
319
|
+
envPatch: normalizeEnvPatch(args.request.envPatch),
|
|
320
|
+
resolveShellPath: args.resolveShellPath,
|
|
321
|
+
readAgentSettingsConfig: args.readAgentSettingsConfig,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
export async function restartAgentDaemonLocal(args) {
|
|
325
|
+
const state = await readDaemonState(args.workspaceRoot, args.daemonId);
|
|
326
|
+
if (!state) {
|
|
327
|
+
throw new Error("daemon not found");
|
|
328
|
+
}
|
|
329
|
+
if (isPidAlive(state.runnerPid) || isPidAlive(state.pid)) {
|
|
330
|
+
await stopAgentDaemonLocal(args.workspaceRoot, args.daemonId);
|
|
331
|
+
}
|
|
332
|
+
return await spawnDaemonLocal({
|
|
333
|
+
workspaceRoot: args.workspaceRoot,
|
|
334
|
+
agentProjectDir: args.agentProjectDir,
|
|
335
|
+
daemonId: state.id,
|
|
336
|
+
command: state.command,
|
|
337
|
+
cwd: state.cwd,
|
|
338
|
+
label: state.label,
|
|
339
|
+
envPatch: {},
|
|
340
|
+
resolveShellPath: args.resolveShellPath,
|
|
341
|
+
readAgentSettingsConfig: args.readAgentSettingsConfig,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
export async function deleteAgentDaemonLocal(workspaceRoot, daemonId) {
|
|
345
|
+
const state = await readDaemonState(workspaceRoot, daemonId);
|
|
346
|
+
if (!state) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (isPidAlive(state.runnerPid) || isPidAlive(state.pid)) {
|
|
350
|
+
await stopAgentDaemonLocal(workspaceRoot, daemonId);
|
|
351
|
+
}
|
|
352
|
+
await rm(getDaemonDir(workspaceRoot, daemonId), { recursive: true, force: true });
|
|
353
|
+
}
|
|
354
|
+
export async function readAgentDaemonLogsLocal(args) {
|
|
355
|
+
const state = await readDaemonState(args.workspaceRoot, args.daemonId);
|
|
356
|
+
if (!state) {
|
|
357
|
+
throw new Error("daemon not found");
|
|
358
|
+
}
|
|
359
|
+
const limit = normalizeLimit(args.limit, 100);
|
|
360
|
+
return {
|
|
361
|
+
daemon: toDaemonSnapshot(state),
|
|
362
|
+
events: await readJsonlTail(getDaemonEventsPath(args.workspaceRoot, args.daemonId), limit),
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
async function executeDaemonRpc(args) {
|
|
366
|
+
const action = normalizeAction(args.request.action);
|
|
367
|
+
if (action === "list") {
|
|
368
|
+
return {
|
|
369
|
+
ok: true,
|
|
370
|
+
action,
|
|
371
|
+
daemons: await listAgentDaemonsLocal(args.workspaceRoot),
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
if (action === "inspect") {
|
|
375
|
+
const daemonId = normalizeDaemonId(args.request.daemonId);
|
|
376
|
+
return {
|
|
377
|
+
ok: true,
|
|
378
|
+
action,
|
|
379
|
+
daemon: await getAgentDaemonLocal(args.workspaceRoot, daemonId),
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
if (action === "start") {
|
|
383
|
+
return {
|
|
384
|
+
ok: true,
|
|
385
|
+
action,
|
|
386
|
+
daemon: await startAgentDaemonLocal({
|
|
387
|
+
workspaceRoot: args.workspaceRoot,
|
|
388
|
+
agentProjectDir: args.agentProjectDir,
|
|
389
|
+
request: args.request,
|
|
390
|
+
resolveShellPath: args.resolveShellPath,
|
|
391
|
+
resolveTaskWorkspace: args.resolveTaskWorkspace,
|
|
392
|
+
readAgentSettingsConfig: args.readAgentSettingsConfig,
|
|
393
|
+
}),
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
const daemonId = normalizeDaemonId(args.request.daemonId);
|
|
397
|
+
if (action === "stop") {
|
|
398
|
+
return {
|
|
399
|
+
ok: true,
|
|
400
|
+
action,
|
|
401
|
+
daemon: await stopAgentDaemonLocal(args.workspaceRoot, daemonId),
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
if (action === "restart") {
|
|
405
|
+
return {
|
|
406
|
+
ok: true,
|
|
407
|
+
action,
|
|
408
|
+
daemon: await restartAgentDaemonLocal({
|
|
409
|
+
workspaceRoot: args.workspaceRoot,
|
|
410
|
+
agentProjectDir: args.agentProjectDir,
|
|
411
|
+
daemonId,
|
|
412
|
+
resolveShellPath: args.resolveShellPath,
|
|
413
|
+
readAgentSettingsConfig: args.readAgentSettingsConfig,
|
|
414
|
+
}),
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
if (action === "delete") {
|
|
418
|
+
await deleteAgentDaemonLocal(args.workspaceRoot, daemonId);
|
|
419
|
+
return {
|
|
420
|
+
ok: true,
|
|
421
|
+
action,
|
|
422
|
+
daemonId,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
const limit = normalizeLimit(args.request.limit, 100);
|
|
426
|
+
return {
|
|
427
|
+
ok: true,
|
|
428
|
+
action,
|
|
429
|
+
...(await readAgentDaemonLogsLocal({ workspaceRoot: args.workspaceRoot, daemonId, limit })),
|
|
430
|
+
limit,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
export async function handleDaemonRpcMessage(args) {
|
|
434
|
+
let requestId = "unknown";
|
|
435
|
+
try {
|
|
436
|
+
const request = JSON.parse(daemonRpcCodec.decode(args.msg.data));
|
|
437
|
+
requestId = typeof request.requestId === "string" ? request.requestId : "unknown";
|
|
438
|
+
const payload = await executeDaemonRpc({
|
|
439
|
+
workspaceRoot: args.workspaceRoot,
|
|
440
|
+
agentProjectDir: args.agentProjectDir,
|
|
441
|
+
request,
|
|
442
|
+
resolveShellPath: args.resolveShellPath,
|
|
443
|
+
resolveTaskWorkspace: args.resolveTaskWorkspace,
|
|
444
|
+
readAgentSettingsConfig: args.readAgentSettingsConfig,
|
|
445
|
+
});
|
|
446
|
+
args.msg.respond(daemonRpcCodec.encode(JSON.stringify({ requestId, ...payload })));
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
450
|
+
args.onError?.(`daemon rpc failed requestId=${requestId} error=${message}`);
|
|
451
|
+
args.msg.respond(daemonRpcCodec.encode(JSON.stringify({ requestId, ok: false, error: message })));
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
export function subscribeToDaemonRpc(args) {
|
|
455
|
+
args.nc.subscribe(args.subject, {
|
|
456
|
+
callback: (error, msg) => {
|
|
457
|
+
if (error) {
|
|
458
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
459
|
+
args.onError(`daemon rpc subscription error: ${message}`);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
void handleDaemonRpcMessage({
|
|
463
|
+
msg,
|
|
464
|
+
nc: args.nc,
|
|
465
|
+
workspaceRoot: args.workspaceRoot,
|
|
466
|
+
agentProjectDir: args.agentProjectDir,
|
|
467
|
+
resolveShellPath: args.resolveShellPath,
|
|
468
|
+
resolveTaskWorkspace: args.resolveTaskWorkspace,
|
|
469
|
+
readAgentSettingsConfig: args.readAgentSettingsConfig,
|
|
470
|
+
onError: args.onError,
|
|
471
|
+
});
|
|
472
|
+
},
|
|
473
|
+
});
|
|
474
|
+
args.onInfo(`daemon rpc subscribed subject=${args.subject}`);
|
|
475
|
+
}
|
package/dist/agent-fs-rpc.js
CHANGED
|
@@ -3,6 +3,7 @@ import { mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "nod
|
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
4
|
import { StringCodec } from "nats";
|
|
5
5
|
import { create as createTar, extract as extractTar } from "tar";
|
|
6
|
+
import { validateImageBytes } from "./agent-runtime-utils.js";
|
|
6
7
|
const fsRpcCodec = StringCodec();
|
|
7
8
|
function normalizeFsRpcPath(workspaceRoot, rawPath) {
|
|
8
9
|
const raw = typeof rawPath === "string" && rawPath.trim() ? rawPath.trim() : ".";
|
|
@@ -295,6 +296,10 @@ async function executeFsRpc(args) {
|
|
|
295
296
|
throw new Error(text || `download failed: ${response.status}`);
|
|
296
297
|
}
|
|
297
298
|
const bytes = Buffer.from(await response.arrayBuffer());
|
|
299
|
+
const validationError = validateImageBytes(abs, bytes);
|
|
300
|
+
if (validationError) {
|
|
301
|
+
throw new Error(validationError);
|
|
302
|
+
}
|
|
298
303
|
const parentDir = path.dirname(abs);
|
|
299
304
|
await mkdir(parentDir, { recursive: true });
|
|
300
305
|
await writeFile(abs, bytes);
|
package/dist/agent-run-state.js
CHANGED
|
@@ -72,10 +72,68 @@ function buildImmediateRunEvent(task, type) {
|
|
|
72
72
|
finishedAt: task.finishedAt,
|
|
73
73
|
};
|
|
74
74
|
}
|
|
75
|
-
|
|
75
|
+
function isPidAlive(pid) {
|
|
76
|
+
if (!Number.isInteger(pid) || (pid ?? 0) <= 0) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
process.kill(pid, 0);
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
const code = error?.code;
|
|
85
|
+
return code === "EPERM";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function removePathIfStale(pathToRemove, pid) {
|
|
89
|
+
if (isPidAlive(pid)) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
await rm(pathToRemove, { recursive: true, force: true }).catch(() => undefined);
|
|
93
|
+
}
|
|
94
|
+
export async function pruneStaleRunsDir(workspaceRoot) {
|
|
76
95
|
const dir = await resolveRunsDir(workspaceRoot);
|
|
77
|
-
await
|
|
78
|
-
|
|
96
|
+
const names = await readdir(dir).catch(() => []);
|
|
97
|
+
for (const name of names) {
|
|
98
|
+
const entryPath = path.join(dir, name);
|
|
99
|
+
if (name === "locks") {
|
|
100
|
+
const lockNames = await readdir(entryPath).catch(() => []);
|
|
101
|
+
for (const lockName of lockNames) {
|
|
102
|
+
const lockPath = path.join(entryPath, lockName);
|
|
103
|
+
const contents = await readFile(lockPath, "utf8").catch(() => null);
|
|
104
|
+
if (!contents) {
|
|
105
|
+
await rm(lockPath, { recursive: true, force: true }).catch(() => undefined);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
let pid = null;
|
|
109
|
+
try {
|
|
110
|
+
const parsed = JSON.parse(contents);
|
|
111
|
+
pid = typeof parsed.pid === "number" && Number.isInteger(parsed.pid) && parsed.pid > 0 ? parsed.pid : null;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
pid = null;
|
|
115
|
+
}
|
|
116
|
+
await removePathIfStale(lockPath, pid);
|
|
117
|
+
}
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (!name.endsWith(".json")) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const contents = await readFile(entryPath, "utf8").catch(() => null);
|
|
124
|
+
if (!contents) {
|
|
125
|
+
await rm(entryPath, { recursive: true, force: true }).catch(() => undefined);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
let task = null;
|
|
129
|
+
try {
|
|
130
|
+
task = normalizePersistedRunTask(JSON.parse(contents));
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
task = null;
|
|
134
|
+
}
|
|
135
|
+
await removePathIfStale(entryPath, task?.processPid ?? null);
|
|
136
|
+
}
|
|
79
137
|
}
|
|
80
138
|
export async function persistRunTask(workspaceRoot, task) {
|
|
81
139
|
const dir = await resolveRunsDir(workspaceRoot);
|