@downcity/agent 1.1.97 → 1.1.100

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 (53) hide show
  1. package/bin/executor/composer/system/default/assets/core.prompt.d.ts +1 -1
  2. package/bin/executor/composer/system/default/assets/core.prompt.d.ts.map +1 -1
  3. package/bin/executor/composer/system/default/assets/core.prompt.js +1 -1
  4. package/bin/executor/composer/system/default/assets/core.prompt.js.map +1 -1
  5. package/bin/executor/tools/shell/ShellToolBridge.d.ts.map +1 -1
  6. package/bin/executor/tools/shell/ShellToolBridge.js +14 -0
  7. package/bin/executor/tools/shell/ShellToolBridge.js.map +1 -1
  8. package/bin/executor/tools/shell/types/ShellPlugin.d.ts +8 -0
  9. package/bin/executor/tools/shell/types/ShellPlugin.d.ts.map +1 -1
  10. package/bin/index.d.ts +1 -1
  11. package/bin/index.d.ts.map +1 -1
  12. package/bin/index.js.map +1 -1
  13. package/bin/plugin/core/ImagePlugin.d.ts +2 -5
  14. package/bin/plugin/core/ImagePlugin.d.ts.map +1 -1
  15. package/bin/plugin/core/ImagePlugin.js +6 -49
  16. package/bin/plugin/core/ImagePlugin.js.map +1 -1
  17. package/bin/sandbox/LinuxBubblewrapSandbox.d.ts +1 -3
  18. package/bin/sandbox/LinuxBubblewrapSandbox.d.ts.map +1 -1
  19. package/bin/sandbox/LinuxBubblewrapSandbox.js +31 -30
  20. package/bin/sandbox/LinuxBubblewrapSandbox.js.map +1 -1
  21. package/bin/sandbox/MacOsSeatbeltSandbox.d.ts +1 -1
  22. package/bin/sandbox/MacOsSeatbeltSandbox.d.ts.map +1 -1
  23. package/bin/sandbox/MacOsSeatbeltSandbox.js +30 -29
  24. package/bin/sandbox/MacOsSeatbeltSandbox.js.map +1 -1
  25. package/bin/sandbox/SandboxConfigResolver.d.ts +1 -0
  26. package/bin/sandbox/SandboxConfigResolver.d.ts.map +1 -1
  27. package/bin/sandbox/SandboxConfigResolver.js +13 -3
  28. package/bin/sandbox/SandboxConfigResolver.js.map +1 -1
  29. package/bin/sandbox/SandboxRunner.d.ts +17 -4
  30. package/bin/sandbox/SandboxRunner.d.ts.map +1 -1
  31. package/bin/sandbox/SandboxRunner.js +20 -5
  32. package/bin/sandbox/SandboxRunner.js.map +1 -1
  33. package/bin/sandbox/types/SandboxRuntime.d.ts +46 -6
  34. package/bin/sandbox/types/SandboxRuntime.d.ts.map +1 -1
  35. package/bin/sandbox/types/SandboxRuntime.js +2 -2
  36. package/bin/types/plugin/ImagePlugin.d.ts +3 -55
  37. package/bin/types/plugin/ImagePlugin.d.ts.map +1 -1
  38. package/package.json +2 -2
  39. package/scripts/image-plugin-job.test.mjs +10 -43
  40. package/scripts/linux-bubblewrap-sandbox.test.mjs +23 -14
  41. package/src/executor/composer/system/default/assets/core.prompt.ts +1 -1
  42. package/src/executor/composer/system/default/assets/core.prompt.ts.txt +5 -0
  43. package/src/executor/tools/shell/ShellToolBridge.ts +14 -0
  44. package/src/executor/tools/shell/types/ShellPlugin.ts +8 -0
  45. package/src/index.ts +0 -3
  46. package/src/plugin/core/ImagePlugin.ts +6 -52
  47. package/src/sandbox/LinuxBubblewrapSandbox.ts +35 -43
  48. package/src/sandbox/MacOsSeatbeltSandbox.ts +35 -41
  49. package/src/sandbox/SandboxConfigResolver.ts +15 -3
  50. package/src/sandbox/SandboxRunner.ts +32 -7
  51. package/src/sandbox/types/SandboxRuntime.ts +54 -6
  52. package/src/types/plugin/ImagePlugin.ts +3 -56
  53. package/tsconfig.tsbuildinfo +1 -1
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * 关键点(中文)
5
5
  * - Agent 只调用 `generate` action。
6
- * - 插件内部负责 image_create/image_result 轮询。
6
+ * - 插件内部直接调用注入的 image 函数。
7
7
  * - 成功后返回 UIMessage,后续由 plugin bridge 落盘 file parts。
8
8
  */
9
9
 
@@ -27,33 +27,13 @@ function create_image_message() {
27
27
  };
28
28
  }
29
29
 
30
- test("ImagePlugin generate polls create/result until the image succeeds", async () => {
30
+ test("ImagePlugin generate calls image and returns the generated message", async () => {
31
31
  const calls = [];
32
32
  const plugin = new ImagePlugin({
33
- create: (input) => {
34
- calls.push(["create", input.prompt]);
35
- return {
36
- job_id: "img_custom",
37
- status: "running",
38
- poll_after_ms: 1,
39
- };
33
+ image: (input) => {
34
+ calls.push(["image", input.prompt]);
35
+ return create_image_message();
40
36
  },
41
- result: () => {
42
- calls.push(["result"]);
43
- return calls.filter(([name]) => name === "result").length === 1
44
- ? {
45
- job_id: "img_custom",
46
- status: "running",
47
- poll_after_ms: 1,
48
- }
49
- : {
50
- job_id: "img_custom",
51
- status: "succeeded",
52
- result: create_image_message(),
53
- };
54
- },
55
- poll_interval_ms: 1,
56
- wait_timeout_ms: 100,
57
37
  });
58
38
 
59
39
  const result = await plugin.actions.generate.execute({
@@ -66,27 +46,14 @@ test("ImagePlugin generate polls create/result until the image succeeds", async
66
46
  assert.equal(result.success, true);
67
47
  assert.equal(result.data.role, "assistant");
68
48
  assert.equal(result.data.parts[0].type, "file");
69
- assert.deepEqual(calls, [
70
- ["create", "draw"],
71
- ["result"],
72
- ["result"],
73
- ]);
49
+ assert.deepEqual(calls, [["image", "draw"]]);
74
50
  });
75
51
 
76
- test("ImagePlugin generate reports job failure", async () => {
52
+ test("ImagePlugin generate reports image failure", async () => {
77
53
  const plugin = new ImagePlugin({
78
- create: () => ({
79
- job_id: "img_failed",
80
- status: "running",
81
- poll_after_ms: 1,
82
- }),
83
- result: () => ({
84
- job_id: "img_failed",
85
- status: "failed",
86
- error: "provider failed",
87
- }),
88
- poll_interval_ms: 1,
89
- wait_timeout_ms: 100,
54
+ image: () => {
55
+ throw new Error("provider failed");
56
+ },
90
57
  });
91
58
 
92
59
  const result = await plugin.actions.generate.execute({
@@ -19,26 +19,29 @@ async function createSandboxFixture() {
19
19
  const projectRoot = path.join(root, "project");
20
20
  const writablePath = path.join(projectRoot, ".downcity");
21
21
  const shellDir = path.join(writablePath, "shell", "sh_test");
22
- const shellHomeDir = path.join(shellDir, "sandbox", "home");
23
- const shellTmpDir = path.join(shellDir, "sandbox", "tmp");
22
+ const sandboxDir = path.join(projectRoot, ".downcity", "sandbox");
23
+ const tmpDir = path.join(sandboxDir, "tmp");
24
+ const cacheDir = path.join(sandboxDir, ".cache");
24
25
 
25
- await fs.mkdir(shellHomeDir, { recursive: true });
26
- await fs.mkdir(shellTmpDir, { recursive: true });
26
+ await fs.mkdir(shellDir, { recursive: true });
27
+ await fs.mkdir(tmpDir, { recursive: true });
28
+ await fs.mkdir(cacheDir, { recursive: true });
27
29
 
28
30
  return {
29
31
  root,
30
32
  projectRoot,
31
33
  writablePath,
32
34
  shellDir,
33
- shellHomeDir,
34
- shellTmpDir,
35
+ sandboxDir,
36
+ tmpDir,
37
+ cacheDir,
35
38
  };
36
39
  }
37
40
 
38
41
  function createParams(fixture, overrides = {}) {
39
42
  return {
40
- shellId: "sh_test",
41
- shellDir: fixture.shellDir,
43
+ executionId: "sh_test",
44
+ executionDir: fixture.shellDir,
42
45
  cmd: "printf hello",
43
46
  cwd: fixture.projectRoot,
44
47
  actualCwd: fixture.projectRoot,
@@ -52,12 +55,14 @@ function createParams(fixture, overrides = {}) {
52
55
  config: {
53
56
  backend: "linux-bubblewrap",
54
57
  rootPath: fixture.projectRoot,
58
+ sandboxDir: fixture.sandboxDir,
59
+ homeDir: fixture.sandboxDir,
60
+ tmpDir: fixture.tmpDir,
61
+ cacheDir: fixture.cacheDir,
55
62
  envAllowlist: ["PATH", "LANG"],
56
- writablePaths: [fixture.writablePath],
63
+ writablePaths: [fixture.projectRoot, fixture.sandboxDir],
57
64
  networkMode: "off",
58
65
  },
59
- shellHomeDir: fixture.shellHomeDir,
60
- shellTmpDir: fixture.shellTmpDir,
61
66
  ...overrides,
62
67
  };
63
68
  }
@@ -96,9 +101,9 @@ test("Linux bubblewrap args isolate network and overlay writable project paths",
96
101
  assert.equal(hasArg(args, "--die-with-parent"), true);
97
102
  assert.equal(hasArg(args, "--unshare-pid"), true);
98
103
  assert.equal(hasArg(args, "--unshare-net"), true);
99
- assert.equal(hasOptionPair(args, "--ro-bind", fixture.projectRoot), true);
100
- assert.equal(hasOptionPair(args, "--bind", fixture.writablePath), true);
101
- assert.equal(hasOptionPair(args, "--bind", fixture.projectRoot), false);
104
+ assert.equal(hasOptionPair(args, "--bind", fixture.projectRoot), true);
105
+ assert.equal(hasOptionPair(args, "--bind", fixture.sandboxDir), false);
106
+ assert.equal(hasOptionPair(args, "--ro-bind", fixture.projectRoot), false);
102
107
  assert.equal(hasOptionValue(args, "--dir", fixture.writablePath), false);
103
108
  assert.deepEqual(args.slice(-5), [
104
109
  "--chdir",
@@ -119,6 +124,10 @@ test("Linux bubblewrap args keep root writable when sandbox writablePaths includ
119
124
  config: {
120
125
  backend: "linux-bubblewrap",
121
126
  rootPath: fixture.projectRoot,
127
+ sandboxDir: fixture.sandboxDir,
128
+ homeDir: fixture.sandboxDir,
129
+ tmpDir: fixture.tmpDir,
130
+ cacheDir: fixture.cacheDir,
122
131
  envAllowlist: ["PATH"],
123
132
  writablePaths: [fixture.projectRoot],
124
133
  networkMode: "full",
@@ -4,6 +4,6 @@
4
4
  */
5
5
 
6
6
  // Source: src/executor/composer/system/default/assets/core.prompt.ts.txt
7
- const TEXT_MODULE_CONTENT = "你拥有且仅拥有当前项目 {{project_path}} 的使用权和修改权。当前年份是 {{current_year}} 年。\n1. `.downcity/` 是 Downcity 的运行时数据目录(通常不需要你手动读取/修改;系统会自动写入与注入)。结构与逻辑如下:\n - `.downcity/agents/<agentId>/sessions/` 是会话消息。\n - `.downcity/memory/` 是中长期记忆。\n - `.downcity/profile/Primary.md`、`.downcity/profile/other.md`:全局 profile 记忆;存在时会自动作为 system prompt 注入。\n - `.downcity/public/`:对外静态资源目录,通过 `GET /downcity/public/<path>` 访问;用于给外部访问的路径。不要存放敏感信息,Town Agent HTTP gateway 会把 `.downcity/public/` 暴露为 HTTP 静态资源:`GET /downcity/public/<path>`,你可以把该 URL 发给用户用于下载/查看生成的文件(注意不要暴露敏感信息)。\n - `.downcity/logs/<YYYY-MM-DD>.jsonl`:运行日志(JSONL);用于排查问题,避免把原始日志整段贴给用户。\n - `.downcity/.cache/`:幂等/去重缓存(ingress/egress);不要手动改。\n - `.downcity/.debug/`:调试产物(Town 托管进程 pid/log/meta、适配器事件抓取等);仅在排查问题时查看。\n - `.downcity/data/`:小型持久化数据(预留)。\n - `.downcity/task/`:Task 目录。\n2. PROFILE.md + SOUL.md + downcity.json 是你的一些配置文件,你不需要读取。\n\n# 最重要\n【关于命令执行工具】(重要)\n- 短命令、一次性命令优先使用 `shell_exec`。\n- 长任务、需要中途查状态、需要 stdin 交互时,使用 `shell_start` / `shell_status` / `shell_read` / `shell_write` / `shell_wait` / `shell_close`。\n- 先用 `shell_start` 启动命令并拿到 `shell_id`。\n- `shell_id` 是 shell 会话标识;它不是 chat `session_id`。\n- 长任务期间,优先使用 `shell_status` 查询进度,或使用 `shell_wait` 等待状态变化;不要自己写高频空轮询循环。\n- 只有在确实需要原始增量输出时,才使用 `shell_read` 按 `from_cursor` 继续读取。\n- 需要向进程 stdin 输入内容时,使用 `shell_write`。\n- 命令会话完成后若不再需要,使用 `shell_close` 主动释放资源。\n- 不要把原始超长 shell 输出直接转发给用户,应先总结。\n\n# 默认决策与澄清\n- 默认先执行,再沟通:对低风险、可回滚、用户意图已经足够明显的请求,优先基于当前日期、时区、聊天上下文与常见默认值直接执行,不要在事件标题、默认平台、显然的时间表达上反复追问。\n- 只有当“缺失信息会实质改变结果”时才追问;例如:会影响日期/对象/金额/账户/发送目标,或会触发不可逆、高风险、涉隐私操作。\n- 处理时间表达时,优先使用当前环境提供的 `current_date`、`current_time` 与 `timezone`;如果入站 `<info>` 明确提供了 `user_timezone`,则优先按 `user_timezone` 解析,否则按 runtime clock 的 `timezone` 解析。像“今天/明天/下午两点/提前两小时”这类表达,应先解析为绝对时间,再执行,并在回复里明确写出绝对日期时间。\n- 当任务依赖外部权限、系统能力或第三方连接(如日历、提醒事项、聊天渠道、系统授权)时,先探测可用性,再决定是否承诺“我来创建/发送/写入”。\n- 如果探测结果显示被系统权限、宿主环境或连接状态阻塞,要直接说明真实阻塞点和下一步,而不是先给出“可以,我来做”的承诺后再多轮追问。\n- 若已经有足够信息可以一次完成多个低风险默认动作,应直接完成,并在结果里简短说明采用了哪些默认假设。\n\n# 很重要\n\n安全与边界\n- 不要执行破坏性命令(如 `rm -rf`、`git reset --hard`)除非用户明确要求。\n- 遇到 API Key、Token、Secret、环境变量、bot 凭据等密钥管理问题时,优先指导用户使用 Console(如 `Global / Env`、`Global / Channel Accounts`)维护,不要要求用户把密钥明文直接发送到当前聊天里。\n- `town keys` 只能列出已配置的 key 名与描述,不会返回密钥值。不要让用户把密钥“发给你自己”或继续尝试通过 `town keys` 获取明文。\n";
7
+ const TEXT_MODULE_CONTENT = "你拥有且仅拥有当前项目 {{project_path}} 的使用权和修改权。当前年份是 {{current_year}} 年。\n1. `.downcity/` 是 Downcity 的运行时数据目录(通常不需要你手动读取/修改;系统会自动写入与注入)。结构与逻辑如下:\n - `.downcity/agents/<agentId>/sessions/` 是会话消息。\n - `.downcity/memory/` 是中长期记忆。\n - `.downcity/profile/Primary.md`、`.downcity/profile/other.md`:全局 profile 记忆;存在时会自动作为 system prompt 注入。\n - `.downcity/public/`:对外静态资源目录,通过 `GET /downcity/public/<path>` 访问;用于给外部访问的路径。不要存放敏感信息,Town Agent HTTP gateway 会把 `.downcity/public/` 暴露为 HTTP 静态资源:`GET /downcity/public/<path>`,你可以把该 URL 发给用户用于下载/查看生成的文件(注意不要暴露敏感信息)。\n - `.downcity/logs/<YYYY-MM-DD>.jsonl`:运行日志(JSONL);用于排查问题,避免把原始日志整段贴给用户。\n - `.downcity/.cache/`:幂等/去重缓存(ingress/egress);不要手动改。\n - `.downcity/.debug/`:调试产物(Town 托管进程 pid/log/meta、适配器事件抓取等);仅在排查问题时查看。\n - `.downcity/data/`:小型持久化数据(预留)。\n - `.downcity/task/`:Task 目录。\n - `.downcity/sandbox/`:当前 agent 的本地命令执行 sandbox HOME/cache/tmp;shell 与 script 命令会共享它。\n2. PROFILE.md + SOUL.md + downcity.json 是你的一些配置文件,你不需要读取。\n\n# 最重要\n【关于命令执行工具】(重要)\n- 短命令、一次性命令优先使用 `shell_exec`。\n- 长任务、需要中途查状态、需要 stdin 交互时,使用 `shell_start` / `shell_status` / `shell_read` / `shell_write` / `shell_wait` / `shell_close`。\n- 先用 `shell_start` 启动命令并拿到 `shell_id`。\n- `shell_id` 是 shell 会话标识;它不是 chat `session_id`。\n- 长任务期间,优先使用 `shell_status` 查询进度,或使用 `shell_wait` 等待状态变化;不要自己写高频空轮询循环。\n- 只有在确实需要原始增量输出时,才使用 `shell_read` 按 `from_cursor` 继续读取。\n- 需要向进程 stdin 输入内容时,使用 `shell_write`。\n- 命令会话完成后若不再需要,使用 `shell_close` 主动释放资源。\n- 不要把原始超长 shell 输出直接转发给用户,应先总结。\n- shell 命令默认在当前 agent 的 sandbox 中执行:项目目录可读写,网络可用,HOME 指向 `.downcity/sandbox/`,真实用户 HOME 与系统目录不可写。\n- 安装 Python 依赖时优先使用项目内 `.venv`,不要使用 `pip install --user`。\n- 不要尝试 `sudo`、`brew install`、Xcode Command Line Tools 安装或写 `/usr/local`、`/opt/homebrew`、`/System` 等宿主系统目录;如果确实缺少系统级依赖,直接告诉用户需要在宿主机安装。\n- 下载模型、工具缓存、临时状态应自然落在 `.downcity/sandbox/` 或项目目录中,不要假设可复用真实用户缓存。\n\n# 默认决策与澄清\n- 默认先执行,再沟通:对低风险、可回滚、用户意图已经足够明显的请求,优先基于当前日期、时区、聊天上下文与常见默认值直接执行,不要在事件标题、默认平台、显然的时间表达上反复追问。\n- 只有当“缺失信息会实质改变结果”时才追问;例如:会影响日期/对象/金额/账户/发送目标,或会触发不可逆、高风险、涉隐私操作。\n- 处理时间表达时,优先使用当前环境提供的 `current_date`、`current_time` 与 `timezone`;如果入站 `<info>` 明确提供了 `user_timezone`,则优先按 `user_timezone` 解析,否则按 runtime clock 的 `timezone` 解析。像“今天/明天/下午两点/提前两小时”这类表达,应先解析为绝对时间,再执行,并在回复里明确写出绝对日期时间。\n- 当任务依赖外部权限、系统能力或第三方连接(如日历、提醒事项、聊天渠道、系统授权)时,先探测可用性,再决定是否承诺“我来创建/发送/写入”。\n- 如果探测结果显示被系统权限、宿主环境或连接状态阻塞,要直接说明真实阻塞点和下一步,而不是先给出“可以,我来做”的承诺后再多轮追问。\n- 若已经有足够信息可以一次完成多个低风险默认动作,应直接完成,并在结果里简短说明采用了哪些默认假设。\n\n# 很重要\n\n安全与边界\n- 不要执行破坏性命令(如 `rm -rf`、`git reset --hard`)除非用户明确要求。\n- 遇到 API Key、Token、Secret、环境变量、bot 凭据等密钥管理问题时,优先指导用户使用 Console(如 `Global / Env`、`Global / Channel Accounts`)维护,不要要求用户把密钥明文直接发送到当前聊天里。\n- `town keys` 只能列出已配置的 key 名与描述,不会返回密钥值。不要让用户把密钥“发给你自己”或继续尝试通过 `town keys` 获取明文。\n";
8
8
 
9
9
  export default TEXT_MODULE_CONTENT;
@@ -9,6 +9,7 @@
9
9
  - `.downcity/.debug/`:调试产物(Town 托管进程 pid/log/meta、适配器事件抓取等);仅在排查问题时查看。
10
10
  - `.downcity/data/`:小型持久化数据(预留)。
11
11
  - `.downcity/task/`:Task 目录。
12
+ - `.downcity/sandbox/`:当前 agent 的本地命令执行 sandbox HOME/cache/tmp;shell 与 script 命令会共享它。
12
13
  2. PROFILE.md + SOUL.md + downcity.json 是你的一些配置文件,你不需要读取。
13
14
 
14
15
  # 最重要
@@ -22,6 +23,10 @@
22
23
  - 需要向进程 stdin 输入内容时,使用 `shell_write`。
23
24
  - 命令会话完成后若不再需要,使用 `shell_close` 主动释放资源。
24
25
  - 不要把原始超长 shell 输出直接转发给用户,应先总结。
26
+ - shell 命令默认在当前 agent 的 sandbox 中执行:项目目录可读写,网络可用,HOME 指向 `.downcity/sandbox/`,真实用户 HOME 与系统目录不可写。
27
+ - 安装 Python 依赖时优先使用项目内 `.venv`,不要使用 `pip install --user`。
28
+ - 不要尝试 `sudo`、`brew install`、Xcode Command Line Tools 安装或写 `/usr/local`、`/opt/homebrew`、`/System` 等宿主系统目录;如果确实缺少系统级依赖,直接告诉用户需要在宿主机安装。
29
+ - 下载模型、工具缓存、临时状态应自然落在 `.downcity/sandbox/` 或项目目录中,不要假设可复用真实用户缓存。
25
30
 
26
31
  # 默认决策与澄清
27
32
  - 默认先执行,再沟通:对低风险、可回滚、用户意图已经足够明显的请求,优先基于当前日期、时区、聊天上下文与常见默认值直接执行,不要在事件标题、默认平台、显然的时间表达上反复追问。
@@ -211,6 +211,13 @@ export function flattenShellActionResponse(params: {
211
211
  status: shell.status,
212
212
  cmd: shell.cmd,
213
213
  cwd: shell.cwd,
214
+ sandboxed: shell.sandboxed === true,
215
+ sandbox_backend: shell.sandboxBackend || null,
216
+ sandbox_network_mode: shell.sandboxNetworkMode || null,
217
+ sandbox_dir: shell.sandboxDir || null,
218
+ sandbox_home_dir: shell.sandboxHomeDir || null,
219
+ sandbox_tmp_dir: shell.sandboxTmpDir || null,
220
+ sandbox_cache_dir: shell.sandboxCacheDir || null,
214
221
  pid: typeof shell.pid === "number" ? shell.pid : null,
215
222
  version: shell.version,
216
223
  started_at: shell.startedAt,
@@ -255,6 +262,13 @@ export function flattenShellExecResponse(params: {
255
262
  status: shell.status,
256
263
  cmd: shell.cmd,
257
264
  cwd: shell.cwd,
265
+ sandboxed: shell.sandboxed === true,
266
+ sandbox_backend: shell.sandboxBackend || null,
267
+ sandbox_network_mode: shell.sandboxNetworkMode || null,
268
+ sandbox_dir: shell.sandboxDir || null,
269
+ sandbox_home_dir: shell.sandboxHomeDir || null,
270
+ sandbox_tmp_dir: shell.sandboxTmpDir || null,
271
+ sandbox_cache_dir: shell.sandboxCacheDir || null,
258
272
  exit_code: typeof shell.exitCode === "number" ? shell.exitCode : null,
259
273
  output: chunk?.output || "",
260
274
  original_chars: chunk?.originalChars ?? 0,
@@ -54,6 +54,14 @@ export type ShellSessionSnapshot = {
54
54
  sandboxBackend?: string;
55
55
  /** 当前 shell 采用的 sandbox 网络模式。 */
56
56
  sandboxNetworkMode?: "off" | "restricted" | "full";
57
+ /** 当前 agent 级 sandbox 的持久目录。 */
58
+ sandboxDir?: string;
59
+ /** 当前 shell 在 sandbox 中使用的 HOME。 */
60
+ sandboxHomeDir?: string;
61
+ /** 当前 shell 在 sandbox 中使用的临时目录。 */
62
+ sandboxTmpDir?: string;
63
+ /** 当前 shell 在 sandbox 中使用的 XDG cache 目录。 */
64
+ sandboxCacheDir?: string;
57
65
  /** 当前 shell 状态。 */
58
66
  status: ShellSessionStatus;
59
67
  /** 子进程 pid;若尚未创建成功则为空。 */
package/src/index.ts CHANGED
@@ -229,9 +229,6 @@ export type {
229
229
  ImagePluginContent,
230
230
  ImagePluginFileContent,
231
231
  ImagePluginInput,
232
- ImagePluginJobCreateResult,
233
- ImagePluginJobResult,
234
- ImagePluginJobStatus,
235
232
  ImagePluginMessage,
236
233
  ImagePluginOptions,
237
234
  ImagePluginTextContent,
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * 关键点(中文)
5
5
  * - 对 Agent 只暴露同步体验的 `generate` action。
6
- * - City / provider 的异步任务细节由插件内部 create + result 轮询封装。
6
+ * - City / provider 的图片能力通过单个 image 函数注入。
7
7
  * - action 返回 AI SDK UIMessage,后续由 plugin tool bridge 抽取 file parts 写回 assistant 消息。
8
8
  */
9
9
 
@@ -20,8 +20,6 @@ const DEFAULT_IMAGE_PLUGIN_NAME = "image";
20
20
  const DEFAULT_IMAGE_PLUGIN_TITLE = "Image";
21
21
  const DEFAULT_IMAGE_PLUGIN_DESCRIPTION =
22
22
  "Generate images and return them as assistant file parts.";
23
- const DEFAULT_WAIT_TIMEOUT_MS = 60_000;
24
- const DEFAULT_POLL_INTERVAL_MS = 3_000;
25
23
 
26
24
  /**
27
25
  * 判断值是否为普通对象。
@@ -53,13 +51,6 @@ function normalize_image_result(result: ImagePluginResult): ImagePluginResult {
53
51
  return result;
54
52
  }
55
53
 
56
- /**
57
- * 等待指定毫秒数。
58
- */
59
- function sleep(ms: number): Promise<void> {
60
- return new Promise((resolve) => setTimeout(resolve, ms));
61
- }
62
-
63
54
  /**
64
55
  * Agent 图片生成插件。
65
56
  */
@@ -79,10 +70,7 @@ export class ImagePlugin extends BasePlugin {
79
70
  */
80
71
  readonly description: string;
81
72
 
82
- private readonly create_job: NonNullable<ImagePluginOptions["create"]>;
83
- private readonly read_job_result: NonNullable<ImagePluginOptions["result"]>;
84
- private readonly wait_timeout_ms: number;
85
- private readonly poll_interval_ms: number;
73
+ private readonly image: NonNullable<ImagePluginOptions["image"]>;
86
74
 
87
75
  constructor(options: ImagePluginOptions) {
88
76
  super();
@@ -90,24 +78,15 @@ export class ImagePlugin extends BasePlugin {
90
78
  if (!name) {
91
79
  throw new Error("ImagePlugin requires a non-empty name");
92
80
  }
93
- if (typeof options.create !== "function" || typeof options.result !== "function") {
94
- throw new Error("ImagePlugin requires create and result functions");
81
+ if (typeof options.image !== "function") {
82
+ throw new Error("ImagePlugin requires an image function");
95
83
  }
96
84
  this.name = name;
97
85
  this.title = String(options.title || DEFAULT_IMAGE_PLUGIN_TITLE).trim();
98
86
  this.description = String(
99
87
  options.description || DEFAULT_IMAGE_PLUGIN_DESCRIPTION,
100
88
  ).trim();
101
- this.create_job = options.create;
102
- this.read_job_result = options.result;
103
- this.wait_timeout_ms =
104
- typeof options.wait_timeout_ms === "number" && options.wait_timeout_ms > 0
105
- ? options.wait_timeout_ms
106
- : DEFAULT_WAIT_TIMEOUT_MS;
107
- this.poll_interval_ms =
108
- typeof options.poll_interval_ms === "number" && options.poll_interval_ms > 0
109
- ? options.poll_interval_ms
110
- : DEFAULT_POLL_INTERVAL_MS;
89
+ this.image = options.image;
111
90
  }
112
91
 
113
92
  /**
@@ -123,32 +102,7 @@ export class ImagePlugin extends BasePlugin {
123
102
  }
124
103
 
125
104
  private async generate_image(input: ImagePluginInput): Promise<ImagePluginResult> {
126
- const job = await this.create_job(input);
127
- const job_id = String(job.job_id || "").trim();
128
- if (!job_id) {
129
- throw new Error("ImagePlugin image_create result requires job_id");
130
- }
131
-
132
- const deadline = Date.now() + this.wait_timeout_ms;
133
- let poll_after_ms =
134
- typeof job.poll_after_ms === "number" && job.poll_after_ms > 0
135
- ? job.poll_after_ms
136
- : this.poll_interval_ms;
137
- while (Date.now() <= deadline) {
138
- const result = await this.read_job_result({ job_id });
139
- if (result.status === "succeeded" && result.result) {
140
- return normalize_image_result(result.result);
141
- }
142
- if (result.status === "failed") {
143
- throw new Error(result.error || result.message || "image job failed");
144
- }
145
- poll_after_ms =
146
- typeof result.poll_after_ms === "number" && result.poll_after_ms > 0
147
- ? result.poll_after_ms
148
- : this.poll_interval_ms;
149
- await sleep(poll_after_ms);
150
- }
151
- throw new Error(`image job timed out: ${job_id}`);
105
+ return normalize_image_result(await this.image(input));
152
106
  }
153
107
 
154
108
  /**
@@ -4,7 +4,7 @@
4
4
  * 关键点(中文)
5
5
  * - 基于 `bwrap` 提供 Linux 本机 shell sandbox。
6
6
  * - 继续保持“shell 命令必须进入 sandbox”的安全语义,不提供宿主机裸跑回退。
7
- * - 边界与 macOS backend 对齐:路径、环境变量、网络、隔离 HOME/TMPDIR。
7
+ * - 边界与 macOS backend 对齐:路径、环境变量、网络、agent 级共享 HOME/TMPDIR/cache
8
8
  */
9
9
 
10
10
  import { spawn } from "node:child_process";
@@ -34,8 +34,9 @@ function dedupeExistingPaths(values: string[]): string[] {
34
34
  function buildReadablePaths(params: {
35
35
  rootPath: string;
36
36
  shellPath: string;
37
- shellHomeDir: string;
38
- shellTmpDir: string;
37
+ sandboxDir: string;
38
+ tmpDir: string;
39
+ cacheDir: string;
39
40
  }): string[] {
40
41
  return dedupeExistingPaths([
41
42
  "/usr",
@@ -45,21 +46,20 @@ function buildReadablePaths(params: {
45
46
  "/lib64",
46
47
  "/etc",
47
48
  params.rootPath,
48
- params.shellHomeDir,
49
- params.shellTmpDir,
49
+ params.sandboxDir,
50
+ params.tmpDir,
51
+ params.cacheDir,
50
52
  path.dirname(params.shellPath),
51
53
  ]);
52
54
  }
53
55
 
54
- function buildWritablePaths(params: SandboxSpawnParams & {
55
- shellHomeDir: string;
56
- shellTmpDir: string;
57
- }): string[] {
56
+ function buildWritablePaths(params: SandboxSpawnParams): string[] {
58
57
  return dedupeExistingPaths([
59
58
  ...params.config.writablePaths,
60
- params.shellDir,
61
- params.shellHomeDir,
62
- params.shellTmpDir,
59
+ params.executionDir,
60
+ params.config.sandboxDir,
61
+ params.config.tmpDir,
62
+ params.config.cacheDir,
63
63
  ]);
64
64
  }
65
65
 
@@ -73,10 +73,7 @@ function isPathCoveredBy(paths: string[], targetPath: string): boolean {
73
73
  });
74
74
  }
75
75
 
76
- function buildSandboxEnv(params: SandboxSpawnParams & {
77
- shellHomeDir: string;
78
- shellTmpDir: string;
79
- }): NodeJS.ProcessEnv {
76
+ function buildSandboxEnv(params: SandboxSpawnParams): NodeJS.ProcessEnv {
80
77
  const env: NodeJS.ProcessEnv = {};
81
78
  for (const key of params.config.envAllowlist) {
82
79
  const value = params.baseEnv[key];
@@ -91,8 +88,13 @@ function buildSandboxEnv(params: SandboxSpawnParams & {
91
88
  }
92
89
 
93
90
  env.PATH = String(env.PATH || params.baseEnv.PATH || DEFAULT_PATH_VALUE);
94
- env.HOME = params.shellHomeDir;
95
- env.TMPDIR = params.shellTmpDir;
91
+ env.HOME = params.config.homeDir;
92
+ env.TMPDIR = params.config.tmpDir;
93
+ env.XDG_CACHE_HOME = params.config.cacheDir;
94
+ env.DC_SANDBOX = "1";
95
+ env.DC_SANDBOX_DIR = params.config.sandboxDir;
96
+ env.DC_SANDBOX_HOME = params.config.homeDir;
97
+ env.DC_SANDBOX_CACHE = params.config.cacheDir;
96
98
  env.SHELL = params.shellPath;
97
99
 
98
100
  return env;
@@ -119,20 +121,15 @@ function addParentDirs(args: string[], targetPath: string, createdDirs: Set<stri
119
121
 
120
122
  export function buildLinuxBubblewrapArgs(params: SandboxSpawnParams & {
121
123
  actualCwd: string;
122
- shellHomeDir: string;
123
- shellTmpDir: string;
124
124
  }): string[] {
125
125
  const readablePaths = buildReadablePaths({
126
126
  rootPath: params.config.rootPath,
127
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,
128
+ sandboxDir: params.config.sandboxDir,
129
+ tmpDir: params.config.tmpDir,
130
+ cacheDir: params.config.cacheDir,
135
131
  });
132
+ const writablePaths = buildWritablePaths(params);
136
133
  const writableSet = new Set(writablePaths);
137
134
  const createdDirs = new Set<string>();
138
135
  const mountedPaths: string[] = [];
@@ -159,9 +156,8 @@ export function buildLinuxBubblewrapArgs(params: SandboxSpawnParams & {
159
156
  }
160
157
 
161
158
  for (const writablePath of writablePaths) {
162
- if (!isPathCoveredBy(mountedPaths, writablePath)) {
163
- addParentDirs(args, writablePath, createdDirs);
164
- }
159
+ if (isPathCoveredBy(mountedPaths, writablePath)) continue;
160
+ addParentDirs(args, writablePath, createdDirs);
165
161
  addWritableBind(args, writablePath);
166
162
  mountedPaths.push(writablePath);
167
163
  }
@@ -192,28 +188,20 @@ export function buildLinuxBubblewrapArgs(params: SandboxSpawnParams & {
192
188
  export async function spawnLinuxBubblewrapSandbox(
193
189
  params: SandboxSpawnParams & { actualCwd: string },
194
190
  ): 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);
191
+ await fs.ensureDir(params.config.sandboxDir);
192
+ await fs.ensureDir(params.config.tmpDir);
193
+ await fs.ensureDir(params.config.cacheDir);
194
+ await fs.ensureDir(params.executionDir);
201
195
  for (const writablePath of params.config.writablePaths) {
202
196
  await fs.ensureDir(writablePath);
203
197
  }
204
198
 
205
199
  const child = spawn("bwrap", buildLinuxBubblewrapArgs({
206
200
  ...params,
207
- shellHomeDir,
208
- shellTmpDir,
209
201
  }), {
210
202
  cwd: params.actualCwd,
211
203
  stdio: "pipe",
212
- env: buildSandboxEnv({
213
- ...params,
214
- shellHomeDir,
215
- shellTmpDir,
216
- }),
204
+ env: buildSandboxEnv(params),
217
205
  });
218
206
 
219
207
  child.stdout.setEncoding("utf8");
@@ -225,5 +213,9 @@ export async function spawnLinuxBubblewrapSandbox(
225
213
  sandboxed: true,
226
214
  backend: "linux-bubblewrap",
227
215
  networkMode: params.config.networkMode,
216
+ sandboxDir: params.config.sandboxDir,
217
+ homeDir: params.config.homeDir,
218
+ tmpDir: params.config.tmpDir,
219
+ cacheDir: params.config.cacheDir,
228
220
  };
229
221
  }
@@ -4,7 +4,7 @@
4
4
  * 关键点(中文)
5
5
  * - 当前最小实现直接基于系统自带 `sandbox-exec`。
6
6
  * - 目标不是抽象完整 provider 体系,而是先把 shell 命令从“宿主机直跑”收敛成“带边界执行”。
7
- * - 边界只保留四类:路径、环境变量、网络、隔离后的 HOME/TMPDIR。
7
+ * - 边界只保留四类:路径、环境变量、网络、agent 级共享 HOME/TMPDIR/cache
8
8
  */
9
9
 
10
10
  import { spawn } from "node:child_process";
@@ -37,8 +37,9 @@ function dedupePaths(values: string[]): string[] {
37
37
  function buildReadablePaths(params: {
38
38
  rootPath: string;
39
39
  shellPath: string;
40
- shellHomeDir: string;
41
- shellTmpDir: string;
40
+ sandboxDir: string;
41
+ tmpDir: string;
42
+ cacheDir: string;
42
43
  }): string[] {
43
44
  return dedupePaths([
44
45
  "/bin",
@@ -50,21 +51,20 @@ function buildReadablePaths(params: {
50
51
  "/opt/homebrew",
51
52
  "/usr/local",
52
53
  params.rootPath,
53
- params.shellHomeDir,
54
- params.shellTmpDir,
54
+ params.sandboxDir,
55
+ params.tmpDir,
56
+ params.cacheDir,
55
57
  path.dirname(params.shellPath),
56
58
  ]);
57
59
  }
58
60
 
59
- function buildWritablePaths(params: SandboxSpawnParams & {
60
- shellHomeDir: string;
61
- shellTmpDir: string;
62
- }): string[] {
61
+ function buildWritablePaths(params: SandboxSpawnParams): string[] {
63
62
  return dedupePaths([
64
63
  ...params.config.writablePaths,
65
- params.shellDir,
66
- params.shellHomeDir,
67
- params.shellTmpDir,
64
+ params.executionDir,
65
+ params.config.sandboxDir,
66
+ params.config.tmpDir,
67
+ params.config.cacheDir,
68
68
  ]);
69
69
  }
70
70
 
@@ -77,20 +77,15 @@ function buildNetworkRules(networkMode: SandboxSpawnParams["config"]["networkMod
77
77
 
78
78
  function buildSeatbeltProfile(params: SandboxSpawnParams & {
79
79
  actualCwd: string;
80
- shellHomeDir: string;
81
- shellTmpDir: string;
82
80
  }): string {
83
81
  const readablePaths = buildReadablePaths({
84
82
  rootPath: params.config.rootPath,
85
83
  shellPath: params.shellPath,
86
- shellHomeDir: params.shellHomeDir,
87
- shellTmpDir: params.shellTmpDir,
88
- });
89
- const writablePaths = buildWritablePaths({
90
- ...params,
91
- shellHomeDir: params.shellHomeDir,
92
- shellTmpDir: params.shellTmpDir,
84
+ sandboxDir: params.config.sandboxDir,
85
+ tmpDir: params.config.tmpDir,
86
+ cacheDir: params.config.cacheDir,
93
87
  });
88
+ const writablePaths = buildWritablePaths(params);
94
89
  const lines = [
95
90
  "(version 1)",
96
91
  "(deny default)",
@@ -116,10 +111,7 @@ function buildSeatbeltProfile(params: SandboxSpawnParams & {
116
111
  return `${lines.join("\n")}\n`;
117
112
  }
118
113
 
119
- function buildSandboxEnv(params: SandboxSpawnParams & {
120
- shellHomeDir: string;
121
- shellTmpDir: string;
122
- }): NodeJS.ProcessEnv {
114
+ function buildSandboxEnv(params: SandboxSpawnParams): NodeJS.ProcessEnv {
123
115
  const env: NodeJS.ProcessEnv = {};
124
116
  for (const key of params.config.envAllowlist) {
125
117
  const value = params.baseEnv[key];
@@ -134,9 +126,14 @@ function buildSandboxEnv(params: SandboxSpawnParams & {
134
126
  }
135
127
 
136
128
  env.PATH = String(env.PATH || params.baseEnv.PATH || DEFAULT_PATH_VALUE);
137
- env.HOME = params.shellHomeDir;
138
- env.ZDOTDIR = params.shellHomeDir;
139
- env.TMPDIR = params.shellTmpDir;
129
+ env.HOME = params.config.homeDir;
130
+ env.ZDOTDIR = params.config.homeDir;
131
+ env.TMPDIR = params.config.tmpDir;
132
+ env.XDG_CACHE_HOME = params.config.cacheDir;
133
+ env.DC_SANDBOX = "1";
134
+ env.DC_SANDBOX_DIR = params.config.sandboxDir;
135
+ env.DC_SANDBOX_HOME = params.config.homeDir;
136
+ env.DC_SANDBOX_CACHE = params.config.cacheDir;
140
137
  env.SHELL = params.shellPath;
141
138
 
142
139
  return env;
@@ -148,18 +145,15 @@ function buildSandboxEnv(params: SandboxSpawnParams & {
148
145
  export async function spawnMacOsSeatbeltSandbox(
149
146
  params: SandboxSpawnParams & { actualCwd: string },
150
147
  ): Promise<SandboxSpawnResult> {
151
- const sandboxRootDir = path.join(params.shellDir, "sandbox");
152
- const shellHomeDir = path.join(sandboxRootDir, "home");
153
- const shellTmpDir = path.join(sandboxRootDir, "tmp");
154
- const profilePath = path.join(sandboxRootDir, "profile.sb");
148
+ const profilePath = path.join(params.executionDir, "sandbox-profile.sb");
155
149
 
156
- await fs.ensureDir(shellHomeDir);
157
- await fs.ensureDir(shellTmpDir);
150
+ await fs.ensureDir(params.config.sandboxDir);
151
+ await fs.ensureDir(params.config.tmpDir);
152
+ await fs.ensureDir(params.config.cacheDir);
153
+ await fs.ensureDir(params.executionDir);
158
154
 
159
155
  const profile = buildSeatbeltProfile({
160
156
  ...params,
161
- shellHomeDir,
162
- shellTmpDir,
163
157
  });
164
158
  await fs.writeFile(profilePath, profile, "utf-8");
165
159
 
@@ -175,11 +169,7 @@ export async function spawnMacOsSeatbeltSandbox(
175
169
  {
176
170
  cwd: params.actualCwd,
177
171
  stdio: "pipe",
178
- env: buildSandboxEnv({
179
- ...params,
180
- shellHomeDir,
181
- shellTmpDir,
182
- }),
172
+ env: buildSandboxEnv(params),
183
173
  },
184
174
  );
185
175
 
@@ -192,5 +182,9 @@ export async function spawnMacOsSeatbeltSandbox(
192
182
  sandboxed: true,
193
183
  backend: "macos-seatbelt",
194
184
  networkMode: params.config.networkMode,
185
+ sandboxDir: params.config.sandboxDir,
186
+ homeDir: params.config.homeDir,
187
+ tmpDir: params.config.tmpDir,
188
+ cacheDir: params.config.cacheDir,
195
189
  };
196
190
  }