Shell 与 Sandbox

Workspace 持久 Sandbox、Shell Session、宿主执行与审批

Downcity 将四种职责分开:

City → Workspace
Workspace → Shell
Shell → SandboxProvider + Workspace 持久 Sandbox + Shell Sessions
Chat Session → 只负责调用

每个 Workspace 拥有一个独立、持久的 Sandbox。同一 Workspace 下的多个 Chat Session 共享 Sandbox HOME、安装工具和缓存;不同 Workspace 不共享可写 Sandbox 状态。Shell Session 使用 shell_id 标识,是独立进程资源,不是 Chat Session。

安装与初始化

pnpm add @downcity/agent @downcity/city @downcity/sandbox-microsandbox
npx microsandbox setup

默认 Provider 使用 microsandbox microVM 与 OCI 镜像,支持相应的 macOS、Linux 和 Windows 宿主。

import { Agent } from "@downcity/agent";
import { City, Shell, Workspace } from "@downcity/city";
import { MicrosandboxProvider } from "@downcity/sandbox-microsandbox";

const workspace = new Workspace({
  id: "project",
  path: process.cwd(),
  shell: new Shell({
    sandbox_provider: new MicrosandboxProvider(),
  }),
});
const city = new City({
  workspaces: [workspace],
});
const agent = new Agent({ id: "repo-helper", model });
city.agents.add(agent);

不使用 City 的嵌入式宿主,需要向 Shell 提供 sandbox_provider,向 Workspace 提供 runtime_path。City 只自动分配运行目录,不选择或持有 Provider。

执行目标

Shell 只保留两个目标:

  • sandbox:默认值。命令在 Workspace microVM 中执行,项目固定挂载到 /workspace。
  • host:使用完整宿主权限,必须提供非空原因并经过宿主审批。
shell_exec({
  cmd: "open ./report.pdf",
  target: "host",
  reason: "在宿主桌面应用中打开生成的报告。",
});

系统不提供部分宿主路径白名单、中间权限模式或静默降级。Sandbox 不可用时直接失败,不会切换到宿主执行。

路径与 Workspace 挂载

文件与搜索工具在宿主进程中执行,Shell 命令在 Sandbox 内执行。两者都接受沙箱内路径,例如 /workspace/src/main.ts:沙箱路径会在路径策略之前映射回项目根,因此 /workspace/src/main.ts 与 src/main.ts 指向同一个文件,返回结果始终是宿主侧路径。挂载之外的路径(例如 /etc/hosts)仍会被拒绝。

工具与生命周期

Shell 提供适合短命令的 shell_exec,以及适合长任务和交互进程的 shell_session。后者支持 start、send、read、wait、list 和 stop。

关闭 Workspace 会释放其 Shell;Shell 先关闭全部 Shell Sessions,再停止 Sandbox 计算资源,同时保留 Sandbox 文件系统。使用相同 City storage 恢复同一 Workspace 时会重新连接该环境。只有显式调用 shell.reset_sandbox() 才会删除 HOME 和已安装工具。

Workspace 挂载在 Sandbox 创建时固定。如果稳定 Workspace ID 后续指向不同宿主路径或 City runtime path,Downcity 会拒绝连接并要求显式 reset,避免静默挂载意外文件。

环境变量

Sandbox 命令只获得显式 Workspace env 与 Downcity 执行标识,不继承完整宿主 process.env。Host 命令从宿主环境开始,再叠加 Workspace env。两种执行都会移除 Downcity 鉴权密钥。

workspace.set_env() 与 workspace.patch_env() 只影响后续命令;已启动进程继续使用创建时的环境快照。

失败提示

命令以非零退出码结束或 Shell 状态为 failed 时,工具返回 success: false 和明确的 error,包含退出码(如有)以及最多 2000 个字符的输出尾部。完整的返回输出仍保留在 output 中;交互会话的当前输出为空时,错误提示使用最近的输出预览。

审批未通过时返回 Host execution was not approved.。这也可能是审批网关或调用身份缺失,不能仅凭这条提示判断用户主动拒绝了请求。

本地 Agent 的 Session ID 不区分大小写,创建和查询入口统一为小写。新建和 fork 使用相同的 12 位随机 ID,例如 k7m2x9p4w6rt;字符排除容易混淆的 0/o、1/i/l。自动生成的 ID 直接作为目录名,存储层仍处理自定义标识中的特殊路径字符和 Windows 保留名称。

旧 Session 需要停机迁移后才能使用新规则。迁移脚本默认只预览:

node public/packages/agent/scripts/migrate-session-ids.mjs /path/to/agent-root
# 停止 Downcity,并检查预览与外部引用后执行:
node public/packages/agent/scripts/migrate-session-ids.mjs /path/to/agent-root --apply --offline

工具检查同一来源分区内的大小写冲突,备份活动和归档目录,再更新数据库身份、消息来源与结构化附件路径并重命名目录。旧 ID 只转小写,不改成短 ID。备份目录中的 manifest.json 提供 ID 和路径映射;Task、Messenger、Power 或自定义 Composer 的外部引用由其维护者依据映射同步更新,完成后再启动 Downcity。迁移失败时保持停机,从备份恢复两棵 Session 目录。

审批

每次 host 启动,以及向 host Shell Session 写 stdin,都会经过当前 Chat Session 注入的审批网关。审批属于当前工具调用,但创建出的 Shell Session 仍是 Workspace Shell 资源。

const pending = await session.interactions();
await session.respond({
  interaction_id: pending[0].request.interaction_id,
  response: {
    type: "approval",
    outcome: "resolved",
    payload: { decision: "approved" },
  },
});

ask 是默认审批模式。always-allow 只影响当前 Chat Session 后续的新 host 审批请求,不改变 Sandbox 隔离边界。