From aa04334e688e7b7a1d622f64e61ba18611fa00b9 Mon Sep 17 00:00:00 2001 From: pikastech Date: Sun, 19 Jul 2026 12:18:37 +0200 Subject: [PATCH] feat: define SuperAPI binding-level skill forcing --- .agents/skills/unidesk-sub2api/SKILL.md | 9 +++- .../specs/PJ2026-01050205-superapi.md | 43 +++++++++++++++++++ scripts/src/web-probe-superapi-smoke.ts | 25 ++++++++++- 3 files changed, 74 insertions(+), 3 deletions(-) diff --git a/.agents/skills/unidesk-sub2api/SKILL.md b/.agents/skills/unidesk-sub2api/SKILL.md index d5014d6e..6c8aef3d 100644 --- a/.agents/skills/unidesk-sub2api/SKILL.md +++ b/.agents/skills/unidesk-sub2api/SKILL.md @@ -110,7 +110,14 @@ bun scripts/cli.ts web-probe product-smoke \ --profile management ``` -- `management l0-smoke` 一次覆盖 PostgreSQL、skill CRUD、API Key 单向保存、绑定启停、历史导入和快照。 +- `management l0-smoke` 一次覆盖 PostgreSQL、skill CRUD、API Key 单向保存、绑定启停、绑定级强制全文、历史导入和快照。 +- 绑定级强制全文必须满足: + - `forceEnabled` 默认关闭,并与普通 `enabled` 分别保存; + - 关闭时保持 `name + description + SKILL.md URL` 注入; + - 开启时把标准化后的完整 `SKILL.md` 注入 instructions; + - `enabled=false` 时不进入 Gateway 快照; + - 没有完整 `SKILL.md` 的摘要 Skill 必须由服务端拒绝开启; + - 配置只影响当前 API Key 与当前 Skill 的绑定,不得变成 Skill 全局开关。 - 普通 Sub2API 用户权限变更使用独立最短链: - 先用 `codex-pool users ensure` 创建隔离普通测试用户和 Key; - 用户、目标和 Key 名通过参数输入,凭据只使用命令输出的 owner-only diff --git a/project-management/PJ2026-01/specs/PJ2026-01050205-superapi.md b/project-management/PJ2026-01/specs/PJ2026-01050205-superapi.md index 03836b12..cbcb41e6 100644 --- a/project-management/PJ2026-01/specs/PJ2026-01050205-superapi.md +++ b/project-management/PJ2026-01/specs/PJ2026-01050205-superapi.md @@ -38,6 +38,7 @@ Agent instructions。SuperAPI 同时提供公开的文档型 Remote Skill regist ### 2.2 范围内 - 每个 API Key 独立绑定零个或多个虚拟 skill 摘要。 +- 每条 API Key 与 skill 绑定可以独立选择是否强制注入完整 `SKILL.md`。 - 在 Responses 请求中保留原 instructions 并追加匹配 Key 的 catalog。 - 支持流式与同步 Responses 请求的透明透传。 - API Key、Authorization 和上游响应的脱敏日志与健康状态。 @@ -148,6 +149,7 @@ SuperAPI 应使用 owning YAML 引用的 native PostgreSQL 保存以下实体: - skill 绑定: - API Key 与 skill 的唯一关系; - 独立启用开关; + - 默认关闭的完整 `SKILL.md` 强制注入开关; - 创建与更新时间。 - 请求历史: - request id; @@ -174,6 +176,7 @@ SuperAPI 应提供左侧根导航和多页面管理工作区: - 点击列表行打开绑定对话框; - 通过下拉菜单添加 skill; - 对每条绑定独立启用或停用; + - 对每条绑定独立启用或停用完整 `SKILL.md` 强制注入; - 删除绑定。 - 请求历史: - 按时间、API Key、路径、状态和 request id 筛选; @@ -377,3 +380,43 @@ Skill 绑定管理、他人资源拒绝、历史导航不可见与历史 API 拒 保留管理员登录和管理工作区回归,并确认浏览器网络、DOM、console 和 SuperAPI 日志中不存在用户密码、access token 或 API Key 明文。本需求不修改 Sub2API 源码、 版本、账号池或运行配置。 + +### 4.20 USER-KEY-SKILL-REQ-020 绑定级完整 Skill 强制注入 + +每条 API Key 与 Skill 绑定应具有独立的 `forceEnabled` 布尔字段,默认值为 +`false`。该字段属于绑定关系,不属于 Skill 全局属性;同一个 Skill 绑定到不同 +API Key 时,各绑定可以采用不同值。现有 `enabled` 字段继续独立控制绑定是否进入 +Gateway 快照,`forceEnabled` 不得隐式启用已停用绑定。 + +`forceEnabled=false` 时,Gateway 应保持现有稳定行为: + +- 注入 `name + description` 摘要; +- 存在公开主文档时提供精确 `SKILL.md` URL; +- 由 Agent 在任务匹配时按 Remote Skill 加载合同一次性读取文档。 + +`forceEnabled=true` 时,Gateway 应在保留稳定 Skill 身份和边界标记的前提下,把 +该绑定对应的完整标准化 `SKILL.md` 原文直接追加到请求 instructions。强制注入 +不得要求客户端增加 header、环境变量或专用配置,也不得读取或注入 references、 +脚本和其他资源。没有完整 `SKILL.md` 的普通摘要 Skill 必须由服务端拒绝开启 +`forceEnabled`,禁止保存无法生效的配置或静默退化为摘要模式。 + +管理 API、默认本地 CLI、显式 `--over-api` CLI 和 API Key 绑定对话框应读写同一 +绑定字段。Web 应为每条绑定分别显示普通启用开关和“强制注入完整 Skill”开关; +不支持强制注入的 Skill 应显示不可用状态,而不是隐藏服务端限制。每次变更都应 +沿既有 dispatcher 和 snapshot refresh 路径原子发布,并继续执行 API Key +所有权校验。 + +L0 应验证: + +- 新建绑定的 `forceEnabled` 默认为 `false`; +- 默认模式只注入摘要和文档 URL,不包含 `SKILL.md` 正文; +- 显式开启后同一 API Key 的 instructions 包含完整正文; +- 同一 Skill 在另一个 API Key 上保持默认关闭; +- 普通摘要 Skill 开启强制注入时被拒绝; +- 关闭普通 `enabled` 后,即使保存 `forceEnabled=true` 也不进入快照; +- 快照和 CLI/API 输出不包含 API Key 明文。 + +L1 应通过普通 Sub2API 测试用户完成自己 API Key 上的强制注入开关读取、开启、 +回读和恢复,并通过 typed Web smoke 验证两个独立开关及不可用状态。公网 Gateway +验收应使用标准 Responses 请求确认默认摘要模式与强制全文模式的实际 instructions +差异;本需求不修改 Sub2API 源码、版本、账号池或运行配置,不进入 L2/L3。 diff --git a/scripts/src/web-probe-superapi-smoke.ts b/scripts/src/web-probe-superapi-smoke.ts index e8d6c445..ee0440f9 100644 --- a/scripts/src/web-probe-superapi-smoke.ts +++ b/scripts/src/web-probe-superapi-smoke.ts @@ -262,13 +262,34 @@ export default async function superApiManagementSmoke({ page, goto, wait, screen width: Math.round(rect.width), }; }); + const bindingControls = await dialog.locator(".binding-row").evaluateAll((rows) => + rows.map((row) => { + const inputs = Array.from(row.querySelectorAll("input[type='checkbox']")); + const enabled = inputs.find((input) => input.getAttribute("aria-label")?.startsWith("启用 ")); + const force = inputs.find((input) => input.getAttribute("aria-label")?.startsWith("强制注入 ")); + return { + enabledControl: enabled !== undefined, + forceControl: force !== undefined, + forceDisabled: force instanceof HTMLInputElement ? force.disabled : null, + }; + })); + const bindingControlsOk = bindingControls.every((row) => + row.enabledControl && row.forceControl); const ok = bindingEvidence.placeholder === "请选择要绑定的 Skill" && bindingEvidence.visible - && bindingEvidence.width >= 240; + && bindingEvidence.width >= 240 + && bindingControlsOk; if (!ok) addFailure("API Key Skill 下拉框占位符或布局不完整"); const interactionShot = await screenshot("superapi-api-key-bindings.png"); screenshots.push(interactionShot); - interactions.push({ id: item.interaction, ok, ...bindingEvidence, screenshot: interactionShot }); + interactions.push({ + id: item.interaction, + ok, + ...bindingEvidence, + bindingControls, + bindingControlsOk, + screenshot: interactionShot, + }); await dialog.getByLabel("关闭", { exact: true }).click(); if (config.actor === "admin") { await page.getByRole("button", { name: "注册 API Key", exact: true }).click();