@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.
Files changed (84) hide show
  1. package/bin/config/AgentInitializer.d.ts.map +1 -1
  2. package/bin/config/AgentInitializer.js +2 -1
  3. package/bin/config/AgentInitializer.js.map +1 -1
  4. package/bin/config/Paths.d.ts +8 -0
  5. package/bin/config/Paths.d.ts.map +1 -1
  6. package/bin/config/Paths.js +10 -0
  7. package/bin/config/Paths.js.map +1 -1
  8. package/bin/executor/Executor.d.ts.map +1 -1
  9. package/bin/executor/Executor.js +3 -0
  10. package/bin/executor/Executor.js.map +1 -1
  11. package/bin/executor/messages/AssistantFileResource.d.ts +31 -0
  12. package/bin/executor/messages/AssistantFileResource.d.ts.map +1 -0
  13. package/bin/executor/messages/AssistantFileResource.js +113 -0
  14. package/bin/executor/messages/AssistantFileResource.js.map +1 -0
  15. package/bin/executor/messages/SessionAttachmentMapper.d.ts +8 -0
  16. package/bin/executor/messages/SessionAttachmentMapper.d.ts.map +1 -1
  17. package/bin/executor/messages/SessionAttachmentMapper.js +54 -0
  18. package/bin/executor/messages/SessionAttachmentMapper.js.map +1 -1
  19. package/bin/executor/messages/SessionMessageCodec.d.ts.map +1 -1
  20. package/bin/executor/messages/SessionMessageCodec.js +5 -3
  21. package/bin/executor/messages/SessionMessageCodec.js.map +1 -1
  22. package/bin/executor/tools/plugin/PluginToolBridge.d.ts.map +1 -1
  23. package/bin/executor/tools/plugin/PluginToolBridge.js +9 -2
  24. package/bin/executor/tools/plugin/PluginToolBridge.js.map +1 -1
  25. package/bin/index.d.ts +1 -1
  26. package/bin/index.d.ts.map +1 -1
  27. package/bin/index.js.map +1 -1
  28. package/bin/plugin/core/ImagePlugin.d.ts +62 -0
  29. package/bin/plugin/core/ImagePlugin.d.ts.map +1 -1
  30. package/bin/plugin/core/ImagePlugin.js +230 -7
  31. package/bin/plugin/core/ImagePlugin.js.map +1 -1
  32. package/bin/sandbox/LinuxBubblewrapSandbox.d.ts +21 -0
  33. package/bin/sandbox/LinuxBubblewrapSandbox.d.ts.map +1 -0
  34. package/bin/sandbox/LinuxBubblewrapSandbox.js +184 -0
  35. package/bin/sandbox/LinuxBubblewrapSandbox.js.map +1 -0
  36. package/bin/sandbox/SandboxConfigResolver.d.ts +5 -0
  37. package/bin/sandbox/SandboxConfigResolver.d.ts.map +1 -1
  38. package/bin/sandbox/SandboxConfigResolver.js +11 -4
  39. package/bin/sandbox/SandboxConfigResolver.js.map +1 -1
  40. package/bin/sandbox/SandboxPreflight.d.ts +73 -0
  41. package/bin/sandbox/SandboxPreflight.d.ts.map +1 -0
  42. package/bin/sandbox/SandboxPreflight.js +122 -0
  43. package/bin/sandbox/SandboxPreflight.js.map +1 -0
  44. package/bin/sandbox/SandboxRunner.d.ts +1 -1
  45. package/bin/sandbox/SandboxRunner.d.ts.map +1 -1
  46. package/bin/sandbox/SandboxRunner.js +11 -3
  47. package/bin/sandbox/SandboxRunner.js.map +1 -1
  48. package/bin/sandbox/types/SandboxRuntime.d.ts +6 -2
  49. package/bin/sandbox/types/SandboxRuntime.d.ts.map +1 -1
  50. package/bin/session/Session.d.ts.map +1 -1
  51. package/bin/session/Session.js +1 -0
  52. package/bin/session/Session.js.map +1 -1
  53. package/bin/session/services/SessionTurnService.d.ts +5 -0
  54. package/bin/session/services/SessionTurnService.d.ts.map +1 -1
  55. package/bin/session/services/SessionTurnService.js +3 -0
  56. package/bin/session/services/SessionTurnService.js.map +1 -1
  57. package/bin/types/executor/SessionRunContext.d.ts +8 -0
  58. package/bin/types/executor/SessionRunContext.d.ts.map +1 -1
  59. package/bin/types/plugin/ImagePlugin.d.ts +79 -2
  60. package/bin/types/plugin/ImagePlugin.d.ts.map +1 -1
  61. package/package.json +2 -2
  62. package/scripts/assistant-file-resource.test.mjs +91 -0
  63. package/scripts/image-plugin-job.test.mjs +155 -0
  64. package/scripts/linux-bubblewrap-sandbox.test.mjs +142 -0
  65. package/scripts/shell-sandbox-preflight.test.mjs +88 -0
  66. package/src/config/AgentInitializer.ts +2 -0
  67. package/src/config/Paths.ts +11 -0
  68. package/src/executor/Executor.ts +3 -0
  69. package/src/executor/messages/AssistantFileResource.ts +155 -0
  70. package/src/executor/messages/SessionAttachmentMapper.ts +59 -0
  71. package/src/executor/messages/SessionMessageCodec.ts +9 -3
  72. package/src/executor/tools/plugin/PluginToolBridge.ts +13 -2
  73. package/src/index.ts +4 -0
  74. package/src/plugin/core/ImagePlugin.ts +284 -7
  75. package/src/sandbox/LinuxBubblewrapSandbox.ts +229 -0
  76. package/src/sandbox/SandboxConfigResolver.ts +13 -7
  77. package/src/sandbox/SandboxPreflight.ts +205 -0
  78. package/src/sandbox/SandboxRunner.ts +11 -3
  79. package/src/sandbox/types/SandboxRuntime.ts +7 -2
  80. package/src/session/Session.ts +1 -0
  81. package/src/session/services/SessionTurnService.ts +8 -0
  82. package/src/types/executor/SessionRunContext.ts +9 -0
  83. package/src/types/plugin/ImagePlugin.ts +79 -2
  84. 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: "macos-seatbelt",
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
- * - 当前版本只接入 macOS seatbelt backend。
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
- return spawnMacOsSeatbeltSandbox({
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: "macos-seatbelt";
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: "macos-seatbelt";
376
+ backend: SandboxBackend;
372
377
 
373
378
  /**
374
379
  * 当前实际采用的网络模式。
@@ -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
- /** 图片生成函数,通常传入 `(input) => city.ai.image(input)`。 */
102
- image: (input: ImagePluginInput) => Promise<ImagePluginResult> | ImagePluginResult;
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
  }