@downcity/plugins 1.0.60 → 1.0.64

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 (98) hide show
  1. package/bin/BuiltinPlugins.d.ts +15 -0
  2. package/bin/BuiltinPlugins.d.ts.map +1 -1
  3. package/bin/BuiltinPlugins.js +7 -1
  4. package/bin/BuiltinPlugins.js.map +1 -1
  5. package/bin/index.d.ts +6 -0
  6. package/bin/index.d.ts.map +1 -1
  7. package/bin/index.js +3 -0
  8. package/bin/index.js.map +1 -1
  9. package/bin/memory/Action.d.ts +15 -10
  10. package/bin/memory/Action.d.ts.map +1 -1
  11. package/bin/memory/Action.js +233 -16
  12. package/bin/memory/Action.js.map +1 -1
  13. package/bin/memory/MemoryPlugin.d.ts +10 -4
  14. package/bin/memory/MemoryPlugin.d.ts.map +1 -1
  15. package/bin/memory/MemoryPlugin.js +79 -37
  16. package/bin/memory/MemoryPlugin.js.map +1 -1
  17. package/bin/memory/runtime/Search.d.ts +1 -1
  18. package/bin/memory/runtime/Search.d.ts.map +1 -1
  19. package/bin/memory/runtime/Search.js +11 -7
  20. package/bin/memory/runtime/Search.js.map +1 -1
  21. package/bin/memory/runtime/Store.d.ts +8 -23
  22. package/bin/memory/runtime/Store.d.ts.map +1 -1
  23. package/bin/memory/runtime/Store.js +28 -43
  24. package/bin/memory/runtime/Store.js.map +1 -1
  25. package/bin/memory/runtime/SystemProvider.d.ts +4 -8
  26. package/bin/memory/runtime/SystemProvider.d.ts.map +1 -1
  27. package/bin/memory/runtime/SystemProvider.js +55 -62
  28. package/bin/memory/runtime/SystemProvider.js.map +1 -1
  29. package/bin/memory/runtime/Writer.d.ts +48 -10
  30. package/bin/memory/runtime/Writer.d.ts.map +1 -1
  31. package/bin/memory/runtime/Writer.js +197 -60
  32. package/bin/memory/runtime/Writer.js.map +1 -1
  33. package/bin/memory/types/Memory.d.ts +222 -33
  34. package/bin/memory/types/Memory.d.ts.map +1 -1
  35. package/bin/memory/types/Memory.js +4 -3
  36. package/bin/memory/types/Memory.js.map +1 -1
  37. package/bin/shell/ShellPlugin.d.ts +2 -1
  38. package/bin/shell/ShellPlugin.d.ts.map +1 -1
  39. package/bin/shell/ShellPlugin.js +41 -4
  40. package/bin/shell/ShellPlugin.js.map +1 -1
  41. package/bin/shell/ShellRuntimeTypes.d.ts +57 -3
  42. package/bin/shell/ShellRuntimeTypes.d.ts.map +1 -1
  43. package/bin/shell/runtime/ShellActionRuntime.d.ts +21 -0
  44. package/bin/shell/runtime/ShellActionRuntime.d.ts.map +1 -1
  45. package/bin/shell/runtime/ShellActionRuntime.js +142 -6
  46. package/bin/shell/runtime/ShellActionRuntime.js.map +1 -1
  47. package/bin/shell/runtime/ShellActionRuntimeSupport.d.ts +14 -5
  48. package/bin/shell/runtime/ShellActionRuntimeSupport.d.ts.map +1 -1
  49. package/bin/shell/runtime/ShellActionRuntimeSupport.js +61 -22
  50. package/bin/shell/runtime/ShellActionRuntimeSupport.js.map +1 -1
  51. package/bin/shell/runtime/ShellApprovalRuntime.d.ts +57 -0
  52. package/bin/shell/runtime/ShellApprovalRuntime.d.ts.map +1 -0
  53. package/bin/shell/runtime/ShellApprovalRuntime.js +182 -0
  54. package/bin/shell/runtime/ShellApprovalRuntime.js.map +1 -0
  55. package/bin/shell/runtime/ShellProcessEvents.js +3 -3
  56. package/bin/shell/runtime/ShellProcessEvents.js.map +1 -1
  57. package/bin/shell/types/ShellPluginOptions.d.ts +103 -0
  58. package/bin/shell/types/ShellPluginOptions.d.ts.map +1 -0
  59. package/bin/shell/types/ShellPluginOptions.js +10 -0
  60. package/bin/shell/types/ShellPluginOptions.js.map +1 -0
  61. package/bin/task/Scheduler.d.ts +8 -0
  62. package/bin/task/Scheduler.d.ts.map +1 -1
  63. package/bin/task/Scheduler.js +7 -9
  64. package/bin/task/Scheduler.js.map +1 -1
  65. package/bin/task/TaskPlugin.d.ts +18 -1
  66. package/bin/task/TaskPlugin.d.ts.map +1 -1
  67. package/bin/task/TaskPlugin.js +23 -1
  68. package/bin/task/TaskPlugin.js.map +1 -1
  69. package/bin/task/types/TaskPluginOptions.d.ts +22 -0
  70. package/bin/task/types/TaskPluginOptions.d.ts.map +1 -0
  71. package/bin/task/types/TaskPluginOptions.js +9 -0
  72. package/bin/task/types/TaskPluginOptions.js.map +1 -0
  73. package/package.json +2 -2
  74. package/scripts/unrestricted-sandbox-approval.test.mjs +156 -0
  75. package/src/BuiltinPlugins.ts +27 -1
  76. package/src/index.ts +35 -0
  77. package/src/memory/Action.ts +292 -25
  78. package/src/memory/MemoryPlugin.ts +82 -40
  79. package/src/memory/runtime/Search.ts +16 -9
  80. package/src/memory/runtime/Store.ts +52 -49
  81. package/src/memory/runtime/SystemProvider.ts +55 -69
  82. package/src/memory/runtime/Writer.ts +262 -81
  83. package/src/memory/types/Memory.ts +296 -35
  84. package/src/shell/ShellPlugin.ts +44 -3
  85. package/src/shell/ShellRuntimeTypes.ts +61 -3
  86. package/src/shell/runtime/ShellActionRuntime.ts +182 -9
  87. package/src/shell/runtime/ShellActionRuntimeSupport.ts +112 -21
  88. package/src/shell/runtime/ShellApprovalRuntime.ts +236 -0
  89. package/src/shell/runtime/ShellProcessEvents.ts +3 -3
  90. package/src/shell/types/ShellPluginOptions.ts +122 -0
  91. package/src/task/Scheduler.ts +15 -9
  92. package/src/task/TaskPlugin.ts +27 -1
  93. package/src/task/types/TaskPluginOptions.ts +22 -0
  94. package/bin/memory/runtime/Flush.d.ts +0 -15
  95. package/bin/memory/runtime/Flush.d.ts.map +0 -1
  96. package/bin/memory/runtime/Flush.js +0 -63
  97. package/bin/memory/runtime/Flush.js.map +0 -1
  98. package/src/memory/runtime/Flush.ts +0 -83
@@ -19,6 +19,7 @@ import { generateId } from "@downcity/agent/internal/utils/Id.js";
19
19
  import { readChatMetaBySessionId } from "@/chat/runtime/ChatMetaStore.js";
20
20
  import type {
21
21
  ShellActionResponse,
22
+ ShellApprovalStatus,
22
23
  ShellCloseRequest,
23
24
  ShellExecRequest,
24
25
  ShellQueryRequest,
@@ -31,12 +32,9 @@ import { getShellDir, getShellOutputPath, getShellSnapshotPath } from "./Paths.j
31
32
  import {
32
33
  buildActionResponse,
33
34
  buildShellEnv,
34
- clampWaitMs,
35
+ clampWaitMsWithOptions,
35
36
  createOutputChunk,
36
37
  createShellPluginState,
37
- DEFAULT_EXEC_TIMEOUT_MS,
38
- DEFAULT_INLINE_WAIT_MS,
39
- DEFAULT_WAIT_TIMEOUT_MS,
40
38
  ensureCapacity,
41
39
  isInMemorySession,
42
40
  isTerminalStatus,
@@ -49,6 +47,12 @@ import {
49
47
  updateSessionSnapshot,
50
48
  } from "./ShellActionRuntimeSupport.js";
51
49
  import { attachShellProcessEventHandlers } from "./ShellProcessEvents.js";
50
+ import {
51
+ listPendingApprovals,
52
+ requestUnrestrictedApproval,
53
+ resolveApproval,
54
+ validateUnrestrictedRequest,
55
+ } from "./ShellApprovalRuntime.js";
52
56
 
53
57
  export { createShellPluginState } from "./ShellActionRuntimeSupport.js";
54
58
 
@@ -65,7 +69,7 @@ export function bindShellRuntime(
65
69
  state: ShellPluginState,
66
70
  context: AgentContext,
67
71
  ): void {
68
- state.boundRuntime = context;
72
+ state.context = context;
69
73
  }
70
74
 
71
75
  /**
@@ -75,6 +79,20 @@ export async function closeAllShellSessions(
75
79
  state: ShellPluginState,
76
80
  force = false,
77
81
  ): Promise<void> {
82
+ for (const approval of Array.from(state.approvals.values())) {
83
+ if (state.context) {
84
+ await resolveApproval({
85
+ state,
86
+ context: state.context,
87
+ approvalId: approval.approvalId,
88
+ decision: "expired",
89
+ }).catch(() => undefined);
90
+ continue;
91
+ }
92
+ clearTimeout(approval.timer);
93
+ state.approvals.delete(approval.approvalId);
94
+ approval.resolve("expired");
95
+ }
78
96
  const closing = Array.from(state.sessions.values()).map(async (session) => {
79
97
  if (
80
98
  session.snapshot.status !== "running" &&
@@ -96,6 +114,65 @@ export async function closeAllShellSessions(
96
114
  await Promise.all(closing);
97
115
  }
98
116
 
117
+ function resolveSandboxMode(value: unknown): "safe" | "unrestricted" {
118
+ return value === "unrestricted" ? "unrestricted" : "safe";
119
+ }
120
+
121
+ function buildDeniedApprovalResponse(params: {
122
+ shellId: string;
123
+ ownerContextId?: string;
124
+ cmd: string;
125
+ cwd: string;
126
+ shellPath: string;
127
+ approvalId: string;
128
+ reason: string;
129
+ approvalStatus: ShellApprovalStatus;
130
+ }): ShellActionResponse {
131
+ const now = nowMs();
132
+ const message = params.approvalStatus === "expired"
133
+ ? "Unrestricted sandbox approval expired."
134
+ : "User denied unrestricted sandbox execution.";
135
+ return buildActionResponse({
136
+ shell: {
137
+ shellId: params.shellId,
138
+ ...(params.ownerContextId ? { ownerContextId: params.ownerContextId } : {}),
139
+ cmd: params.cmd,
140
+ cwd: params.cwd,
141
+ shellPath: params.shellPath,
142
+ sandboxed: false,
143
+ sandboxMode: "unrestricted",
144
+ sandboxBackend: "unrestricted-host",
145
+ sandboxNetworkMode: "full",
146
+ approvalStatus: params.approvalStatus,
147
+ approvalId: params.approvalId,
148
+ approvalReason: params.reason,
149
+ stdinWritable: false,
150
+ status: params.approvalStatus === "expired" ? "expired" : "failed",
151
+ startedAt: now,
152
+ updatedAt: now,
153
+ endedAt: now,
154
+ exitCode: -1,
155
+ lastOutputPreview: message,
156
+ outputChars: message.length,
157
+ droppedChars: 0,
158
+ version: 1,
159
+ autoNotifyOnExit: false,
160
+ notificationSent: false,
161
+ externalRefs: [],
162
+ },
163
+ chunk: {
164
+ shellId: params.shellId,
165
+ output: message,
166
+ startCursor: 0,
167
+ endCursor: message.length,
168
+ originalChars: message.length,
169
+ originalLines: 1,
170
+ hasMoreOutput: false,
171
+ },
172
+ note: message,
173
+ });
174
+ }
175
+
99
176
  /**
100
177
  * 启动一个 shell session。
101
178
  */
@@ -114,6 +191,8 @@ export async function startShellSession(
114
191
  const shellPath =
115
192
  String(request.shell || resolveDefaultShellPath()).trim() || resolveDefaultShellPath();
116
193
  const login = request.login !== false;
194
+ const sandboxMode = resolveSandboxMode(request.sandbox);
195
+ const reason = String(request.reason || "").trim();
117
196
  const ownerContextId = resolveOwnerContextId(request.ownerContextId);
118
197
  const canAutoNotifyByContext = ownerContextId
119
198
  ? Boolean(
@@ -127,6 +206,37 @@ export async function startShellSession(
127
206
  await fs.ensureDir(shellDir);
128
207
  await fs.writeFile(outputFilePath, "", "utf-8");
129
208
 
209
+ let approvalId: string | undefined;
210
+ let approvalStatus: ShellApprovalStatus | undefined;
211
+ if (sandboxMode === "unrestricted") {
212
+ const validationError = validateUnrestrictedRequest({ cmd, reason });
213
+ if (validationError) throw new Error(validationError);
214
+ const approval = await requestUnrestrictedApproval({
215
+ state,
216
+ context,
217
+ shellId,
218
+ toolName: request.approvalToolName || "shell_start",
219
+ cmd,
220
+ cwd,
221
+ reason,
222
+ ...(ownerContextId ? { ownerContextId } : {}),
223
+ });
224
+ approvalId = approval.approvalId;
225
+ approvalStatus = approval.status;
226
+ if (approval.status !== "approved") {
227
+ return buildDeniedApprovalResponse({
228
+ shellId,
229
+ ...(ownerContextId ? { ownerContextId } : {}),
230
+ cmd,
231
+ cwd,
232
+ shellPath,
233
+ approvalId: approval.approvalId,
234
+ reason,
235
+ approvalStatus: approval.status,
236
+ });
237
+ }
238
+ }
239
+
130
240
  const spawnResult = await spawnShellProcess({
131
241
  context,
132
242
  shellId,
@@ -136,6 +246,7 @@ export async function startShellSession(
136
246
  shellPath,
137
247
  login,
138
248
  baseEnv: buildShellEnv(context),
249
+ sandboxMode,
139
250
  });
140
251
  const child = spawnResult.child;
141
252
  const actualCwd = spawnResult.cwd;
@@ -153,12 +264,17 @@ export async function startShellSession(
153
264
  cwd: actualCwd,
154
265
  shellPath,
155
266
  sandboxed: spawnResult.sandboxed,
267
+ sandboxMode: spawnResult.sandboxMode || sandboxMode,
156
268
  sandboxBackend: spawnResult.backend,
157
269
  sandboxNetworkMode: spawnResult.networkMode,
158
270
  sandboxDir: spawnResult.sandboxDir,
159
271
  sandboxHomeDir: spawnResult.homeDir,
160
272
  sandboxTmpDir: spawnResult.tmpDir,
161
273
  sandboxCacheDir: spawnResult.cacheDir,
274
+ ...(approvalStatus ? { approvalStatus } : {}),
275
+ ...(approvalId ? { approvalId } : {}),
276
+ ...(reason ? { approvalReason: reason } : {}),
277
+ stdinWritable: sandboxMode === "safe",
162
278
  status: "running",
163
279
  ...(typeof child.pid === "number" ? { pid: child.pid } : {}),
164
280
  startedAt,
@@ -189,7 +305,11 @@ export async function startShellSession(
189
305
  attachShellProcessEventHandlers({ state, session });
190
306
  await persistSnapshot(session);
191
307
 
192
- const inlineWaitMs = clampWaitMs(request.inlineWaitMs, DEFAULT_INLINE_WAIT_MS);
308
+ const inlineWaitMs = clampWaitMsWithOptions(
309
+ state.options,
310
+ request.inlineWaitMs,
311
+ state.options.defaultInlineWaitMs,
312
+ );
193
313
  await waitShellSession(state, context, {
194
314
  shellId,
195
315
  afterVersion: 1,
@@ -301,6 +421,9 @@ export async function writeShellSession(
301
421
  if (!session.child.stdin.writable) {
302
422
  throw new Error(`shell session ${shellId} stdin is closed`);
303
423
  }
424
+ if (session.snapshot.stdinWritable === false) {
425
+ throw new Error(`shell session ${shellId} does not allow stdin writes`);
426
+ }
304
427
  await new Promise<void>((resolve, reject) => {
305
428
  session.child.stdin.write(chars, (error) => {
306
429
  if (error) {
@@ -351,7 +474,11 @@ export async function waitShellSession(
351
474
  };
352
475
  const timer = setTimeout(() => {
353
476
  finish();
354
- }, clampWaitMs(request.timeoutMs, DEFAULT_WAIT_TIMEOUT_MS));
477
+ }, clampWaitMsWithOptions(
478
+ state.options,
479
+ request.timeoutMs,
480
+ state.options.defaultWaitTimeoutMs,
481
+ ));
355
482
  waiter = {
356
483
  resolve: finish,
357
484
  timer,
@@ -445,13 +572,20 @@ export async function execShellCommand(
445
572
  context: AgentContext,
446
573
  request: ShellExecRequest,
447
574
  ): Promise<ShellActionResponse> {
448
- const timeoutMs = clampWaitMs(request.timeoutMs, DEFAULT_EXEC_TIMEOUT_MS);
575
+ const timeoutMs = clampWaitMsWithOptions(
576
+ state.options,
577
+ request.timeoutMs,
578
+ state.options.defaultExecTimeoutMs,
579
+ );
449
580
  const started = await startShellSession(state, context, {
450
581
  cmd: request.cmd,
451
582
  ...(request.cwd ? { cwd: request.cwd } : {}),
452
583
  ...(request.shell ? { shell: request.shell } : {}),
453
584
  login: request.login,
454
- inlineWaitMs: Math.min(DEFAULT_INLINE_WAIT_MS, timeoutMs),
585
+ sandbox: request.sandbox,
586
+ reason: request.reason,
587
+ approvalToolName: "shell_exec",
588
+ inlineWaitMs: Math.min(state.options.defaultInlineWaitMs, timeoutMs),
455
589
  maxOutputTokens: request.maxOutputTokens,
456
590
  autoNotifyOnExit: false,
457
591
  });
@@ -553,3 +687,42 @@ export async function execShellCommand(
553
687
  note: "shell exec completed in one-shot mode",
554
688
  });
555
689
  }
690
+
691
+ /**
692
+ * 列出 pending unrestricted sandbox 审批。
693
+ */
694
+ export function listShellApprovals(state: ShellPluginState) {
695
+ return listPendingApprovals(state);
696
+ }
697
+
698
+ /**
699
+ * 批准 pending unrestricted sandbox 审批。
700
+ */
701
+ export async function approveShellApproval(
702
+ state: ShellPluginState,
703
+ context: AgentContext,
704
+ approvalId: string,
705
+ ): Promise<boolean> {
706
+ return await resolveApproval({
707
+ state,
708
+ context,
709
+ approvalId,
710
+ decision: "approved",
711
+ });
712
+ }
713
+
714
+ /**
715
+ * 拒绝 pending unrestricted sandbox 审批。
716
+ */
717
+ export async function denyShellApproval(
718
+ state: ShellPluginState,
719
+ context: AgentContext,
720
+ approvalId: string,
721
+ ): Promise<boolean> {
722
+ return await resolveApproval({
723
+ state,
724
+ context,
725
+ approvalId,
726
+ decision: "denied",
727
+ });
728
+ }
@@ -13,6 +13,10 @@ import type {
13
13
  ShellPluginState,
14
14
  ShellSessionRuntimeState,
15
15
  } from "@/shell/ShellRuntimeTypes.js";
16
+ import type {
17
+ ResolvedShellPluginOptions,
18
+ ShellPluginOptions,
19
+ } from "@/shell/types/ShellPluginOptions.js";
16
20
  import type {
17
21
  ShellQueryRequest,
18
22
  ShellSessionSnapshot,
@@ -32,35 +36,110 @@ export {
32
36
  createOutputChunk,
33
37
  } from "./ShellActionResponse.js";
34
38
 
35
- const MAX_ACTIVE_SHELLS = 64;
36
- const SESSION_CLEANUP_DELAY_MS = 10 * 60 * 1000;
37
- const MAX_IN_MEMORY_OUTPUT_CHARS = 1_000_000;
38
- const MIN_WAIT_MS = 50;
39
- const MAX_WAIT_MS = 30_000;
40
- const OUTPUT_PREVIEW_CHARS = 280;
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
+ };
41
51
 
42
52
  /**
43
53
  * shell.start 默认内联等待时间。
44
54
  */
45
- export const DEFAULT_INLINE_WAIT_MS = 1_200;
55
+ export const DEFAULT_INLINE_WAIT_MS = DEFAULT_SHELL_PLUGIN_OPTIONS.defaultInlineWaitMs;
46
56
 
47
57
  /**
48
58
  * shell.wait 默认等待超时。
49
59
  */
50
- export const DEFAULT_WAIT_TIMEOUT_MS = 10_000;
60
+ export const DEFAULT_WAIT_TIMEOUT_MS = DEFAULT_SHELL_PLUGIN_OPTIONS.defaultWaitTimeoutMs;
51
61
 
52
62
  /**
53
63
  * shell.exec 默认总超时。
54
64
  */
55
- export const DEFAULT_EXEC_TIMEOUT_MS = 60_000;
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
+ }
56
131
 
57
132
  /**
58
133
  * 创建 shell plugin runtime 初始状态。
59
134
  */
60
- export function createShellPluginState(): ShellPluginState {
135
+ export function createShellPluginState(
136
+ options: ShellPluginOptions = {},
137
+ ): ShellPluginState {
61
138
  return {
139
+ options: resolveShellPluginOptions(options),
62
140
  sessions: new Map<string, ShellSessionRuntimeState>(),
63
- boundRuntime: null,
141
+ approvals: new Map(),
142
+ context: null,
64
143
  };
65
144
  }
66
145
 
@@ -75,11 +154,22 @@ export function nowMs(): number {
75
154
  * 归一化 wait/timeout 参数。
76
155
  */
77
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 {
78
168
  const raw =
79
169
  typeof value === "number" && Number.isFinite(value)
80
170
  ? Math.floor(value)
81
171
  : fallback;
82
- return Math.min(MAX_WAIT_MS, Math.max(MIN_WAIT_MS, raw));
172
+ return Math.min(options.maxWaitMs, Math.max(options.minWaitMs, raw));
83
173
  }
84
174
 
85
175
  function normalizeOutputChunk(raw: string): string {
@@ -223,6 +313,7 @@ export async function updateSessionSnapshot(
223
313
  * 追加 shell 输出并同步更新快照。
224
314
  */
225
315
  export async function appendSessionOutput(
316
+ state: ShellPluginState,
226
317
  session: ShellSessionRuntimeState,
227
318
  raw: string,
228
319
  ): Promise<void> {
@@ -230,8 +321,8 @@ export async function appendSessionOutput(
230
321
  if (!text) return;
231
322
 
232
323
  session.outputText += text;
233
- if (session.outputText.length > MAX_IN_MEMORY_OUTPUT_CHARS) {
234
- const overflow = session.outputText.length - MAX_IN_MEMORY_OUTPUT_CHARS;
324
+ if (session.outputText.length > state.options.maxInMemoryOutputChars) {
325
+ const overflow = session.outputText.length - state.options.maxInMemoryOutputChars;
235
326
  session.outputText = session.outputText.slice(overflow);
236
327
  session.snapshot.droppedChars += overflow;
237
328
  }
@@ -239,7 +330,7 @@ export async function appendSessionOutput(
239
330
  session.snapshot.outputChars += text.length;
240
331
  session.snapshot.lastOutputAt = nowMs();
241
332
  session.snapshot.lastOutputPreview = session.outputText
242
- .slice(-OUTPUT_PREVIEW_CHARS)
333
+ .slice(-state.options.outputPreviewChars)
243
334
  .trim();
244
335
  session.snapshot.externalRefs = extractExternalRefsFromText(
245
336
  text,
@@ -260,7 +351,7 @@ export function scheduleCleanup(state: ShellPluginState, shellId: string): void
260
351
  const current = state.sessions.get(shellId);
261
352
  if (!current) return;
262
353
  state.sessions.delete(shellId);
263
- }, SESSION_CLEANUP_DELAY_MS);
354
+ }, state.options.cleanupDelayMs);
264
355
  if (typeof session.cleanupTimer.unref === "function") {
265
356
  session.cleanupTimer.unref();
266
357
  }
@@ -270,15 +361,15 @@ export function scheduleCleanup(state: ShellPluginState, shellId: string): void
270
361
  * 控制 in-memory shell session 容量。
271
362
  */
272
363
  export function ensureCapacity(state: ShellPluginState): void {
273
- if (state.sessions.size < MAX_ACTIVE_SHELLS) return;
364
+ if (state.sessions.size < state.options.maxActiveShells) return;
274
365
  const removable = Array.from(state.sessions.values())
275
366
  .filter((item) => item.snapshot.status !== "running" && item.snapshot.status !== "starting")
276
367
  .sort((a, b) => a.snapshot.updatedAt - b.snapshot.updatedAt);
277
368
  for (const item of removable) {
278
- if (state.sessions.size < MAX_ACTIVE_SHELLS) break;
369
+ if (state.sessions.size < state.options.maxActiveShells) break;
279
370
  state.sessions.delete(item.snapshot.shellId);
280
371
  }
281
- if (state.sessions.size >= MAX_ACTIVE_SHELLS) {
372
+ if (state.sessions.size >= state.options.maxActiveShells) {
282
373
  throw new Error(
283
374
  `Too many active shell sessions (${state.sessions.size}). Please close or wait older sessions first.`,
284
375
  );
@@ -377,9 +468,9 @@ export async function finalizeExit(
377
468
  if (
378
469
  session.snapshot.autoNotifyOnExit &&
379
470
  session.snapshot.notificationSent === false &&
380
- state.boundRuntime
471
+ state.context
381
472
  ) {
382
- await emitChatCompletionEvent(state.boundRuntime, session.snapshot);
473
+ await emitChatCompletionEvent(state.context, session.snapshot);
383
474
  session.snapshot.notificationSent = true;
384
475
  await persistSnapshot(session);
385
476
  }