@epoch-agent/plugin-terminal 0.1.0 → 0.3.1
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 +148 -14
- package/dist/index.d.ts +510 -135
- package/dist/index.js +825 -355
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,25 +1,37 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { isAbsolute, resolve } from "path";
|
|
3
|
-
import {
|
|
4
|
-
checkDangerousCommand,
|
|
5
|
-
isInWorkspace,
|
|
6
|
-
sandboxModeForLevel
|
|
7
|
-
} from "@epoch-agent/infra";
|
|
8
|
-
|
|
2
|
+
import { isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
|
|
3
|
+
import { checkDangerousCommand as checkDangerousCommand2, isInWorkspace as isInWorkspace2 } from "@epoch-agent/infra";
|
|
9
4
|
// src/background.ts
|
|
10
5
|
import {
|
|
11
6
|
artifactsDir,
|
|
7
|
+
clearAllJobs,
|
|
8
|
+
getJob,
|
|
9
|
+
jobDetail,
|
|
12
10
|
killTrackedProcess,
|
|
13
|
-
|
|
11
|
+
listJobs,
|
|
12
|
+
processTableFor,
|
|
13
|
+
readJob,
|
|
14
|
+
registerJob,
|
|
15
|
+
rekeyJobs,
|
|
14
16
|
shellSpawnArgs,
|
|
15
|
-
|
|
17
|
+
stopJob,
|
|
18
|
+
waitJob
|
|
16
19
|
} from "@epoch-agent/infra";
|
|
17
|
-
|
|
18
20
|
// src/sandbox.ts
|
|
19
21
|
import {
|
|
20
22
|
classifyFailure,
|
|
21
|
-
confine
|
|
23
|
+
confine,
|
|
24
|
+
sandboxModeForLevel
|
|
22
25
|
} from "@epoch-agent/infra";
|
|
26
|
+
function sandboxPolicyFor(ctx, workspaceRoot) {
|
|
27
|
+
return {
|
|
28
|
+
mode: sandboxModeForLevel(ctx.permissionLevel),
|
|
29
|
+
workspaceRoot,
|
|
30
|
+
...ctx.extraRoots ? { extraRoots: ctx.extraRoots } : {},
|
|
31
|
+
allowNetwork: true,
|
|
32
|
+
enabled: ctx.sandboxTerminal !== false
|
|
33
|
+
};
|
|
34
|
+
}
|
|
23
35
|
function unconfined(skipped) {
|
|
24
36
|
return { mode: null, backend: "none", enforcement: null, skipped, failure: null };
|
|
25
37
|
}
|
|
@@ -64,203 +76,232 @@ function confineShell(sh, policy, cwd) {
|
|
|
64
76
|
})
|
|
65
77
|
};
|
|
66
78
|
}
|
|
67
|
-
|
|
68
79
|
// src/background.ts
|
|
69
80
|
var MAX_TASKS = 8;
|
|
70
81
|
var MAX_TASK_OUTPUT = 256 * 1024;
|
|
71
82
|
var MAX_OUTPUT_CHUNK = 8 * 1024;
|
|
72
|
-
var bySession = /* @__PURE__ */ new Map();
|
|
73
|
-
var counter = 0;
|
|
74
|
-
function nextId() {
|
|
75
|
-
counter += 1;
|
|
76
|
-
return `t${counter}`;
|
|
77
|
-
}
|
|
78
|
-
function bucketOf(sessionId) {
|
|
79
|
-
const existing = bySession.get(sessionId);
|
|
80
|
-
if (existing) return existing;
|
|
81
|
-
const fresh = /* @__PURE__ */ new Map();
|
|
82
|
-
bySession.set(sessionId, fresh);
|
|
83
|
-
return fresh;
|
|
84
|
-
}
|
|
85
|
-
function taskIn(sessionId, id) {
|
|
86
|
-
return bySession.get(sessionId)?.get(id);
|
|
87
|
-
}
|
|
88
83
|
function startTask(sessionId, command, cwd, homeDir, sandbox) {
|
|
89
|
-
const
|
|
90
|
-
const running = [...bucket.values()].filter((t) => t.status === "running");
|
|
84
|
+
const running = listJobs(sessionId, "command").filter((j) => j.status === "running");
|
|
91
85
|
if (running.length >= MAX_TASKS) {
|
|
92
|
-
const list = running.map((
|
|
86
|
+
const list = running.map((j) => `${j.id} (${j.label})`).join("\u3001");
|
|
93
87
|
return {
|
|
94
88
|
error: `\u540E\u53F0\u4EFB\u52A1\u5DF2\u8FBE\u4E0A\u9650 ${MAX_TASKS} \u4E2A\uFF0C\u5148\u7528 task_stop \u505C\u6389\u4E00\u4E2A\u3002\u5728\u8DD1\u7684\uFF1A${list}`
|
|
95
89
|
};
|
|
96
90
|
}
|
|
97
|
-
const id = nextId();
|
|
98
|
-
let finish;
|
|
99
|
-
const done2 = new Promise((resolve2) => {
|
|
100
|
-
finish = resolve2;
|
|
101
|
-
});
|
|
102
91
|
const raw = shellSpawnArgs(command);
|
|
103
92
|
const sh = confineShell({ file: raw.file, args: raw.args }, sandbox, cwd);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
93
|
+
let pid = 0;
|
|
94
|
+
const job = registerJob({
|
|
95
|
+
sessionId,
|
|
96
|
+
kind: "command",
|
|
97
|
+
label: command,
|
|
107
98
|
...cwd ? { cwd } : {},
|
|
108
|
-
|
|
109
|
-
status: "running",
|
|
110
|
-
startedAt: Date.now(),
|
|
111
|
-
buffer: "",
|
|
112
|
-
totalBytes: 0,
|
|
113
|
-
droppedBytes: 0,
|
|
99
|
+
maxOutput: MAX_TASK_OUTPUT,
|
|
114
100
|
artifactsDir: artifactsDir(sessionId, homeDir),
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const started =
|
|
101
|
+
artifactLabel: "task",
|
|
102
|
+
stop: () => killTrackedProcess(pid),
|
|
103
|
+
terminates: true,
|
|
104
|
+
detail: { sandbox: sh.outcome }
|
|
105
|
+
});
|
|
106
|
+
const started = processTableFor(sessionId).start({
|
|
121
107
|
file: sh.file,
|
|
122
108
|
args: sh.args,
|
|
123
109
|
...cwd ? { cwd } : {},
|
|
124
110
|
...raw.options ? { spawnOptions: raw.options } : {},
|
|
125
|
-
onOutput: (chunk) => append(
|
|
111
|
+
onOutput: (chunk) => job.append(chunk),
|
|
126
112
|
onExit: (code) => {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
task.endedAt = Date.now();
|
|
132
|
-
if (task.status === "exited" && code !== null && code !== 0) {
|
|
133
|
-
task.sandbox = sh.classify(code, task.buffer);
|
|
113
|
+
const status = code === null ? "failed" : "exited";
|
|
114
|
+
if (job.status() === "running" && status === "exited" && code !== null && code !== 0) {
|
|
115
|
+
job.detail.sandbox = sh.classify(code, bufferOf(sessionId, job.id));
|
|
134
116
|
}
|
|
135
|
-
|
|
136
|
-
task.finish();
|
|
117
|
+
job.finish(status, code ?? void 0);
|
|
137
118
|
}
|
|
138
119
|
});
|
|
139
|
-
|
|
140
|
-
|
|
120
|
+
pid = started.pid;
|
|
121
|
+
job.setPid(pid);
|
|
122
|
+
return { task: toInfo(job.info()), sandbox: sh.outcome };
|
|
123
|
+
}
|
|
124
|
+
function bufferOf(sessionId, id) {
|
|
125
|
+
return readJob(sessionId, id, 0, MAX_TASK_OUTPUT)?.output ?? "";
|
|
141
126
|
}
|
|
142
127
|
function taskSandbox(sessionId, id) {
|
|
143
|
-
return
|
|
144
|
-
}
|
|
145
|
-
function append(task, chunk) {
|
|
146
|
-
task.totalBytes += chunk.length;
|
|
147
|
-
task.buffer += chunk;
|
|
148
|
-
if (task.buffer.length <= MAX_TASK_OUTPUT) return;
|
|
149
|
-
if (task.spill === void 0) {
|
|
150
|
-
task.spill = openArtifact(task.artifactsDir, `task-${task.id}`, ".txt") ?? null;
|
|
151
|
-
task.spill?.append(task.buffer);
|
|
152
|
-
} else {
|
|
153
|
-
task.spill?.append(chunk);
|
|
154
|
-
}
|
|
155
|
-
const drop = task.buffer.length - MAX_TASK_OUTPUT;
|
|
156
|
-
task.buffer = task.buffer.slice(drop);
|
|
157
|
-
task.droppedBytes += drop;
|
|
128
|
+
return jobDetail(sessionId, id)?.sandbox;
|
|
158
129
|
}
|
|
159
130
|
function listTasks(sessionId) {
|
|
160
|
-
|
|
161
|
-
|
|
131
|
+
return listJobs(sessionId).map(toInfo);
|
|
132
|
+
}
|
|
133
|
+
function commandJob(sessionId, id) {
|
|
134
|
+
const info = getJob(sessionId, id);
|
|
135
|
+
return info?.kind === "command" ? info : void 0;
|
|
162
136
|
}
|
|
163
137
|
function taskOutput(sessionId, id, since = 0) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const slice = task.buffer.slice(from - task.droppedBytes);
|
|
169
|
-
const output = slice.slice(0, MAX_OUTPUT_CHUNK);
|
|
170
|
-
return {
|
|
171
|
-
info: toInfo(task),
|
|
172
|
-
output,
|
|
173
|
-
nextCursor: from + output.length,
|
|
174
|
-
missed,
|
|
175
|
-
hasMore: output.length < slice.length
|
|
176
|
-
};
|
|
138
|
+
if (!getJob(sessionId, id)) return void 0;
|
|
139
|
+
const read = readJob(sessionId, id, since, MAX_OUTPUT_CHUNK);
|
|
140
|
+
if (!read) return void 0;
|
|
141
|
+
return { ...read, info: toInfo(read.info) };
|
|
177
142
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
task.status = "killed";
|
|
182
|
-
task.endedAt = Date.now();
|
|
183
|
-
await killTrackedProcess(task.pid);
|
|
184
|
-
task.spill?.close();
|
|
185
|
-
task.finish();
|
|
186
|
-
return true;
|
|
143
|
+
function stopTask(sessionId, id) {
|
|
144
|
+
if (!commandJob(sessionId, id)) return Promise.resolve(false);
|
|
145
|
+
return stopJob(sessionId, id);
|
|
187
146
|
}
|
|
188
147
|
async function waitTask(sessionId, id, timeoutMs) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (
|
|
192
|
-
|
|
193
|
-
const timeout = new Promise((resolve2) => {
|
|
194
|
-
timer = setTimeout(() => resolve2("timeout"), timeoutMs);
|
|
195
|
-
timer.unref?.();
|
|
196
|
-
});
|
|
197
|
-
try {
|
|
198
|
-
const winner = await Promise.race([task.done.then(() => "done"), timeout]);
|
|
199
|
-
return { info: toInfo(task), timedOut: winner === "timeout" };
|
|
200
|
-
} finally {
|
|
201
|
-
if (timer) clearTimeout(timer);
|
|
202
|
-
}
|
|
148
|
+
if (!commandJob(sessionId, id)) return void 0;
|
|
149
|
+
const result = await waitJob(sessionId, id, timeoutMs);
|
|
150
|
+
if (!result) return void 0;
|
|
151
|
+
return { info: toInfo(result.info), timedOut: result.timedOut };
|
|
203
152
|
}
|
|
204
153
|
function rekeyTasks(from, to) {
|
|
205
|
-
|
|
206
|
-
const moving = bySession.get(from);
|
|
207
|
-
if (!moving) return;
|
|
208
|
-
bySession.delete(from);
|
|
209
|
-
const target = bySession.get(to);
|
|
210
|
-
if (!target) {
|
|
211
|
-
bySession.set(to, moving);
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
for (const [id, task] of moving) target.set(id, task);
|
|
154
|
+
rekeyJobs(from, to, ["command"]);
|
|
215
155
|
}
|
|
216
156
|
function clearAllTasks() {
|
|
217
|
-
|
|
218
|
-
for (const task of bucket.values()) task.spill?.close();
|
|
219
|
-
}
|
|
220
|
-
bySession.clear();
|
|
221
|
-
counter = 0;
|
|
157
|
+
clearAllJobs(["command"]);
|
|
222
158
|
}
|
|
223
|
-
function toInfo(
|
|
159
|
+
function toInfo(info) {
|
|
224
160
|
return {
|
|
225
|
-
id:
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
...
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
161
|
+
id: info.id,
|
|
162
|
+
kind: info.kind,
|
|
163
|
+
label: info.label,
|
|
164
|
+
...info.kind === "command" ? { command: info.label } : {},
|
|
165
|
+
...info.cwd ? { cwd: info.cwd } : {},
|
|
166
|
+
pid: info.pid,
|
|
167
|
+
status: taskStatus(info),
|
|
168
|
+
...info.exitCode === void 0 ? {} : { exitCode: info.exitCode },
|
|
169
|
+
startedAt: info.startedAt,
|
|
170
|
+
...info.endedAt === void 0 ? {} : { endedAt: info.endedAt },
|
|
171
|
+
outputBytes: info.outputBytes,
|
|
172
|
+
truncated: info.truncated,
|
|
173
|
+
...info.artifact ? { artifact: info.artifact } : {}
|
|
236
174
|
};
|
|
237
175
|
}
|
|
176
|
+
function taskStatus(info) {
|
|
177
|
+
return info.status;
|
|
178
|
+
}
|
|
238
179
|
function describeTask(info) {
|
|
239
180
|
const dur = Math.round(((info.endedAt ?? Date.now()) - info.startedAt) / 1e3);
|
|
240
181
|
const state = info.status === "running" ? `\u8FD0\u884C\u4E2D ${dur}s` : info.status === "exited" ? `\u5DF2\u7ED3\u675F\uFF0C\u9000\u51FA\u7801 ${info.exitCode}` : info.status === "killed" ? "\u5DF2\u505C\u6B62" : "\u542F\u52A8\u5931\u8D25";
|
|
241
182
|
const trunc = info.truncated ? "\uFF0C\u5F00\u5934\u5DF2\u6EDA\u51FA\u7F13\u51B2" : "";
|
|
242
183
|
return `${info.id} ${state}\uFF08${info.outputBytes} \u5B57\u8282\u8F93\u51FA${trunc}\uFF09 ${info.command}`;
|
|
243
184
|
}
|
|
244
|
-
|
|
245
185
|
// src/exec.ts
|
|
246
186
|
import { spawn } from "child_process";
|
|
247
187
|
import {
|
|
248
188
|
createStreamDecoder,
|
|
249
|
-
|
|
250
|
-
|
|
189
|
+
killProcessTree,
|
|
190
|
+
processFallbackTable,
|
|
251
191
|
shellSpawnArgs as shellSpawnArgs2
|
|
252
192
|
} from "@epoch-agent/infra";
|
|
253
|
-
|
|
254
193
|
// src/limits.ts
|
|
255
194
|
var MAX_CAPTURE_BYTES = 10 * 1024 * 1024;
|
|
256
195
|
var OVER_CAPTURE_NOTICE = `
|
|
257
196
|
[\u8F93\u51FA\u8D85\u8FC7 ${MAX_CAPTURE_BYTES / 1024 / 1024} MB\uFF0C\u5DF2\u622A\u65AD\u5E76\u7EC8\u6B62\u8FDB\u7A0B\uFF1A\u8FD9\u4E0D\u662F\u4E00\u6B21\u6B63\u5E38\u7684\u547D\u4EE4\u8F93\u51FA\uFF08\u591A\u534A\u662F cat \u4E86\u4E8C\u8FDB\u5236\u6587\u4EF6\u6216\u8FDB\u4E86\u6B7B\u5FAA\u73AF\uFF09\u3002\u8BF7\u7F29\u5C0F\u8F93\u51FA\u8303\u56F4\u91CD\u8BD5\uFF0C\u6BD4\u5982\u52A0 head / tail / grep\u3002]`;
|
|
258
197
|
var NO_OUTPUT = "(\u65E0\u8F93\u51FA)";
|
|
259
|
-
|
|
198
|
+
// src/exec.ts
|
|
199
|
+
var FORCE_SETTLE_MS = 3e3;
|
|
200
|
+
function killChildTree(child) {
|
|
201
|
+
void killProcessTree({
|
|
202
|
+
pid: child.pid ?? 0,
|
|
203
|
+
detached: false,
|
|
204
|
+
escalate: true,
|
|
205
|
+
isExited: () => child.exitCode !== null || child.signalCode !== null
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
function collectStreams(child, onOverflow, onOutput) {
|
|
209
|
+
let stdout = "";
|
|
210
|
+
let stderr = "";
|
|
211
|
+
let truncated = false;
|
|
212
|
+
const outDecoder = createStreamDecoder();
|
|
213
|
+
const errDecoder = createStreamDecoder();
|
|
214
|
+
child.stdout?.on("data", (chunk) => {
|
|
215
|
+
if (truncated) return;
|
|
216
|
+
const piece = outDecoder.write(chunk);
|
|
217
|
+
if (piece) {
|
|
218
|
+
stdout += piece;
|
|
219
|
+
onOutput?.(piece);
|
|
220
|
+
}
|
|
221
|
+
if (stdout.length > MAX_CAPTURE_BYTES) {
|
|
222
|
+
truncated = true;
|
|
223
|
+
stdout = stdout.slice(0, MAX_CAPTURE_BYTES);
|
|
224
|
+
onOverflow();
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
child.stderr?.on("data", (chunk) => {
|
|
228
|
+
if (stderr.length >= MAX_CAPTURE_BYTES) return;
|
|
229
|
+
const piece = errDecoder.write(chunk);
|
|
230
|
+
if (!piece) return;
|
|
231
|
+
stderr += piece;
|
|
232
|
+
onOutput?.(piece);
|
|
233
|
+
});
|
|
234
|
+
return () => {
|
|
235
|
+
if (!truncated) stdout += outDecoder.end();
|
|
236
|
+
if (stderr.length < MAX_CAPTURE_BYTES) stderr += errDecoder.end();
|
|
237
|
+
const tail = truncated ? OVER_CAPTURE_NOTICE : "";
|
|
238
|
+
return { stdout: (stdout + tail).trimEnd(), stderr: stderr.trimEnd() };
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function runCommand(command, options) {
|
|
242
|
+
return new Promise((resolve3, reject) => {
|
|
243
|
+
const raw = shellSpawnArgs2(command);
|
|
244
|
+
const sh = confineShell({ file: raw.file, args: raw.args }, options.sandbox, options.cwd);
|
|
245
|
+
const child = spawn(sh.file, sh.args, {
|
|
246
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
247
|
+
cwd: options.cwd,
|
|
248
|
+
...raw.options
|
|
249
|
+
});
|
|
250
|
+
let killedBy = null;
|
|
251
|
+
let settled = false;
|
|
252
|
+
let forceTimer;
|
|
253
|
+
const drain = collectStreams(child, () => kill("overflow"), options.onOutput);
|
|
254
|
+
const timer = setTimeout(() => kill("timeout"), options.timeout);
|
|
255
|
+
const abortCheck = setInterval(() => {
|
|
256
|
+
if (options.signal?.aborted) kill("abort");
|
|
257
|
+
}, 100);
|
|
258
|
+
function clearTimers() {
|
|
259
|
+
clearTimeout(timer);
|
|
260
|
+
clearTimeout(forceTimer);
|
|
261
|
+
clearInterval(abortCheck);
|
|
262
|
+
}
|
|
263
|
+
function finish() {
|
|
264
|
+
if (settled) return;
|
|
265
|
+
settled = true;
|
|
266
|
+
clearTimers();
|
|
267
|
+
const { stdout, stderr } = drain();
|
|
268
|
+
const exitCode = child.exitCode ?? -1;
|
|
269
|
+
resolve3({
|
|
270
|
+
stdout,
|
|
271
|
+
stderr,
|
|
272
|
+
exitCode,
|
|
273
|
+
...killedBy === "timeout" ? { timedOut: true } : {},
|
|
274
|
+
...killedBy === "abort" ? { aborted: true } : {},
|
|
275
|
+
sandbox: killedBy ? sh.outcome : sh.classify(exitCode, stderr)
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
function kill(why) {
|
|
279
|
+
if (killedBy) return;
|
|
280
|
+
killedBy = why;
|
|
281
|
+
killChildTree(child);
|
|
282
|
+
forceTimer = setTimeout(finish, FORCE_SETTLE_MS);
|
|
283
|
+
}
|
|
284
|
+
if (options.signal?.aborted) kill("abort");
|
|
285
|
+
child.on("close", finish);
|
|
286
|
+
child.on("error", (err) => {
|
|
287
|
+
if (settled) return;
|
|
288
|
+
settled = true;
|
|
289
|
+
clearTimers();
|
|
290
|
+
reject(err);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
async function cleanupBackgroundProcesses(table) {
|
|
295
|
+
await (table ?? processFallbackTable()).killAll();
|
|
296
|
+
}
|
|
260
297
|
// src/pty.ts
|
|
261
298
|
import { spawn as spawnPty } from "node-pty";
|
|
262
|
-
import {
|
|
263
|
-
|
|
299
|
+
import {
|
|
300
|
+
IS_WINDOWS,
|
|
301
|
+
killProcessTree as killProcessTree2,
|
|
302
|
+
processTableFor as processTableFor2,
|
|
303
|
+
shellPtyArgs
|
|
304
|
+
} from "@epoch-agent/infra";
|
|
264
305
|
// src/pty-helper.ts
|
|
265
306
|
import { accessSync, chmodSync, constants, existsSync } from "fs";
|
|
266
307
|
import { createRequire } from "module";
|
|
@@ -302,12 +343,24 @@ function ptyHelperHint() {
|
|
|
302
343
|
node-pty \u7684 spawn-helper \u6CA1\u6709\u6267\u884C\u6743\u9650\uFF0C\u4E14\u81EA\u52A8\u4FEE\u590D\u5931\u8D25\uFF08${failure}\uFF09\u3002
|
|
303
344
|
\u624B\u52A8\u4FEE\uFF1Achmod +x <\u4E0A\u9762\u8FD9\u4E2A\u8DEF\u5F84>`;
|
|
304
345
|
}
|
|
305
|
-
|
|
306
346
|
// src/pty.ts
|
|
307
347
|
var PTY_IS_GROUP_LEADER = !IS_WINDOWS;
|
|
348
|
+
function asKeystrokes(input) {
|
|
349
|
+
return IS_WINDOWS ? input.replaceAll(/\r?\n/g, "\r") : input;
|
|
350
|
+
}
|
|
308
351
|
var ptySessions = /* @__PURE__ */ new Map();
|
|
352
|
+
var killing = /* @__PURE__ */ new WeakMap();
|
|
309
353
|
function killPtyTree(pty) {
|
|
310
|
-
|
|
354
|
+
const started = killing.get(pty);
|
|
355
|
+
if (started) return started;
|
|
356
|
+
const running = killProcessTree2({
|
|
357
|
+
pid: pty.pid,
|
|
358
|
+
detached: PTY_IS_GROUP_LEADER,
|
|
359
|
+
escalate: true,
|
|
360
|
+
pty
|
|
361
|
+
});
|
|
362
|
+
killing.set(pty, running);
|
|
363
|
+
return running;
|
|
311
364
|
}
|
|
312
365
|
function confinePty(raw, policy, cwd) {
|
|
313
366
|
if (!Array.isArray(raw.args)) {
|
|
@@ -318,10 +371,10 @@ function confinePty(raw, policy, cwd) {
|
|
|
318
371
|
return { file: sh.file, args: sh.args, outcome: sh.outcome, classify: sh.classify };
|
|
319
372
|
}
|
|
320
373
|
function runCommandPty(command, options) {
|
|
321
|
-
return new Promise((
|
|
374
|
+
return new Promise((resolve3) => {
|
|
322
375
|
const previous = ptySessions.get(options.tag);
|
|
323
376
|
if (previous) {
|
|
324
|
-
killPtyTree(previous);
|
|
377
|
+
void killPtyTree(previous);
|
|
325
378
|
ptySessions.delete(options.tag);
|
|
326
379
|
}
|
|
327
380
|
const raw = shellPtyArgs(command);
|
|
@@ -337,7 +390,7 @@ function runCommandPty(command, options) {
|
|
|
337
390
|
rows: 40
|
|
338
391
|
});
|
|
339
392
|
} catch (err) {
|
|
340
|
-
|
|
393
|
+
resolve3({
|
|
341
394
|
stdout: `PTY \u542F\u52A8\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}${ptyHelperHint()}`,
|
|
342
395
|
exitCode: 1,
|
|
343
396
|
sandbox: sh.outcome
|
|
@@ -345,6 +398,10 @@ function runCommandPty(command, options) {
|
|
|
345
398
|
return;
|
|
346
399
|
}
|
|
347
400
|
ptySessions.set(options.tag, pty);
|
|
401
|
+
const untrack = processTableFor2(options.sessionId).trackForeign(
|
|
402
|
+
pty.pid,
|
|
403
|
+
() => killPtyTree(pty)
|
|
404
|
+
);
|
|
348
405
|
let output = "";
|
|
349
406
|
let truncated = false;
|
|
350
407
|
let settled = false;
|
|
@@ -357,21 +414,21 @@ function runCommandPty(command, options) {
|
|
|
357
414
|
clearTimeout(timer);
|
|
358
415
|
clearInterval(abortCheck);
|
|
359
416
|
releaseSlot();
|
|
360
|
-
|
|
417
|
+
resolve3(result);
|
|
361
418
|
};
|
|
362
419
|
const timer = setTimeout(() => {
|
|
363
|
-
killPtyTree(pty);
|
|
420
|
+
void killPtyTree(pty);
|
|
364
421
|
finish({ stdout: output.trimEnd() || "(\u8D85\u65F6\uFF0C\u65E0\u8F93\u51FA)", exitCode: -1, sandbox: sh.outcome });
|
|
365
422
|
}, options.timeout);
|
|
366
423
|
const abortCheck = setInterval(() => {
|
|
367
424
|
if (!options.signal?.aborted) return;
|
|
368
|
-
killPtyTree(pty);
|
|
425
|
+
void killPtyTree(pty);
|
|
369
426
|
finish({ stdout: output.trimEnd(), exitCode: -1, aborted: true, sandbox: sh.outcome });
|
|
370
427
|
}, 100);
|
|
371
428
|
if (options.stdin) {
|
|
372
429
|
setTimeout(() => {
|
|
373
430
|
try {
|
|
374
|
-
pty.write((options.stdin ?? "") + "\n");
|
|
431
|
+
pty.write(asKeystrokes((options.stdin ?? "") + "\n"));
|
|
375
432
|
} catch {
|
|
376
433
|
}
|
|
377
434
|
}, 200);
|
|
@@ -383,136 +440,19 @@ function runCommandPty(command, options) {
|
|
|
383
440
|
if (output.length > MAX_CAPTURE_BYTES) {
|
|
384
441
|
truncated = true;
|
|
385
442
|
output = output.slice(0, MAX_CAPTURE_BYTES) + OVER_CAPTURE_NOTICE;
|
|
386
|
-
killPtyTree(pty);
|
|
443
|
+
void killPtyTree(pty);
|
|
387
444
|
}
|
|
388
445
|
});
|
|
389
446
|
pty.onExit(({ exitCode }) => {
|
|
447
|
+
untrack();
|
|
390
448
|
finish({
|
|
391
449
|
stdout: output.trimEnd() || NO_OUTPUT,
|
|
392
450
|
exitCode,
|
|
393
|
-
// PTY 只有一股流,**没有独立的 stderr** —— 而拒绝方言和 runner
|
|
394
|
-
// 致命行都是 runner 往 stderr 打的。这里把整股流当 stderr 喂进去是
|
|
395
|
-
// 对的:那两组正则都锚在行首的后端自称上(`sandbox-exec:`),
|
|
396
|
-
// 混进来的 stdout 不会把它们撞出假阳性
|
|
397
451
|
sandbox: sh.classify(exitCode, output)
|
|
398
452
|
});
|
|
399
453
|
});
|
|
400
454
|
});
|
|
401
455
|
}
|
|
402
|
-
function killAllPtySessions() {
|
|
403
|
-
const pending = [];
|
|
404
|
-
for (const [tag, pty] of ptySessions) {
|
|
405
|
-
pending.push(
|
|
406
|
-
killProcessTree({ pid: pty.pid, detached: PTY_IS_GROUP_LEADER, escalate: true, pty })
|
|
407
|
-
);
|
|
408
|
-
ptySessions.delete(tag);
|
|
409
|
-
}
|
|
410
|
-
return pending;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// src/exec.ts
|
|
414
|
-
var FORCE_SETTLE_MS = 3e3;
|
|
415
|
-
function killChildTree(child) {
|
|
416
|
-
void killProcessTree2({
|
|
417
|
-
pid: child.pid ?? 0,
|
|
418
|
-
detached: false,
|
|
419
|
-
escalate: true,
|
|
420
|
-
isExited: () => child.exitCode !== null || child.signalCode !== null
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
|
-
function collectStreams(child, onOverflow, onOutput) {
|
|
424
|
-
let stdout = "";
|
|
425
|
-
let stderr = "";
|
|
426
|
-
let truncated = false;
|
|
427
|
-
const outDecoder = createStreamDecoder();
|
|
428
|
-
const errDecoder = createStreamDecoder();
|
|
429
|
-
child.stdout?.on("data", (chunk) => {
|
|
430
|
-
if (truncated) return;
|
|
431
|
-
const piece = outDecoder.write(chunk);
|
|
432
|
-
if (piece) {
|
|
433
|
-
stdout += piece;
|
|
434
|
-
onOutput?.(piece);
|
|
435
|
-
}
|
|
436
|
-
if (stdout.length > MAX_CAPTURE_BYTES) {
|
|
437
|
-
truncated = true;
|
|
438
|
-
stdout = stdout.slice(0, MAX_CAPTURE_BYTES);
|
|
439
|
-
onOverflow();
|
|
440
|
-
}
|
|
441
|
-
});
|
|
442
|
-
child.stderr?.on("data", (chunk) => {
|
|
443
|
-
if (stderr.length >= MAX_CAPTURE_BYTES) return;
|
|
444
|
-
const piece = errDecoder.write(chunk);
|
|
445
|
-
if (!piece) return;
|
|
446
|
-
stderr += piece;
|
|
447
|
-
onOutput?.(piece);
|
|
448
|
-
});
|
|
449
|
-
return () => {
|
|
450
|
-
if (!truncated) stdout += outDecoder.end();
|
|
451
|
-
if (stderr.length < MAX_CAPTURE_BYTES) stderr += errDecoder.end();
|
|
452
|
-
const tail = truncated ? OVER_CAPTURE_NOTICE : "";
|
|
453
|
-
return { stdout: (stdout + tail).trimEnd(), stderr: stderr.trimEnd() };
|
|
454
|
-
};
|
|
455
|
-
}
|
|
456
|
-
function runCommand(command, options) {
|
|
457
|
-
return new Promise((resolve2, reject) => {
|
|
458
|
-
const raw = shellSpawnArgs2(command);
|
|
459
|
-
const sh = confineShell({ file: raw.file, args: raw.args }, options.sandbox, options.cwd);
|
|
460
|
-
const child = spawn(sh.file, sh.args, {
|
|
461
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
462
|
-
cwd: options.cwd,
|
|
463
|
-
...raw.options
|
|
464
|
-
});
|
|
465
|
-
let killedBy = null;
|
|
466
|
-
let settled = false;
|
|
467
|
-
let forceTimer;
|
|
468
|
-
const drain = collectStreams(child, () => kill("overflow"), options.onOutput);
|
|
469
|
-
const timer = setTimeout(() => kill("timeout"), options.timeout);
|
|
470
|
-
const abortCheck = setInterval(() => {
|
|
471
|
-
if (options.signal?.aborted) kill("abort");
|
|
472
|
-
}, 100);
|
|
473
|
-
function clearTimers() {
|
|
474
|
-
clearTimeout(timer);
|
|
475
|
-
clearTimeout(forceTimer);
|
|
476
|
-
clearInterval(abortCheck);
|
|
477
|
-
}
|
|
478
|
-
function finish() {
|
|
479
|
-
if (settled) return;
|
|
480
|
-
settled = true;
|
|
481
|
-
clearTimers();
|
|
482
|
-
const { stdout, stderr } = drain();
|
|
483
|
-
const exitCode = child.exitCode ?? -1;
|
|
484
|
-
resolve2({
|
|
485
|
-
stdout,
|
|
486
|
-
stderr,
|
|
487
|
-
exitCode,
|
|
488
|
-
...killedBy === "timeout" ? { timedOut: true } : {},
|
|
489
|
-
...killedBy === "abort" ? { aborted: true } : {},
|
|
490
|
-
// 分类只在**我们没有主动杀它**的时候做:超时 / 中断的非零退出是我们
|
|
491
|
-
// 自己造成的,往分类器里送只会得到一句「命令自己失败了」的废话,
|
|
492
|
-
// 而调用方那边已经有 `timedOut` / `aborted` 两个更准确的字段
|
|
493
|
-
sandbox: killedBy ? sh.outcome : sh.classify(exitCode, stderr)
|
|
494
|
-
});
|
|
495
|
-
}
|
|
496
|
-
function kill(why) {
|
|
497
|
-
if (killedBy) return;
|
|
498
|
-
killedBy = why;
|
|
499
|
-
killChildTree(child);
|
|
500
|
-
forceTimer = setTimeout(finish, FORCE_SETTLE_MS);
|
|
501
|
-
}
|
|
502
|
-
if (options.signal?.aborted) kill("abort");
|
|
503
|
-
child.on("close", finish);
|
|
504
|
-
child.on("error", (err) => {
|
|
505
|
-
if (settled) return;
|
|
506
|
-
settled = true;
|
|
507
|
-
clearTimers();
|
|
508
|
-
reject(err);
|
|
509
|
-
});
|
|
510
|
-
});
|
|
511
|
-
}
|
|
512
|
-
async function cleanupBackgroundProcesses() {
|
|
513
|
-
await Promise.all([killAllTrackedProcesses(), ...killAllPtySessions()]);
|
|
514
|
-
}
|
|
515
|
-
|
|
516
456
|
// src/sandbox-notice.ts
|
|
517
457
|
var MODE_BOUNDARY = {
|
|
518
458
|
"read-only": "\u53EA\u8BFB\uFF1A\u5199\u4EFB\u4F55\u6587\u4EF6\u90FD\u4F1A\u88AB OS \u6321\u4E0B",
|
|
@@ -521,6 +461,9 @@ var MODE_BOUNDARY = {
|
|
|
521
461
|
};
|
|
522
462
|
function sandboxNotice(sb) {
|
|
523
463
|
if (sb.skipped === "not-requested") return "";
|
|
464
|
+
if (sb.skipped === "config-disabled") {
|
|
465
|
+
return "[\u6C99\u7BB1: \u5173] \u914D\u7F6E\u91CC sandbox.terminal \u662F false\uFF0C\u672C\u6B21\u6CA1\u6709 OS \u7EA7\u9694\u79BB\uFF0C\u547D\u4EE4\u80FD\u8BFB\u5199\u6574\u4E2A\u6587\u4EF6\u7CFB\u7EDF\u3002\u8FD9\u4E00\u6863\u548C\u6743\u9650\u6863\u4F4D\u65E0\u5173\uFF08\u5BA1\u6279\u7167\u65E7\uFF09\uFF0C\u5371\u9669\u547D\u4EE4\u8868\u4ECD\u7136\u5728\u5C97\u3002";
|
|
466
|
+
}
|
|
524
467
|
if (sb.skipped === "mode-disabled") {
|
|
525
468
|
return "[\u6C99\u7BB1: \u5173] \u5F53\u524D\u6743\u9650\u6863\u4F4D\u662F\u300C\u5141\u8BB8\u4E00\u5207\u300D\uFF0C\u672C\u6B21\u6CA1\u6709 OS \u7EA7\u9694\u79BB\u3002\u5371\u9669\u547D\u4EE4\u8868\u4ECD\u7136\u5728\u5C97\u3002";
|
|
526
469
|
}
|
|
@@ -539,18 +482,26 @@ function sandboxError(sb) {
|
|
|
539
482
|
if (failure2.kind === "runner-failed") {
|
|
540
483
|
return {
|
|
541
484
|
message: `\u6C99\u7BB1\u542F\u52A8\u5931\u8D25\uFF0C\u547D\u4EE4\u6CA1\u6709\u6267\u884C\uFF1A${failure2.evidence ?? "(\u6C99\u7BB1\u672A\u7ED9\u51FA\u539F\u56E0)"}`,
|
|
542
|
-
suggestion: "\u8FD9\u4E0D\u662F\u547D\u4EE4\u672C\u8EAB\u7684\u95EE\u9898\uFF0C\u522B\u6539\u547D\u4EE4\u3002\u628A\u8FD9\u6761\u539F\u6587\u62A5\u7ED9\u7528\u6237\uFF0C\
|
|
485
|
+
suggestion: "\u8FD9\u4E0D\u662F\u547D\u4EE4\u672C\u8EAB\u7684\u95EE\u9898\uFF0C\u522B\u6539\u547D\u4EE4\u3002\u628A\u8FD9\u6761\u539F\u6587\u62A5\u7ED9\u7528\u6237 \u2014\u2014 \u8981\u7ED5\u5F00\u7684\u8BDD\uFF0C\u8BA9\u4ED6\u5728 config.yaml \u91CC\u5199 sandbox.terminal: false\uFF08\u53EA\u5173\u6C99\u7BB1\uFF0C\u5BA1\u6279\u4E0D\u52A8\uFF09\uFF0C\u522B\u529D\u4ED6\u5207\u300C\u5141\u8BB8\u4E00\u5207\u300D\u6863\u4F4D\u3002"
|
|
543
486
|
};
|
|
544
487
|
}
|
|
545
488
|
if (failure2.kind === "sandbox-denied") {
|
|
546
489
|
return {
|
|
547
490
|
message: `\u547D\u4EE4\u88AB\u6C99\u7BB1\u6321\u4E0B\uFF1A${failure2.evidence ?? "(\u6C99\u7BB1\u672A\u7ED9\u51FA\u539F\u56E0)"}`,
|
|
548
|
-
suggestion: sb.mode === "read-only" ? "\u5F53\u524D\u662F\u53EA\u8BFB\u6863\u4F4D\uFF0C\u4EFB\u4F55\u5199\u5165\u90FD\u4F1A\u88AB\u6321\u3002\u8981\u771F\u7684\u6539\u4E1C\u897F\uFF0C\u5148\u8BF7\u7528\u6237\u5207\u6362\u6743\u9650\u6863\u4F4D\u3002" : "\u6C99\u7BB1\u53EA\u653E\u5F00\u5DE5\u4F5C\u533A\u3001\u4E34\u65F6\u76EE\u5F55\u548C\u5DE5\u5177\u94FE\u7F13\u5B58\u3002\u8981\u5199\u5230\u522B\u5904\uFF0C\u5148\u8BF7\u7528\u6237\u786E\u8BA4\u3002"
|
|
491
|
+
suggestion: sb.mode === "read-only" ? "\u5F53\u524D\u662F\u53EA\u8BFB\u6863\u4F4D\uFF0C\u4EFB\u4F55\u5199\u5165\u90FD\u4F1A\u88AB\u6321\u3002\u8981\u771F\u7684\u6539\u4E1C\u897F\uFF0C\u5148\u8BF7\u7528\u6237\u5207\u6362\u6743\u9650\u6863\u4F4D\u3002" : "\u6C99\u7BB1\u53EA\u653E\u5F00\u5DE5\u4F5C\u533A\u3001\u4E34\u65F6\u76EE\u5F55\u548C\u5DE5\u5177\u94FE\u7F13\u5B58\u3002\u8981\u5199\u5230\u522B\u5904\uFF0C\u5148\u8BF7\u7528\u6237\u786E\u8BA4\uFF1B\u5982\u679C\u88AB\u6321\u7684\u662F\u4E00\u6761\u672C\u6765\u8BE5\u80FD\u8DD1\u7684\u547D\u4EE4\uFF08\u6BD4\u5982\u67D0\u4E2A\u6CA1\u767B\u8BB0\u7684\u5DE5\u5177\u94FE\u7F13\u5B58\u76EE\u5F55\uFF09\uFF0C\u4ED6\u53EF\u4EE5\u5728 config.yaml \u91CC\u5199 sandbox.terminal: false \u6574\u4E2A\u5173\u6389\u3002"
|
|
549
492
|
};
|
|
550
493
|
}
|
|
551
494
|
return null;
|
|
552
495
|
}
|
|
553
|
-
|
|
496
|
+
// src/shell/tools.ts
|
|
497
|
+
import { isAbsolute, resolve } from "path";
|
|
498
|
+
import { checkDangerousCommand, isInWorkspace, t } from "@epoch-agent/infra";
|
|
499
|
+
// src/shell/limits.ts
|
|
500
|
+
var MAX_SHELLS = 4;
|
|
501
|
+
var MAX_SHELL_OUTPUT = 256 * 1024;
|
|
502
|
+
var MAX_SHELL_CHUNK = 8 * 1024;
|
|
503
|
+
var SHELL_IDLE_MS = 10 * 60 * 1e3;
|
|
504
|
+
var SHELL_SWEEP_MS = 60 * 1e3;
|
|
554
505
|
// src/spill.ts
|
|
555
506
|
import { artifactsDir as artifactsDir2, headCodePoints, tailCodePoints, writeArtifact } from "@epoch-agent/infra";
|
|
556
507
|
var PREVIEW_HEAD = 2e3;
|
|
@@ -587,48 +538,567 @@ function describeMissed(missed, locator) {
|
|
|
587
538
|
const where = locator ? retrievalHint(locator) : NOT_STORED;
|
|
588
539
|
return `\uFF08\u5F00\u5934 ${missed} \u5B57\u8282\u5DF2\u7ECF\u6EDA\u51FA\u73AF\u5F62\u7F13\u51B2\uFF09${where}`;
|
|
589
540
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
541
|
+
function describeMore(nextCursor) {
|
|
542
|
+
return `\uFF08\u8FD8\u6709\u66F4\u591A\uFF0C\u7528 since: ${nextCursor} \u7EE7\u7EED\u53D6\uFF09`;
|
|
543
|
+
}
|
|
544
|
+
// src/shell/notice.ts
|
|
545
|
+
function stateOf(info) {
|
|
546
|
+
const alive = Math.round((Date.now() - info.startedAt) / 1e3);
|
|
547
|
+
switch (info.status) {
|
|
548
|
+
case "running": {
|
|
549
|
+
const idle = Math.round((Date.now() - info.lastActivity) / 1e3);
|
|
550
|
+
return `\u6D3B\u7740 ${alive}s\uFF08\u7A7A\u95F2 ${idle}s\uFF09`;
|
|
551
|
+
}
|
|
552
|
+
case "exited":
|
|
553
|
+
return `shell \u81EA\u5DF1\u9000\u4E86\uFF0C\u9000\u51FA\u7801 ${info.exitCode}`;
|
|
554
|
+
case "closed":
|
|
555
|
+
return "\u5DF2\u5173\u95ED";
|
|
556
|
+
case "reaped":
|
|
557
|
+
return `\u7A7A\u95F2\u8D85\u8FC7 ${SHELL_IDLE_MS / 6e4} \u5206\u949F\uFF0C\u5DF2\u56DE\u6536`;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
function describeShell(info) {
|
|
561
|
+
const trunc = info.truncated ? "\uFF0C\u5F00\u5934\u5DF2\u6EDA\u51FA\u7F13\u51B2" : "";
|
|
562
|
+
return `${info.id} ${stateOf(info)} cwd=${info.cwd}\uFF08${info.outputBytes} \u5B57\u8282\u8F93\u51FA${trunc}\uFF09`;
|
|
563
|
+
}
|
|
564
|
+
function openedNotice(info) {
|
|
565
|
+
return [
|
|
566
|
+
`\u6301\u4E45 shell \u4F1A\u8BDD ${info.id} \u5DF2\u5F00\uFF1A${info.shell}\uFF0C\u5DE5\u4F5C\u76EE\u5F55 ${info.cwd}`,
|
|
567
|
+
"shell_send \u53D1\u8F93\u5165\uFF08\u8BB0\u5F97\u81EA\u5DF1\u5E26 \\n\uFF09\xB7 shell_read \u589E\u91CF\u8BFB \xB7 shell_close \u5173\u6389 \xB7 shell_list \u770B\u5168\u90E8\u3002",
|
|
568
|
+
`\u6CE8\u610F\uFF1A\u4F1A\u8BDD\u6D3B\u4E0D\u8FC7 epoch \u8FDB\u7A0B\uFF0C\u9000\u51FA\u65F6\u4F1A\u88AB\u4E00\u8D77\u6536\u6389\uFF1B\u7A7A\u95F2 ${SHELL_IDLE_MS / 6e4} \u5206\u949F\u4E5F\u4F1A\u88AB\u56DE\u6536\u3002`
|
|
569
|
+
].join("\n");
|
|
570
|
+
}
|
|
571
|
+
function closedNotice(id) {
|
|
572
|
+
return `${id} \u5DF2\u5173\u95ED\uFF08\u542B\u5B83\u8D77\u7684\u5B50\u8FDB\u7A0B\uFF09\u3002`;
|
|
573
|
+
}
|
|
574
|
+
function alreadyGoneNotice(id) {
|
|
575
|
+
return `${id} \u5DF2\u7ECF\u4E0D\u5728\u8FD0\u884C\u4E2D\u4E86\u3002`;
|
|
576
|
+
}
|
|
577
|
+
var NO_SHELLS = "\u6CA1\u6709\u6301\u4E45 shell \u4F1A\u8BDD\u3002\u7528 shell_open \u5F00\u4E00\u4E2A\u3002";
|
|
578
|
+
function listHead(count) {
|
|
579
|
+
return `${count} \u4E2A\u6301\u4E45 shell \u4F1A\u8BDD\uFF08\u540C\u65F6\u6700\u591A ${MAX_SHELLS} \u4E2A\uFF09`;
|
|
580
|
+
}
|
|
581
|
+
var NO_NEW_OUTPUT = "\uFF08\u8FD9\u4E00\u6BB5\u6CA1\u6709\u65B0\u8F93\u51FA\u3002\u547D\u4EE4\u53EF\u80FD\u8FD8\u5728\u8DD1\uFF0C\u7B49\u4E00\u4F1A\u513F\u518D shell_read\uFF09";
|
|
582
|
+
function writeFailedNotice(message) {
|
|
583
|
+
return `
|
|
584
|
+
[\u5199\u5165\u5931\u8D25\uFF1A${message}]
|
|
585
|
+
`;
|
|
586
|
+
}
|
|
587
|
+
function readNotice(result) {
|
|
588
|
+
const notes = [];
|
|
589
|
+
if (result.missed > 0) notes.push(describeMissed(result.missed, result.info.artifact));
|
|
590
|
+
if (result.hasMore) notes.push(describeMore(result.nextCursor));
|
|
591
|
+
return [
|
|
592
|
+
describeShell(result.info),
|
|
593
|
+
...notes,
|
|
594
|
+
"",
|
|
595
|
+
result.output || NO_NEW_OUTPUT,
|
|
596
|
+
"",
|
|
597
|
+
`nextCursor: ${result.nextCursor}`
|
|
598
|
+
].join("\n");
|
|
599
|
+
}
|
|
600
|
+
// src/shell/sessions.ts
|
|
601
|
+
import { spawn as spawnPty2 } from "node-pty";
|
|
602
|
+
import {
|
|
603
|
+
allJobs,
|
|
604
|
+
appendJob,
|
|
605
|
+
artifactsDir as artifactsDir3,
|
|
606
|
+
clearAllJobs as clearAllJobs2,
|
|
607
|
+
getDefaultShell,
|
|
608
|
+
getJob as getJob2,
|
|
609
|
+
IS_WINDOWS as IS_WINDOWS2,
|
|
610
|
+
jobDetail as jobDetail2,
|
|
611
|
+
listJobs as listJobs2,
|
|
612
|
+
processTableFor as processTableFor3,
|
|
613
|
+
readJob as readJob2,
|
|
614
|
+
registerJob as registerJob2,
|
|
615
|
+
stopJob as stopJob2,
|
|
616
|
+
touchJob
|
|
617
|
+
} from "@epoch-agent/infra";
|
|
618
|
+
var CSI_PREAMBLE = /^(?:\x1b\[[0-9;?]*[ -/]*[@-~])+/;
|
|
619
|
+
var CONPTY_SIGNATURE = "\x1B[?9001h";
|
|
620
|
+
function dropConPtyPreamble(chunk) {
|
|
621
|
+
if (!IS_WINDOWS2 || !chunk.startsWith(CONPTY_SIGNATURE)) return chunk;
|
|
622
|
+
return chunk.slice(chunk.match(CSI_PREAMBLE)?.[0].length ?? 0);
|
|
623
|
+
}
|
|
624
|
+
function toShellInfo(info, detail) {
|
|
625
|
+
return {
|
|
626
|
+
id: info.id,
|
|
627
|
+
pid: info.pid,
|
|
628
|
+
cwd: info.cwd ?? info.label,
|
|
629
|
+
shell: detail?.shell ?? "",
|
|
630
|
+
status: shellStatus(info.status),
|
|
631
|
+
...info.exitCode === void 0 ? {} : { exitCode: info.exitCode },
|
|
632
|
+
startedAt: info.startedAt,
|
|
633
|
+
lastActivity: info.lastActivity,
|
|
634
|
+
outputBytes: info.outputBytes,
|
|
635
|
+
truncated: info.truncated,
|
|
636
|
+
...info.artifact ? { artifact: info.artifact } : {}
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
function shellStatus(status) {
|
|
640
|
+
switch (status) {
|
|
641
|
+
case "running":
|
|
642
|
+
case "exited":
|
|
643
|
+
case "reaped":
|
|
644
|
+
return status;
|
|
645
|
+
default:
|
|
646
|
+
return "closed";
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
function shellIn(sessionId, id) {
|
|
650
|
+
const info = getJob2(sessionId, id);
|
|
651
|
+
if (!info || info.kind !== "shell") return void 0;
|
|
652
|
+
const detail = jobDetail2(sessionId, id);
|
|
653
|
+
return detail ? { info, detail } : void 0;
|
|
654
|
+
}
|
|
655
|
+
function toShellRead(read, detail) {
|
|
656
|
+
return { ...read, info: toShellInfo(read.info, detail) };
|
|
657
|
+
}
|
|
658
|
+
function openShell(sessionId, cwd, homeDir, sandbox) {
|
|
659
|
+
const running = listJobs2(sessionId, "shell").filter((s) => s.status === "running");
|
|
660
|
+
if (running.length >= MAX_SHELLS) {
|
|
661
|
+
return {
|
|
662
|
+
error: "limit",
|
|
663
|
+
limit: MAX_SHELLS,
|
|
664
|
+
running: running.map((info) => toShellInfo(info, jobDetail2(sessionId, info.id)))
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
const file = getDefaultShell();
|
|
668
|
+
const sh = confineShell({ file, args: [] }, sandbox, cwd);
|
|
669
|
+
let pty;
|
|
670
|
+
try {
|
|
671
|
+
ensurePtyHelperExecutable();
|
|
672
|
+
pty = spawnPty2(sh.file, sh.args, {
|
|
673
|
+
name: "xterm-256color",
|
|
674
|
+
cwd,
|
|
675
|
+
env: { ...process.env, TERM: "xterm-256color", FORCE_COLOR: "1" },
|
|
676
|
+
cols: 120,
|
|
677
|
+
rows: 40
|
|
678
|
+
});
|
|
679
|
+
} catch (err) {
|
|
680
|
+
return {
|
|
681
|
+
error: "spawn-failed",
|
|
682
|
+
message: err instanceof Error ? err.message : String(err),
|
|
683
|
+
hint: ptyHelperHint()
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
const detail = {
|
|
687
|
+
pty,
|
|
688
|
+
shell: file,
|
|
689
|
+
sandbox: sh.outcome,
|
|
690
|
+
fresh: true,
|
|
691
|
+
untrack: () => {
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
const job = registerJob2({
|
|
695
|
+
sessionId,
|
|
696
|
+
kind: "shell",
|
|
697
|
+
label: cwd,
|
|
698
|
+
cwd,
|
|
699
|
+
maxOutput: MAX_SHELL_OUTPUT,
|
|
700
|
+
artifactsDir: artifactsDir3(sessionId, homeDir),
|
|
701
|
+
artifactLabel: "shell",
|
|
702
|
+
stop: () => killPtyTree(pty),
|
|
703
|
+
stoppedStatus: "closed",
|
|
704
|
+
terminates: false,
|
|
705
|
+
detail
|
|
706
|
+
});
|
|
707
|
+
job.setPid(pty.pid);
|
|
708
|
+
detail.untrack = processTableFor3(sessionId).trackForeign(pty.pid, () => killPtyTree(pty));
|
|
709
|
+
armSweep();
|
|
710
|
+
pty.onData((chunk) => {
|
|
711
|
+
if (detail.fresh) {
|
|
712
|
+
detail.fresh = false;
|
|
713
|
+
job.append(dropConPtyPreamble(chunk));
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
job.append(chunk);
|
|
717
|
+
});
|
|
718
|
+
pty.onExit(({ exitCode }) => {
|
|
719
|
+
job.finish("exited", exitCode);
|
|
720
|
+
afterEnd(detail);
|
|
721
|
+
});
|
|
722
|
+
return { shell: toShellInfo(job.info(), detail), sandbox: sh.outcome };
|
|
723
|
+
}
|
|
724
|
+
function afterEnd(detail) {
|
|
725
|
+
detail.untrack();
|
|
726
|
+
disarmSweepIfIdle();
|
|
727
|
+
}
|
|
728
|
+
async function sendToShell(sessionId, id, input, waitMs) {
|
|
729
|
+
sweepIdleShells();
|
|
730
|
+
const found = shellIn(sessionId, id);
|
|
731
|
+
if (!found) return {};
|
|
732
|
+
if (found.info.status !== "running") return { info: toShellInfo(found.info, found.detail) };
|
|
733
|
+
const cursor = found.info.outputBytes;
|
|
734
|
+
touchJob(sessionId, id);
|
|
735
|
+
try {
|
|
736
|
+
found.detail.pty.write(asKeystrokes(input));
|
|
737
|
+
} catch (err) {
|
|
738
|
+
appendJob(sessionId, id, writeFailedNotice(err instanceof Error ? err.message : String(err)));
|
|
739
|
+
}
|
|
740
|
+
await sleep(waitMs);
|
|
741
|
+
touchJob(sessionId, id);
|
|
742
|
+
const read = readJob2(sessionId, id, cursor, MAX_SHELL_CHUNK);
|
|
743
|
+
return read ? toShellRead(read, found.detail) : {};
|
|
744
|
+
}
|
|
745
|
+
function readShell(sessionId, id, since = 0) {
|
|
746
|
+
sweepIdleShells();
|
|
747
|
+
const found = shellIn(sessionId, id);
|
|
748
|
+
if (!found) return {};
|
|
749
|
+
touchJob(sessionId, id);
|
|
750
|
+
const read = readJob2(sessionId, id, since, MAX_SHELL_CHUNK);
|
|
751
|
+
return read ? toShellRead(read, found.detail) : {};
|
|
752
|
+
}
|
|
753
|
+
async function closeShell(sessionId, id) {
|
|
754
|
+
const found = shellIn(sessionId, id);
|
|
755
|
+
if (!found || found.info.status !== "running") return false;
|
|
756
|
+
const closed = await stopJob2(sessionId, id);
|
|
757
|
+
if (closed) afterEnd(found.detail);
|
|
758
|
+
return closed;
|
|
759
|
+
}
|
|
760
|
+
function listShells(sessionId) {
|
|
761
|
+
sweepIdleShells();
|
|
762
|
+
return listJobs2(sessionId, "shell").map(
|
|
763
|
+
(info) => toShellInfo(info, jobDetail2(sessionId, info.id))
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
function clearAllShells() {
|
|
767
|
+
for (const { sessionId, info } of allJobs("shell")) {
|
|
768
|
+
jobDetail2(sessionId, info.id)?.untrack();
|
|
769
|
+
}
|
|
770
|
+
clearAllJobs2(["shell"]);
|
|
771
|
+
clearInterval(sweepTimer);
|
|
772
|
+
sweepTimer = void 0;
|
|
773
|
+
}
|
|
774
|
+
var sweepTimer;
|
|
775
|
+
function sweepIdleShells(now = Date.now()) {
|
|
776
|
+
const reaped = [];
|
|
777
|
+
for (const { sessionId, info } of allJobs("shell")) {
|
|
778
|
+
if (info.status !== "running") continue;
|
|
779
|
+
if (now - info.lastActivity < SHELL_IDLE_MS) continue;
|
|
780
|
+
const detail = jobDetail2(sessionId, info.id);
|
|
781
|
+
if (!detail) continue;
|
|
782
|
+
void stopJob2(sessionId, info.id, "reaped").then(() => afterEnd(detail));
|
|
783
|
+
reaped.push(toShellInfo({ ...info, status: "reaped" }, detail));
|
|
784
|
+
}
|
|
785
|
+
return reaped;
|
|
786
|
+
}
|
|
787
|
+
function armSweep() {
|
|
788
|
+
if (sweepTimer) return;
|
|
789
|
+
sweepTimer = setInterval(() => void sweepIdleShells(), SHELL_SWEEP_MS);
|
|
790
|
+
sweepTimer.unref?.();
|
|
791
|
+
}
|
|
792
|
+
function disarmSweepIfIdle() {
|
|
793
|
+
if (allJobs("shell").some((j) => j.info.status === "running")) return;
|
|
794
|
+
clearInterval(sweepTimer);
|
|
795
|
+
sweepTimer = void 0;
|
|
796
|
+
}
|
|
797
|
+
function sleep(ms) {
|
|
798
|
+
return new Promise((resolve3) => {
|
|
799
|
+
const timer = setTimeout(resolve3, ms);
|
|
800
|
+
timer.unref?.();
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
function shellSandbox(sessionId, id) {
|
|
804
|
+
return shellIn(sessionId, id)?.detail.sandbox;
|
|
805
|
+
}
|
|
806
|
+
// src/shell/tools.ts
|
|
807
|
+
var DEFAULT_WAIT_MS = 2e3;
|
|
808
|
+
var MAX_WAIT_MS = 3e4;
|
|
594
809
|
function ok(output) {
|
|
595
810
|
return { success: true, output, duration: 0 };
|
|
596
811
|
}
|
|
597
|
-
function
|
|
812
|
+
function fail(code, message, suggestion) {
|
|
598
813
|
return {
|
|
599
814
|
success: false,
|
|
600
815
|
output: "",
|
|
601
|
-
error: {
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
816
|
+
error: { code, message, ...suggestion ? { suggestion } : {} },
|
|
817
|
+
duration: 0
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
function notFound(id) {
|
|
821
|
+
return fail(2101, t("shell.not_found", { id }), t("shell.not_found_suggestion"));
|
|
822
|
+
}
|
|
823
|
+
function notRunning(info) {
|
|
824
|
+
if (info.status === "reaped") {
|
|
825
|
+
return fail(
|
|
826
|
+
2102,
|
|
827
|
+
t("shell.reaped", { id: info.id, minutes: SHELL_IDLE_MS / 6e4 }),
|
|
828
|
+
t("shell.reaped_suggestion")
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
return fail(2102, t("shell.not_running", { id: info.id }), t("shell.not_running_suggestion"));
|
|
832
|
+
}
|
|
833
|
+
function toolResultFor(id, result, ctx) {
|
|
834
|
+
if (!("output" in result)) {
|
|
835
|
+
return result.info ? notRunning(result.info) : notFound(id);
|
|
836
|
+
}
|
|
837
|
+
const sb = shellSandbox(ctx.sessionId, id);
|
|
838
|
+
const sbErr = sb ? sandboxError(sb) : null;
|
|
839
|
+
const body = readNotice(result);
|
|
840
|
+
return ok(sbErr ? `${body}
|
|
841
|
+
${sbErr.message}
|
|
842
|
+
${sbErr.suggestion}` : body);
|
|
843
|
+
}
|
|
844
|
+
var shellOpen = {
|
|
845
|
+
name: "shell_open",
|
|
846
|
+
description: `\u5F00\u4E00\u4E2A**\u6301\u4E45** shell \u4F1A\u8BDD\uFF08PTY\uFF09\uFF0C\u8FD4\u56DE\u4F1A\u8BDD id\u3002\u548C terminal \u7684\u533A\u522B\u662F\u5B83\u8DE8\u591A\u6B21\u5DE5\u5177\u8C03\u7528\u6D3B\u7740\uFF1Acd / export / source venv/bin/activate \u90FD\u4F1A\u7559\u5B58\uFF0CREPL \u8FDB\u53BB\u4E86\u8FD8\u80FD\u51FA\u6765\u3002\u9002\u5408\u300C\u5148\u6FC0\u6D3B\u73AF\u5883\u518D\u8DD1\u51E0\u6761\u547D\u4EE4\u300D\u300C\u591A\u8F6E\u4EA4\u4E92\u7684 npm init\u300D\u300CNode / Python REPL\u300D\u3002\u540C\u65F6\u6700\u591A ${MAX_SHELLS} \u4E2A\uFF1B\u7A7A\u95F2 ${SHELL_IDLE_MS / 6e4} \u5206\u949F\u4F1A\u88AB\u56DE\u6536\uFF1B**\u4F1A\u8BDD\u6D3B\u4E0D\u8FC7 epoch \u8FDB\u7A0B**\uFF08\u9000\u51FA\u65F6\u4E00\u8D77\u6536\u6389\uFF0C\u522B\u6307\u671B\u4E0B\u6B21\u8FD0\u884C\u8FD8\u5728\uFF09\u3002\u4E00\u6B21\u6027\u547D\u4EE4\u4ECD\u7136\u7528 terminal \u2014\u2014 \u90A3\u6761\u4E0D\u5FC5\u5F00\u4F1A\u8BDD\u3002`,
|
|
847
|
+
parameters: {
|
|
848
|
+
type: "object",
|
|
849
|
+
properties: {
|
|
850
|
+
workdir: {
|
|
851
|
+
type: "string",
|
|
852
|
+
description: "\u4F1A\u8BDD\u7684\u521D\u59CB\u5DE5\u4F5C\u76EE\u5F55\uFF08\u5FC5\u987B\u5728\u5DE5\u4F5C\u533A\u5185\uFF0C\u76F8\u5BF9\u8DEF\u5F84\u6309\u5DE5\u4F5C\u533A\u6839\u89E3\u6790\uFF09"
|
|
853
|
+
}
|
|
854
|
+
},
|
|
855
|
+
required: []
|
|
856
|
+
},
|
|
857
|
+
supportsParallel: false,
|
|
858
|
+
annotations: {
|
|
859
|
+
readOnlyHint: false,
|
|
860
|
+
destructiveHint: true,
|
|
861
|
+
idempotentHint: false,
|
|
862
|
+
openWorldHint: true
|
|
863
|
+
},
|
|
864
|
+
operation: "command",
|
|
865
|
+
describeTarget: (args) => String(args["workdir"] ?? ""),
|
|
866
|
+
execute: (args, ctx) => {
|
|
867
|
+
const workdir = args["workdir"] ? String(args["workdir"]) : void 0;
|
|
868
|
+
const baseDir = ctx.workDir || process.cwd();
|
|
869
|
+
if (workdir && !isInWorkspace(workdir, baseDir, ctx.extraRoots)) {
|
|
870
|
+
return Promise.resolve(
|
|
871
|
+
fail(
|
|
872
|
+
2006,
|
|
873
|
+
t("shell.workdir_out_of_bounds", { workdir, root: baseDir }),
|
|
874
|
+
t("shell.workdir_suggestion")
|
|
875
|
+
)
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
const cwd = workdir ? resolveWorkdir(workdir, baseDir) : baseDir;
|
|
879
|
+
const opened = openShell(ctx.sessionId, cwd, ctx.homeDir, sandboxPolicyFor(ctx, baseDir));
|
|
880
|
+
if ("error" in opened) {
|
|
881
|
+
if (opened.error === "limit") {
|
|
882
|
+
const list = opened.running.map(describeShell).join("\n");
|
|
883
|
+
return Promise.resolve(
|
|
884
|
+
fail(
|
|
885
|
+
2006,
|
|
886
|
+
t("shell.limit_reached", { limit: opened.limit, list }),
|
|
887
|
+
t("shell.limit_suggestion")
|
|
888
|
+
)
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
return Promise.resolve(
|
|
892
|
+
fail(
|
|
893
|
+
2005,
|
|
894
|
+
t("shell.spawn_failed", { message: opened.message + opened.hint }),
|
|
895
|
+
t("shell.spawn_failed_suggestion")
|
|
896
|
+
)
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
const notice = sandboxNotice(opened.sandbox);
|
|
900
|
+
return Promise.resolve(
|
|
901
|
+
ok([...notice ? [notice] : [], openedNotice(opened.shell)].join("\n"))
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
function resolveWorkdir(workdir, baseDir) {
|
|
906
|
+
return isAbsolute(workdir) ? resolve(workdir) : resolve(baseDir, workdir);
|
|
907
|
+
}
|
|
908
|
+
var shellSend = {
|
|
909
|
+
name: "shell_send",
|
|
910
|
+
description: '\u5F80\u4E00\u4E2A\u6301\u4E45\u4F1A\u8BDD\u91CC\u53D1\u8F93\u5165\uFF0C\u7B49 wait \u6BEB\u79D2\u4E4B\u540E\u628A\u8FD9\u6BB5\u65F6\u95F4\u7684\u8F93\u51FA\u4EA4\u56DE\u6765\u3002**\u6362\u884C\u8981\u81EA\u5DF1\u5E26**\uFF1Ainput \u662F\u9010\u5B57\u5199\u8FDB\u53BB\u7684\uFF0C"pwd" \u53EA\u662F\u628A\u4E09\u4E2A\u5B57\u7B26\u6253\u4E0A\u53BB\uFF0C"pwd\\n" \u624D\u4F1A\u6267\u884C\u3002Ctrl+C \u5C31\u662F\u53D1 "\\x03"\uFF08\u8FD9\u6BD4\u7ED9\u8FDB\u7A0B\u53D1\u4FE1\u53F7\u66F4\u51C6\uFF0C\u7528\u6237\u5728\u771F\u7EC8\u7AEF\u91CC\u6309\u7684\u6B63\u662F\u8FD9\u6761\u8DEF\uFF09\u3002\u8F93\u51FA\u6CA1\u7B49\u591F\u5C31\u518D\u7528 shell_read \u63A5\u7740\u53D6\uFF08\u6E38\u6807\u63A5\u4E0A\u6B21\u7684 nextCursor\uFF09\u3002\u53D1\u8FDB\u53BB\u7684\u5185\u5BB9**\u548C terminal \u8D70\u540C\u4E00\u6761\u5371\u9669\u547D\u4EE4\u8868\u548C\u6743\u9650\u5224\u5B9A** \u2014\u2014 \u6301\u4E45\u4F1A\u8BDD\u4E0D\u662F\u65C1\u8DEF\u3002',
|
|
911
|
+
parameters: {
|
|
912
|
+
type: "object",
|
|
913
|
+
properties: {
|
|
914
|
+
id: { type: "string", description: "\u4F1A\u8BDD id\uFF0C\u4F8B\u5982 s1" },
|
|
915
|
+
input: { type: "string", description: "\u9010\u5B57\u5199\u8FDB\u4F1A\u8BDD\u7684\u8F93\u5165\uFF0C\u901A\u5E38\u8981\u4EE5 \\n \u7ED3\u5C3E" },
|
|
916
|
+
wait: {
|
|
917
|
+
type: "number",
|
|
918
|
+
description: `\u7B49\u591A\u4E45\u518D\u8FD4\u56DE\u8F93\u51FA\uFF08\u6BEB\u79D2\uFF0C\u9ED8\u8BA4 ${DEFAULT_WAIT_MS}\uFF0C\u6700\u5927 ${MAX_WAIT_MS}\uFF09`
|
|
919
|
+
}
|
|
920
|
+
},
|
|
921
|
+
required: ["id", "input"]
|
|
922
|
+
},
|
|
923
|
+
supportsParallel: false,
|
|
924
|
+
annotations: {
|
|
925
|
+
readOnlyHint: false,
|
|
926
|
+
destructiveHint: true,
|
|
927
|
+
idempotentHint: false,
|
|
928
|
+
openWorldHint: true
|
|
929
|
+
},
|
|
930
|
+
operation: "command",
|
|
931
|
+
describeTarget: (args) => String(args["input"] ?? ""),
|
|
932
|
+
execute: async (args, ctx) => {
|
|
933
|
+
const id = String(args["id"] ?? "");
|
|
934
|
+
const input = String(args["input"] ?? "");
|
|
935
|
+
if (!input) return fail(2004, t("shell.empty_input"), t("shell.empty_input_suggestion"));
|
|
936
|
+
const danger = checkDangerousCommand(input);
|
|
937
|
+
if (danger.dangerous) {
|
|
938
|
+
return fail(
|
|
939
|
+
2003,
|
|
940
|
+
t("shell.dangerous", { desc: danger.desc ?? "" }),
|
|
941
|
+
t("shell.dangerous_suggestion", { input: input.trim() })
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
const requested = Number(args["wait"]);
|
|
945
|
+
const wait = Math.min(
|
|
946
|
+
Number.isFinite(requested) && requested >= 0 ? requested : DEFAULT_WAIT_MS,
|
|
947
|
+
MAX_WAIT_MS
|
|
948
|
+
);
|
|
949
|
+
return toolResultFor(id, await sendToShell(ctx.sessionId, id, input, wait), ctx);
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
var shellRead = {
|
|
953
|
+
name: "shell_read",
|
|
954
|
+
description: `\u589E\u91CF\u8BFB\u4E00\u4E2A\u6301\u4E45\u4F1A\u8BDD\u7684\u8F93\u51FA\u3002\u53C2\u6570: id(\u5FC5\u586B), since(\u4E0A\u6B21\u8FD4\u56DE\u7684 nextCursor\uFF0C\u53EA\u53D6\u65B0\u589E\u90E8\u5206)\u3002\u5355\u6B21\u6700\u591A ${MAX_SHELL_CHUNK} \u5B57\u8282\uFF0C\u8F93\u51FA\u5F88\u957F\u65F6\u5206\u6B21\u53D6\uFF1B\u4F1A\u8BDD\u4E0D\u5728\u8DD1\u4E86\u4E5F\u8BFB\u5F97\u5230\uFF08\u7F13\u51B2\u91CC\u90A3\u6BB5\u8F93\u51FA\u8FD8\u5728\uFF09\uFF0C\u6D3B\u4E0D\u6D3B\u7740\u770B\u56DE\u6267\u7B2C\u4E00\u884C\u3002`,
|
|
955
|
+
parameters: {
|
|
956
|
+
type: "object",
|
|
957
|
+
properties: {
|
|
958
|
+
id: { type: "string", description: "\u4F1A\u8BDD id\uFF0C\u4F8B\u5982 s1" },
|
|
959
|
+
since: { type: "number", description: "\u4E0A\u6B21\u8FD4\u56DE\u7684 nextCursor\uFF1B\u4E0D\u7ED9\u5C31\u4ECE\u5934\u53D6" }
|
|
605
960
|
},
|
|
961
|
+
required: ["id"]
|
|
962
|
+
},
|
|
963
|
+
annotations: { readOnlyHint: true, idempotentHint: false, openWorldHint: true },
|
|
964
|
+
operation: "file_read",
|
|
965
|
+
describeTarget: (args) => String(args["id"] ?? ""),
|
|
966
|
+
execute: (args, ctx) => {
|
|
967
|
+
const id = String(args["id"] ?? "");
|
|
968
|
+
const since = Number(args["since"]);
|
|
969
|
+
const result = readShell(ctx.sessionId, id, Number.isFinite(since) && since > 0 ? since : 0);
|
|
970
|
+
return Promise.resolve(toolResultFor(id, result, ctx));
|
|
971
|
+
}
|
|
972
|
+
};
|
|
973
|
+
var shellClose = {
|
|
974
|
+
name: "shell_close",
|
|
975
|
+
description: "\u5173\u6389\u4E00\u4E2A\u6301\u4E45\u4F1A\u8BDD\uFF08\u6740\u6574\u68F5\u8FDB\u7A0B\u6811\uFF0C\u542B\u5B83\u8D77\u7684\u5B50\u8FDB\u7A0B\uFF09\u3002\u53C2\u6570: id(\u5FC5\u586B)\u3002\u540D\u989D\u6EE1\u4E86\u3001\u6216\u8005\u8FD9\u4E2A\u73AF\u5883\u4E0D\u518D\u9700\u8981\u4E86\u5C31\u5173\u6389 \u2014\u2014 \u4F1A\u8BDD\u6D3B\u7740\u5C31\u4E00\u76F4\u5360\u4E00\u4E2A\u540D\u989D\u3002",
|
|
976
|
+
parameters: {
|
|
977
|
+
type: "object",
|
|
978
|
+
properties: { id: { type: "string", description: "\u4F1A\u8BDD id" } },
|
|
979
|
+
required: ["id"]
|
|
980
|
+
},
|
|
981
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
982
|
+
operation: "file_read",
|
|
983
|
+
describeTarget: (args) => String(args["id"] ?? ""),
|
|
984
|
+
execute: async (args, ctx) => {
|
|
985
|
+
const id = String(args["id"] ?? "");
|
|
986
|
+
if (await closeShell(ctx.sessionId, id)) return ok(closedNotice(id));
|
|
987
|
+
const exists = listShells(ctx.sessionId).some((s) => s.id === id);
|
|
988
|
+
return exists ? ok(alreadyGoneNotice(id)) : notFound(id);
|
|
989
|
+
}
|
|
990
|
+
};
|
|
991
|
+
var shellList = {
|
|
992
|
+
name: "shell_list",
|
|
993
|
+
description: "\u5217\u51FA\u672C\u6B21\u4F1A\u8BDD\u5F00\u8FC7\u7684\u6301\u4E45 shell \u4F1A\u8BDD\uFF1Aid\u3001\u5DE5\u4F5C\u76EE\u5F55\u3001\u6D3B\u4E86\u591A\u4E45\u3001\u7A7A\u95F2\u591A\u4E45\u3001\u8F93\u51FA\u5B57\u8282\u6570\u3002\u4E0D\u786E\u5B9A\u624B\u91CC\u6709\u54EA\u4E9B\u4F1A\u8BDD\u3001\u6216\u8005\u649E\u4E0A\u540D\u989D\u4E0A\u9650\u65F6\u5148\u770B\u8FD9\u4E2A\u3002",
|
|
994
|
+
parameters: { type: "object", properties: {}, required: [] },
|
|
995
|
+
annotations: { readOnlyHint: true, idempotentHint: false, openWorldHint: false },
|
|
996
|
+
operation: "file_read",
|
|
997
|
+
execute: (_args, ctx) => {
|
|
998
|
+
const shells = listShells(ctx.sessionId);
|
|
999
|
+
if (shells.length === 0) return Promise.resolve(ok(NO_SHELLS));
|
|
1000
|
+
return Promise.resolve(ok([listHead(shells.length), ...shells.map(describeShell)].join("\n")));
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
var SHELL_TOOLS = [
|
|
1004
|
+
shellOpen,
|
|
1005
|
+
shellSend,
|
|
1006
|
+
shellRead,
|
|
1007
|
+
shellClose,
|
|
1008
|
+
shellList
|
|
1009
|
+
];
|
|
1010
|
+
// src/tasks.ts
|
|
1011
|
+
import { getJob as getJob3, listJobs as listJobs3, readJob as readJob3, stopJob as stopJob3, waitJob as waitJob2 } from "@epoch-agent/infra";
|
|
1012
|
+
// src/job-notice.ts
|
|
1013
|
+
var KIND_MARK = {
|
|
1014
|
+
command: "\u547D\u4EE4",
|
|
1015
|
+
shell: "\u4F1A\u8BDD",
|
|
1016
|
+
agent: "\u5B50 agent"
|
|
1017
|
+
};
|
|
1018
|
+
function stateOf2(status, exitCode, seconds) {
|
|
1019
|
+
switch (status) {
|
|
1020
|
+
case "running":
|
|
1021
|
+
return `\u8FD0\u884C\u4E2D ${seconds}s`;
|
|
1022
|
+
case "exited":
|
|
1023
|
+
return `\u5DF2\u7ED3\u675F\uFF0C\u9000\u51FA\u7801 ${exitCode}`;
|
|
1024
|
+
case "killed":
|
|
1025
|
+
return "\u5DF2\u505C\u6B62";
|
|
1026
|
+
case "closed":
|
|
1027
|
+
return "\u5DF2\u5173\u95ED";
|
|
1028
|
+
case "reaped":
|
|
1029
|
+
return "\u7A7A\u95F2\u5DF2\u88AB\u56DE\u6536";
|
|
1030
|
+
case "failed":
|
|
1031
|
+
return "\u542F\u52A8\u5931\u8D25";
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
function describeJob(info) {
|
|
1035
|
+
const dur = Math.round(((info.endedAt ?? Date.now()) - info.startedAt) / 1e3);
|
|
1036
|
+
const trunc = info.truncated ? "\uFF0C\u5F00\u5934\u5DF2\u6EDA\u51FA\u7F13\u51B2" : "";
|
|
1037
|
+
return `${info.id} [${KIND_MARK[info.kind]}] ${stateOf2(info.status, info.exitCode, dur)}\uFF08${info.outputBytes} \u5B57\u8282\u8F93\u51FA${trunc}\uFF09 ${info.label}`;
|
|
1038
|
+
}
|
|
1039
|
+
var NO_JOBS = "\u6CA1\u6709\u540E\u53F0\u4F5C\u4E1A\u3002\u7528 terminal \u7684 background: true \u8D77\u4E00\u6761\u540E\u53F0\u547D\u4EE4\uFF0C\u6216\u8005 shell_open \u5F00\u4E00\u4E2A\u6301\u4E45 shell \u4F1A\u8BDD\u3002";
|
|
1040
|
+
function listHead2(count) {
|
|
1041
|
+
return `${count} \u4E2A\u540E\u53F0\u4F5C\u4E1A\uFF08\u540E\u53F0\u547D\u4EE4 + \u6301\u4E45 shell \u4F1A\u8BDD\uFF0C\u4E00\u8D77\u5217\uFF09`;
|
|
1042
|
+
}
|
|
1043
|
+
function notFoundNotice(id) {
|
|
1044
|
+
return `\u6CA1\u6709\u7F16\u53F7\u4E3A ${id} \u7684\u540E\u53F0\u4F5C\u4E1A`;
|
|
1045
|
+
}
|
|
1046
|
+
var NOT_FOUND_SUGGESTION = "\u7528 task_list \u770B\u5F53\u524D\u6709\u54EA\u4E9B\uFF08\u540E\u53F0\u547D\u4EE4\u548C\u6301\u4E45\u4F1A\u8BDD\u90FD\u5728\u91CC\u9762\uFF09";
|
|
1047
|
+
var NO_NEW_OUTPUT2 = "\uFF08\u8FD9\u4E00\u6BB5\u6CA1\u6709\u65B0\u8F93\u51FA\uFF09";
|
|
1048
|
+
function waitTimedOutNotice(seconds) {
|
|
1049
|
+
return `
|
|
1050
|
+
\u7B49\u4E86 ${seconds}s \u8FD8\u6CA1\u7ED3\u675F\uFF0C**\u6CA1\u6709\u505C\u5B83**\u3002\u53EF\u4EE5\u63A5\u7740\u5E72\u522B\u7684\uFF0C\u6216\u518D\u7B49\u4E00\u6B21\u3002`;
|
|
1051
|
+
}
|
|
1052
|
+
var WAIT_DONE = "\n\u7528 task_output \u53D6\u8F93\u51FA\u3002";
|
|
1053
|
+
function neverEndsNotice(id) {
|
|
1054
|
+
return `
|
|
1055
|
+
${id} \u662F\u4E00\u4E2A\u6301\u4E45 shell \u4F1A\u8BDD\uFF0C\u5B83\u6CA1\u6709\u300C\u7ED3\u675F\u300D\u8FD9\u4E2A\u72B6\u6001 \u2014\u2014 \u4E00\u76F4\u7B49\u4E0B\u53BB\u4E5F\u4E0D\u4F1A\u6709\u7ED3\u679C\u3002
|
|
1056
|
+
\u8981\u770B\u5B83\u73B0\u5728\u7684\u8F93\u51FA\u7528 shell_read({ id: '${id}' })\uFF0C\u8981\u53D1\u4E0B\u4E00\u53E5\u7528 shell_send\uFF0C\u8981\u6536\u6389\u5B83\u7528 shell_close \u6216 task_stop\u3002`;
|
|
1057
|
+
}
|
|
1058
|
+
function stoppedNotice(id, kind) {
|
|
1059
|
+
return kind === "shell" ? `${id} \u5DF2\u5173\u95ED\uFF08\u542B\u5B83\u8D77\u7684\u5B50\u8FDB\u7A0B\uFF09\u3002\u90A3\u4E2A\u4F1A\u8BDD\u91CC\u7684\u73AF\u5883\uFF08cd / venv / REPL\uFF09\u6CA1\u4E86\uFF0C\u8981\u7528\u5F97\u91CD\u5F00\u4E00\u4E2A\u3002` : `${id} \u5DF2\u505C\u6B62\uFF08\u542B\u5B83\u8D77\u7684\u5B50\u8FDB\u7A0B\uFF09\u3002`;
|
|
1060
|
+
}
|
|
1061
|
+
function alreadyStoppedNotice(id) {
|
|
1062
|
+
return `${id} \u5DF2\u7ECF\u4E0D\u5728\u8FD0\u884C\u4E2D\u4E86\u3002`;
|
|
1063
|
+
}
|
|
1064
|
+
// src/jobs.ts
|
|
1065
|
+
import { jobDetail as jobDetail3 } from "@epoch-agent/infra";
|
|
1066
|
+
function jobSandbox(sessionId, id) {
|
|
1067
|
+
return jobDetail3(sessionId, id)?.sandbox;
|
|
1068
|
+
}
|
|
1069
|
+
// src/tasks.ts
|
|
1070
|
+
var DEFAULT_WAIT_MS2 = 3e4;
|
|
1071
|
+
var MAX_WAIT_MS2 = 3e5;
|
|
1072
|
+
function ok2(output) {
|
|
1073
|
+
return { success: true, output, duration: 0 };
|
|
1074
|
+
}
|
|
1075
|
+
function notFound2(id) {
|
|
1076
|
+
return {
|
|
1077
|
+
success: false,
|
|
1078
|
+
output: "",
|
|
1079
|
+
error: { code: 2101, message: notFoundNotice(id), suggestion: NOT_FOUND_SUGGESTION },
|
|
606
1080
|
duration: 0
|
|
607
1081
|
};
|
|
608
1082
|
}
|
|
609
1083
|
var taskList = {
|
|
610
1084
|
name: "task_list",
|
|
611
|
-
description: "\u5217\u51FA\u672C\u6B21\u4F1A\u8BDD\u8D77\u8FC7\u7684
|
|
1085
|
+
description: "\u5217\u51FA\u672C\u6B21\u4F1A\u8BDD\u8D77\u8FC7\u7684**\u540E\u53F0\u4F5C\u4E1A** \u2014\u2014 terminal \u7684 background: true \u8D77\u7684\u540E\u53F0\u547D\u4EE4\uFF0C\u548C shell_open \u5F00\u7684\u6301\u4E45 shell \u4F1A\u8BDD\uFF0C\u4E24\u79CD\u4E00\u8D77\u5217\uFF0C\u6BCF\u884C\u6807\u51FA\u662F\u54EA\u4E00\u79CD\uFF08[\u547D\u4EE4] / [\u4F1A\u8BDD]\uFF09\u3002\u542B\u72B6\u6001\u3001\u9000\u51FA\u7801\u3001\u8F93\u51FA\u5B57\u8282\u6570\u3002\u4E0D\u786E\u5B9A\u624B\u91CC\u6709\u4EC0\u4E48\u5728\u8DD1\u5C31\u5148\u770B\u8FD9\u4E2A\u3002",
|
|
612
1086
|
parameters: { type: "object", properties: {}, required: [] },
|
|
613
1087
|
annotations: { readOnlyHint: true, idempotentHint: false, openWorldHint: false },
|
|
614
1088
|
operation: "file_read",
|
|
615
1089
|
execute: (_args, ctx) => {
|
|
616
|
-
const
|
|
617
|
-
if (
|
|
618
|
-
|
|
619
|
-
}
|
|
620
|
-
return Promise.resolve(
|
|
621
|
-
ok([`${tasks.length} \u4E2A\u540E\u53F0\u4EFB\u52A1`, ...tasks.map(describeTask)].join("\n"))
|
|
622
|
-
);
|
|
1090
|
+
const jobs = listJobs3(ctx.sessionId);
|
|
1091
|
+
if (jobs.length === 0) return Promise.resolve(ok2(NO_JOBS));
|
|
1092
|
+
return Promise.resolve(ok2([listHead2(jobs.length), ...jobs.map(describeJob)].join("\n")));
|
|
623
1093
|
}
|
|
624
1094
|
};
|
|
625
1095
|
var taskOutputTool = {
|
|
626
1096
|
name: "task_output",
|
|
627
|
-
description: `\u53D6\u4E00\u4E2A\u540E\u53F0\
|
|
1097
|
+
description: `\u53D6\u4E00\u4E2A\u540E\u53F0\u4F5C\u4E1A\u7684\u8F93\u51FA\uFF08\u540E\u53F0\u547D\u4EE4 t1 \u548C\u6301\u4E45\u4F1A\u8BDD s1 \u90FD\u884C\uFF09\u3002\u53C2\u6570: id(\u5FC5\u586B), since(\u4E0A\u6B21\u8FD4\u56DE\u7684 nextCursor\uFF0C\u7528\u6765\u53EA\u53D6\u65B0\u589E\u90E8\u5206)\u3002\u5355\u6B21\u6700\u591A ${MAX_OUTPUT_CHUNK} \u5B57\u8282\uFF1B\u8F93\u51FA\u5F88\u957F\u65F6\u4F1A\u5206\u6B21\u53D6\uFF0C\u8FD4\u56DE\u91CC\u4F1A\u8BF4\u660E\u8FD8\u6709\u6CA1\u6709\u66F4\u591A\u3002`,
|
|
628
1098
|
parameters: {
|
|
629
1099
|
type: "object",
|
|
630
1100
|
properties: {
|
|
631
|
-
id: { type: "string", description: "\
|
|
1101
|
+
id: { type: "string", description: "\u4F5C\u4E1A id\uFF0C\u4F8B\u5982 t1\uFF08\u540E\u53F0\u547D\u4EE4\uFF09\u6216 s1\uFF08\u6301\u4E45\u4F1A\u8BDD\uFF09" },
|
|
632
1102
|
since: { type: "number", description: "\u4E0A\u6B21\u8FD4\u56DE\u7684 nextCursor\uFF1B\u4E0D\u7ED9\u5C31\u4ECE\u5934\u53D6" }
|
|
633
1103
|
},
|
|
634
1104
|
required: ["id"]
|
|
@@ -639,29 +1109,42 @@ var taskOutputTool = {
|
|
|
639
1109
|
execute: (args, ctx) => {
|
|
640
1110
|
const id = String(args["id"] ?? "");
|
|
641
1111
|
const since = Number(args["since"]);
|
|
642
|
-
const result =
|
|
643
|
-
|
|
644
|
-
|
|
1112
|
+
const result = readJob3(
|
|
1113
|
+
ctx.sessionId,
|
|
1114
|
+
id,
|
|
1115
|
+
Number.isFinite(since) && since > 0 ? since : 0,
|
|
1116
|
+
MAX_OUTPUT_CHUNK
|
|
1117
|
+
);
|
|
1118
|
+
if (!result) return Promise.resolve(notFound2(id));
|
|
1119
|
+
const head = describeJob(result.info);
|
|
645
1120
|
const notes = [];
|
|
646
1121
|
if (result.missed > 0) notes.push(describeMissed(result.missed, result.info.artifact));
|
|
647
|
-
if (result.hasMore) notes.push(
|
|
648
|
-
const sb =
|
|
1122
|
+
if (result.hasMore) notes.push(describeMore(result.nextCursor));
|
|
1123
|
+
const sb = jobSandbox(ctx.sessionId, id);
|
|
649
1124
|
const sbErr = sb ? sandboxError(sb) : null;
|
|
650
1125
|
if (sbErr) notes.push(`${sbErr.message}
|
|
651
1126
|
${sbErr.suggestion}`);
|
|
652
|
-
const body = result.output || "\uFF08\u8FD9\u4E00\u6BB5\u6CA1\u6709\u65B0\u8F93\u51FA\uFF09";
|
|
653
1127
|
return Promise.resolve(
|
|
654
|
-
|
|
1128
|
+
ok2(
|
|
1129
|
+
[
|
|
1130
|
+
head,
|
|
1131
|
+
...notes,
|
|
1132
|
+
"",
|
|
1133
|
+
result.output || NO_NEW_OUTPUT2,
|
|
1134
|
+
"",
|
|
1135
|
+
`nextCursor: ${result.nextCursor}`
|
|
1136
|
+
].join("\n")
|
|
1137
|
+
)
|
|
655
1138
|
);
|
|
656
1139
|
}
|
|
657
1140
|
};
|
|
658
1141
|
var taskWait = {
|
|
659
1142
|
name: "task_wait",
|
|
660
|
-
description: "\u963B\u585E\u7B49\u4E00\u4E2A\u540E\u53F0\
|
|
1143
|
+
description: "\u963B\u585E\u7B49\u4E00\u4E2A\u540E\u53F0\u4F5C\u4E1A\u7ED3\u675F\u3002\u53C2\u6570: id(\u5FC5\u586B), timeoutMs(\u9ED8\u8BA4 30000\uFF0C\u6700\u5927 300000)\u3002**\u8D85\u65F6\u4E0D\u4F1A\u6740\u5B83** \u2014\u2014 \u53EA\u662F\u544A\u8BC9\u4F60\u8FD8\u5728\u8DD1\uFF0C\u8981\u505C\u7528 task_stop\u3002\u6301\u4E45 shell \u4F1A\u8BDD\uFF08s1\uFF09\u538B\u6839\u6CA1\u6709\u300C\u7ED3\u675F\u300D\u8FD9\u4E2A\u72B6\u6001\uFF0C\u7B49\u5B83\u4F1A**\u7ACB\u523B**\u8FD4\u56DE\u4E00\u53E5\u8BF4\u660E\u800C\u4E0D\u662F\u5E72\u7B49\u3002",
|
|
661
1144
|
parameters: {
|
|
662
1145
|
type: "object",
|
|
663
1146
|
properties: {
|
|
664
|
-
id: { type: "string", description: "\
|
|
1147
|
+
id: { type: "string", description: "\u4F5C\u4E1A id" },
|
|
665
1148
|
timeoutMs: { type: "number", description: "\u6700\u591A\u7B49\u591A\u4E45\uFF08\u6BEB\u79D2\uFF09" }
|
|
666
1149
|
},
|
|
667
1150
|
required: ["id"]
|
|
@@ -673,41 +1156,37 @@ var taskWait = {
|
|
|
673
1156
|
const id = String(args["id"] ?? "");
|
|
674
1157
|
const requested = Number(args["timeoutMs"]);
|
|
675
1158
|
const timeout = Math.min(
|
|
676
|
-
Number.isFinite(requested) && requested > 0 ? requested :
|
|
677
|
-
|
|
1159
|
+
Number.isFinite(requested) && requested > 0 ? requested : DEFAULT_WAIT_MS2,
|
|
1160
|
+
MAX_WAIT_MS2
|
|
678
1161
|
);
|
|
679
|
-
const result = await
|
|
680
|
-
if (!result) return
|
|
681
|
-
const tail = result.timedOut ?
|
|
682
|
-
|
|
683
|
-
return ok(describeTask(result.info) + tail);
|
|
1162
|
+
const result = await waitJob2(ctx.sessionId, id, timeout);
|
|
1163
|
+
if (!result) return notFound2(id);
|
|
1164
|
+
const tail = result.neverEnds ? neverEndsNotice(id) : result.timedOut ? waitTimedOutNotice(Math.round(timeout / 1e3)) : WAIT_DONE;
|
|
1165
|
+
return ok2(describeJob(result.info) + tail);
|
|
684
1166
|
}
|
|
685
1167
|
};
|
|
686
1168
|
var taskStop = {
|
|
687
1169
|
name: "task_stop",
|
|
688
|
-
description: "\u505C\u6389\u4E00\u4E2A\u540E\u53F0\
|
|
1170
|
+
description: "\u505C\u6389\u4E00\u4E2A\u540E\u53F0\u4F5C\u4E1A\uFF08\u6740\u6574\u68F5\u8FDB\u7A0B\u6811\uFF0C\u542B\u5B83\u8D77\u7684\u5B50\u8FDB\u7A0B\uFF09\u3002\u53C2\u6570: id(\u5FC5\u586B)\u3002\u540E\u53F0\u547D\u4EE4\uFF08t1\uFF09\u548C\u6301\u4E45\u4F1A\u8BDD\uFF08s1\uFF09\u90FD\u80FD\u505C \u2014\u2014 \u505C\u4E00\u4E2A\u6301\u4E45\u4F1A\u8BDD\u7B49\u4EF7\u4E8E shell_close\uFF0C\u90A3\u4E2A\u4F1A\u8BDD\u91CC\u7684\u73AF\u5883\uFF08cd / venv / REPL\uFF09\u4F1A\u4E00\u8D77\u6CA1\u3002",
|
|
689
1171
|
parameters: {
|
|
690
1172
|
type: "object",
|
|
691
|
-
properties: { id: { type: "string", description: "\
|
|
1173
|
+
properties: { id: { type: "string", description: "\u4F5C\u4E1A id" } },
|
|
692
1174
|
required: ["id"]
|
|
693
1175
|
},
|
|
694
|
-
// 它真的会杀进程,所以**不是** readOnly
|
|
695
1176
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
696
1177
|
operation: "file_read",
|
|
697
1178
|
describeTarget: (args) => String(args["id"] ?? ""),
|
|
698
1179
|
execute: async (args, ctx) => {
|
|
699
1180
|
const id = String(args["id"] ?? "");
|
|
700
|
-
const
|
|
701
|
-
if (!
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
return exists ? ok(`${id} \u5DF2\u7ECF\u4E0D\u5728\u8FD0\u884C\u4E2D\u4E86\u3002`) : notFound(id);
|
|
1181
|
+
const before = getJob3(ctx.sessionId, id);
|
|
1182
|
+
if (!before) return notFound2(id);
|
|
1183
|
+
if (!await stopJob3(ctx.sessionId, id)) {
|
|
1184
|
+
return ok2(alreadyStoppedNotice(id));
|
|
705
1185
|
}
|
|
706
|
-
return
|
|
1186
|
+
return ok2(stoppedNotice(id, before.kind));
|
|
707
1187
|
}
|
|
708
1188
|
};
|
|
709
1189
|
var TASK_TOOLS = [taskList, taskOutputTool, taskWait, taskStop];
|
|
710
|
-
|
|
711
1190
|
// src/index.ts
|
|
712
1191
|
var DEFAULT_TIMEOUT = 6e4;
|
|
713
1192
|
var PTY_TIMEOUT = 3e4;
|
|
@@ -720,14 +1199,6 @@ function detectCd(command) {
|
|
|
720
1199
|
if (!rawTarget) return null;
|
|
721
1200
|
return rawTarget.trim().replace(/^["']|["']$/g, "");
|
|
722
1201
|
}
|
|
723
|
-
function sandboxPolicyFor(ctx, workspaceRoot) {
|
|
724
|
-
return {
|
|
725
|
-
mode: sandboxModeForLevel(ctx.permissionLevel),
|
|
726
|
-
workspaceRoot,
|
|
727
|
-
...ctx.extraRoots ? { extraRoots: ctx.extraRoots } : {},
|
|
728
|
-
allowNetwork: true
|
|
729
|
-
};
|
|
730
|
-
}
|
|
731
1202
|
function toSandboxFailure(sb, output, duration) {
|
|
732
1203
|
const err = sandboxError(sb);
|
|
733
1204
|
if (!err) return null;
|
|
@@ -735,8 +1206,6 @@ function toSandboxFailure(sb, output, duration) {
|
|
|
735
1206
|
success: false,
|
|
736
1207
|
output,
|
|
737
1208
|
error: {
|
|
738
|
-
// 2008 = 被沙箱挡下(命令跑了,动作被拒)
|
|
739
|
-
// 2009 = 沙箱自己没起来(命令**没有**跑)
|
|
740
1209
|
code: sb.failure?.kind === "runner-failed" ? 2009 : 2008,
|
|
741
1210
|
message: err.message,
|
|
742
1211
|
suggestion: err.suggestion
|
|
@@ -775,9 +1244,6 @@ var terminalTool = {
|
|
|
775
1244
|
required: ["command"]
|
|
776
1245
|
},
|
|
777
1246
|
supportsParallel: false,
|
|
778
|
-
// 任意 shell 命令:能删数据、能联网,两个方向都到顶。
|
|
779
|
-
// `openWorldHint: true` 同时让 core 把输出包进 `<tool_output untrusted="true">`
|
|
780
|
-
// —— `curl` 抓回来的东西和 web_fetch 抓的没有区别。
|
|
781
1247
|
annotations: {
|
|
782
1248
|
readOnlyHint: false,
|
|
783
1249
|
destructiveHint: true,
|
|
@@ -785,7 +1251,6 @@ var terminalTool = {
|
|
|
785
1251
|
openWorldHint: true
|
|
786
1252
|
},
|
|
787
1253
|
operation: "command",
|
|
788
|
-
// 审批目标就是完整命令串(五候选里的 command 也能猜到,这里写明确的)
|
|
789
1254
|
describeTarget: (args) => String(args["command"] ?? ""),
|
|
790
1255
|
async execute(args, ctx) {
|
|
791
1256
|
const command = String(args["command"] ?? "");
|
|
@@ -801,7 +1266,7 @@ var terminalTool = {
|
|
|
801
1266
|
duration: 0
|
|
802
1267
|
};
|
|
803
1268
|
}
|
|
804
|
-
const danger =
|
|
1269
|
+
const danger = checkDangerousCommand2(command);
|
|
805
1270
|
if (danger.dangerous) {
|
|
806
1271
|
return {
|
|
807
1272
|
success: false,
|
|
@@ -817,7 +1282,7 @@ var terminalTool = {
|
|
|
817
1282
|
const baseDir = ctx.workDir || process.cwd();
|
|
818
1283
|
let cwd = baseDir;
|
|
819
1284
|
if (workdir) {
|
|
820
|
-
if (!
|
|
1285
|
+
if (!isInWorkspace2(workdir, baseDir, ctx.extraRoots)) {
|
|
821
1286
|
return {
|
|
822
1287
|
success: false,
|
|
823
1288
|
output: "",
|
|
@@ -829,7 +1294,7 @@ var terminalTool = {
|
|
|
829
1294
|
duration: 0
|
|
830
1295
|
};
|
|
831
1296
|
}
|
|
832
|
-
cwd =
|
|
1297
|
+
cwd = isAbsolute2(workdir) ? resolve2(workdir) : resolve2(baseDir, workdir);
|
|
833
1298
|
}
|
|
834
1299
|
if (isBackground) {
|
|
835
1300
|
const started = startTask(
|
|
@@ -867,12 +1332,10 @@ var terminalTool = {
|
|
|
867
1332
|
timeout: PTY_TIMEOUT,
|
|
868
1333
|
cwd,
|
|
869
1334
|
tag: ctx.sessionId + "::pty",
|
|
1335
|
+
sessionId: ctx.sessionId,
|
|
870
1336
|
...stdin ? { stdin } : {},
|
|
871
1337
|
...ctx.signal ? { signal: ctx.signal } : {},
|
|
872
1338
|
...ctx.onOutput ? { onOutput: ctx.onOutput } : {},
|
|
873
|
-
// 方案 46 PR-5:PTY 这条路 2026-08-17 起也有 OS 强制的写入边界。
|
|
874
|
-
// tty 分配 / 窗口尺寸 / 信号转发三样在真 Seatbelt 上逐条验过照旧,
|
|
875
|
-
// 实测表在 pty.ts 的文件头 —— 那三样正是方案 §三 点名要单独验的
|
|
876
1339
|
sandbox: sandboxPolicyFor(ctx, baseDir)
|
|
877
1340
|
});
|
|
878
1341
|
const duration = Date.now() - startTime;
|
|
@@ -920,10 +1383,6 @@ ${CD_NOTICE(cdTarget)}`;
|
|
|
920
1383
|
signal: ctx.signal,
|
|
921
1384
|
cwd,
|
|
922
1385
|
...ctx.onOutput ? { onOutput: ctx.onOutput } : {},
|
|
923
|
-
// 方案 46 PR-1:`terminal` 的默认路径从这里起有 OS 强制的写入边界。
|
|
924
|
-
// **边界按工作区主根算,不按 `cwd` 算** —— `cwd` 可能是工作区的子目录
|
|
925
|
-
// (模型传了 workdir),按它算的话「写工作区里另一个目录」会被挡,
|
|
926
|
-
// 而那是一次完全正常的操作
|
|
927
1386
|
sandbox: sandboxPolicyFor(ctx, baseDir)
|
|
928
1387
|
});
|
|
929
1388
|
const duration = Date.now() - startTime;
|
|
@@ -979,22 +1438,33 @@ ${CD_NOTICE(cdTarget)}`;
|
|
|
979
1438
|
var terminalPlugin = {
|
|
980
1439
|
name: "@epoch-agent/plugin-terminal",
|
|
981
1440
|
version: "0.0.0",
|
|
982
|
-
description: "\u7EC8\u7AEF\u547D\u4EE4\u6267\u884C + PTY \u4EA4\u4E92 + \u540E\u53F0\u4EFB\u52A1 [Hermes/Codex]",
|
|
983
|
-
|
|
984
|
-
// 就说一句「没有」。按「有没有任务」动态注册的话,模型第一次起后台任务之后
|
|
985
|
-
// 工具表会变,而它这一轮看到的还是旧的
|
|
986
|
-
tools: [terminalTool, ...TASK_TOOLS]
|
|
1441
|
+
description: "\u7EC8\u7AEF\u547D\u4EE4\u6267\u884C + PTY \u4EA4\u4E92 + \u540E\u53F0\u4EFB\u52A1 + \u6301\u4E45 shell \u4F1A\u8BDD [Hermes/Codex]",
|
|
1442
|
+
tools: [terminalTool, ...TASK_TOOLS, ...SHELL_TOOLS]
|
|
987
1443
|
};
|
|
988
1444
|
export {
|
|
1445
|
+
MAX_SHELLS,
|
|
1446
|
+
MAX_SHELL_CHUNK,
|
|
1447
|
+
MAX_SHELL_OUTPUT,
|
|
989
1448
|
MAX_TASKS,
|
|
1449
|
+
SHELL_IDLE_MS,
|
|
1450
|
+
SHELL_SWEEP_MS,
|
|
1451
|
+
SHELL_TOOLS,
|
|
990
1452
|
TASK_TOOLS,
|
|
991
1453
|
cleanupBackgroundProcesses,
|
|
1454
|
+
clearAllShells,
|
|
992
1455
|
clearAllTasks,
|
|
1456
|
+
closeShell,
|
|
1457
|
+
describeShell,
|
|
993
1458
|
describeTask,
|
|
1459
|
+
listShells,
|
|
994
1460
|
listTasks,
|
|
1461
|
+
openShell,
|
|
1462
|
+
readShell,
|
|
995
1463
|
rekeyTasks,
|
|
1464
|
+
sendToShell,
|
|
996
1465
|
startTask,
|
|
997
1466
|
stopTask,
|
|
1467
|
+
sweepIdleShells,
|
|
998
1468
|
taskOutput,
|
|
999
1469
|
taskSandbox,
|
|
1000
1470
|
terminalPlugin,
|