@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.
- package/bin/BuiltinPlugins.d.ts +15 -0
- package/bin/BuiltinPlugins.d.ts.map +1 -1
- package/bin/BuiltinPlugins.js +7 -1
- package/bin/BuiltinPlugins.js.map +1 -1
- package/bin/index.d.ts +6 -0
- package/bin/index.d.ts.map +1 -1
- package/bin/index.js +3 -0
- package/bin/index.js.map +1 -1
- package/bin/memory/Action.d.ts +15 -10
- package/bin/memory/Action.d.ts.map +1 -1
- package/bin/memory/Action.js +233 -16
- package/bin/memory/Action.js.map +1 -1
- package/bin/memory/MemoryPlugin.d.ts +10 -4
- package/bin/memory/MemoryPlugin.d.ts.map +1 -1
- package/bin/memory/MemoryPlugin.js +79 -37
- package/bin/memory/MemoryPlugin.js.map +1 -1
- package/bin/memory/runtime/Search.d.ts +1 -1
- package/bin/memory/runtime/Search.d.ts.map +1 -1
- package/bin/memory/runtime/Search.js +11 -7
- package/bin/memory/runtime/Search.js.map +1 -1
- package/bin/memory/runtime/Store.d.ts +8 -23
- package/bin/memory/runtime/Store.d.ts.map +1 -1
- package/bin/memory/runtime/Store.js +28 -43
- package/bin/memory/runtime/Store.js.map +1 -1
- package/bin/memory/runtime/SystemProvider.d.ts +4 -8
- package/bin/memory/runtime/SystemProvider.d.ts.map +1 -1
- package/bin/memory/runtime/SystemProvider.js +55 -62
- package/bin/memory/runtime/SystemProvider.js.map +1 -1
- package/bin/memory/runtime/Writer.d.ts +48 -10
- package/bin/memory/runtime/Writer.d.ts.map +1 -1
- package/bin/memory/runtime/Writer.js +197 -60
- package/bin/memory/runtime/Writer.js.map +1 -1
- package/bin/memory/types/Memory.d.ts +222 -33
- package/bin/memory/types/Memory.d.ts.map +1 -1
- package/bin/memory/types/Memory.js +4 -3
- package/bin/memory/types/Memory.js.map +1 -1
- package/bin/shell/ShellPlugin.d.ts +2 -1
- package/bin/shell/ShellPlugin.d.ts.map +1 -1
- package/bin/shell/ShellPlugin.js +41 -4
- package/bin/shell/ShellPlugin.js.map +1 -1
- package/bin/shell/ShellRuntimeTypes.d.ts +57 -3
- package/bin/shell/ShellRuntimeTypes.d.ts.map +1 -1
- package/bin/shell/runtime/ShellActionRuntime.d.ts +21 -0
- package/bin/shell/runtime/ShellActionRuntime.d.ts.map +1 -1
- package/bin/shell/runtime/ShellActionRuntime.js +142 -6
- package/bin/shell/runtime/ShellActionRuntime.js.map +1 -1
- package/bin/shell/runtime/ShellActionRuntimeSupport.d.ts +14 -5
- package/bin/shell/runtime/ShellActionRuntimeSupport.d.ts.map +1 -1
- package/bin/shell/runtime/ShellActionRuntimeSupport.js +61 -22
- package/bin/shell/runtime/ShellActionRuntimeSupport.js.map +1 -1
- package/bin/shell/runtime/ShellApprovalRuntime.d.ts +57 -0
- package/bin/shell/runtime/ShellApprovalRuntime.d.ts.map +1 -0
- package/bin/shell/runtime/ShellApprovalRuntime.js +182 -0
- package/bin/shell/runtime/ShellApprovalRuntime.js.map +1 -0
- package/bin/shell/runtime/ShellProcessEvents.js +3 -3
- package/bin/shell/runtime/ShellProcessEvents.js.map +1 -1
- package/bin/shell/types/ShellPluginOptions.d.ts +103 -0
- package/bin/shell/types/ShellPluginOptions.d.ts.map +1 -0
- package/bin/shell/types/ShellPluginOptions.js +10 -0
- package/bin/shell/types/ShellPluginOptions.js.map +1 -0
- package/bin/task/Scheduler.d.ts +8 -0
- package/bin/task/Scheduler.d.ts.map +1 -1
- package/bin/task/Scheduler.js +7 -9
- package/bin/task/Scheduler.js.map +1 -1
- package/bin/task/TaskPlugin.d.ts +18 -1
- package/bin/task/TaskPlugin.d.ts.map +1 -1
- package/bin/task/TaskPlugin.js +23 -1
- package/bin/task/TaskPlugin.js.map +1 -1
- package/bin/task/types/TaskPluginOptions.d.ts +22 -0
- package/bin/task/types/TaskPluginOptions.d.ts.map +1 -0
- package/bin/task/types/TaskPluginOptions.js +9 -0
- package/bin/task/types/TaskPluginOptions.js.map +1 -0
- package/package.json +2 -2
- package/scripts/unrestricted-sandbox-approval.test.mjs +156 -0
- package/src/BuiltinPlugins.ts +27 -1
- package/src/index.ts +35 -0
- package/src/memory/Action.ts +292 -25
- package/src/memory/MemoryPlugin.ts +82 -40
- package/src/memory/runtime/Search.ts +16 -9
- package/src/memory/runtime/Store.ts +52 -49
- package/src/memory/runtime/SystemProvider.ts +55 -69
- package/src/memory/runtime/Writer.ts +262 -81
- package/src/memory/types/Memory.ts +296 -35
- package/src/shell/ShellPlugin.ts +44 -3
- package/src/shell/ShellRuntimeTypes.ts +61 -3
- package/src/shell/runtime/ShellActionRuntime.ts +182 -9
- package/src/shell/runtime/ShellActionRuntimeSupport.ts +112 -21
- package/src/shell/runtime/ShellApprovalRuntime.ts +236 -0
- package/src/shell/runtime/ShellProcessEvents.ts +3 -3
- package/src/shell/types/ShellPluginOptions.ts +122 -0
- package/src/task/Scheduler.ts +15 -9
- package/src/task/TaskPlugin.ts +27 -1
- package/src/task/types/TaskPluginOptions.ts +22 -0
- package/bin/memory/runtime/Flush.d.ts +0 -15
- package/bin/memory/runtime/Flush.d.ts.map +0 -1
- package/bin/memory/runtime/Flush.js +0 -63
- package/bin/memory/runtime/Flush.js.map +0 -1
- 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
|
-
|
|
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.
|
|
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 =
|
|
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
|
-
},
|
|
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 =
|
|
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
|
-
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
135
|
+
export function createShellPluginState(
|
|
136
|
+
options: ShellPluginOptions = {},
|
|
137
|
+
): ShellPluginState {
|
|
61
138
|
return {
|
|
139
|
+
options: resolveShellPluginOptions(options),
|
|
62
140
|
sessions: new Map<string, ShellSessionRuntimeState>(),
|
|
63
|
-
|
|
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(
|
|
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 >
|
|
234
|
-
const overflow = session.outputText.length -
|
|
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(-
|
|
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
|
-
},
|
|
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 <
|
|
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 <
|
|
369
|
+
if (state.sessions.size < state.options.maxActiveShells) break;
|
|
279
370
|
state.sessions.delete(item.snapshot.shellId);
|
|
280
371
|
}
|
|
281
|
-
if (state.sessions.size >=
|
|
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.
|
|
471
|
+
state.context
|
|
381
472
|
) {
|
|
382
|
-
await emitChatCompletionEvent(state.
|
|
473
|
+
await emitChatCompletionEvent(state.context, session.snapshot);
|
|
383
474
|
session.snapshot.notificationSent = true;
|
|
384
475
|
await persistSnapshot(session);
|
|
385
476
|
}
|