@downcity/plugins 1.0.61 → 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/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 +50 -1
- 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 +136 -0
- 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 +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/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 +156 -0
- package/src/shell/ShellPlugin.ts +40 -0
- package/src/shell/ShellRuntimeTypes.ts +54 -1
- package/src/shell/runtime/ShellActionRuntime.ts +164 -0
- package/src/shell/runtime/ShellActionRuntimeSupport.ts +6 -0
- package/src/shell/runtime/ShellApprovalRuntime.ts +236 -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,65 @@ 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: 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
|
+
|
|
96
176
|
/**
|
|
97
177
|
* 启动一个 shell session。
|
|
98
178
|
*/
|
|
@@ -111,6 +191,8 @@ export async function startShellSession(
|
|
|
111
191
|
const shellPath =
|
|
112
192
|
String(request.shell || resolveDefaultShellPath()).trim() || resolveDefaultShellPath();
|
|
113
193
|
const login = request.login !== false;
|
|
194
|
+
const sandboxMode = resolveSandboxMode(request.sandbox);
|
|
195
|
+
const reason = String(request.reason || "").trim();
|
|
114
196
|
const ownerContextId = resolveOwnerContextId(request.ownerContextId);
|
|
115
197
|
const canAutoNotifyByContext = ownerContextId
|
|
116
198
|
? Boolean(
|
|
@@ -124,6 +206,37 @@ export async function startShellSession(
|
|
|
124
206
|
await fs.ensureDir(shellDir);
|
|
125
207
|
await fs.writeFile(outputFilePath, "", "utf-8");
|
|
126
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
|
+
|
|
127
240
|
const spawnResult = await spawnShellProcess({
|
|
128
241
|
context,
|
|
129
242
|
shellId,
|
|
@@ -133,6 +246,7 @@ export async function startShellSession(
|
|
|
133
246
|
shellPath,
|
|
134
247
|
login,
|
|
135
248
|
baseEnv: buildShellEnv(context),
|
|
249
|
+
sandboxMode,
|
|
136
250
|
});
|
|
137
251
|
const child = spawnResult.child;
|
|
138
252
|
const actualCwd = spawnResult.cwd;
|
|
@@ -150,12 +264,17 @@ export async function startShellSession(
|
|
|
150
264
|
cwd: actualCwd,
|
|
151
265
|
shellPath,
|
|
152
266
|
sandboxed: spawnResult.sandboxed,
|
|
267
|
+
sandboxMode: spawnResult.sandboxMode || sandboxMode,
|
|
153
268
|
sandboxBackend: spawnResult.backend,
|
|
154
269
|
sandboxNetworkMode: spawnResult.networkMode,
|
|
155
270
|
sandboxDir: spawnResult.sandboxDir,
|
|
156
271
|
sandboxHomeDir: spawnResult.homeDir,
|
|
157
272
|
sandboxTmpDir: spawnResult.tmpDir,
|
|
158
273
|
sandboxCacheDir: spawnResult.cacheDir,
|
|
274
|
+
...(approvalStatus ? { approvalStatus } : {}),
|
|
275
|
+
...(approvalId ? { approvalId } : {}),
|
|
276
|
+
...(reason ? { approvalReason: reason } : {}),
|
|
277
|
+
stdinWritable: sandboxMode === "safe",
|
|
159
278
|
status: "running",
|
|
160
279
|
...(typeof child.pid === "number" ? { pid: child.pid } : {}),
|
|
161
280
|
startedAt,
|
|
@@ -302,6 +421,9 @@ export async function writeShellSession(
|
|
|
302
421
|
if (!session.child.stdin.writable) {
|
|
303
422
|
throw new Error(`shell session ${shellId} stdin is closed`);
|
|
304
423
|
}
|
|
424
|
+
if (session.snapshot.stdinWritable === false) {
|
|
425
|
+
throw new Error(`shell session ${shellId} does not allow stdin writes`);
|
|
426
|
+
}
|
|
305
427
|
await new Promise<void>((resolve, reject) => {
|
|
306
428
|
session.child.stdin.write(chars, (error) => {
|
|
307
429
|
if (error) {
|
|
@@ -460,6 +582,9 @@ export async function execShellCommand(
|
|
|
460
582
|
...(request.cwd ? { cwd: request.cwd } : {}),
|
|
461
583
|
...(request.shell ? { shell: request.shell } : {}),
|
|
462
584
|
login: request.login,
|
|
585
|
+
sandbox: request.sandbox,
|
|
586
|
+
reason: request.reason,
|
|
587
|
+
approvalToolName: "shell_exec",
|
|
463
588
|
inlineWaitMs: Math.min(state.options.defaultInlineWaitMs, timeoutMs),
|
|
464
589
|
maxOutputTokens: request.maxOutputTokens,
|
|
465
590
|
autoNotifyOnExit: false,
|
|
@@ -562,3 +687,42 @@ export async function execShellCommand(
|
|
|
562
687
|
note: "shell exec completed in one-shot mode",
|
|
563
688
|
});
|
|
564
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
|
+
}
|
|
@@ -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,236 @@
|
|
|
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
|
+
}
|
|
@@ -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
|
}
|