架构说明

August 18, 2026 · View on GitHub

为什么不用 web.fetch

DSH Host 的 web 服务(ctx.web.fetch)只接受 { url },设计目标是「安全检索」, 不能携带自定义请求头与 POST body。而 PostgREST 必须带 apikey / Authorization 头, 写操作还要发 JSON body。因此本插件通过 ctx.shell 执行外部 HTTP 客户端作为传输层。

传输层:为什么是 Node fetch 而不是 curl?

v0.1.0 曾以 curl 为主力,实测(Windows + 沙箱环境)暴露出两个问题:

  1. schannel TLS 故障:Windows 自带 curl.exe 使用 schannel 作为 TLS 后端, 在受限执行环境下报 SEC_E_NO_CREDENTIALS (0x8009030e),无法建立 HTTPS 连接。 Node 使用 OpenSSL,不受影响;且 DSH 本身运行在 Node 上,node 必然可用。
  2. stdin 管道被拦PowerShell 管道 | node -(原生进程 stdin)在沙箱下静默失效; 而 node -e 参数方式可用。

最终方案(v0.1.1+):

  • node -e "<脚本>" 执行一次 fetch;脚本只用单引号、不含 $ 与反引号, 因此同一个命令串可同时被 pwsh 和 bash 正确解析;
  • 密钥 / URL / 方法 / 请求体全部走环境变量SB_URL / SB_METHOD / SB_KEY / SB_BODY);
  • 响应尾部附加 \nXHTTPSTATUS:<code> 标记以获取真实状态码;
  • Node 不可用时降级到 curl 路径(保留了 stdin 传 body 与 -w 状态码标记)。

踩坑记录(Windows 沙箱)

现象结论
curl.exe schannelSEC_E_NO_CREDENTIALSWindows 原生 TLS 后端在受限环境不可用,用 Node/OpenSSL 绕开
pwsh 原生参数引号单引号内的双引号传给 node -e 后被吞掉双引号包裹、脚本内部只用单引号;反之亦然
PowerShell 管道 → 原生 stdin`"code"node -` 无输出、脚本没执行

为什么不用 Invoke-RestMethod?

  • 它同样依赖 schannel(.NET SslStream),无法绕开上面的 TLS 问题;
  • PowerShell ConstrainedLanguage 沙箱对 .NET 调用的限制会增加不确定性。

执行器与传输探测

  • 首次调用执行 $PSVersionTable.PSVersion.ToString():退出码 0 → pwsh(Windows),否则 bash;
  • 首次调用执行 node --version:退出码 0 → Node 传输,否则 curl 后备;
  • 结果缓存在插件内存中(插件生命周期内有效)。

密钥与注入安全

边界做法
命令行(argv)密钥不进入命令字符串(Node 脚本从 process.env.SB_KEY 读取)
子进程环境通过 ShellExecRequest.env 注入,每次调用单独构建
请求体JSON 串写入环境变量 SB_BODY(受 Windows 环境块 ~32 KiB 限制,插件设 16 KiB 软上限)
工具结果supabase_configure 只返回「已存储」状态,绝不回显密钥
持久化credentials.set('SUPABASE_*') 写入 DSH 凭据服务;只读源拒绝时降级为会话内存

配置解析顺序

插件内存(本会话 configure 过) > 凭据服务 resolve(环境变量)

每个请求实时解析,不跨操作缓存(凭据变更后下一次调用即生效)。 插件升级(cordis update)会重建实例、清空内存,但凭据服务中的配置仍然有效。

HTTP 状态码怎么拿到的

fetch / curl 在 4xx/5xx 时都不会「抛错」(curl 未加 -f),响应体照常输出。 为了让「PostgREST 报错」和「正常数据行里恰好有 code/message 列」可区分, 响应尾部追加 \nXHTTPSTATUS:<code>,解析时从尾部提取:

  • >= 400:按 PostgREST 错误对象解析,返回 message,可选 code / details / hint
  • < 400:解析 JSON 数组,返回 rows / count

无损 JSON 契约

沙箱要求工具 execute 的返回值是纯无损 JSON:任何值为 undefined 的字段都会让 调用整体失败(harness.defineTool execute result ... must be lossless JSON data)。 因此错误对象必须条件化构造——v0.1.2 修复的正是 PostgREST 报错体缺 details/hint 字段时的崩溃。

新版 Key 语义(publishable / secret)

  • 新版项目的 publishable key 无法读取根 OpenAPIGET /rest/v1/ 返回 401 Secret API key required),但可以正常访问数据表端点;
  • 因此 supabase_test 把「收到结构化 HTTP 响应(含 401/403)」判定为网关可达, 只有传输层失败(退出码非 0 / 超时 / 取消)才报错;
  • publishable key 对应旧版 anon key → 填入 anonKey;secret key 对应 service_role → 填入 serviceKey

输出契约

每个工具声明 output.schema = { type: 'json' }render 将规范值以 JSON.stringify(value, null, 2) 文本块交给模型。 timeoutMs 声明协作超时;exec.signal 会转发给 shell 执行器以便取消。

v0.2 新增通道

Management API(supabase_sql

  • 端点:POST https://api.supabase.com/v1/projects/{ref}/database/query
  • 鉴权:Authorization: Bearer <PAT>sbp_ 开头,Dashboard → Account → Access Tokens);
  • 权限:只需要「数据库 读写」scope,其余 scope 均未使用;PAT 不缓存,每次调用实时解析;
  • 多语句注意:多条 SQL 只返回最后一个结果集;DDL/DML 成功时返回空数组;
  • 安全:该端点拥有数据库全部权限,工具描述中显式警告模型谨慎使用。

Storage API(建桶 / 上传)

  • 建桶:POST /storage/v1/bucket,body {name, public, file_size_limit, allowed_mime_types}
  • 上传:POST /storage/v1/object/{bucket}/{path},请求体为原始字节
  • 二进制方案:Node 脚本检测 SB_FILE 环境变量后 require('fs').readFileSync 直接读盘, 字节不经过环境变量、不经过对话文本;Content-Type 按扩展名推断(SB_CONTENT_TYPE 可覆盖);
  • 公开桶:public=true 时 Supabase 自动生成公开读策略, URL 形如 https://<ref>.supabase.co/storage/v1/object/public/{bucket}/{path}

外键的现实一课(实测案例)

实测项目中 user.user_id 存在外键 → auth.users,且列默认值 gen_random_uuid(), 直接 INSERT 必然违反约束(23503)。这是 Supabase 模板的认证外键模式: 正确生产流程是「用户先注册(Auth)→ 拿 auth uid → 写 profile 行」。 插件的错误路径把 code/detail/hint 完整交给模型,Agent 能立即看懂约束并调整策略, 而不是盲试。

沙箱降级(v0.2.1 踩坑)

DSH 若以用户主目录为工作区启动(如 C:\Users\xxx),Windows ACL 沙箱运行器 会因「临时目录必须位于工作区外」拒绝启动,所有 ctx.shell 默认策略调用抛 SANDBOX_UNAVAILABLE。插件的对策:捕获该类错误后,在请求上显式传入 sandboxPolicy: { mode: 'danger-full-access', workspaceRoot: <运行时值> } 重试 (执行器确认该模式走非隔离直跑)。风险评估:传输子进程只执行固定 node -e 脚本、不写盘,用户数据仅经环境变量/stdin 传入,攻击面为固定脚本本身。 根治建议:从项目目录启动 DSH,让工作区正确指向项目而非主目录。

工具注册

harness.defineTool(...) 生成带标记的工具定义,harness.registerTool(ctx, tool) 注册。 二者都绑定当前 Fiber:插件 stop / update / undefine 时自动注销,无泄漏。

已知限制(v0.2.x)

  • 单项目配置(profiles 在路线图中);
  • 响应上限 1 MiB(stdoutMaxBytes),超出会截断并报「响应不是合法 JSON」;
  • JSON 请求体 16 KiB 软上限(环境变量通道);二进制上传不受此限制(子进程直接读盘);
  • supabase_sql 多条语句只返回最后一个结果集;
  • 无 Storage 下载 / 删除 / 列表,无 Auth 管理;
  • 结果以文本形式呈现,暂无可视化数据卡片(Client UI 在路线图中);
  • 无法用 publishable key 做 schema 自省(需要 secret key 读 OpenAPI)。