本页目录(30 节)

自定义工具与工具包

Spark Work 自定义工具完整指南:Tool Studio 的 http / code / provider-vision 三种适配器与真实字段约束,以及 Tool Package 的 spark-tool.json manifest、五个 runtime.adapter、六条安装来源、受管工程 install/build、OS 行为与 Spark Capability 权限模型、密钥与启用卸载规则。附 11 个 custom_tools_* 与 22 个 tool_packages_* 工具清单和常见坑排查表。

阅读约 20 分钟最后核对 2026-09-19

Spark Work 里有两套彼此独立的“自定义工具”体系,名字很像但存储表、运行时、权限模型、 Agent 工具面全都不同:Tool Studio 自定义工具(声明式,适合接一个 API 或写一段纯逻辑) 与 Tool Package 工具包(工程化,可带多文件代码、依赖与自己的进程)。 分不清该用哪个,是这一块最常见的困惑,所以本文先从两者的边界讲起,再分别给出字段、 真实限制与排查方法。

1. 两套子系统与入口

1.1 Tool Studio 与 Tool Package 不是一回事

两者在代码里是并列的两个子系统,各自的数据库表都不同——迁移脚本里明确写了 「These tables are independent from the legacy custom_tools tables」。

维度Tool Studio 自定义工具Tool Package 工具包
存储表custom_tools / custom_tool_versions / custom_tool_invocationstool_packages / tool_package_versions / tool_package_tools / tool_package_config / tool_package_permissions
运行时适配器http / code / provider-visionprocess / remote-http / declarative-http / mcp-import / legacy-custom-tool
能不能带依赖、多文件不能。代码工具是单文件 TypeScript,且禁止 import能。可以是完整工程,pnpm install / npm install 后跑构建
版本模型草稿版本 + 稳定版本,支持回滚不可变版本快照,可存多版本、可删单个版本
权限模型只有 risk / effect / idempotency 三档标注额外有 OS 行为声明与 Spark Capability 授权,未授权会被运行时拦截

两者的唯一联系是一条单向兼容桥:工具包的 manifest 可以写 adapter: "legacy-custom-tool" 委托给某个已有的自定义工具, 反过来不行。

该用哪个?平台自己的 Agent 系统提示词给了明确口径:当用户要通过对话造一个“有能力的”工具时,先调 tool_packages_guide,并且明确要求 「Do not reduce the design to legacy http/code/provider-vision types」。 也就是说:需要依赖、多文件、独立进程 → 工具包;只是接一个 REST API 或写一小段纯逻辑 → Tool Studio。

1.2 入口:扩展中心 → 自定义工具

界面入口是左侧导航的「扩展中心」(nav.extensions), 页签依次是「MCP / 自定义工具 / 连接器 / 团队商店」。自定义工具页签里没有二级菜单, 而是顶部四个视图页签:

视图页签内容
工具 n已创建的自定义工具列表,行内可开关启停、删除
工具包 n已安装的 Tool Package 列表、环境变量、权限审批、诊断
开发中 n只有存在未发布草稿的工具
运行记录本机调用轨迹(默认保留 30 天,可改 7/30/90/365 天,可清空)

页头副标题的原文是「开发、测试、发布和观测自定义工具;草稿不会影响 Agent 当前使用的版本。」

除了这个页面,还有两个容易被忽略的消费面:

  • 工作流节点可以直接调用:工作流里的「平台工具直调(自定义工具/工具包)」节点可以选具体工具; 运行时每次调用都会即时读取目录,所以刚发布/刚启用的工具不用重启工作流。
  • 聊天里的图像理解活动卡片可以深链回来:卡片上的「在 Tool Studio 打开」会跳回编辑页。

2. Tool Studio:三种可跑适配器

2.1 实际可执行的只有 http / code / provider-vision

工具类型在代码里有三套口径,长度还不一样,这是最容易被文档写错的地方:

口径取值说明
协议枚举(TypeScript)http / sql / command / prompt / code / provider-vision6 个,都能通过 schema 校验
数据库 CHECK 约束上面 6 个 + composite7 个。composite 只存在于 SQLite 约束里,代码中零引用,是历史迁移遗留
实际可创建 / 可执行http / code / provider-vision只有 3 个。其余类型调用时直接抛未实现错误

也就是说,sql / command / prompt 虽然协议里有完整字段定义, 但服务层用白名单把它们挡在外面(错误信息是「「sql」类型工具尚未开放(当前版本支持 http、code、provider-vision)」)。 导入一份老的定义、或让 Agent 生成这几种类型,都会在创建/保存/测试时被拒。

界面上「创建工具」有 6 个入口:从空白创建 / 粘贴 cURL / 导入 OpenAPI / 编写 TypeScript / 使用模板 / 导入工具包。其中「使用模板」下只有两个二级模板:HTTP API 与图像理解。 贴 cURL 会自动拆出方法、Header、Body 与密钥引用;导入 OpenAPI 支持 JSON / YAML, 选完 operation 会批量生成待审草稿。

2.2 HTTP 适配器

适合已经有 REST / 内网接口的场景。由宿主统一执行,SSRF 防护、重定向、超时和响应大小都在宿主侧治理。

字段约束
method仅 GET / POST / PUT / PATCH / DELETE
urlTemplate≤ 2000 字符,必须以 http:// 或 https:// 开头;参数占位符写成 {{参数名}}
headers最多 32 条;每条要提供 valueTemplate 或 secretRef,二选一,不能都给
body{ mode: "json", jsonTemplate: "…" },模板 ≤ 100000 字符,保存期就做结构校验
response.formatjson / text / markdown-table
response.extract最多 32 条 { label, jsonPath },用于从大响应里挑字段
response.maxSizeBytes上限 1 MiB,界面默认 262144(256 KiB)
allowPrivateNetwork默认 true。只有显式设为 false 时才挂上内网黑名单(覆盖 IPv4 15 段 + IPv6 11 段,且每个重定向跳都生效)

两条硬性限制要注意:

  • URL 里不能带账号密码(https://user:pass@host),也不能把 token 放在查询参数里—— 会被直接拒绝,提示改走 Keychain 请求头。
  • 敏感请求头必须绑定密钥:像 Authorization、X-Api-Key、Cookie 这类名字,写了 valueTemplate 会被拒;必须给 secretRef。

输出的 token 预算是 8000,超出后按头尾截断,所以在提示词里别指望拿到完整的大 JSON。

2.3 代码适配器

用来写纯逻辑,并且组合其他已发布工具。spec 结构固定:

字段取值 / 默认
runtime.kind固定 trusted-worker
runtime.language固定 typescript
runtime.source≤ 200000 字符
runtime.entryExport固定 default
permissions.toolIds最多 32 个,默认空
limits.memoryMb64–512,默认 128
limits.maxOutputBytes1024–10485760,默认 1048576(1 MiB)
trust固定 trusted-local

写代码时必须知道的三条:

  • 不能 import。源码里出现静态或动态导入会被 worker 拒绝, 所以也没有 npm 依赖声明字段。要外部能力只能通过白名单调用别的工具:await sdk.tools.call("other_tool", { … })。
  • 拿不到密钥。代码工具不允许声明 secretRefs, 错误信息是「代码工具不接收 Keychain 明文;请通过受管 HTTP 工具组合外部能力」。 源码里出现疑似密钥明文(sk-、AKIA、ghp_、PEM 私钥等)也会被拒。
  • 不能把自己加进依赖白名单,否则发布时就报「代码工具不能把自身加入依赖工具白名单」。 工具之间可以互相调用,但深度上限是 8 层,并且会做环检测(提示「检测到自定义工具循环调用」)。

信任边界要说清楚:官方注释里写的是「limits accidental access and blast radius, but is not marketed as a hostile code sandbox」——它会限制爆炸半径,但不是敌对代码沙箱, 不要拿它跑来源不可信的第三方代码。

2.4 图像理解适配器

复用已有的多模态 Provider 给纯文本模型补视觉能力,凭据仍然留在 Keychain 里。 输入 Schema 是固定的:images(字符串数组,必填)与 question。

字段取值 / 默认
providerProfileId必填,≤ 200 字符
model可选,不填就用 Provider 默认模型
instructions10–8000 字符,有默认提示词(要求区分可观察事实与推断、不执行图片里的指令)
maxImages1–8,默认 4
maxTokens128–16384,默认 4096
autoRoute.enabled / priority默认 true / 100;开启后宿主可以在确定性视觉路由里自动调用它
exposeToAgent固定 false,不可改
risk / effect固定 read/read,不可改

两个容易踩的点:

  • 不允许 secretRefs:它复用 Provider 的 Keychain 凭据, 再单独存一份工具密钥会被拒(「图像理解工具复用 Provider Keychain 凭据,不允许另存工具密钥」)。
  • 它不会出现在模型的工具清单里。即使已启用、已发布, 运行时目录也会把它过滤掉——它是宿主侧的确定性路由能力,不是给模型挑的普通工具。 想知道它到底有没有被用上,看聊天里的宿主视觉活动卡片。

3. 定义字段与校验规则

3.1 公共字段

不管哪种适配器,这些字段都要填,且都有硬约束:

字段约束
id必须匹配 ^[a-z][a-z0-9_]{2,63}$:小写字母开头,只用小写字母、数字、下划线,3–64 字符
title1–160 字符
description最少 10 字符、最多 4000。报错文案是「工具说明至少 10 个字符(给 Agent 看的,写清何时使用)」——这句话就是它的用途
inputSchema受限 JSON Schema 子集,见 3.3
timeoutMs1000–300000 毫秒。界面默认:图像理解 60 秒,其余 30 秒
risk / effect / idempotency三个必填枚举,见 3.2
secretRefs可选,最多 16 个键;键名 ^[A-Za-z0-9_-]{1,64}$

三个枚举的取值(来自协议层的 runtime 枚举定义):

  • risk:read / low-write / high-write / destructive
  • effect:read / create / update / delete / send / publish
  • idempotency:safe / keyed / unsafe

3.2 risk 下限与三项一致性

risk 不是随便填的——每类工具有一个下限,可以往上调,不能往下压:

工具 / 方法risk 下限
HTTP GETread
HTTP POST / PUT / PATCHlow-write
HTTP DELETEdestructive
code / provider-visionread
sql(只读模式)read
sql(读写模式)high-write
commandlow-write

另有两条一致性规则,违反会直接被拒:

  • risk 是 read 时,effect 必须是 read。
  • risk 是 destructive 时,idempotency 必须是 unsafe。

界面会在你改 method 时自动派生这三个值:GET → read/read/safe,DELETE → destructive/delete/unsafe,POST → low-write/create/unsafe,PUT → low-write/update/keyed,PATCH → low-write/update/unsafe。risk 还会影响一件事:只有 risk 为 read 的工具才会被模型自动放行, 其余都要走确认。

3.3 输入 Schema 是受限子集

这不是完整的 JSON Schema。它是手写不合法、由编辑器表单生成的受限形式,约束如下:

  • 顶层固定 { type: "object", properties: {…}, required: […] },不允许别的写法。
  • 参数名必须匹配 ^[a-zA-Z_][a-zA-Z0-9_]*$,参数最多 32 个。
  • 参数类型只有 5 种:string / number / integer / boolean / array。
  • 每种类型可带 title(≤120)、description(≤1000)、default、enum(最多 50 项)。
  • items 只能用在 array 上,且元素只能是原始类型。
  • required 里出现的名字必须在 properties 中已声明。

没有 outputSchema 字段——工具返回什么,由执行器决定: HTTP 由 response.format 与 extract 决定,代码工具就是默认导出函数的返回值(序列化后按maxOutputBytes 校验),统一以 markdown 文本形式回给模型,并可能被截断。

模板占位符(URL、Body、Header)只能引用已声明的参数,否则保存期就报错, 所以「先加参数、再写模板」这个顺序不要反过来。

4. 密钥:只声明引用,不填值

这是整个自定义工具体系里设计得最收紧的一块,三层都禁明文。

密钥的实际存放位置是系统 Keychain(keytar),不进 SQLite:

  • 密钥引用(secretRefs)的标准形式是 custom-tool:<工具ID>:<密钥名>。
  • 数据库里存的只是这个引用字符串,明文永不落库。
  • 写入入口只有一个:Studio 编辑页里的「本机密钥」表单,文案是 「密钥写入系统 Keychain,不进入 SQLite、导出文件或工具描述。」

三个拒绝明文的位置:

  1. Agent 工具面没有写密钥的工具。custom_tools_* 一共 11 个, 其中没有任何一个能写入密钥值;custom_tools_create_draft 的描述里就写着 「密钥只能声明 secretRefs,禁止在参数中传密钥值」。
  2. 定义里不能出现疑似密钥明文。URL 模板、Body 模板、Header 模板、代码源码都会被扫; 命中 sk-…、AKIA…、ghp_…、xox…、 硬编码 bearer …、PEM 私钥等形态就报「模板中检测到疑似密钥明文,请改用密钥库(secretRefs)存储」。
  3. 声明了就必须用上。secretRefs 里的每个名字都必须被某个请求头的 secretRef 实际引用,否则报「密钥 X 未被任何请求头引用」。

另外 custom_tools_get 只返回「密钥位是否已配置」(每项一个布尔值),永不返回密钥值;Agent 侧测试的输出也会用 Keychain 真值加常见凭据形态做脱敏替换。 发布或回滚之后,不再被引用的密钥会被自动清理;删除工具时其专属密钥会被一并删除, 若 Keychain 清理失败,工具会保留记录并提示重试,不会留下“删了一半”的状态。

5. 生命周期与 Agent 工具面

5.1 草稿版本与 CAS

每个自定义工具有两个版本号:publishedVersion(Agent 当前实际使用的稳定版本,可以为空) 和 draftVersion(工作副本)。draftVersion > publishedVersion 就表示存在未发布草稿。

操作真实行为
创建草稿生成一条禁用草稿:enabled=false、publishedVersion=null、draftVersion=1
保存草稿只动草稿版本,不影响在线版本。已有待发布草稿就复写同一版本号,否则开新版本号。类型与 ID 创建后不可修改
发布把草稿提升为稳定版本,旧稳定版本转 archived
回滚不是回退,而是从历史版本新建一个稳定版本,当前版本仍留在历史里可以再恢复。前提是当前没有待发布草稿

发布带 CAS 保护:传入的草稿版本号与数据库不一致会冲突(会提示「草稿已在其他窗口更新,请刷新后重试」), 避免两个窗口同时改一个工具时互相覆盖。Agent 侧对应的参数是 expectedDraftVersion。

第一次发布会自动启用——这是存储层的行为(后续发布沿用当前的启用状态)。 所以「我明明只发布了、没点启用,Agent 怎么能用了」不是 bug。

5.2 什么条件下才进 Agent 工具面

一个自定义工具要出现在模型的工具清单里,必须同时满足三个条件:

  • enabled = 1;
  • publishedVersion != null(发布过);
  • 类型不是 provider-vision。

进工具面后,模型看到的工具名是 custom_ 加工具 ID(例如 custom_weather_lookup)。 刷新的时机是下一轮对话:本轮发布不会立刻影响这一轮已经在跑的工具快照, 要下一轮才生效。

各操作的前置条件(都是硬性拦截):

操作前置条件
测试无——不需要先发布或启用,用正式执行器真跑
发布所有 secretRefs 都必须已写入 Keychain;代码工具的依赖必须已发布
启用已发布 + 稳定版本密钥齐备;未发布时开关是灰的
回滚当前没有待发布草稿 + 目标版本存在 + 目标版本密钥齐备
删除先禁用(若开着)→ 删密钥 → 删记录;Keychain 清理失败会保留记录并提示重试

6. 让 Agent 自己造工具:custom_tools_* 十一个

Agent 可以通过平台工具面完成整套流程。共 11 个工具,按正常顺序如下:

工具作用需要确认参数
custom_tools_guide取开发指南、适配器说明、安全边界与 HTTP / TypeScript 示例—
custom_tools_list列出工具与草稿/发布/启用状态。limit 默认 50、最大 100,返回带 total/truncated—
custom_tools_get读完整工作区(草稿、当前发布版本、版本历史、密钥位是否已配置)—
custom_tools_validate只读校验完整定义,返回精确字段问题与归一化结果—
custom_tools_create_draft创建禁用草稿,不进稳定工具面—
custom_tools_save_draft保存草稿,不影响当前发布版本—
custom_tools_test用正式执行器真实测试confirmExecute(必填)
custom_tools_publish原子发布草稿为稳定版本confirmPublish(必填)
custom_tools_set_enabled启用 / 停用已发布工具confirmEnable 仅启用时需要(不在 required 列表里)
custom_tools_rollback把稳定版本回滚到指定历史版本confirmRollback(必填)
custom_tools_delete删除工具、版本与关联密钥引用confirmDelete(必填)

所有 confirmXxx 的判据都是严格的 === true; 缺了会返回「<动作>需要用户明确确认」。注意 confirmExecute 的含义不只是“我要执行”, 而是用户已经明确知情并同意——真实测试可能访问网络、调用 Provider、产生费用或副作用。

Agent 侧的流水线在指南里写死了六步,照着走就行:

  1. 先 custom_tools_list 查重,避免重复创建或覆盖已有工具。
  2. 生成完整定义后调 custom_tools_validate,不要猜 API、鉴权或副作用。
  3. custom_tools_create_draft 存一份禁用草稿。
  4. 需要真调用时,先向用户说明目标、费用和副作用,再 custom_tools_test。
  5. 用户确认后发布;首次发布会进稳定工具面。
  6. 发布后用下一轮工具清单或真实会话验证;要停用就 set_enabled。

7. Tool Package:spark-tool.json 与五个适配器

7.1 manifest 字段

工具包的核心是根目录一个 spark-tool.json。完整字段与约束:

字段必填约束
schemaVersion是目前固定为 1
id是^[a-z0-9][a-z0-9._-]{2,95}$
version是语义化版本,如 1.0.0,可带预发布后缀
name / description是1–160 / 1–8000 字符
author否{ name, url? }
runtime是五选一的适配器,见 7.2
development否{ installCommand?, buildCommand? },各 ≤ 500 字符
guidance否给模型看的概述 / 共享指令 / 前置条件
tools是数组,最少 1 个、最多 200 个
environment否最多 200 个变量声明,默认空数组
permissions否默认三个空数组

每个 tools[] 条目必填 name / title / description / inputSchema / risk / effect / idempotency,可选 outputSchema 与 guidance。inputSchema 必须是对象类型且能被解析。跨字段校验规则:

  • risk 为 read 时 effect 必须是 read;
  • risk 为 destructive 时 idempotency 必须是 unsafe;
  • 工具名不得重复,环境变量名不得重复;
  • 必要能力与可选能力不能声明同一个名字。

校验失败时没有一套“错误码表”——schema 不通过就是校验器抛出的字段级问题列表, 运行时兜底的错误码常量是 TOOL_PACKAGE_EXECUTION_FAILED; 能力调用子系统的错误码则是 CAPABILITY_NOT_DECLARED /CAPABILITY_NOT_AUTHORIZED / CAPABILITY_UNAVAILABLE /CAPABILITY_FAILED。想快速定位问题,用 tool_packages_inspect 做只读检查更直接。

7.2 五个 runtime.adapter

adapter必填字段用途
processcommand + protocol: "spark-tool-process-v1"真正的进程式工具。可选 args(≤128 个)、lifecycle(默认 per-call)、workingDirectory
remote-httpbaseUrl远端 HTTP 工具服务。可选 headers(≤32,值支持 ${ENV_NAME} 模板)与 timeoutMs
declarative-httptools 映射免代码 HTTP 适配器:把工具名映射到 HTTP spec。映射必须与顶层 tools 一一对应
mcp-importserverId把已有 MCP 服务器的工具包装成工具包,调用时仍代理回原服务器。可选 toolNameOverrides 记录规范化映射
legacy-custom-tooltoolId兼容桥:委托给某个已有的 Tool Studio 自定义工具

写 process 时最容易踩的坑:command 必须是单个可执行文件,参数一律放 runtime.args。 进程不经 shell 拉起,所以 "node index.js" 这种写法会在启动时直接报ENOENT。

两种生命周期的区别:

  • per-call(默认):每次调用新起一个进程,initialize → invoke → shutdown 后退出。
  • persistent:按「包 + 版本 + 环境」复用进程;配置或权限变更后会失效重建。

协议上,stdout 只允许输出协议帧(换行分隔的 JSON,单帧上限 4 MB),任何杂散输出都会破坏帧解析;日志必须写 stderr。

7.3 五条安装路径

先分清两个不同的枚举,它们经常被混为一谈:runtime.adapter(上面五个,决定怎么执行) 与安装来源 source(六个值:managed-project /local-directory / local-archive / registry /remote / mcp-import)。

入口落库 source说明
install_directory(managed-project)managed-project把受管工程复制为不可变版本
install_directory(local-directory)local-directory把任意本地目录复制为不可变版本
install_archivelocal-archive解压 .zip;支持单层包裹目录,自动跳过 .git、__MACOSX、.DS_Store
install_gitregistry浅克隆(--depth 1)。注意 source 落的是 registry 而不是 git
install_remoteremote只登记 manifest,不落代码、不发任何网络请求
install_mcp_importmcp-import自动生成 manifest;工具风险硬编码为 low-write / update / unsafe

所有安装都遵守同一条不变量:装完保持一致禁用,且不执行任何包内代码。

几个需要留意的边界:

  • 安装新版本不会自动启用它。数据库写入时状态固定为 installed-disabled; 对已启用的包再装新版本,旧版本仍然是指向中的启用版本,需要你手动切换。
  • 同一个版本号不能改内容。已存在同版本时,内容摘要不同会被拒绝;相同则幂等返回。
  • 界面只有四条导入路径:本地目录、压缩包、Git 仓库、MCP 服务器。remote-http 目前没有界面入口,只能通过 Agent 工具或 API 安装。
  • Git 导入会校验 ref 与子目录;浅克隆默认超时 5 分钟、上限 15 分钟。找不到 git 时会给安装指引。

8. 受管工程:让 Agent 写多文件工具

8.1 五个工程工具

受管工程是 Agent 从零写一个工具包的工作目录(默认在应用数据目录的 tool-projects 下)。 五个工具的语义:

工具语义
tool_packages_create_project写一个已校验的 spark-tool.json 和初始文件,不安装、不构建、不执行、不启用。 文件数 ≤100、总字节 ≤4 MB;目录已存在会报错;文件列表里不能再放 spark-tool.json
tool_packages_write_project_file写单个 UTF-8 文件,单个 ≤ 2 MB;拒绝符号链接与路径逃逸
tool_packages_read_project_file读单个 ≤ 2 MB 的 UTF-8 文件
tool_packages_list_project_files只列路径与字节数;跳过 node_modules/.git;上限 50000 个文件
tool_packages_run_project_step在工程目录真实执行 install 或 build,需要 confirmExecute=true

界面上的「源码工程」编辑器对应这几个能力:选文件、改源码、保存文件、安装新版本。注意安装前必须先保存,否则装进去的还是旧内容。

8.2 install / build 步骤

两个步骤的命令来源与推断规则不同:

步骤命令来源
install优先用 manifest 的 development.installCommand;没声明就按 lockfile 推断:pnpm-lock.yaml → pnpm install,yarn.lock → yarn install,bun.lockb → bun install,只有 package.json → npm install。一个都没有就报错,必须显式声明 installCommand
build没有推断:必须声明 development.buildCommand, 否则报错提示「add it to spark-tool.json or skip the build step」

执行时的真实行为:

  • 工作目录是工程根目录,环境变量只透传 PATH/HOME/TEMP 等白名单,另加 SPARK_TOOL_PACKAGE_ID、SPARK_TOOL_PACKAGE_VERSION、SPARK_TOOL_PROJECT_STEP=1,并关掉 npm 的 fund 与 audit 提示。
  • 默认超时 10 分钟,上限 30 分钟;超时或取消会杀掉整个进程树。
  • 输出上限 256 KB,超出保留尾部 64 KB,结果里会标「输出已截断」。
  • 结果会标明命令是「声明命令」还是「推断命令」,并给出退出码、是否超时、是否已取消。

这是真的在本机跑命令,而且进程拥有当前用户权限。执行前必须把要跑的命令展示给用户并拿到同意, 再传 confirmExecute=true。

9. 权限:OS 行为告知与 Spark Capability

9.1 declaredOsEffects 是告知,不是沙箱

manifest 的 permissions.declaredOsEffects 可声明这些值:network、filesystem.read、filesystem.write、process.spawn、clipboard、browser。

但它在代码里只是告知项,没有任何执行期拦截。官方的口径写在指南文本里:

进程具有当前用户权限;declaredOsEffects 是告知与启用确认,不是细粒度 OS 沙箱。

界面上的措辞同样直接:「trusted-local 工具进程拥有当前用户权限。manifest 中的 OS 行为是告知项, 不是操作系统沙箱;Spark Capability 会按授权强制拦截。」 换句话说,启用一个工具包等于信任它的代码,装之前请自己核对命令和声明的 OS 行为。

9.2 Spark Capability 才会被真正拦截

与 OS 行为不同,Spark Capability 有执行期强制校验:进程请求一个能力时, 宿主会先检查它有没有在 manifest 里声明、有没有被授权,未通过就返回 CAPABILITY_NOT_DECLARED 或 CAPABILITY_NOT_AUTHORIZED。

manifest 里分两组声明,名字必须带命名空间(形如 a.b):

  • requiredSparkCapabilities:最多 64 个,必须全部授权工具包才能启用;
  • optionalSparkCapabilities:最多 64 个,缺失不影响启用。

宿主提供的能力按家族划分,常用的包括:

家族能力 id
网络与存储http.fetch、storage.kv.get、storage.kv.set、storage.kv.delete、storage.kv.list
文件files.read、files.write、files.list、files.stat、files.copy、files.move、files.upload、files.present、files.trash
进程与系统process.exec、notifications.show
模型与 Agentmodels.list、models.get、models.invoke、agents.list、agents.get、agents.invoke
桌面交互clipboard.read、clipboard.write、browser.open、dialogs.open、dialogs.save、artifacts.present
浏览器自动化browser.automation.windows、browser.automation.open、browser.automation.navigate、browser.automation.screenshot、browser.automation.inspect、browser.automation.evaluate、browser.automation.close
电脑操作computer.capabilities、computer.inspect、computer.execute
多媒体media.models、media.generate
工作流workflows.list、workflows.run、workflows.status

其中一部分是逐次确认的高风险能力,每次调用都会弹确认框:process.exec、files.trash、media.generate、computer.execute、workflows.run,以及大部分 browser.automation.*。 执行 process.exec 前会把实际命令展示出来给你确认。另外 process.exec 接收的是参数数组、不经 shell。

授权状态用 tool_packages_set_permission 管理,kind 取 os-effect 或 spark-capability,state 取 pending / granted / denied。 界面上必须在确认框里勾选后才写库。有个重要副作用:把一个必需能力改回非 granted,会自动把该工具包停用,避免它带着未授权状态继续跑。

10. 环境变量与密钥

manifest 的 environment 声明变量名、类型、是否必填、是否密钥。变量名必须匹配 ^[A-Z_][A-Z0-9_]{0,127}$,类型取string / number / integer / boolean / json。

两条与密钥有关的硬约束:

  • 密钥变量不能有默认值(会直接报错);
  • 密钥变量只能是 string 类型。

secret 与普通变量在存储和权限上是两套完全不同的路径:

维度普通变量secret
存储位置SQLite 的 value_json 列系统 Keychain,库里只存引用
Agent 能否写能,但需要 manifest 声明 agentConfigurable: true不能,只能发起一次性安全输入请求
读取状态接口会回显值只返回「是否已配置」
日志原样子进程 stderr 中出现密钥值会被替换成脱敏标记

密钥的填写流程是:Agent 调 tool_packages_request_secret 创建请求(只返回请求状态), 应用内弹出受保护的输入表单,用户填完点「保存到 Keychain」。 表单上的说明是「密钥不会发送给 Agent,也不会写入会话记录或 SQLite;主进程收到后会直接保存到系统 Keychain。」 请求有有效期(默认 15 分钟),过期需要重新发起;同一目标已有待处理请求时会直接复用,不会重复弹窗。

普通变量的配置可以限定作用域:package / tool / project / agent / workflow / session。 同一个 API 在 Agent 侧以 agent 身份调用、在界面以 user 身份调用,权限不同—— Agent 只能改声明了 agentConfigurable 的变量。

在 process 适配器里,声明过的变量(含从 Keychain 取出的密钥)会注入到工具进程, 另外还有基础变量与 SPARK_TOOL_PACKAGE_ID / SPARK_TOOL_PACKAGE_VERSION / SPARK_TOOL_PROCESS_PROTOCOL。

11. 启用、测试、卸载与删版本

工具包的启用前置条件比自定义工具严格得多,缺一条都不给启用:

  1. 目标版本存在且状态是已安装;
  2. 信任级别不是 blocked;
  3. 适配器在可执行白名单内;
  4. mcp-import 类型要求原 MCP 服务器仍然存在;
  5. 所有必需能力在当前宿主上都可用;
  6. 所有必需权限都已经是 granted;
  7. 所有必填环境变量都已配置。

启用后,包内工具在下一次 Agent 循环动态注册,之后就和内置工具一样被模型自主调用。 停用不需要确认;启用需要(confirmEnable=true)。

其余三个操作的硬约束:

操作约束
tool_packages_test不需要先启用,按你指定的版本和工具名真实执行,需要 confirmExecute=true。 成功返回结果、耗时与 correlationId,失败返回错误码与信息
tool_packages_uninstall必须先停用;会终止进程、删除全部不可变版本、数据库记录与 Keychain 密钥。 受管工程源码默认保留,只有传 removeManagedProject=true 才删
tool_packages_delete_version不能删当前启用的版本,也不能删最后一个版本(那种情况只能整体卸载)

卸载受管工程前界面会问两次:先确认卸载,再单独确认要不要删源码,文案是 「保留受管工程源码时可以继续开发并重新安装;删除后源码目录(tool-projects)不可恢复。」 启用状态下点卸载会直接被拦下并提示「请先停用工具包,再执行卸载」。

另外,tool_packages_test 会在诊断区展示运行进度、日志流与调用轨迹; 要排查「装了但跑不起来」,先看诊断区,再决定是改 manifest 还是改代码。

12. 常见坑与排查

现象原因与处理
只「发布」了没点启用,Agent 却能调用它首次发布会自动启用,这是设计行为。不想要就发布后手动停用
启用了、也发布了,但模型工具清单里没有它检查类型是不是 provider-vision——它按设计不进入模型工具面。 另外刷新发生在下一轮对话,本轮不生效
发布报「草稿已在其他窗口更新」草稿版本 CAS 冲突:另一个窗口改过这个工具。刷新后重试
回滚按钮点了没反应 / 报错回滚要求当前没有待发布草稿。先处理掉草稿(发布或丢弃)再回滚
保存定义时报「密钥 X 未被任何请求头引用」声明了 secretRefs 但没在请求头里用。要么补上引用,要么删掉这条声明
写 Authorization 头被拒敏感头名不能写明文 valueTemplate,必须绑定 secretRef
代码工具里 import 报错代码工具禁止任何导入。要外部能力就用 permissions.toolIds 声明依赖,再用 sdk.tools.call
调用另一个工具报「检测到自定义工具循环调用」依赖链成环,或嵌套深度超过 8 层。捋一下依赖图
工具包进程起不来,报 ENOENTcommand 必须是单个可执行文件,参数放 runtime.args。"node index.js" 这类整串写法必错
工具包进程一起就崩,日志里没有自己的输出stdout 只能输出协议帧,杂散输出会破坏帧解析。日志改写到 stderr
install 步骤报没有可用 lockfile认的是 pnpm-lock.yaml/yarn.lock/bun.lockb/package.json。都没有就在 manifest 里显式声明 development.installCommand
build 步骤报错说没声明命令build 不做推断,必须在 manifest 里声明 development.buildCommand
装了新版本,跑的还是老版本安装不会自动切换启用版本,要到详情里显式切到新版本
同一个版本号重新装被拒不可变版本要求内容摘要一致;改了内容请升版本号
调用能力报 CAPABILITY_NOT_AUTHORIZED能力已声明但没授权。去权限区把它设为 granted(必需能力未授权会导致包被自动停用)
Agent 说写不了密钥设计如此。Agent 只能发安全输入请求,密钥必须由你在应用内表单填写
卸载时提示先停用先关掉启用开关再卸载
想把最后一个版本删掉不允许。最后一个版本只能整体卸载工具包

最后提醒一句:截至当前版本,Tool Studio 之外还有 sql / command / prompt 三种类型的协议定义与完整的 spec schema,但它们尚未进入稳定运行面,创建和调用都会被拒绝。 看到别人写的这类定义,不要在文档或提示词里把它们当成可用能力。

速查表

界面入口
扩展中心 → 自定义工具页签(顶部四个视图:工具 / 工具包 / 开发中 / 运行记录)
可执行的适配器
http、code、provider-vision(sql/command/prompt 协议保留但未开放)
工具 ID 规则
^[a-z][a-z0-9_]{2,63}$,即小写字母开头、3–64 字符
工具说明最短
10 字符(给 Agent 看的,最长 4000)
超时范围
1000–300000 毫秒;界面默认图像理解 60s、其余 30s
输入参数上限
32 个;类型仅 string/number/integer/boolean/array
密钥引用格式
custom-tool:<工具ID>:<密钥名>,最多 16 个,存系统 Keychain
进 Agent 工具面的条件
enabled=1 + 已发布 + 类型非 provider-vision(下一轮生效)
自定义工具组合深度
最多 8 层,且做循环调用检测
manifest 文件名
spark-tool.json(schemaVersion 固定为 1)
runtime.adapter 取值
process、remote-http、declarative-http、mcp-import、legacy-custom-tool
安装 source 取值
managed-project、local-directory、local-archive、registry、remote、mcp-import
受管工程单文件上限
2 MB;文件数上限 50000;创建工程时文件数 ≤100、总字节 ≤4 MB
step 超时
默认 10 分钟,上限 30 分钟;输出上限 256 KB(保留尾部 64 KB)
install 命令推断
pnpm-lock.yaml → pnpm,yarn.lock → yarn,bun.lockb → bun,package.json → npm;build 无推断
OS 行为枚举
network、filesystem.read、filesystem.write、process.spawn、clipboard、browser(仅告知)
密钥请求有效期
默认 15 分钟(范围 1–60 分钟);同一目标重复请求会复用
运行记录保留
自定义工具默认 30 天,可选 7/30/90/365 天

常见问题

展开 6 条常见问题
  • Tool Studio 自定义工具和 Tool Package 工具包,我该用哪个?

    看需不需要依赖和多文件代码。只是接一个 REST 接口、或写一小段纯逻辑,用 Tool Studio(http / code / provider-vision 三种适配器,单文件、禁止 import)。需要 npm 依赖、多文件工程、独立进程,或者想把已有 MCP 服务器的工具包装起来,用 Tool Package。平台的 Agent 系统提示词也是这个口径:需要“有能力的”工具时先走 tool_packages_guide,并明确要求不要把设计缩减成 legacy 的 http/code/provider-vision。

  • 我的自定义工具发布了,为什么模型的工具清单里没有它?

    三个条件要同时满足:已启用、有发布版本、类型不是 provider-vision。图像理解工具按设计不进入模型工具面,它是宿主侧的确定性路由能力,想知道有没有生效要看聊天里的宿主视觉活动卡片。另外刷新发生在下一轮对话,本轮发布不会立刻生效。

  • 我只点了发布,没点启用,Agent 怎么就能用了?

    首次发布会自动启用,这是存储层的既定行为(后续发布会沿用当前启用状态)。不想要的话,发布完手动停用即可。

  • Agent 能不能帮我填 API Key?

    不能,这是两端都做了硬限制的。自定义工具侧:11 个 custom_tools_* 里没有任何写密钥的工具,create_draft 的描述明确写着「密钥只能声明 secretRefs,禁止在参数中传密钥值」。工具包侧:configure_environment 遇到 secret 会直接拒绝,只能通过 request_secret 发起一次性安全输入请求,由你在应用内的受保护表单里填写,直接写入系统 Keychain。

  • 安装工具包后它会不会自动跑起来?

    不会。所有安装路径都保持一致禁用且不执行包内代码。装完需要你自己核对 OS 行为声明、完成必需能力的授权、补齐必填环境变量,然后手动启用;启用时会有明确确认。另外安装新版本不会自动切换到新版本,旧版本仍是指向中的启用版本。

  • 工具包的 declaredOsEffects 会限制它访问文件或网络吗?

    不会。declaredOsEffects 在代码里只用于落库、警告和启用确认,没有任何执行期拦截;官方口径是「进程具有当前用户权限;declaredOsEffects 是告知与启用确认,不是细粒度 OS 沙箱」。真正会被运行时强制拦截的是 Spark Capability(未声明或未授权会返回 CAPABILITY_NOT_DECLARED / CAPABILITY_NOT_AUTHORIZED),其中一部分高风险能力还会逐次弹确认框。