@downcity/agent 1.1.86 → 1.1.92
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/config/AgentInitializer.d.ts.map +1 -1
- package/bin/config/AgentInitializer.js +2 -1
- package/bin/config/AgentInitializer.js.map +1 -1
- package/bin/config/Paths.d.ts +8 -0
- package/bin/config/Paths.d.ts.map +1 -1
- package/bin/config/Paths.js +10 -0
- package/bin/config/Paths.js.map +1 -1
- package/bin/executor/Executor.d.ts.map +1 -1
- package/bin/executor/Executor.js +3 -0
- package/bin/executor/Executor.js.map +1 -1
- package/bin/executor/messages/AssistantFileResource.d.ts +31 -0
- package/bin/executor/messages/AssistantFileResource.d.ts.map +1 -0
- package/bin/executor/messages/AssistantFileResource.js +113 -0
- package/bin/executor/messages/AssistantFileResource.js.map +1 -0
- package/bin/executor/messages/SessionAttachmentMapper.d.ts +8 -0
- package/bin/executor/messages/SessionAttachmentMapper.d.ts.map +1 -1
- package/bin/executor/messages/SessionAttachmentMapper.js +54 -0
- package/bin/executor/messages/SessionAttachmentMapper.js.map +1 -1
- package/bin/executor/messages/SessionMessageCodec.d.ts.map +1 -1
- package/bin/executor/messages/SessionMessageCodec.js +5 -3
- package/bin/executor/messages/SessionMessageCodec.js.map +1 -1
- package/bin/executor/tools/plugin/PluginToolBridge.d.ts.map +1 -1
- package/bin/executor/tools/plugin/PluginToolBridge.js +9 -2
- package/bin/executor/tools/plugin/PluginToolBridge.js.map +1 -1
- package/bin/index.d.ts +1 -1
- package/bin/index.d.ts.map +1 -1
- package/bin/index.js.map +1 -1
- package/bin/plugin/core/ImagePlugin.d.ts +62 -0
- package/bin/plugin/core/ImagePlugin.d.ts.map +1 -1
- package/bin/plugin/core/ImagePlugin.js +230 -7
- package/bin/plugin/core/ImagePlugin.js.map +1 -1
- package/bin/sandbox/LinuxBubblewrapSandbox.d.ts +21 -0
- package/bin/sandbox/LinuxBubblewrapSandbox.d.ts.map +1 -0
- package/bin/sandbox/LinuxBubblewrapSandbox.js +184 -0
- package/bin/sandbox/LinuxBubblewrapSandbox.js.map +1 -0
- package/bin/sandbox/SandboxConfigResolver.d.ts +5 -0
- package/bin/sandbox/SandboxConfigResolver.d.ts.map +1 -1
- package/bin/sandbox/SandboxConfigResolver.js +11 -4
- package/bin/sandbox/SandboxConfigResolver.js.map +1 -1
- package/bin/sandbox/SandboxPreflight.d.ts +73 -0
- package/bin/sandbox/SandboxPreflight.d.ts.map +1 -0
- package/bin/sandbox/SandboxPreflight.js +122 -0
- package/bin/sandbox/SandboxPreflight.js.map +1 -0
- package/bin/sandbox/SandboxRunner.d.ts +1 -1
- package/bin/sandbox/SandboxRunner.d.ts.map +1 -1
- package/bin/sandbox/SandboxRunner.js +11 -3
- package/bin/sandbox/SandboxRunner.js.map +1 -1
- package/bin/sandbox/types/SandboxRuntime.d.ts +6 -2
- package/bin/sandbox/types/SandboxRuntime.d.ts.map +1 -1
- package/bin/session/Session.d.ts.map +1 -1
- package/bin/session/Session.js +1 -0
- package/bin/session/Session.js.map +1 -1
- package/bin/session/services/SessionTurnService.d.ts +5 -0
- package/bin/session/services/SessionTurnService.d.ts.map +1 -1
- package/bin/session/services/SessionTurnService.js +3 -0
- package/bin/session/services/SessionTurnService.js.map +1 -1
- package/bin/types/executor/SessionRunContext.d.ts +8 -0
- package/bin/types/executor/SessionRunContext.d.ts.map +1 -1
- package/bin/types/plugin/ImagePlugin.d.ts +79 -2
- package/bin/types/plugin/ImagePlugin.d.ts.map +1 -1
- package/package.json +2 -2
- package/scripts/assistant-file-resource.test.mjs +91 -0
- package/scripts/image-plugin-job.test.mjs +155 -0
- package/scripts/linux-bubblewrap-sandbox.test.mjs +142 -0
- package/scripts/shell-sandbox-preflight.test.mjs +88 -0
- package/src/config/AgentInitializer.ts +2 -0
- package/src/config/Paths.ts +11 -0
- package/src/executor/Executor.ts +3 -0
- package/src/executor/messages/AssistantFileResource.ts +155 -0
- package/src/executor/messages/SessionAttachmentMapper.ts +59 -0
- package/src/executor/messages/SessionMessageCodec.ts +9 -3
- package/src/executor/tools/plugin/PluginToolBridge.ts +13 -2
- package/src/index.ts +4 -0
- package/src/plugin/core/ImagePlugin.ts +284 -7
- package/src/sandbox/LinuxBubblewrapSandbox.ts +229 -0
- package/src/sandbox/SandboxConfigResolver.ts +13 -7
- package/src/sandbox/SandboxPreflight.ts +205 -0
- package/src/sandbox/SandboxRunner.ts +11 -3
- package/src/sandbox/types/SandboxRuntime.ts +7 -2
- package/src/session/Session.ts +1 -0
- package/src/session/services/SessionTurnService.ts +8 -0
- package/src/types/executor/SessionRunContext.ts +9 -0
- package/src/types/plugin/ImagePlugin.ts +79 -2
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linux Bubblewrap sandbox backend。
|
|
3
|
+
*
|
|
4
|
+
* 关键点(中文)
|
|
5
|
+
* - 基于 `bwrap` 提供 Linux 本机 shell sandbox。
|
|
6
|
+
* - 继续保持“shell 命令必须进入 sandbox”的安全语义,不提供宿主机裸跑回退。
|
|
7
|
+
* - 边界与 macOS backend 对齐:路径、环境变量、网络、隔离 HOME/TMPDIR。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { spawn } from "node:child_process";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import fs from "fs-extra";
|
|
13
|
+
import type {
|
|
14
|
+
SandboxSpawnParams,
|
|
15
|
+
SandboxSpawnResult,
|
|
16
|
+
} from "@/sandbox/types/SandboxRuntime.js";
|
|
17
|
+
|
|
18
|
+
const DEFAULT_PATH_VALUE =
|
|
19
|
+
"/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin";
|
|
20
|
+
|
|
21
|
+
function dedupeExistingPaths(values: string[]): string[] {
|
|
22
|
+
const seen = new Set<string>();
|
|
23
|
+
const result: string[] = [];
|
|
24
|
+
for (const value of values) {
|
|
25
|
+
const normalized = path.resolve(String(value || "").trim());
|
|
26
|
+
if (!normalized || seen.has(normalized)) continue;
|
|
27
|
+
if (!fs.existsSync(normalized)) continue;
|
|
28
|
+
seen.add(normalized);
|
|
29
|
+
result.push(normalized);
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function buildReadablePaths(params: {
|
|
35
|
+
rootPath: string;
|
|
36
|
+
shellPath: string;
|
|
37
|
+
shellHomeDir: string;
|
|
38
|
+
shellTmpDir: string;
|
|
39
|
+
}): string[] {
|
|
40
|
+
return dedupeExistingPaths([
|
|
41
|
+
"/usr",
|
|
42
|
+
"/bin",
|
|
43
|
+
"/sbin",
|
|
44
|
+
"/lib",
|
|
45
|
+
"/lib64",
|
|
46
|
+
"/etc",
|
|
47
|
+
params.rootPath,
|
|
48
|
+
params.shellHomeDir,
|
|
49
|
+
params.shellTmpDir,
|
|
50
|
+
path.dirname(params.shellPath),
|
|
51
|
+
]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function buildWritablePaths(params: SandboxSpawnParams & {
|
|
55
|
+
shellHomeDir: string;
|
|
56
|
+
shellTmpDir: string;
|
|
57
|
+
}): string[] {
|
|
58
|
+
return dedupeExistingPaths([
|
|
59
|
+
...params.config.writablePaths,
|
|
60
|
+
params.shellDir,
|
|
61
|
+
params.shellHomeDir,
|
|
62
|
+
params.shellTmpDir,
|
|
63
|
+
]);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isPathCoveredBy(paths: string[], targetPath: string): boolean {
|
|
67
|
+
const normalizedTarget = path.resolve(targetPath);
|
|
68
|
+
return paths.some((value) => {
|
|
69
|
+
const normalizedValue = path.resolve(value);
|
|
70
|
+
if (normalizedValue === normalizedTarget) return true;
|
|
71
|
+
const relative = path.relative(normalizedValue, normalizedTarget);
|
|
72
|
+
return Boolean(relative) && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function buildSandboxEnv(params: SandboxSpawnParams & {
|
|
77
|
+
shellHomeDir: string;
|
|
78
|
+
shellTmpDir: string;
|
|
79
|
+
}): NodeJS.ProcessEnv {
|
|
80
|
+
const env: NodeJS.ProcessEnv = {};
|
|
81
|
+
for (const key of params.config.envAllowlist) {
|
|
82
|
+
const value = params.baseEnv[key];
|
|
83
|
+
if (typeof value !== "string" || !value.trim()) continue;
|
|
84
|
+
env[key] = value;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (const [key, value] of Object.entries(params.baseEnv)) {
|
|
88
|
+
if (!key.startsWith("DC_")) continue;
|
|
89
|
+
if (typeof value !== "string" || !value.trim()) continue;
|
|
90
|
+
env[key] = value;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
env.PATH = String(env.PATH || params.baseEnv.PATH || DEFAULT_PATH_VALUE);
|
|
94
|
+
env.HOME = params.shellHomeDir;
|
|
95
|
+
env.TMPDIR = params.shellTmpDir;
|
|
96
|
+
env.SHELL = params.shellPath;
|
|
97
|
+
|
|
98
|
+
return env;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function addReadOnlyBind(args: string[], sourcePath: string): void {
|
|
102
|
+
args.push("--ro-bind", sourcePath, sourcePath);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function addWritableBind(args: string[], sourcePath: string): void {
|
|
106
|
+
args.push("--bind", sourcePath, sourcePath);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function addParentDirs(args: string[], targetPath: string, createdDirs: Set<string>): void {
|
|
110
|
+
const parts = path.resolve(targetPath).split(path.sep).filter(Boolean);
|
|
111
|
+
let current = "";
|
|
112
|
+
for (let index = 0; index < parts.length - 1; index += 1) {
|
|
113
|
+
current = `${current}/${parts[index]}`;
|
|
114
|
+
if (createdDirs.has(current)) continue;
|
|
115
|
+
createdDirs.add(current);
|
|
116
|
+
args.push("--dir", current);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function buildLinuxBubblewrapArgs(params: SandboxSpawnParams & {
|
|
121
|
+
actualCwd: string;
|
|
122
|
+
shellHomeDir: string;
|
|
123
|
+
shellTmpDir: string;
|
|
124
|
+
}): string[] {
|
|
125
|
+
const readablePaths = buildReadablePaths({
|
|
126
|
+
rootPath: params.config.rootPath,
|
|
127
|
+
shellPath: params.shellPath,
|
|
128
|
+
shellHomeDir: params.shellHomeDir,
|
|
129
|
+
shellTmpDir: params.shellTmpDir,
|
|
130
|
+
});
|
|
131
|
+
const writablePaths = buildWritablePaths({
|
|
132
|
+
...params,
|
|
133
|
+
shellHomeDir: params.shellHomeDir,
|
|
134
|
+
shellTmpDir: params.shellTmpDir,
|
|
135
|
+
});
|
|
136
|
+
const writableSet = new Set(writablePaths);
|
|
137
|
+
const createdDirs = new Set<string>();
|
|
138
|
+
const mountedPaths: string[] = [];
|
|
139
|
+
const args = [
|
|
140
|
+
"--die-with-parent",
|
|
141
|
+
"--unshare-pid",
|
|
142
|
+
"--proc",
|
|
143
|
+
"/proc",
|
|
144
|
+
"--dev",
|
|
145
|
+
"/dev",
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
if (params.config.networkMode === "off") {
|
|
149
|
+
args.push("--unshare-net");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for (const readablePath of readablePaths) {
|
|
153
|
+
if (writableSet.has(readablePath)) continue;
|
|
154
|
+
if (!isPathCoveredBy(mountedPaths, readablePath)) {
|
|
155
|
+
addParentDirs(args, readablePath, createdDirs);
|
|
156
|
+
}
|
|
157
|
+
addReadOnlyBind(args, readablePath);
|
|
158
|
+
mountedPaths.push(readablePath);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
for (const writablePath of writablePaths) {
|
|
162
|
+
if (!isPathCoveredBy(mountedPaths, writablePath)) {
|
|
163
|
+
addParentDirs(args, writablePath, createdDirs);
|
|
164
|
+
}
|
|
165
|
+
addWritableBind(args, writablePath);
|
|
166
|
+
mountedPaths.push(writablePath);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (
|
|
170
|
+
!isPathCoveredBy(readablePaths, params.actualCwd) &&
|
|
171
|
+
!isPathCoveredBy(writablePaths, params.actualCwd)
|
|
172
|
+
) {
|
|
173
|
+
if (!isPathCoveredBy(mountedPaths, params.actualCwd)) {
|
|
174
|
+
addParentDirs(args, params.actualCwd, createdDirs);
|
|
175
|
+
}
|
|
176
|
+
addReadOnlyBind(args, params.actualCwd);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
args.push(
|
|
180
|
+
"--chdir",
|
|
181
|
+
params.actualCwd,
|
|
182
|
+
params.shellPath,
|
|
183
|
+
params.login ? "-lc" : "-c",
|
|
184
|
+
params.cmd,
|
|
185
|
+
);
|
|
186
|
+
return args;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 在 Linux bubblewrap sandbox 中启动 shell 子进程。
|
|
191
|
+
*/
|
|
192
|
+
export async function spawnLinuxBubblewrapSandbox(
|
|
193
|
+
params: SandboxSpawnParams & { actualCwd: string },
|
|
194
|
+
): Promise<SandboxSpawnResult> {
|
|
195
|
+
const sandboxRootDir = path.join(params.shellDir, "sandbox");
|
|
196
|
+
const shellHomeDir = path.join(sandboxRootDir, "home");
|
|
197
|
+
const shellTmpDir = path.join(sandboxRootDir, "tmp");
|
|
198
|
+
|
|
199
|
+
await fs.ensureDir(shellHomeDir);
|
|
200
|
+
await fs.ensureDir(shellTmpDir);
|
|
201
|
+
for (const writablePath of params.config.writablePaths) {
|
|
202
|
+
await fs.ensureDir(writablePath);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const child = spawn("bwrap", buildLinuxBubblewrapArgs({
|
|
206
|
+
...params,
|
|
207
|
+
shellHomeDir,
|
|
208
|
+
shellTmpDir,
|
|
209
|
+
}), {
|
|
210
|
+
cwd: params.actualCwd,
|
|
211
|
+
stdio: "pipe",
|
|
212
|
+
env: buildSandboxEnv({
|
|
213
|
+
...params,
|
|
214
|
+
shellHomeDir,
|
|
215
|
+
shellTmpDir,
|
|
216
|
+
}),
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
child.stdout.setEncoding("utf8");
|
|
220
|
+
child.stderr.setEncoding("utf8");
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
child,
|
|
224
|
+
cwd: params.actualCwd,
|
|
225
|
+
sandboxed: true,
|
|
226
|
+
backend: "linux-bubblewrap",
|
|
227
|
+
networkMode: params.config.networkMode,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import type { AgentContext } from "@/types/runtime/agent/AgentContext.js";
|
|
12
|
+
import type { SandboxBackend } from "@/sandbox/types/SandboxRuntime.js";
|
|
12
13
|
import type { ResolvedSandboxConfig } from "@/sandbox/types/SandboxRuntime.js";
|
|
13
14
|
|
|
14
15
|
const DEFAULT_ENV_ALLOWLIST = [
|
|
@@ -84,6 +85,17 @@ function normalizeWritablePaths(params: {
|
|
|
84
85
|
return result;
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
/**
|
|
89
|
+
* 根据宿主平台解析当前 sandbox backend。
|
|
90
|
+
*/
|
|
91
|
+
export function resolveSandboxBackend(): SandboxBackend {
|
|
92
|
+
if (process.platform === "darwin") return "macos-seatbelt";
|
|
93
|
+
if (process.platform === "linux") return "linux-bubblewrap";
|
|
94
|
+
throw new Error(
|
|
95
|
+
`sandbox backend is required for shell execution, but current platform is unsupported: ${process.platform}`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
87
99
|
/**
|
|
88
100
|
* 解析当前请求最终使用的 sandbox 配置。
|
|
89
101
|
*/
|
|
@@ -91,14 +103,8 @@ export function resolveSandboxConfig(context: AgentContext): ResolvedSandboxConf
|
|
|
91
103
|
const rootPath = path.resolve(context.rootPath);
|
|
92
104
|
const projectConfig = context.config?.sandbox;
|
|
93
105
|
|
|
94
|
-
if (process.platform !== "darwin") {
|
|
95
|
-
throw new Error(
|
|
96
|
-
`sandbox backend is required for shell execution, but current platform is unsupported: ${process.platform}`,
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
106
|
return {
|
|
101
|
-
backend:
|
|
107
|
+
backend: resolveSandboxBackend(),
|
|
102
108
|
rootPath,
|
|
103
109
|
envAllowlist: normalizeEnvAllowlist(projectConfig?.envAllowlist),
|
|
104
110
|
writablePaths: normalizeWritablePaths({
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SandboxPreflight:本机 shell sandbox 依赖预检。
|
|
3
|
+
*
|
|
4
|
+
* 关键点(中文)
|
|
5
|
+
* - shell 命令必须进入 sandbox;这里提前检查 backend 依赖,避免启动后首次 shell 执行才失败。
|
|
6
|
+
* - Linux backend 基于 bubblewrap,本质使用 Linux namespaces / bind mount 等内核能力。
|
|
7
|
+
* - 本模块只诊断并给出修复建议,不自动安装软件,也不修改宿主机 sysctl。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { access, readFile } from "node:fs/promises";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { delimiter } from "node:path";
|
|
13
|
+
import type { SandboxBackend } from "@/sandbox/types/SandboxRuntime.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* sandbox 预检失败原因。
|
|
17
|
+
*/
|
|
18
|
+
export type SandboxPreflightIssueCode =
|
|
19
|
+
| "unsupported-platform"
|
|
20
|
+
| "missing-command"
|
|
21
|
+
| "userns-disabled";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 单条 sandbox 预检失败。
|
|
25
|
+
*/
|
|
26
|
+
export interface SandboxPreflightIssue {
|
|
27
|
+
/**
|
|
28
|
+
* 机器可读的失败原因。
|
|
29
|
+
*/
|
|
30
|
+
code: SandboxPreflightIssueCode;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 人类可读的失败说明。
|
|
34
|
+
*/
|
|
35
|
+
message: string;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 可复制的修复建议列表。
|
|
39
|
+
*/
|
|
40
|
+
fixes: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* sandbox 预检结果。
|
|
45
|
+
*/
|
|
46
|
+
export interface SandboxPreflightResult {
|
|
47
|
+
/**
|
|
48
|
+
* 当前平台是否满足 shell sandbox 启动要求。
|
|
49
|
+
*/
|
|
50
|
+
ok: boolean;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 当前宿主平台。
|
|
54
|
+
*/
|
|
55
|
+
platform: NodeJS.Platform;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 当前平台对应的 sandbox backend。
|
|
59
|
+
*/
|
|
60
|
+
backend?: SandboxBackend;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 失败原因集合。
|
|
64
|
+
*/
|
|
65
|
+
issues: SandboxPreflightIssue[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* sandbox 预检宿主探测依赖。
|
|
70
|
+
*/
|
|
71
|
+
export interface ShellSandboxPreflightProbe {
|
|
72
|
+
/**
|
|
73
|
+
* 判断命令是否存在于 PATH 中。
|
|
74
|
+
*/
|
|
75
|
+
commandExists(command: string): Promise<boolean>;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 读取 `/proc` 下整数配置。
|
|
79
|
+
*/
|
|
80
|
+
readProcInt(filePath: string): Promise<number | null>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function commandExists(command: string): Promise<boolean> {
|
|
84
|
+
const pathValue = String(process.env.PATH || "").trim();
|
|
85
|
+
const dirs = pathValue ? pathValue.split(delimiter) : [];
|
|
86
|
+
for (const dir of dirs) {
|
|
87
|
+
const candidate = path.join(dir, command);
|
|
88
|
+
try {
|
|
89
|
+
await access(candidate);
|
|
90
|
+
return true;
|
|
91
|
+
} catch {
|
|
92
|
+
// continue
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function readProcInt(filePath: string): Promise<number | null> {
|
|
99
|
+
try {
|
|
100
|
+
const raw = await readFile(filePath, "utf-8");
|
|
101
|
+
const value = Number.parseInt(raw.trim(), 10);
|
|
102
|
+
return Number.isFinite(value) && !Number.isNaN(value) ? value : null;
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function isLinuxUserNamespaceEnabled(
|
|
109
|
+
probe: ShellSandboxPreflightProbe,
|
|
110
|
+
): Promise<boolean> {
|
|
111
|
+
const unprivilegedUsernsClone = await probe.readProcInt(
|
|
112
|
+
"/proc/sys/kernel/unprivileged_userns_clone",
|
|
113
|
+
);
|
|
114
|
+
if (unprivilegedUsernsClone === 0) return false;
|
|
115
|
+
|
|
116
|
+
const maxUserNamespaces = await probe.readProcInt("/proc/sys/user/max_user_namespaces");
|
|
117
|
+
if (maxUserNamespaces === 0) return false;
|
|
118
|
+
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 检查当前宿主是否满足 shell sandbox 运行要求。
|
|
124
|
+
*/
|
|
125
|
+
export async function checkShellSandboxPreflight(): Promise<SandboxPreflightResult> {
|
|
126
|
+
return await checkShellSandboxPreflightWithProbe({
|
|
127
|
+
commandExists,
|
|
128
|
+
readProcInt,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 使用注入探针检查当前宿主是否满足 shell sandbox 运行要求。
|
|
134
|
+
*/
|
|
135
|
+
export async function checkShellSandboxPreflightWithProbe(
|
|
136
|
+
probe: ShellSandboxPreflightProbe,
|
|
137
|
+
): Promise<SandboxPreflightResult> {
|
|
138
|
+
const platform = process.platform;
|
|
139
|
+
const issues: SandboxPreflightIssue[] = [];
|
|
140
|
+
|
|
141
|
+
if (platform === "darwin") {
|
|
142
|
+
if (!(await probe.commandExists("sandbox-exec"))) {
|
|
143
|
+
issues.push({
|
|
144
|
+
code: "missing-command",
|
|
145
|
+
message: "macOS shell sandbox requires sandbox-exec, but it was not found.",
|
|
146
|
+
fixes: [
|
|
147
|
+
"Use a macOS system that includes /usr/bin/sandbox-exec.",
|
|
148
|
+
],
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
ok: issues.length === 0,
|
|
153
|
+
platform,
|
|
154
|
+
backend: "macos-seatbelt",
|
|
155
|
+
issues,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (platform === "linux") {
|
|
160
|
+
if (!(await probe.commandExists("bwrap"))) {
|
|
161
|
+
issues.push({
|
|
162
|
+
code: "missing-command",
|
|
163
|
+
message: "Linux shell sandbox requires bubblewrap (bwrap), but it was not found.",
|
|
164
|
+
fixes: [
|
|
165
|
+
"Debian / Ubuntu: sudo apt install bubblewrap",
|
|
166
|
+
"Fedora: sudo dnf install bubblewrap",
|
|
167
|
+
"Arch: sudo pacman -S bubblewrap",
|
|
168
|
+
],
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!(await isLinuxUserNamespaceEnabled(probe))) {
|
|
173
|
+
issues.push({
|
|
174
|
+
code: "userns-disabled",
|
|
175
|
+
message: "Linux user namespaces are disabled, so bubblewrap cannot create the sandbox.",
|
|
176
|
+
fixes: [
|
|
177
|
+
"Check: cat /proc/sys/kernel/unprivileged_userns_clone",
|
|
178
|
+
"Check: cat /proc/sys/user/max_user_namespaces",
|
|
179
|
+
"Debian / Ubuntu: sudo sysctl kernel.unprivileged_userns_clone=1",
|
|
180
|
+
],
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
ok: issues.length === 0,
|
|
186
|
+
platform,
|
|
187
|
+
backend: "linux-bubblewrap",
|
|
188
|
+
issues,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
ok: false,
|
|
194
|
+
platform,
|
|
195
|
+
issues: [
|
|
196
|
+
{
|
|
197
|
+
code: "unsupported-platform",
|
|
198
|
+
message: `Shell sandbox is not supported on this platform: ${platform}.`,
|
|
199
|
+
fixes: [
|
|
200
|
+
"Use macOS or Linux for local shell execution.",
|
|
201
|
+
],
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
};
|
|
205
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 关键点(中文)
|
|
5
5
|
* - 这里不实现完整的 session/read/write 协议,只负责 shell 子进程创建时统一进入 sandbox backend。
|
|
6
|
-
* -
|
|
6
|
+
* - 当前版本接入 macOS seatbelt 与 Linux bubblewrap backend。
|
|
7
7
|
* - shell 命令不再允许回退到宿主机普通子进程执行。
|
|
8
8
|
*/
|
|
9
9
|
|
|
@@ -11,6 +11,7 @@ import type { AgentContext } from "@/types/runtime/agent/AgentContext.js";
|
|
|
11
11
|
import type { SandboxSpawnResult } from "@/sandbox/types/SandboxRuntime.js";
|
|
12
12
|
import { resolveSandboxConfig, resolveSandboxCwd } from "@/sandbox/SandboxConfigResolver.js";
|
|
13
13
|
import { spawnMacOsSeatbeltSandbox } from "@/sandbox/MacOsSeatbeltSandbox.js";
|
|
14
|
+
import { spawnLinuxBubblewrapSandbox } from "@/sandbox/LinuxBubblewrapSandbox.js";
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* 启动 shell 子进程。
|
|
@@ -31,7 +32,7 @@ export async function spawnShellProcess(params: {
|
|
|
31
32
|
requestedCwd: params.cwd,
|
|
32
33
|
context: params.context,
|
|
33
34
|
});
|
|
34
|
-
|
|
35
|
+
const spawnParams = {
|
|
35
36
|
shellId: params.shellId,
|
|
36
37
|
shellDir: params.shellDir,
|
|
37
38
|
cmd: params.cmd,
|
|
@@ -41,7 +42,14 @@ export async function spawnShellProcess(params: {
|
|
|
41
42
|
baseEnv: params.baseEnv,
|
|
42
43
|
config,
|
|
43
44
|
actualCwd,
|
|
44
|
-
}
|
|
45
|
+
};
|
|
46
|
+
if (config.backend === "macos-seatbelt") {
|
|
47
|
+
return spawnMacOsSeatbeltSandbox(spawnParams);
|
|
48
|
+
}
|
|
49
|
+
if (config.backend === "linux-bubblewrap") {
|
|
50
|
+
return spawnLinuxBubblewrapSandbox(spawnParams);
|
|
51
|
+
}
|
|
52
|
+
throw new Error(`unsupported sandbox backend: ${config.backend}`);
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
/**
|
|
@@ -11,6 +11,11 @@ import type { ChildProcessWithoutNullStreams } from "node:child_process";
|
|
|
11
11
|
import type { SandboxConfig } from "@/sandbox/types/Sandbox.js";
|
|
12
12
|
import type { SandboxNetworkMode } from "@/sandbox/types/Sandbox.js";
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* 当前内置支持的 sandbox backend。
|
|
16
|
+
*/
|
|
17
|
+
export type SandboxBackend = "macos-seatbelt" | "linux-bubblewrap";
|
|
18
|
+
|
|
14
19
|
/**
|
|
15
20
|
* sandbox 会话状态。
|
|
16
21
|
*
|
|
@@ -298,7 +303,7 @@ export interface ResolvedSandboxConfig extends SandboxConfig {
|
|
|
298
303
|
/**
|
|
299
304
|
* 当前运行时选中的 backend。
|
|
300
305
|
*/
|
|
301
|
-
backend:
|
|
306
|
+
backend: SandboxBackend;
|
|
302
307
|
}
|
|
303
308
|
|
|
304
309
|
/**
|
|
@@ -368,7 +373,7 @@ export interface SandboxSpawnResult {
|
|
|
368
373
|
/**
|
|
369
374
|
* 当前使用的 backend 名称。
|
|
370
375
|
*/
|
|
371
|
-
backend:
|
|
376
|
+
backend: SandboxBackend;
|
|
372
377
|
|
|
373
378
|
/**
|
|
374
379
|
* 当前实际采用的网络模式。
|
package/src/session/Session.ts
CHANGED
|
@@ -130,6 +130,7 @@ export class Session implements AgentSession {
|
|
|
130
130
|
});
|
|
131
131
|
this.turnService = new SessionTurnService({
|
|
132
132
|
session_id: this.id,
|
|
133
|
+
project_root: this.projectRoot,
|
|
133
134
|
executor: this.executor,
|
|
134
135
|
state_service: this.stateService,
|
|
135
136
|
event_hub: this.eventHub,
|
|
@@ -32,6 +32,11 @@ type SessionTurnServiceOptions = {
|
|
|
32
32
|
*/
|
|
33
33
|
session_id: string;
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* 当前项目根目录。
|
|
37
|
+
*/
|
|
38
|
+
project_root: string;
|
|
39
|
+
|
|
35
40
|
/**
|
|
36
41
|
* 当前 session 执行器。
|
|
37
42
|
*/
|
|
@@ -53,6 +58,7 @@ type SessionTurnServiceOptions = {
|
|
|
53
58
|
*/
|
|
54
59
|
export class SessionTurnService {
|
|
55
60
|
private readonly session_id: string;
|
|
61
|
+
private readonly project_root: string;
|
|
56
62
|
private readonly executor: Executor;
|
|
57
63
|
private readonly state_service: SessionStateService;
|
|
58
64
|
private readonly event_hub: SessionEventHub;
|
|
@@ -60,6 +66,7 @@ export class SessionTurnService {
|
|
|
60
66
|
|
|
61
67
|
constructor(options: SessionTurnServiceOptions) {
|
|
62
68
|
this.session_id = options.session_id;
|
|
69
|
+
this.project_root = options.project_root;
|
|
63
70
|
this.executor = options.executor;
|
|
64
71
|
this.state_service = options.state_service;
|
|
65
72
|
this.event_hub = options.event_hub;
|
|
@@ -134,6 +141,7 @@ export class SessionTurnService {
|
|
|
134
141
|
const tool_name_by_call_id = new Map<string, string>();
|
|
135
142
|
const run_context: SessionRunContext = {
|
|
136
143
|
sessionId: this.session_id,
|
|
144
|
+
projectRoot: this.project_root,
|
|
137
145
|
onStepCallback: input.onStepMerge,
|
|
138
146
|
onAssistantStepCallback: async (step) => {
|
|
139
147
|
this.publish_event({
|
|
@@ -23,6 +23,15 @@ export interface SessionRunContext {
|
|
|
23
23
|
*/
|
|
24
24
|
sessionId: string;
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* 当前执行所属的项目根目录。
|
|
28
|
+
*
|
|
29
|
+
* 关键点(中文)
|
|
30
|
+
* - 用于 tool/plugin 运行期把二进制资源写入项目级 `.downcity/resources`。
|
|
31
|
+
* - 未提供时,底层资源写入逻辑会回退到当前进程工作目录,兼容旧入口。
|
|
32
|
+
*/
|
|
33
|
+
projectRoot?: string;
|
|
34
|
+
|
|
26
35
|
/**
|
|
27
36
|
* step 边界合并回调。
|
|
28
37
|
*
|
|
@@ -88,6 +88,73 @@ export interface ImagePluginInput {
|
|
|
88
88
|
*/
|
|
89
89
|
export type ImagePluginResult = UIMessage;
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* ImagePlugin 图片任务状态。
|
|
93
|
+
*/
|
|
94
|
+
export type ImagePluginJobStatus = "queued" | "running" | "succeeded" | "failed";
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* ImagePlugin 图片任务创建结果。
|
|
98
|
+
*/
|
|
99
|
+
export interface ImagePluginJobCreateResult {
|
|
100
|
+
/** 图片任务唯一 ID。 */
|
|
101
|
+
job_id: string;
|
|
102
|
+
/** 当前任务状态。 */
|
|
103
|
+
status: ImagePluginJobStatus;
|
|
104
|
+
/** 查询任务状态的路径或 URL。 */
|
|
105
|
+
status_path?: string;
|
|
106
|
+
/** 读取任务结果的路径或 URL。 */
|
|
107
|
+
result_path?: string;
|
|
108
|
+
/** 人类可读状态说明。 */
|
|
109
|
+
message?: string;
|
|
110
|
+
/** 建议下次轮询前等待的毫秒数。 */
|
|
111
|
+
poll_after_ms?: number;
|
|
112
|
+
/** 任务创建时间。 */
|
|
113
|
+
created_at?: string;
|
|
114
|
+
/** 任务更新时间。 */
|
|
115
|
+
updated_at?: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* ImagePlugin 图片任务状态查询结果。
|
|
120
|
+
*/
|
|
121
|
+
export interface ImagePluginJobStatusResult {
|
|
122
|
+
/** 图片任务唯一 ID。 */
|
|
123
|
+
job_id: string;
|
|
124
|
+
/** 当前任务状态。 */
|
|
125
|
+
status: ImagePluginJobStatus;
|
|
126
|
+
/** 人类可读状态说明。 */
|
|
127
|
+
message?: string;
|
|
128
|
+
/** 失败时的错误信息。 */
|
|
129
|
+
error?: string;
|
|
130
|
+
/** 建议下次轮询前等待的毫秒数。 */
|
|
131
|
+
poll_after_ms?: number;
|
|
132
|
+
/** 任务创建时间。 */
|
|
133
|
+
created_at?: string;
|
|
134
|
+
/** 任务更新时间。 */
|
|
135
|
+
updated_at?: string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* ImagePlugin 图片任务结果查询结果。
|
|
140
|
+
*/
|
|
141
|
+
export interface ImagePluginJobResult {
|
|
142
|
+
/** 图片任务唯一 ID。 */
|
|
143
|
+
job_id: string;
|
|
144
|
+
/** 当前任务状态。 */
|
|
145
|
+
status: ImagePluginJobStatus;
|
|
146
|
+
/** 成功时的图片结果。 */
|
|
147
|
+
result?: ImagePluginResult;
|
|
148
|
+
/** 失败时的错误信息。 */
|
|
149
|
+
error?: string;
|
|
150
|
+
/** 人类可读状态说明。 */
|
|
151
|
+
message?: string;
|
|
152
|
+
/** 任务创建时间。 */
|
|
153
|
+
created_at?: string;
|
|
154
|
+
/** 任务更新时间。 */
|
|
155
|
+
updated_at?: string;
|
|
156
|
+
}
|
|
157
|
+
|
|
91
158
|
/**
|
|
92
159
|
* ImagePlugin 构造参数。
|
|
93
160
|
*/
|
|
@@ -98,6 +165,16 @@ export interface ImagePluginOptions {
|
|
|
98
165
|
title?: string;
|
|
99
166
|
/** Plugin 用途说明。 */
|
|
100
167
|
description?: string;
|
|
101
|
-
/**
|
|
102
|
-
image
|
|
168
|
+
/** 可选:图片生成函数;未传 `create/status/result` 时用于本地后台任务兼容。 */
|
|
169
|
+
image?: (input: ImagePluginInput) => Promise<ImagePluginResult> | ImagePluginResult;
|
|
170
|
+
/** 可选:创建图片生成任务,通常传入 `(input) => city.ai.imageJobCreate(input)`。 */
|
|
171
|
+
create?: (input: ImagePluginInput) => Promise<ImagePluginJobCreateResult> | ImagePluginJobCreateResult;
|
|
172
|
+
/** 可选:查询图片生成任务状态,通常传入 `(input) => city.ai.imageJobStatus(input)`。 */
|
|
173
|
+
status?: (input: { job_id: string }) => Promise<ImagePluginJobStatusResult> | ImagePluginJobStatusResult;
|
|
174
|
+
/** 可选:读取图片生成任务结果,通常传入 `(input) => city.ai.imageJobResult(input)`。 */
|
|
175
|
+
result?: (input: { job_id: string }) => Promise<ImagePluginJobResult> | ImagePluginJobResult;
|
|
176
|
+
/** 兼容 `generate` 动作等待任务完成的最长毫秒数。 */
|
|
177
|
+
wait_timeout_ms?: number;
|
|
178
|
+
/** 兼容 `generate` 动作每次轮询间隔毫秒数。 */
|
|
179
|
+
poll_interval_ms?: number;
|
|
103
180
|
}
|