架构说明
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 + 沙箱环境)暴露出两个问题:
- schannel TLS 故障:Windows 自带
curl.exe使用 schannel 作为 TLS 后端, 在受限执行环境下报SEC_E_NO_CREDENTIALS (0x8009030e),无法建立 HTTPS 连接。 Node 使用 OpenSSL,不受影响;且 DSH 本身运行在 Node 上,node必然可用。 - 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 schannel | SEC_E_NO_CREDENTIALS | Windows 原生 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 无法读取根 OpenAPI(
GET /rest/v1/返回 401Secret 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)。