@downcity/plugins 1.0.61 → 1.0.66
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/shell/ShellPlugin.d.ts.map +1 -1
- package/bin/shell/ShellPlugin.js +38 -1
- package/bin/shell/ShellPlugin.js.map +1 -1
- package/bin/shell/ShellRuntimeTypes.d.ts +65 -1
- package/bin/shell/ShellRuntimeTypes.d.ts.map +1 -1
- package/bin/shell/runtime/ShellActionRuntime.d.ts +24 -0
- package/bin/shell/runtime/ShellActionRuntime.d.ts.map +1 -1
- package/bin/shell/runtime/ShellActionRuntime.js +197 -1
- package/bin/shell/runtime/ShellActionRuntime.js.map +1 -1
- package/bin/shell/runtime/ShellActionRuntimeSupport.d.ts.map +1 -1
- package/bin/shell/runtime/ShellActionRuntimeSupport.js +3 -0
- package/bin/shell/runtime/ShellActionRuntimeSupport.js.map +1 -1
- package/bin/shell/runtime/ShellApprovalRuntime.d.ts +62 -0
- package/bin/shell/runtime/ShellApprovalRuntime.d.ts.map +1 -0
- package/bin/shell/runtime/ShellApprovalRuntime.js +215 -0
- package/bin/shell/runtime/ShellApprovalRuntime.js.map +1 -0
- package/bin/shell/types/ShellPluginOptions.d.ts +8 -0
- package/bin/shell/types/ShellPluginOptions.d.ts.map +1 -1
- package/package.json +2 -2
- package/scripts/unrestricted-sandbox-approval.test.mjs +335 -0
- package/src/shell/ShellPlugin.ts +40 -0
- package/src/shell/ShellRuntimeTypes.ts +70 -1
- package/src/shell/runtime/ShellActionRuntime.ts +231 -1
- package/src/shell/runtime/ShellActionRuntimeSupport.ts +6 -0
- package/src/shell/runtime/ShellApprovalRuntime.ts +276 -0
- package/src/shell/types/ShellPluginOptions.ts +10 -0
|
@@ -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,
|
|
@@ -46,6 +47,12 @@ import {
|
|
|
46
47
|
updateSessionSnapshot,
|
|
47
48
|
} from "./ShellActionRuntimeSupport.js";
|
|
48
49
|
import { attachShellProcessEventHandlers } from "./ShellProcessEvents.js";
|
|
50
|
+
import {
|
|
51
|
+
listPendingApprovals,
|
|
52
|
+
requestUnrestrictedApproval,
|
|
53
|
+
resolveApproval,
|
|
54
|
+
validateUnrestrictedRequest,
|
|
55
|
+
} from "./ShellApprovalRuntime.js";
|
|
49
56
|
|
|
50
57
|
export { createShellPluginState } from "./ShellActionRuntimeSupport.js";
|
|
51
58
|
|
|
@@ -72,6 +79,20 @@ export async function closeAllShellSessions(
|
|
|
72
79
|
state: ShellPluginState,
|
|
73
80
|
force = false,
|
|
74
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
|
+
}
|
|
75
96
|
const closing = Array.from(state.sessions.values()).map(async (session) => {
|
|
76
97
|
if (
|
|
77
98
|
session.snapshot.status !== "running" &&
|
|
@@ -93,6 +114,95 @@ export async function closeAllShellSessions(
|
|
|
93
114
|
await Promise.all(closing);
|
|
94
115
|
}
|
|
95
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: true,
|
|
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
|
+
|
|
176
|
+
function buildDeniedWriteApprovalResponse(params: {
|
|
177
|
+
session: ShellSessionRuntimeState;
|
|
178
|
+
approvalId: string;
|
|
179
|
+
reason: string;
|
|
180
|
+
approvalStatus: ShellApprovalStatus;
|
|
181
|
+
}): ShellActionResponse {
|
|
182
|
+
const message = params.approvalStatus === "expired"
|
|
183
|
+
? "Unrestricted sandbox approval expired."
|
|
184
|
+
: "User denied unrestricted sandbox execution.";
|
|
185
|
+
return buildActionResponse({
|
|
186
|
+
shell: {
|
|
187
|
+
...params.session.snapshot,
|
|
188
|
+
approvalStatus: params.approvalStatus,
|
|
189
|
+
approvalId: params.approvalId,
|
|
190
|
+
approvalReason: params.reason,
|
|
191
|
+
stdinWritable: true,
|
|
192
|
+
},
|
|
193
|
+
chunk: {
|
|
194
|
+
shellId: params.session.snapshot.shellId,
|
|
195
|
+
output: message,
|
|
196
|
+
startCursor: 0,
|
|
197
|
+
endCursor: message.length,
|
|
198
|
+
originalChars: message.length,
|
|
199
|
+
originalLines: 1,
|
|
200
|
+
hasMoreOutput: false,
|
|
201
|
+
},
|
|
202
|
+
note: message,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
96
206
|
/**
|
|
97
207
|
* 启动一个 shell session。
|
|
98
208
|
*/
|
|
@@ -111,6 +221,8 @@ export async function startShellSession(
|
|
|
111
221
|
const shellPath =
|
|
112
222
|
String(request.shell || resolveDefaultShellPath()).trim() || resolveDefaultShellPath();
|
|
113
223
|
const login = request.login !== false;
|
|
224
|
+
const sandboxMode = resolveSandboxMode(request.sandbox);
|
|
225
|
+
const reason = String(request.reason || "").trim();
|
|
114
226
|
const ownerContextId = resolveOwnerContextId(request.ownerContextId);
|
|
115
227
|
const canAutoNotifyByContext = ownerContextId
|
|
116
228
|
? Boolean(
|
|
@@ -124,6 +236,37 @@ export async function startShellSession(
|
|
|
124
236
|
await fs.ensureDir(shellDir);
|
|
125
237
|
await fs.writeFile(outputFilePath, "", "utf-8");
|
|
126
238
|
|
|
239
|
+
let approvalId: string | undefined;
|
|
240
|
+
let approvalStatus: ShellApprovalStatus | undefined;
|
|
241
|
+
if (sandboxMode === "unrestricted") {
|
|
242
|
+
const validationError = validateUnrestrictedRequest({ cmd, reason });
|
|
243
|
+
if (validationError) throw new Error(validationError);
|
|
244
|
+
const approval = await requestUnrestrictedApproval({
|
|
245
|
+
state,
|
|
246
|
+
context,
|
|
247
|
+
shellId,
|
|
248
|
+
toolName: request.approvalToolName || "shell_start",
|
|
249
|
+
cmd,
|
|
250
|
+
cwd,
|
|
251
|
+
reason,
|
|
252
|
+
...(ownerContextId ? { ownerContextId } : {}),
|
|
253
|
+
});
|
|
254
|
+
approvalId = approval.approvalId;
|
|
255
|
+
approvalStatus = approval.status;
|
|
256
|
+
if (approval.status !== "approved") {
|
|
257
|
+
return buildDeniedApprovalResponse({
|
|
258
|
+
shellId,
|
|
259
|
+
...(ownerContextId ? { ownerContextId } : {}),
|
|
260
|
+
cmd,
|
|
261
|
+
cwd,
|
|
262
|
+
shellPath,
|
|
263
|
+
approvalId: approval.approvalId,
|
|
264
|
+
reason,
|
|
265
|
+
approvalStatus: approval.status,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
127
270
|
const spawnResult = await spawnShellProcess({
|
|
128
271
|
context,
|
|
129
272
|
shellId,
|
|
@@ -133,6 +276,7 @@ export async function startShellSession(
|
|
|
133
276
|
shellPath,
|
|
134
277
|
login,
|
|
135
278
|
baseEnv: buildShellEnv(context),
|
|
279
|
+
sandboxMode,
|
|
136
280
|
});
|
|
137
281
|
const child = spawnResult.child;
|
|
138
282
|
const actualCwd = spawnResult.cwd;
|
|
@@ -150,12 +294,17 @@ export async function startShellSession(
|
|
|
150
294
|
cwd: actualCwd,
|
|
151
295
|
shellPath,
|
|
152
296
|
sandboxed: spawnResult.sandboxed,
|
|
297
|
+
sandboxMode: spawnResult.sandboxMode || sandboxMode,
|
|
153
298
|
sandboxBackend: spawnResult.backend,
|
|
154
299
|
sandboxNetworkMode: spawnResult.networkMode,
|
|
155
300
|
sandboxDir: spawnResult.sandboxDir,
|
|
156
301
|
sandboxHomeDir: spawnResult.homeDir,
|
|
157
302
|
sandboxTmpDir: spawnResult.tmpDir,
|
|
158
303
|
sandboxCacheDir: spawnResult.cacheDir,
|
|
304
|
+
...(approvalStatus ? { approvalStatus } : {}),
|
|
305
|
+
...(approvalId ? { approvalId } : {}),
|
|
306
|
+
...(reason ? { approvalReason: reason } : {}),
|
|
307
|
+
stdinWritable: true,
|
|
159
308
|
status: "running",
|
|
160
309
|
...(typeof child.pid === "number" ? { pid: child.pid } : {}),
|
|
161
310
|
startedAt,
|
|
@@ -302,6 +451,39 @@ export async function writeShellSession(
|
|
|
302
451
|
if (!session.child.stdin.writable) {
|
|
303
452
|
throw new Error(`shell session ${shellId} stdin is closed`);
|
|
304
453
|
}
|
|
454
|
+
if (session.snapshot.stdinWritable === false) {
|
|
455
|
+
throw new Error(`shell session ${shellId} stdin is closed`);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
let approvalId: string | undefined;
|
|
459
|
+
let approvalStatus: ShellApprovalStatus | undefined;
|
|
460
|
+
const reason = String(request.reason || "").trim();
|
|
461
|
+
if (session.snapshot.sandboxMode === "unrestricted") {
|
|
462
|
+
const validationError = validateUnrestrictedRequest({ cmd: chars, reason });
|
|
463
|
+
if (validationError) throw new Error(validationError);
|
|
464
|
+
const approval = await requestUnrestrictedApproval({
|
|
465
|
+
state,
|
|
466
|
+
context,
|
|
467
|
+
shellId,
|
|
468
|
+
toolName: "shell_write",
|
|
469
|
+
cmd: chars,
|
|
470
|
+
cwd: session.snapshot.cwd,
|
|
471
|
+
reason,
|
|
472
|
+
...(session.snapshot.ownerContextId ? { ownerContextId: session.snapshot.ownerContextId } : {}),
|
|
473
|
+
inputPreview: chars,
|
|
474
|
+
inputChars: chars.length,
|
|
475
|
+
});
|
|
476
|
+
approvalId = approval.approvalId;
|
|
477
|
+
approvalStatus = approval.status;
|
|
478
|
+
if (approval.status !== "approved") {
|
|
479
|
+
return buildDeniedWriteApprovalResponse({
|
|
480
|
+
session,
|
|
481
|
+
approvalId: approval.approvalId,
|
|
482
|
+
reason,
|
|
483
|
+
approvalStatus: approval.status,
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
}
|
|
305
487
|
await new Promise<void>((resolve, reject) => {
|
|
306
488
|
session.child.stdin.write(chars, (error) => {
|
|
307
489
|
if (error) {
|
|
@@ -312,7 +494,13 @@ export async function writeShellSession(
|
|
|
312
494
|
});
|
|
313
495
|
});
|
|
314
496
|
return buildActionResponse({
|
|
315
|
-
shell:
|
|
497
|
+
shell: {
|
|
498
|
+
...session.snapshot,
|
|
499
|
+
...(approvalStatus ? { approvalStatus } : {}),
|
|
500
|
+
...(approvalId ? { approvalId } : {}),
|
|
501
|
+
...(reason ? { approvalReason: reason } : {}),
|
|
502
|
+
stdinWritable: true,
|
|
503
|
+
},
|
|
316
504
|
note: chars ? "stdin written" : "no chars written",
|
|
317
505
|
});
|
|
318
506
|
}
|
|
@@ -460,6 +648,9 @@ export async function execShellCommand(
|
|
|
460
648
|
...(request.cwd ? { cwd: request.cwd } : {}),
|
|
461
649
|
...(request.shell ? { shell: request.shell } : {}),
|
|
462
650
|
login: request.login,
|
|
651
|
+
sandbox: request.sandbox,
|
|
652
|
+
reason: request.reason,
|
|
653
|
+
approvalToolName: "shell_exec",
|
|
463
654
|
inlineWaitMs: Math.min(state.options.defaultInlineWaitMs, timeoutMs),
|
|
464
655
|
maxOutputTokens: request.maxOutputTokens,
|
|
465
656
|
autoNotifyOnExit: false,
|
|
@@ -562,3 +753,42 @@ export async function execShellCommand(
|
|
|
562
753
|
note: "shell exec completed in one-shot mode",
|
|
563
754
|
});
|
|
564
755
|
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* 列出 pending unrestricted sandbox 审批。
|
|
759
|
+
*/
|
|
760
|
+
export function listShellApprovals(state: ShellPluginState) {
|
|
761
|
+
return listPendingApprovals(state);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* 批准 pending unrestricted sandbox 审批。
|
|
766
|
+
*/
|
|
767
|
+
export async function approveShellApproval(
|
|
768
|
+
state: ShellPluginState,
|
|
769
|
+
context: AgentContext,
|
|
770
|
+
approvalId: string,
|
|
771
|
+
): Promise<boolean> {
|
|
772
|
+
return await resolveApproval({
|
|
773
|
+
state,
|
|
774
|
+
context,
|
|
775
|
+
approvalId,
|
|
776
|
+
decision: "approved",
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* 拒绝 pending unrestricted sandbox 审批。
|
|
782
|
+
*/
|
|
783
|
+
export async function denyShellApproval(
|
|
784
|
+
state: ShellPluginState,
|
|
785
|
+
context: AgentContext,
|
|
786
|
+
approvalId: string,
|
|
787
|
+
): Promise<boolean> {
|
|
788
|
+
return await resolveApproval({
|
|
789
|
+
state,
|
|
790
|
+
context,
|
|
791
|
+
approvalId,
|
|
792
|
+
decision: "denied",
|
|
793
|
+
});
|
|
794
|
+
}
|
|
@@ -46,6 +46,7 @@ const DEFAULT_SHELL_PLUGIN_OPTIONS: ResolvedShellPluginOptions = {
|
|
|
46
46
|
defaultInlineWaitMs: 1_200,
|
|
47
47
|
defaultWaitTimeoutMs: 10_000,
|
|
48
48
|
defaultExecTimeoutMs: 60_000,
|
|
49
|
+
defaultApprovalTimeoutMs: 120_000,
|
|
49
50
|
};
|
|
50
51
|
|
|
51
52
|
/**
|
|
@@ -121,6 +122,10 @@ export function resolveShellPluginOptions(
|
|
|
121
122
|
options.defaultExecTimeoutMs,
|
|
122
123
|
DEFAULT_SHELL_PLUGIN_OPTIONS.defaultExecTimeoutMs,
|
|
123
124
|
),
|
|
125
|
+
defaultApprovalTimeoutMs: readPositiveInteger(
|
|
126
|
+
options.defaultApprovalTimeoutMs,
|
|
127
|
+
DEFAULT_SHELL_PLUGIN_OPTIONS.defaultApprovalTimeoutMs,
|
|
128
|
+
),
|
|
124
129
|
};
|
|
125
130
|
}
|
|
126
131
|
|
|
@@ -133,6 +138,7 @@ export function createShellPluginState(
|
|
|
133
138
|
return {
|
|
134
139
|
options: resolveShellPluginOptions(options),
|
|
135
140
|
sessions: new Map<string, ShellSessionRuntimeState>(),
|
|
141
|
+
approvals: new Map(),
|
|
136
142
|
context: null,
|
|
137
143
|
};
|
|
138
144
|
}
|
|
@@ -0,0 +1,276 @@
|
|
|
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 启动,或单次 shell_write 输入。
|
|
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 {
|
|
16
|
+
ShellApprovalStatus,
|
|
17
|
+
ShellApprovalToolName,
|
|
18
|
+
} from "@downcity/agent/internal/executor/tools/shell/types/ShellPlugin.js";
|
|
19
|
+
import type { ShellPluginState } from "@/shell/ShellRuntimeTypes.js";
|
|
20
|
+
import { nowMs } from "./ShellActionRuntimeSupport.js";
|
|
21
|
+
|
|
22
|
+
const DANGEROUS_COMMAND_PATTERNS = [
|
|
23
|
+
/\bsudo\b/,
|
|
24
|
+
/\brm\s+-[^&|;\n]*r[^&|;\n]*f\s+\/(?:\s|$)/,
|
|
25
|
+
/\bchmod\s+-R\s+777\s+\/(?:\s|$)/,
|
|
26
|
+
/\bssh-keygen\b/,
|
|
27
|
+
/\bsecurity\s+(?:add|delete|unlock|set|import|export)-/i,
|
|
28
|
+
/(?:^|[\s;&|])(?:nohup\s+)?[^;&|\n]*(?:&)\s*$/,
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function isDangerousCommand(cmd: string): boolean {
|
|
32
|
+
return DANGEROUS_COMMAND_PATTERNS.some((pattern) => pattern.test(cmd));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function resolveApprovalOperation(toolName: ShellApprovalToolName): "exec" | "start" | "write" {
|
|
36
|
+
if (toolName === "shell_write") return "write";
|
|
37
|
+
if (toolName === "shell_exec") return "exec";
|
|
38
|
+
return "start";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function buildInputPreview(value: string): string {
|
|
42
|
+
const normalized = String(value || "");
|
|
43
|
+
if (normalized.length <= 240) return normalized;
|
|
44
|
+
return `${normalized.slice(0, 240)}...`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function resolveAuditPath(context: AgentContext): string {
|
|
48
|
+
return path.join(context.rootPath, ".downcity", "logs", "unrestricted-sandbox-audit.jsonl");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function appendAudit(params: {
|
|
52
|
+
context: AgentContext;
|
|
53
|
+
record: Record<string, unknown>;
|
|
54
|
+
}): Promise<void> {
|
|
55
|
+
const filePath = resolveAuditPath(params.context);
|
|
56
|
+
await fs.ensureDir(path.dirname(filePath));
|
|
57
|
+
await fs.appendFile(filePath, `${JSON.stringify(params.record)}\n`, "utf-8");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function publishApprovalResult(params: {
|
|
61
|
+
context: AgentContext;
|
|
62
|
+
ownerContextId?: string;
|
|
63
|
+
approvalId: string;
|
|
64
|
+
shellId: string;
|
|
65
|
+
toolName: ShellApprovalToolName;
|
|
66
|
+
decision: ShellApprovalStatus;
|
|
67
|
+
}): void {
|
|
68
|
+
const sessionId = String(params.ownerContextId || "").trim();
|
|
69
|
+
if (!sessionId) return;
|
|
70
|
+
const turnId = String(getSessionRunContext()?.turnId || sessionId).trim();
|
|
71
|
+
try {
|
|
72
|
+
params.context.session.get(sessionId).publishEvent({
|
|
73
|
+
type: "tool-approval-result",
|
|
74
|
+
turnId,
|
|
75
|
+
toolCallId: params.shellId,
|
|
76
|
+
toolName: params.toolName,
|
|
77
|
+
approvalId: params.approvalId,
|
|
78
|
+
decision: params.decision,
|
|
79
|
+
});
|
|
80
|
+
} catch {
|
|
81
|
+
// ignore event delivery failures
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 校验 unrestricted sandbox 请求。
|
|
87
|
+
*/
|
|
88
|
+
export function validateUnrestrictedRequest(params: {
|
|
89
|
+
cmd: string;
|
|
90
|
+
reason?: string;
|
|
91
|
+
}): string | null {
|
|
92
|
+
const reason = String(params.reason || "").trim();
|
|
93
|
+
if (!reason) {
|
|
94
|
+
return "unrestricted sandbox requires a non-empty reason";
|
|
95
|
+
}
|
|
96
|
+
if (isDangerousCommand(params.cmd)) {
|
|
97
|
+
return "unrestricted sandbox rejected a dangerous command";
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 请求用户批准 unrestricted sandbox 执行。
|
|
104
|
+
*/
|
|
105
|
+
export async function requestUnrestrictedApproval(params: {
|
|
106
|
+
state: ShellPluginState;
|
|
107
|
+
context: AgentContext;
|
|
108
|
+
shellId: string;
|
|
109
|
+
toolName: ShellApprovalToolName;
|
|
110
|
+
cmd: string;
|
|
111
|
+
cwd: string;
|
|
112
|
+
reason: string;
|
|
113
|
+
ownerContextId?: string;
|
|
114
|
+
inputPreview?: string;
|
|
115
|
+
inputChars?: number;
|
|
116
|
+
}): Promise<{
|
|
117
|
+
approvalId: string;
|
|
118
|
+
status: ShellApprovalStatus;
|
|
119
|
+
}> {
|
|
120
|
+
const approvalId = `ap_${generateId()}`;
|
|
121
|
+
const createdAt = nowMs();
|
|
122
|
+
const ownerContextId = String(params.ownerContextId || "").trim() || undefined;
|
|
123
|
+
const operation = resolveApprovalOperation(params.toolName);
|
|
124
|
+
const inputPreview = params.inputPreview !== undefined
|
|
125
|
+
? buildInputPreview(params.inputPreview)
|
|
126
|
+
: undefined;
|
|
127
|
+
|
|
128
|
+
const status = await new Promise<ShellApprovalStatus>((resolve) => {
|
|
129
|
+
const timer = setTimeout(() => {
|
|
130
|
+
resolveApproval({
|
|
131
|
+
state: params.state,
|
|
132
|
+
context: params.context,
|
|
133
|
+
approvalId,
|
|
134
|
+
decision: "expired",
|
|
135
|
+
}).catch(() => undefined);
|
|
136
|
+
}, params.state.options.defaultApprovalTimeoutMs);
|
|
137
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
138
|
+
|
|
139
|
+
params.state.approvals.set(approvalId, {
|
|
140
|
+
approvalId,
|
|
141
|
+
shellId: params.shellId,
|
|
142
|
+
...(ownerContextId ? { ownerContextId } : {}),
|
|
143
|
+
toolName: params.toolName,
|
|
144
|
+
cmd: params.cmd,
|
|
145
|
+
operation,
|
|
146
|
+
...(inputPreview !== undefined ? { inputPreview } : {}),
|
|
147
|
+
...(typeof params.inputChars === "number" ? { inputChars: params.inputChars } : {}),
|
|
148
|
+
cwd: params.cwd,
|
|
149
|
+
reason: params.reason,
|
|
150
|
+
createdAt,
|
|
151
|
+
timer,
|
|
152
|
+
resolve,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (ownerContextId) {
|
|
156
|
+
const turnId = String(getSessionRunContext()?.turnId || ownerContextId).trim();
|
|
157
|
+
try {
|
|
158
|
+
params.context.session.get(ownerContextId).publishEvent({
|
|
159
|
+
type: "tool-approval-request",
|
|
160
|
+
turnId,
|
|
161
|
+
toolCallId: params.shellId,
|
|
162
|
+
toolName: params.toolName,
|
|
163
|
+
approvalId,
|
|
164
|
+
sandbox: "unrestricted",
|
|
165
|
+
cmd: params.cmd,
|
|
166
|
+
cwd: params.cwd,
|
|
167
|
+
reason: params.reason,
|
|
168
|
+
status: "pending",
|
|
169
|
+
operation,
|
|
170
|
+
shellId: params.shellId,
|
|
171
|
+
...(inputPreview !== undefined ? { inputPreview } : {}),
|
|
172
|
+
...(typeof params.inputChars === "number" ? { inputChars: params.inputChars } : {}),
|
|
173
|
+
});
|
|
174
|
+
} catch {
|
|
175
|
+
// ignore event delivery failures
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
appendAudit({
|
|
180
|
+
context: params.context,
|
|
181
|
+
record: {
|
|
182
|
+
event: "approval_requested",
|
|
183
|
+
approval_id: approvalId,
|
|
184
|
+
session_id: ownerContextId || null,
|
|
185
|
+
tool_call_id: params.shellId,
|
|
186
|
+
agent_id: params.context.config?.id || null,
|
|
187
|
+
cmd: params.cmd,
|
|
188
|
+
operation,
|
|
189
|
+
...(inputPreview !== undefined ? { input_preview: inputPreview } : {}),
|
|
190
|
+
...(typeof params.inputChars === "number" ? { input_chars: params.inputChars } : {}),
|
|
191
|
+
cwd: params.cwd,
|
|
192
|
+
reason: params.reason,
|
|
193
|
+
created_at: new Date(createdAt).toISOString(),
|
|
194
|
+
},
|
|
195
|
+
}).catch(() => undefined);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
return { approvalId, status };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 兑现 unrestricted sandbox 审批。
|
|
203
|
+
*/
|
|
204
|
+
export async function resolveApproval(params: {
|
|
205
|
+
state: ShellPluginState;
|
|
206
|
+
context: AgentContext;
|
|
207
|
+
approvalId: string;
|
|
208
|
+
decision: ShellApprovalStatus;
|
|
209
|
+
}): Promise<boolean> {
|
|
210
|
+
const approval = params.state.approvals.get(params.approvalId);
|
|
211
|
+
if (!approval) return false;
|
|
212
|
+
params.state.approvals.delete(params.approvalId);
|
|
213
|
+
clearTimeout(approval.timer);
|
|
214
|
+
approval.resolve(params.decision);
|
|
215
|
+
|
|
216
|
+
publishApprovalResult({
|
|
217
|
+
context: params.context,
|
|
218
|
+
ownerContextId: approval.ownerContextId,
|
|
219
|
+
approvalId: approval.approvalId,
|
|
220
|
+
shellId: approval.shellId,
|
|
221
|
+
toolName: approval.toolName,
|
|
222
|
+
decision: params.decision,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
await appendAudit({
|
|
226
|
+
context: params.context,
|
|
227
|
+
record: {
|
|
228
|
+
event: "approval_resolved",
|
|
229
|
+
approval_id: approval.approvalId,
|
|
230
|
+
session_id: approval.ownerContextId || null,
|
|
231
|
+
tool_call_id: approval.shellId,
|
|
232
|
+
agent_id: params.context.config?.id || null,
|
|
233
|
+
cmd: approval.cmd,
|
|
234
|
+
operation: approval.operation,
|
|
235
|
+
...(approval.inputPreview !== undefined ? { input_preview: approval.inputPreview } : {}),
|
|
236
|
+
...(typeof approval.inputChars === "number" ? { input_chars: approval.inputChars } : {}),
|
|
237
|
+
cwd: approval.cwd,
|
|
238
|
+
reason: approval.reason,
|
|
239
|
+
decision: params.decision,
|
|
240
|
+
resolved_at: new Date(nowMs()).toISOString(),
|
|
241
|
+
},
|
|
242
|
+
}).catch(() => undefined);
|
|
243
|
+
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* 列出 pending unrestricted sandbox 审批。
|
|
249
|
+
*/
|
|
250
|
+
export function listPendingApprovals(state: ShellPluginState): Array<{
|
|
251
|
+
approvalId: string;
|
|
252
|
+
shellId: string;
|
|
253
|
+
ownerContextId?: string;
|
|
254
|
+
toolName: ShellApprovalToolName;
|
|
255
|
+
cmd: string;
|
|
256
|
+
operation: "exec" | "start" | "write";
|
|
257
|
+
inputPreview?: string;
|
|
258
|
+
inputChars?: number;
|
|
259
|
+
cwd: string;
|
|
260
|
+
reason: string;
|
|
261
|
+
createdAt: number;
|
|
262
|
+
}> {
|
|
263
|
+
return Array.from(state.approvals.values()).map((approval) => ({
|
|
264
|
+
approvalId: approval.approvalId,
|
|
265
|
+
shellId: approval.shellId,
|
|
266
|
+
...(approval.ownerContextId ? { ownerContextId: approval.ownerContextId } : {}),
|
|
267
|
+
toolName: approval.toolName,
|
|
268
|
+
cmd: approval.cmd,
|
|
269
|
+
operation: approval.operation,
|
|
270
|
+
...(approval.inputPreview !== undefined ? { inputPreview: approval.inputPreview } : {}),
|
|
271
|
+
...(typeof approval.inputChars === "number" ? { inputChars: approval.inputChars } : {}),
|
|
272
|
+
cwd: approval.cwd,
|
|
273
|
+
reason: approval.reason,
|
|
274
|
+
createdAt: approval.createdAt,
|
|
275
|
+
}));
|
|
276
|
+
}
|
|
@@ -59,6 +59,11 @@ export interface ShellPluginOptions {
|
|
|
59
59
|
* `shell.exec` 默认总超时,单位毫秒。
|
|
60
60
|
*/
|
|
61
61
|
defaultExecTimeoutMs?: number;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* unrestricted sandbox 审批默认超时时间,单位毫秒。
|
|
65
|
+
*/
|
|
66
|
+
defaultApprovalTimeoutMs?: number;
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
/**
|
|
@@ -109,4 +114,9 @@ export interface ResolvedShellPluginOptions {
|
|
|
109
114
|
* `shell.exec` 默认总超时,单位毫秒。
|
|
110
115
|
*/
|
|
111
116
|
defaultExecTimeoutMs: number;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* unrestricted sandbox 审批默认超时时间,单位毫秒。
|
|
120
|
+
*/
|
|
121
|
+
defaultApprovalTimeoutMs: number;
|
|
112
122
|
}
|