@downcity/plugins 1.0.64 → 1.0.71

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.
Files changed (75) hide show
  1. package/bin/BuiltinPlugins.d.ts +0 -5
  2. package/bin/BuiltinPlugins.d.ts.map +1 -1
  3. package/bin/BuiltinPlugins.js +1 -5
  4. package/bin/BuiltinPlugins.js.map +1 -1
  5. package/bin/index.d.ts +0 -2
  6. package/bin/index.d.ts.map +1 -1
  7. package/bin/index.js +0 -1
  8. package/bin/index.js.map +1 -1
  9. package/bin/task/runtime/TaskRunnerRound.d.ts.map +1 -1
  10. package/bin/task/runtime/TaskRunnerRound.js +7 -2
  11. package/bin/task/runtime/TaskRunnerRound.js.map +1 -1
  12. package/bin/task/runtime/TaskRunnerSession.d.ts.map +1 -1
  13. package/bin/task/runtime/TaskRunnerSession.js +10 -3
  14. package/bin/task/runtime/TaskRunnerSession.js.map +1 -1
  15. package/package.json +4 -3
  16. package/scripts/unrestricted-sandbox-approval.test.mjs +184 -5
  17. package/src/BuiltinPlugins.ts +0 -9
  18. package/src/index.ts +0 -5
  19. package/src/task/runtime/TaskRunnerRound.ts +15 -5
  20. package/src/task/runtime/TaskRunnerSession.ts +11 -4
  21. package/bin/shell/Index.d.ts +0 -9
  22. package/bin/shell/Index.d.ts.map +0 -1
  23. package/bin/shell/Index.js +0 -9
  24. package/bin/shell/Index.js.map +0 -1
  25. package/bin/shell/ShellPlugin.d.ts +0 -65
  26. package/bin/shell/ShellPlugin.d.ts.map +0 -1
  27. package/bin/shell/ShellPlugin.js +0 -175
  28. package/bin/shell/ShellPlugin.js.map +0 -1
  29. package/bin/shell/ShellRuntimeTypes.d.ts +0 -145
  30. package/bin/shell/ShellRuntimeTypes.d.ts.map +0 -1
  31. package/bin/shell/ShellRuntimeTypes.js +0 -10
  32. package/bin/shell/ShellRuntimeTypes.js.map +0 -1
  33. package/bin/shell/runtime/Paths.d.ts +0 -12
  34. package/bin/shell/runtime/Paths.d.ts.map +0 -1
  35. package/bin/shell/runtime/Paths.js +0 -21
  36. package/bin/shell/runtime/Paths.js.map +0 -1
  37. package/bin/shell/runtime/ShellActionResponse.d.ts +0 -52
  38. package/bin/shell/runtime/ShellActionResponse.d.ts.map +0 -1
  39. package/bin/shell/runtime/ShellActionResponse.js +0 -73
  40. package/bin/shell/runtime/ShellActionResponse.js.map +0 -1
  41. package/bin/shell/runtime/ShellActionRuntime.d.ts +0 -70
  42. package/bin/shell/runtime/ShellActionRuntime.d.ts.map +0 -1
  43. package/bin/shell/runtime/ShellActionRuntime.js +0 -587
  44. package/bin/shell/runtime/ShellActionRuntime.js.map +0 -1
  45. package/bin/shell/runtime/ShellActionRuntimeSupport.d.ts +0 -88
  46. package/bin/shell/runtime/ShellActionRuntimeSupport.d.ts.map +0 -1
  47. package/bin/shell/runtime/ShellActionRuntimeSupport.js +0 -353
  48. package/bin/shell/runtime/ShellActionRuntimeSupport.js.map +0 -1
  49. package/bin/shell/runtime/ShellApprovalRuntime.d.ts +0 -57
  50. package/bin/shell/runtime/ShellApprovalRuntime.d.ts.map +0 -1
  51. package/bin/shell/runtime/ShellApprovalRuntime.js +0 -182
  52. package/bin/shell/runtime/ShellApprovalRuntime.js.map +0 -1
  53. package/bin/shell/runtime/ShellProcessEvents.d.ts +0 -22
  54. package/bin/shell/runtime/ShellProcessEvents.d.ts.map +0 -1
  55. package/bin/shell/runtime/ShellProcessEvents.js +0 -41
  56. package/bin/shell/runtime/ShellProcessEvents.js.map +0 -1
  57. package/bin/shell/runtime/ShellRuntimeEnvironment.d.ts +0 -21
  58. package/bin/shell/runtime/ShellRuntimeEnvironment.d.ts.map +0 -1
  59. package/bin/shell/runtime/ShellRuntimeEnvironment.js +0 -70
  60. package/bin/shell/runtime/ShellRuntimeEnvironment.js.map +0 -1
  61. package/bin/shell/types/ShellPluginOptions.d.ts +0 -103
  62. package/bin/shell/types/ShellPluginOptions.d.ts.map +0 -1
  63. package/bin/shell/types/ShellPluginOptions.js +0 -10
  64. package/bin/shell/types/ShellPluginOptions.js.map +0 -1
  65. package/src/shell/Index.ts +0 -9
  66. package/src/shell/ShellPlugin.ts +0 -239
  67. package/src/shell/ShellRuntimeTypes.ts +0 -152
  68. package/src/shell/runtime/Paths.ts +0 -28
  69. package/src/shell/runtime/ShellActionResponse.ts +0 -135
  70. package/src/shell/runtime/ShellActionRuntime.ts +0 -728
  71. package/src/shell/runtime/ShellActionRuntimeSupport.ts +0 -477
  72. package/src/shell/runtime/ShellApprovalRuntime.ts +0 -236
  73. package/src/shell/runtime/ShellProcessEvents.ts +0 -65
  74. package/src/shell/runtime/ShellRuntimeEnvironment.ts +0 -71
  75. package/src/shell/types/ShellPluginOptions.ts +0 -122
@@ -1,477 +0,0 @@
1
- /**
2
- * Shell action 运行时辅助能力。
3
- *
4
- * 关键点(中文)
5
- * - 统一承载 ShellActionRuntime 的内部共享逻辑:环境组装、持久化、waiter 协调、session 查找。
6
- * - 对外暴露给 ShellActionRuntime 的只有纯运行时辅助函数,不直接承担 plugin action 编排。
7
- */
8
-
9
- import path from "node:path";
10
- import fs from "fs-extra";
11
- import type { AgentContext } from "@downcity/agent/internal/types/runtime/agent/AgentContext.js";
12
- import type {
13
- ShellPluginState,
14
- ShellSessionRuntimeState,
15
- } from "@/shell/ShellRuntimeTypes.js";
16
- import type {
17
- ResolvedShellPluginOptions,
18
- ShellPluginOptions,
19
- } from "@/shell/types/ShellPluginOptions.js";
20
- import type {
21
- ShellQueryRequest,
22
- ShellSessionSnapshot,
23
- ShellSessionStatus,
24
- } from "@downcity/agent/internal/executor/tools/shell/types/ShellPlugin.js";
25
- import { getShellOutputPath, getShellSnapshotPath } from "./Paths.js";
26
- import { readChatMetaBySessionId } from "@/chat/runtime/ChatMetaStore.js";
27
- import { resolveChatQueueStore } from "@/chat/runtime/ChatQueueStore.js";
28
- import { resolveOwnerContextId } from "./ShellRuntimeEnvironment.js";
29
- export {
30
- buildShellEnv,
31
- resolveOwnerContextId,
32
- resolveShellCwd,
33
- } from "./ShellRuntimeEnvironment.js";
34
- export {
35
- buildActionResponse,
36
- createOutputChunk,
37
- } from "./ShellActionResponse.js";
38
-
39
- const DEFAULT_SHELL_PLUGIN_OPTIONS: ResolvedShellPluginOptions = {
40
- maxActiveShells: 64,
41
- cleanupDelayMs: 10 * 60 * 1000,
42
- maxInMemoryOutputChars: 1_000_000,
43
- outputPreviewChars: 280,
44
- minWaitMs: 50,
45
- maxWaitMs: 30_000,
46
- defaultInlineWaitMs: 1_200,
47
- defaultWaitTimeoutMs: 10_000,
48
- defaultExecTimeoutMs: 60_000,
49
- defaultApprovalTimeoutMs: 120_000,
50
- };
51
-
52
- /**
53
- * shell.start 默认内联等待时间。
54
- */
55
- export const DEFAULT_INLINE_WAIT_MS = DEFAULT_SHELL_PLUGIN_OPTIONS.defaultInlineWaitMs;
56
-
57
- /**
58
- * shell.wait 默认等待超时。
59
- */
60
- export const DEFAULT_WAIT_TIMEOUT_MS = DEFAULT_SHELL_PLUGIN_OPTIONS.defaultWaitTimeoutMs;
61
-
62
- /**
63
- * shell.exec 默认总超时。
64
- */
65
- export const DEFAULT_EXEC_TIMEOUT_MS = DEFAULT_SHELL_PLUGIN_OPTIONS.defaultExecTimeoutMs;
66
-
67
- function readPositiveInteger(
68
- value: number | undefined,
69
- fallback: number,
70
- ): number {
71
- if (typeof value !== "number" || !Number.isFinite(value)) {
72
- return fallback;
73
- }
74
- return Math.max(1, Math.floor(value));
75
- }
76
-
77
- /**
78
- * 归一化 ShellPlugin 可选运行参数。
79
- */
80
- export function resolveShellPluginOptions(
81
- options: ShellPluginOptions = {},
82
- ): ResolvedShellPluginOptions {
83
- const minWaitMs = readPositiveInteger(
84
- options.minWaitMs,
85
- DEFAULT_SHELL_PLUGIN_OPTIONS.minWaitMs,
86
- );
87
- const maxWaitMs = Math.max(
88
- minWaitMs,
89
- readPositiveInteger(
90
- options.maxWaitMs,
91
- DEFAULT_SHELL_PLUGIN_OPTIONS.maxWaitMs,
92
- ),
93
- );
94
- return {
95
- maxActiveShells: readPositiveInteger(
96
- options.maxActiveShells,
97
- DEFAULT_SHELL_PLUGIN_OPTIONS.maxActiveShells,
98
- ),
99
- cleanupDelayMs: readPositiveInteger(
100
- options.cleanupDelayMs,
101
- DEFAULT_SHELL_PLUGIN_OPTIONS.cleanupDelayMs,
102
- ),
103
- maxInMemoryOutputChars: readPositiveInteger(
104
- options.maxInMemoryOutputChars,
105
- DEFAULT_SHELL_PLUGIN_OPTIONS.maxInMemoryOutputChars,
106
- ),
107
- outputPreviewChars: readPositiveInteger(
108
- options.outputPreviewChars,
109
- DEFAULT_SHELL_PLUGIN_OPTIONS.outputPreviewChars,
110
- ),
111
- minWaitMs,
112
- maxWaitMs,
113
- defaultInlineWaitMs: readPositiveInteger(
114
- options.defaultInlineWaitMs,
115
- DEFAULT_SHELL_PLUGIN_OPTIONS.defaultInlineWaitMs,
116
- ),
117
- defaultWaitTimeoutMs: readPositiveInteger(
118
- options.defaultWaitTimeoutMs,
119
- DEFAULT_SHELL_PLUGIN_OPTIONS.defaultWaitTimeoutMs,
120
- ),
121
- defaultExecTimeoutMs: readPositiveInteger(
122
- options.defaultExecTimeoutMs,
123
- DEFAULT_SHELL_PLUGIN_OPTIONS.defaultExecTimeoutMs,
124
- ),
125
- defaultApprovalTimeoutMs: readPositiveInteger(
126
- options.defaultApprovalTimeoutMs,
127
- DEFAULT_SHELL_PLUGIN_OPTIONS.defaultApprovalTimeoutMs,
128
- ),
129
- };
130
- }
131
-
132
- /**
133
- * 创建 shell plugin runtime 初始状态。
134
- */
135
- export function createShellPluginState(
136
- options: ShellPluginOptions = {},
137
- ): ShellPluginState {
138
- return {
139
- options: resolveShellPluginOptions(options),
140
- sessions: new Map<string, ShellSessionRuntimeState>(),
141
- approvals: new Map(),
142
- context: null,
143
- };
144
- }
145
-
146
- /**
147
- * 返回当前毫秒时间戳。
148
- */
149
- export function nowMs(): number {
150
- return Date.now();
151
- }
152
-
153
- /**
154
- * 归一化 wait/timeout 参数。
155
- */
156
- export function clampWaitMs(value: number | undefined, fallback: number): number {
157
- return clampWaitMsWithOptions(DEFAULT_SHELL_PLUGIN_OPTIONS, value, fallback);
158
- }
159
-
160
- /**
161
- * 结合 ShellPlugin options 归一化 wait/timeout 参数。
162
- */
163
- export function clampWaitMsWithOptions(
164
- options: ResolvedShellPluginOptions,
165
- value: number | undefined,
166
- fallback: number,
167
- ): number {
168
- const raw =
169
- typeof value === "number" && Number.isFinite(value)
170
- ? Math.floor(value)
171
- : fallback;
172
- return Math.min(options.maxWaitMs, Math.max(options.minWaitMs, raw));
173
- }
174
-
175
- function normalizeOutputChunk(raw: string): string {
176
- if (!raw) return "";
177
- return raw
178
- .replace(/\r\n/g, "\n")
179
- .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "");
180
- }
181
-
182
- function deriveExitStatus(exitCode: number | undefined): ShellSessionStatus {
183
- if (exitCode === -9 || exitCode === 137) return "killed";
184
- if (typeof exitCode === "number" && exitCode === 0) return "completed";
185
- return "failed";
186
- }
187
-
188
- /**
189
- * 判断 shell 是否已进入终态。
190
- */
191
- export function isTerminalStatus(status: ShellSessionStatus): boolean {
192
- return (
193
- status === "completed" ||
194
- status === "failed" ||
195
- status === "killed" ||
196
- status === "expired"
197
- );
198
- }
199
-
200
- function extractExternalRefsFromText(
201
- text: string,
202
- current: ShellSessionSnapshot["externalRefs"],
203
- ): ShellSessionSnapshot["externalRefs"] {
204
- const next = [...current];
205
- const register = (kind: string, value: string, label?: string): void => {
206
- const normalized = String(value || "").trim();
207
- if (!normalized) return;
208
- if (next.some((item) => item.kind === kind && item.value === normalized)) return;
209
- next.push({ kind, value: normalized, ...(label ? { label } : {}) });
210
- };
211
-
212
- const threadIdRegex = /thread_id[:=]\s*([a-zA-Z0-9_-]{6,})/g;
213
- for (const match of text.matchAll(threadIdRegex)) {
214
- register("thread_id", String(match[1] || ""), "external thread id");
215
- }
216
- return next;
217
- }
218
-
219
- /**
220
- * 持久化 shell snapshot。
221
- */
222
- export async function persistSnapshot(session: ShellSessionRuntimeState): Promise<void> {
223
- await fs.ensureDir(path.dirname(session.snapshotFilePath));
224
- await fs.writeJson(session.snapshotFilePath, session.snapshot, { spaces: 2 });
225
- }
226
-
227
- function enqueuePersistedAppend(
228
- session: ShellSessionRuntimeState,
229
- text: string,
230
- ): Promise<void> {
231
- session.writeChain = session.writeChain.then(async () => {
232
- await fs.ensureDir(path.dirname(session.outputFilePath));
233
- await fs.appendFile(session.outputFilePath, text, "utf-8");
234
- });
235
- return session.writeChain;
236
- }
237
-
238
- function notifyWaiters(session: ShellSessionRuntimeState): void {
239
- for (const waiter of Array.from(session.waiters)) {
240
- clearTimeout(waiter.timer);
241
- session.waiters.delete(waiter);
242
- waiter.resolve();
243
- }
244
- }
245
-
246
- async function emitChatCompletionEvent(
247
- context: AgentContext,
248
- snapshot: ShellSessionSnapshot,
249
- ): Promise<void> {
250
- const ownerContextId = String(snapshot.ownerContextId || "").trim();
251
- if (!ownerContextId || snapshot.notificationSent !== false) return;
252
-
253
- const meta = await readChatMetaBySessionId({ context, sessionId: ownerContextId });
254
- if (!meta) return;
255
-
256
- const lines = [
257
- "[内部 shell 状态通知]",
258
- `shell_id: ${snapshot.shellId}`,
259
- `status: ${snapshot.status}`,
260
- `exit_code: ${typeof snapshot.exitCode === "number" ? snapshot.exitCode : "null"}`,
261
- `cmd: ${snapshot.cmd}`,
262
- ];
263
- if (snapshot.lastOutputPreview) {
264
- lines.push(`last_output_preview: ${snapshot.lastOutputPreview}`);
265
- }
266
- if (snapshot.externalRefs.length > 0) {
267
- const refs = snapshot.externalRefs.map((item) => `${item.kind}=${item.value}`);
268
- lines.push(`external_refs: ${refs.join(", ")}`);
269
- }
270
- lines.push("请根据当前 shell 的状态,主动向用户简洁汇报结果或最新进展。");
271
- const text = lines.join("\n");
272
-
273
- resolveChatQueueStore(context).enqueue({
274
- kind: "exec",
275
- channel: meta.channel,
276
- targetId: meta.chatId,
277
- sessionId: ownerContextId,
278
- text,
279
- ...(meta.targetType ? { targetType: meta.targetType } : {}),
280
- ...(typeof meta.threadId === "number" ? { threadId: meta.threadId } : {}),
281
- ...(meta.messageId ? { messageId: meta.messageId } : {}),
282
- ...(meta.actorId ? { actorId: meta.actorId } : {}),
283
- ...(meta.actorName ? { actorName: meta.actorName } : {}),
284
- extra: {
285
- note: "shell_session_auto_notify",
286
- internal: true,
287
- shellId: snapshot.shellId,
288
- shellStatus: snapshot.status,
289
- exitCode:
290
- typeof snapshot.exitCode === "number" ? snapshot.exitCode : null,
291
- },
292
- });
293
- }
294
-
295
- /**
296
- * 更新 session snapshot 并唤醒等待者。
297
- */
298
- export async function updateSessionSnapshot(
299
- session: ShellSessionRuntimeState,
300
- updater: (snapshot: ShellSessionSnapshot) => void | ShellSessionSnapshot,
301
- ): Promise<void> {
302
- const result = updater(session.snapshot);
303
- if (result) {
304
- session.snapshot = result;
305
- }
306
- session.snapshot.updatedAt = nowMs();
307
- session.snapshot.version += 1;
308
- await persistSnapshot(session);
309
- notifyWaiters(session);
310
- }
311
-
312
- /**
313
- * 追加 shell 输出并同步更新快照。
314
- */
315
- export async function appendSessionOutput(
316
- state: ShellPluginState,
317
- session: ShellSessionRuntimeState,
318
- raw: string,
319
- ): Promise<void> {
320
- const text = normalizeOutputChunk(raw);
321
- if (!text) return;
322
-
323
- session.outputText += text;
324
- if (session.outputText.length > state.options.maxInMemoryOutputChars) {
325
- const overflow = session.outputText.length - state.options.maxInMemoryOutputChars;
326
- session.outputText = session.outputText.slice(overflow);
327
- session.snapshot.droppedChars += overflow;
328
- }
329
-
330
- session.snapshot.outputChars += text.length;
331
- session.snapshot.lastOutputAt = nowMs();
332
- session.snapshot.lastOutputPreview = session.outputText
333
- .slice(-state.options.outputPreviewChars)
334
- .trim();
335
- session.snapshot.externalRefs = extractExternalRefsFromText(
336
- text,
337
- session.snapshot.externalRefs,
338
- );
339
- await enqueuePersistedAppend(session, text);
340
- await updateSessionSnapshot(session, () => undefined);
341
- }
342
-
343
- /**
344
- * 为终态 shell 安排延迟清理。
345
- */
346
- export function scheduleCleanup(state: ShellPluginState, shellId: string): void {
347
- const session = state.sessions.get(shellId);
348
- if (!session) return;
349
- if (session.cleanupTimer) clearTimeout(session.cleanupTimer);
350
- session.cleanupTimer = setTimeout(() => {
351
- const current = state.sessions.get(shellId);
352
- if (!current) return;
353
- state.sessions.delete(shellId);
354
- }, state.options.cleanupDelayMs);
355
- if (typeof session.cleanupTimer.unref === "function") {
356
- session.cleanupTimer.unref();
357
- }
358
- }
359
-
360
- /**
361
- * 控制 in-memory shell session 容量。
362
- */
363
- export function ensureCapacity(state: ShellPluginState): void {
364
- if (state.sessions.size < state.options.maxActiveShells) return;
365
- const removable = Array.from(state.sessions.values())
366
- .filter((item) => item.snapshot.status !== "running" && item.snapshot.status !== "starting")
367
- .sort((a, b) => a.snapshot.updatedAt - b.snapshot.updatedAt);
368
- for (const item of removable) {
369
- if (state.sessions.size < state.options.maxActiveShells) break;
370
- state.sessions.delete(item.snapshot.shellId);
371
- }
372
- if (state.sessions.size >= state.options.maxActiveShells) {
373
- throw new Error(
374
- `Too many active shell sessions (${state.sessions.size}). Please close or wait older sessions first.`,
375
- );
376
- }
377
- }
378
-
379
- async function loadPersistedSnapshot(
380
- context: AgentContext,
381
- shellId: string,
382
- ): Promise<ShellSessionSnapshot | null> {
383
- const file = getShellSnapshotPath(context.rootPath, shellId);
384
- if (!(await fs.pathExists(file))) return null;
385
- const raw = await fs.readJson(file).catch(() => null);
386
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
387
- const snapshot = raw as ShellSessionSnapshot;
388
- return typeof snapshot.shellId === "string" ? snapshot : null;
389
- }
390
-
391
- async function readPersistedOutput(
392
- context: AgentContext,
393
- shellId: string,
394
- ): Promise<string> {
395
- const file = getShellOutputPath(context.rootPath, shellId);
396
- if (!(await fs.pathExists(file))) return "";
397
- return await fs.readFile(file, "utf-8");
398
- }
399
-
400
- /**
401
- * 按 shellId 或 ownerContext 解析目标 session。
402
- */
403
- export async function resolveSession(
404
- state: ShellPluginState,
405
- context: AgentContext,
406
- query: ShellQueryRequest,
407
- ): Promise<ShellSessionRuntimeState | { snapshot: ShellSessionSnapshot; outputText: string } | null> {
408
- const explicitShellId = String(query.shellId || "").trim();
409
- if (explicitShellId) {
410
- const inMemory = state.sessions.get(explicitShellId);
411
- if (inMemory) return inMemory;
412
- const snapshot = await loadPersistedSnapshot(context, explicitShellId);
413
- if (!snapshot) return null;
414
- return {
415
- snapshot,
416
- outputText: await readPersistedOutput(context, explicitShellId),
417
- };
418
- }
419
-
420
- const ownerContextId = resolveOwnerContextId(query.ownerContextId);
421
- const cmd = String(query.cmd || "").trim().toLowerCase();
422
- if (!ownerContextId) return null;
423
- const includeCompleted = query.includeCompleted === true;
424
- const matched = Array.from(state.sessions.values())
425
- .filter((item) => {
426
- if (item.snapshot.ownerContextId !== ownerContextId) return false;
427
- if (!includeCompleted) {
428
- if (
429
- item.snapshot.status !== "running" &&
430
- item.snapshot.status !== "starting"
431
- ) {
432
- return false;
433
- }
434
- }
435
- if (!cmd) return true;
436
- return item.snapshot.cmd.toLowerCase().includes(cmd);
437
- })
438
- .sort((a, b) => b.snapshot.updatedAt - a.snapshot.updatedAt);
439
- return matched[0] || null;
440
- }
441
-
442
- /**
443
- * 判断解析出的 session 是否仍在内存中活动。
444
- */
445
- export function isInMemorySession(
446
- value: ShellSessionRuntimeState | { snapshot: ShellSessionSnapshot; outputText: string },
447
- ): value is ShellSessionRuntimeState {
448
- return "child" in value;
449
- }
450
-
451
- /**
452
- * 处理 shell 退出后的状态收口。
453
- */
454
- export async function finalizeExit(
455
- state: ShellPluginState,
456
- session: ShellSessionRuntimeState,
457
- exitCode: number,
458
- ): Promise<void> {
459
- await updateSessionSnapshot(session, (snapshot) => {
460
- snapshot.status = deriveExitStatus(exitCode);
461
- snapshot.exitCode = exitCode;
462
- snapshot.endedAt = nowMs();
463
- snapshot.pid = session.child.pid ?? snapshot.pid;
464
- });
465
- session.resolveCompletion();
466
- scheduleCleanup(state, session.snapshot.shellId);
467
-
468
- if (
469
- session.snapshot.autoNotifyOnExit &&
470
- session.snapshot.notificationSent === false &&
471
- state.context
472
- ) {
473
- await emitChatCompletionEvent(state.context, session.snapshot);
474
- session.snapshot.notificationSent = true;
475
- await persistSnapshot(session);
476
- }
477
- }
@@ -1,236 +0,0 @@
1
- /**
2
- * Shell unrestricted sandbox 审批运行时。
3
- *
4
- * 关键点(中文)
5
- * - agent 只能通过 shell tool 请求 unrestricted sandbox;真正执行前必须等待用户确认。
6
- * - 审批结果最终回到原 tool result;session event 只用于 UI/CLI/Console 展示和操作。
7
- * - V1 授权粒度固定为单次命令或单次 shell_start 创建的命令会话。
8
- */
9
-
10
- import fs from "fs-extra";
11
- import path from "node:path";
12
- import { generateId } from "@downcity/agent/internal/utils/Id.js";
13
- import { getSessionRunContext } from "@downcity/agent/internal/executor/SessionRunScope.js";
14
- import type { AgentContext } from "@downcity/agent/internal/types/runtime/agent/AgentContext.js";
15
- import type { ShellApprovalStatus } from "@downcity/agent/internal/executor/tools/shell/types/ShellPlugin.js";
16
- import type { ShellPluginState } from "@/shell/ShellRuntimeTypes.js";
17
- import { nowMs } from "./ShellActionRuntimeSupport.js";
18
-
19
- const DANGEROUS_COMMAND_PATTERNS = [
20
- /\bsudo\b/,
21
- /\brm\s+-[^&|;\n]*r[^&|;\n]*f\s+\/(?:\s|$)/,
22
- /\bchmod\s+-R\s+777\s+\/(?:\s|$)/,
23
- /\bssh-keygen\b/,
24
- /\bsecurity\s+(?:add|delete|unlock|set|import|export)-/i,
25
- /(?:^|[\s;&|])(?:nohup\s+)?[^;&|\n]*(?:&)\s*$/,
26
- ];
27
-
28
- function isDangerousCommand(cmd: string): boolean {
29
- return DANGEROUS_COMMAND_PATTERNS.some((pattern) => pattern.test(cmd));
30
- }
31
-
32
- function resolveAuditPath(context: AgentContext): string {
33
- return path.join(context.rootPath, ".downcity", "logs", "unrestricted-sandbox-audit.jsonl");
34
- }
35
-
36
- async function appendAudit(params: {
37
- context: AgentContext;
38
- record: Record<string, unknown>;
39
- }): Promise<void> {
40
- const filePath = resolveAuditPath(params.context);
41
- await fs.ensureDir(path.dirname(filePath));
42
- await fs.appendFile(filePath, `${JSON.stringify(params.record)}\n`, "utf-8");
43
- }
44
-
45
- function publishApprovalResult(params: {
46
- context: AgentContext;
47
- ownerContextId?: string;
48
- approvalId: string;
49
- shellId: string;
50
- toolName: "shell_exec" | "shell_start";
51
- decision: ShellApprovalStatus;
52
- }): void {
53
- const sessionId = String(params.ownerContextId || "").trim();
54
- if (!sessionId) return;
55
- const turnId = String(getSessionRunContext()?.turnId || sessionId).trim();
56
- try {
57
- params.context.session.get(sessionId).publishEvent({
58
- type: "tool-approval-result",
59
- turnId,
60
- toolCallId: params.shellId,
61
- toolName: params.toolName,
62
- approvalId: params.approvalId,
63
- decision: params.decision,
64
- });
65
- } catch {
66
- // ignore event delivery failures
67
- }
68
- }
69
-
70
- /**
71
- * 校验 unrestricted sandbox 请求。
72
- */
73
- export function validateUnrestrictedRequest(params: {
74
- cmd: string;
75
- reason?: string;
76
- }): string | null {
77
- const reason = String(params.reason || "").trim();
78
- if (!reason) {
79
- return "unrestricted sandbox requires a non-empty reason";
80
- }
81
- if (isDangerousCommand(params.cmd)) {
82
- return "unrestricted sandbox rejected a dangerous command";
83
- }
84
- return null;
85
- }
86
-
87
- /**
88
- * 请求用户批准 unrestricted sandbox 执行。
89
- */
90
- export async function requestUnrestrictedApproval(params: {
91
- state: ShellPluginState;
92
- context: AgentContext;
93
- shellId: string;
94
- toolName: "shell_exec" | "shell_start";
95
- cmd: string;
96
- cwd: string;
97
- reason: string;
98
- ownerContextId?: string;
99
- }): Promise<{
100
- approvalId: string;
101
- status: ShellApprovalStatus;
102
- }> {
103
- const approvalId = `ap_${generateId()}`;
104
- const createdAt = nowMs();
105
- const ownerContextId = String(params.ownerContextId || "").trim() || undefined;
106
-
107
- const status = await new Promise<ShellApprovalStatus>((resolve) => {
108
- const timer = setTimeout(() => {
109
- resolveApproval({
110
- state: params.state,
111
- context: params.context,
112
- approvalId,
113
- decision: "expired",
114
- }).catch(() => undefined);
115
- }, params.state.options.defaultApprovalTimeoutMs);
116
- if (typeof timer.unref === "function") timer.unref();
117
-
118
- params.state.approvals.set(approvalId, {
119
- approvalId,
120
- shellId: params.shellId,
121
- ...(ownerContextId ? { ownerContextId } : {}),
122
- toolName: params.toolName,
123
- cmd: params.cmd,
124
- cwd: params.cwd,
125
- reason: params.reason,
126
- createdAt,
127
- timer,
128
- resolve,
129
- });
130
-
131
- if (ownerContextId) {
132
- const turnId = String(getSessionRunContext()?.turnId || ownerContextId).trim();
133
- try {
134
- params.context.session.get(ownerContextId).publishEvent({
135
- type: "tool-approval-request",
136
- turnId,
137
- toolCallId: params.shellId,
138
- toolName: params.toolName,
139
- approvalId,
140
- sandbox: "unrestricted",
141
- cmd: params.cmd,
142
- cwd: params.cwd,
143
- reason: params.reason,
144
- status: "pending",
145
- });
146
- } catch {
147
- // ignore event delivery failures
148
- }
149
- }
150
-
151
- appendAudit({
152
- context: params.context,
153
- record: {
154
- event: "approval_requested",
155
- approval_id: approvalId,
156
- session_id: ownerContextId || null,
157
- tool_call_id: params.shellId,
158
- agent_id: params.context.config?.id || null,
159
- cmd: params.cmd,
160
- cwd: params.cwd,
161
- reason: params.reason,
162
- created_at: new Date(createdAt).toISOString(),
163
- },
164
- }).catch(() => undefined);
165
- });
166
-
167
- return { approvalId, status };
168
- }
169
-
170
- /**
171
- * 兑现 unrestricted sandbox 审批。
172
- */
173
- export async function resolveApproval(params: {
174
- state: ShellPluginState;
175
- context: AgentContext;
176
- approvalId: string;
177
- decision: ShellApprovalStatus;
178
- }): Promise<boolean> {
179
- const approval = params.state.approvals.get(params.approvalId);
180
- if (!approval) return false;
181
- params.state.approvals.delete(params.approvalId);
182
- clearTimeout(approval.timer);
183
- approval.resolve(params.decision);
184
-
185
- publishApprovalResult({
186
- context: params.context,
187
- ownerContextId: approval.ownerContextId,
188
- approvalId: approval.approvalId,
189
- shellId: approval.shellId,
190
- toolName: approval.toolName,
191
- decision: params.decision,
192
- });
193
-
194
- await appendAudit({
195
- context: params.context,
196
- record: {
197
- event: "approval_resolved",
198
- approval_id: approval.approvalId,
199
- session_id: approval.ownerContextId || null,
200
- tool_call_id: approval.shellId,
201
- agent_id: params.context.config?.id || null,
202
- cmd: approval.cmd,
203
- cwd: approval.cwd,
204
- reason: approval.reason,
205
- decision: params.decision,
206
- resolved_at: new Date(nowMs()).toISOString(),
207
- },
208
- }).catch(() => undefined);
209
-
210
- return true;
211
- }
212
-
213
- /**
214
- * 列出 pending unrestricted sandbox 审批。
215
- */
216
- export function listPendingApprovals(state: ShellPluginState): Array<{
217
- approvalId: string;
218
- shellId: string;
219
- ownerContextId?: string;
220
- toolName: "shell_exec" | "shell_start";
221
- cmd: string;
222
- cwd: string;
223
- reason: string;
224
- createdAt: number;
225
- }> {
226
- return Array.from(state.approvals.values()).map((approval) => ({
227
- approvalId: approval.approvalId,
228
- shellId: approval.shellId,
229
- ...(approval.ownerContextId ? { ownerContextId: approval.ownerContextId } : {}),
230
- toolName: approval.toolName,
231
- cmd: approval.cmd,
232
- cwd: approval.cwd,
233
- reason: approval.reason,
234
- createdAt: approval.createdAt,
235
- }));
236
- }