我们最近对 Suna 框架做了一次深度的能力升级——全面支持 Anthropic 强推的 Agent Skills 规范。在这个过程中,我们的目标不仅仅是"能跑通几个新脚本",而是要将它彻底揉进 Suna 已有的多租户架构与 Daytona 远程沙盒体系中去。
下面是我们从设计、实现,最后到解决诡异基础环境 Bug 的全记录,希望能为你之后的 Agentic 开发提供避坑指南。
一、架构选型:为什么是"渐进式暴露"?
Agent Skills 官方提倡通过详细的指令本(SKILL.md)直接教授大模型如何使用特定的复杂工具链(比如金融技术分析打标算法)。但这里存在一个巨大的坑:如果一个用户的专属 Agent 挂载了 20 个技能,每个 SKILL.md 有上千字,把它们全部在初始化时"塞进"System Prompt,结果就是 Token 直接被挤爆,不仅极度烧钱,还会让大模型产生幻觉,抓不到主次。
为了解决这个问题,我们为 Suna 设计了渐进式暴露(Progressive Disclosure)与 JIT (Just-In-Time) 加载机制:
第一层(目录导览):在每一次对话前,Suna 后端只会为 Agent 注入非常精简的 <available_skills> 列表,仅包含名称和一句话简介。模型知道"我有一个图表分析技能"。
第二层(按需激活):面临实际诉求时,Agent 会利用我们预先埋放的核心元工具——activate_skill(skill_name="ta-chartbot")。这个工具被调用不仅会把整本 SKILL.md 手册传阅给模型,还会触发后端向靶向沙箱环境里的动态代码注射。
二、后端流水线:技能目录与数据库同步
首先要解决服务如何感知这些新外挂:
构建 Scanner 解析器:我们写了一个轻量级的 SkillScanner(在 core/skills/scanner.py 中),它会自动扫描 Suna 后端的公共外挂库(backend/skills/public)。它可以稳健地剥离解析 SKILL.md 中的 YAML Frontmatter 元数据(提取出工具名称、版本和描述说明)。
多租户与 Agent 绑定引擎:我们重构了数据库(通过 Supabase/PostgreSQL 的 Junction Table),使得不同的企业隔离管理属于他们自己的账号级 / Agent 级自定义技能,做到既可使用公共预设,也可以开发本地专有脚本。
改造 System Prompt:我们在向大模型投喂上下文时,在 prompt_manager.py 里拦截拼装了当前 Agent 已启用的所有 Skill 元数据,做到热插拔生效。
三、文件跨端同步:让 Daytona 的沙箱"临时长出触手"
这是最精妙的一环。因为不想让每一个无技能调用的极简沙箱都在启动时傻傻背负几百个脚本文件,我们利用了 Daytona API 强大的底层文件系统穿透。
我们在 activate_skill(实现于 expand_msg_tool.py 中)编写了一套 JIT 复制钩子。当模型真正按下 activate_skill 这个扳机的一刹那,后端才根据这个项目绑定的 Daytona 沙箱对象,调用 sandbox.fs.upload_file 发起跨服代码投递:
把物理机硬盘里的分析脚本提取出来,在远端沙箱的 /workspace/.skills/ta-chartbot/scripts/ 下一比一克隆。
随后模型顺理成章地用 Bash 指令执行被唤醒的脚本。此时执行环境才真正充盈饱满起来。
四、渡劫:基础架构与权限治理的重拳出击
跑通主流程后,考验这套系统上限的就是真机的连环 Bug 与 Permission Hell(权限地狱)了。我们在对接 TA-Chartbot 这个对环境依赖很重的复杂 Python 分析技能时,遇到了三大劫难:
劫难 1:“不能言语"的 Locale 警告风暴
在模型跑终端指令时,输出里被疯狂塞满了数不尽的 cannot change locale (en_US.UTF-8) 警告。这种噪音既浪费 Token 又迷惑 AI 判断。
破局点:在生成 Daytona 沙箱所依赖的核心基础镜像的 Dockerfile 中,直接前置安装了 locales 工具,配置并拉起 en_US.UTF-8 为默认语言集。直接在底座里将这个噪音掐死。
劫难 2:神秘失踪的目录与 Permission Denied
因为我们需要将代码动态下发到 /workspace。但某些不同底层策略拉起的 Daytona 环境中,如果采用精简包或者非 Devcontainer 包,这个目录根本不存在。而由于权限太低,普通用户态的模型用虚拟 bash 也无法凭空按需创建,导致各种 Permission Denied,整个系统崩溃。
破局点(三重火力覆盖):
- 改写镜像底座(Docker):我们在全局系统镜像的 Dockerfile 层
RUN mkdir -p /workspace && chmod 777 /workspace,进行提前占位并抛开权限枷锁。 - 沙箱伴随初始化生命周期(Sandbox Lifecycle):光有 Docker 不够,为了防止 Daytona API 强行覆写重置权限,我们在 Suna 创建沙盒的核心逻辑类
sandbox.py里强行注入了一个"防呆"Session 过程:只要新建了沙箱对象实例,立刻发出原生钩子拦截命令:sudo mkdir -p /workspace && sudo chmod 777 /workspace。 - 彻底以 Root 模式接管:对创建该实例的 Daytona API
CreateSandboxFromSnapshotParams参数里直接新增配置user="root",保证 Suna 所有后向调用直接降临最高特权。
劫难 3:脚本创建导致的后续隔离壁垒
技能附带的 Bash bootstrap.sh 在建立虚拟环境 .venv 时如果用的不是统一的用户态,会导致 Python 生成文件所有权错乱。
破局点:我们连夜对 Agent 的公开技能库代码进行了加固。不管外部体系给什么环境,在技能初始化的时候强制使用 sudo mkdir -p 提权后,紧跟一行 sudo chown -R $(whoami) ... 把该目录所有权还给当前执行身份。做到在任何复杂、被污染的文件系统属主链中保证自己的可用性。
结语
从业务侧的"渐进式 Prompt"思维创新,到核心系统级别的同步下发,再到对 Daytona 虚拟化底层的严防死守——我们把 Agent Skills 在 Suna 这一套体系跑通了。现在的 Suna 完全能够在沙箱内外通过极小的负担,赋予大语言模型调用千万级别的复杂 Python 后端工具的能力。而且,整个启动都是静默的、秒级的、安全的。